package find

import (
	"errors"
	"reflect"
	"strings"

	"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
	"gorm.io/gorm"
)

type Paging struct {
	Page     uint `json:"p" example:"5"`
	Elements uint `json:"elems" example:"30"`
}

func (p Paging) Offset() int {
	return int(p.Elements) * int(p.Page-1)
}

func (p Paging) Limit() int {
	return int(p.Elements)
}

type Found[T any] struct {
	Items []T                    `json:"items,omitempty"`
	Count uint                   `json:"items_count" example:"56"`
	Spec  map[string]interface{} `json:"spec,omitempty"`
}

// Wraps given query adding limit, offset clauses and SQL_CALC_FOUND_ROWS to it
// and running SELECT FOUND_ROWS() afterwards to fetch the total number
// (ignoring LIMIT) of results. The final results are wrapped into the
// [find.Found] type.
func Paginate[T any](langID uint, paging Paging, stmt *gorm.DB) (Found[T], error) {
	var items []T
	var count uint64

	// stmt.Debug()

	err := stmt.
		Clauses(SqlCalcFound()).
		Offset(paging.Offset()).
		Limit(paging.Limit()).
		Find(&items).
		Error
	if err != nil {
		return Found[T]{}, err
	}

	countInterface, ok := stmt.Get(FOUND_ROWS_CTX_KEY)
	if !ok {
		return Found[T]{}, errors.New(FOUND_ROWS_CTX_KEY + " value was not found in the gorm db context")
	}
	if count, ok = countInterface.(uint64); !ok {
		return Found[T]{}, errors.New("failed to cast value under " + FOUND_ROWS_CTX_KEY + " to uint64")
	}

	columnsSpec := GetColumnsSpec[T](langID)

	return Found[T]{
		Items: items,
		Count: uint(count),
		Spec: map[string]interface{}{
			"columns": columnsSpec,
		},
	}, err
}

// GetColumnsSpec[T any] generates a column specification map for a given struct type T.
// Each key is the JSON property name, and the value is a map containing:
//   - "filter_type": suggested filter type based on field type or `filt` tag
//   - To disable filtering for a field, set `filt:"none"` in the struct tag
//   - "sortable": currently hardcoded to true
//   - "order": order of fields as they appear
//
// Returns nil if T is not a struct.
func GetColumnsSpec[T any](langID uint) map[string]map[string]interface{} {
	result := make(map[string]map[string]interface{})
	typ := reflect.TypeOf((*T)(nil)).Elem()
	if typ.Kind() != reflect.Struct {
		return nil
	}
	order := 1
	processStructFields(langID, typ, result, &order)
	return result
}

type FilterType string

const (
	FilterTypeRange     FilterType = "range"
	FilterTypeTimerange FilterType = "timerange"
	FilterTypeLike      FilterType = "like"
	FilterTypeSwitch    FilterType = "switch"
	FilterTypeNone      FilterType = "none"
)

func isValidFilterType(ft string) bool {
	switch FilterType(ft) {
	case FilterTypeRange, FilterTypeTimerange, FilterTypeLike, FilterTypeSwitch:
		return true
	default:
		return false
	}
}

// processStructFields recursively processes struct fields to populate the result map.
// It handles inline structs, reads `json` and `filt` tags, and determines filter types
// based on the field type when `filt` tag is absent.
// `order` is incremented for each field to track field ordering.
func processStructFields(langID uint, typ reflect.Type, result map[string]map[string]interface{}, order *int) {
	for i := 0; i < typ.NumField(); i++ {
		field := typ.Field(i)
		jsonTag := field.Tag.Get("json")
		if jsonTag == "" || jsonTag == "-" {
			continue
		}
		propName := strings.Split(jsonTag, ",")[0]
		if propName == "" {
			propName = field.Name
		}
		if strings.Contains(jsonTag, ",inline") && field.Type.Kind() == reflect.Struct {
			processStructFields(langID, field.Type, result, order)
			continue
		}

		filterType := field.Tag.Get("filt")
		if filterType != "" {
			if !isValidFilterType(filterType) {
				filterType = string(FilterTypeNone)
			}
		} else {
			fieldType := field.Type.String()
			switch {
			case strings.HasPrefix(fieldType, "int"), strings.HasPrefix(fieldType, "uint"), strings.HasPrefix(fieldType, "float"), strings.HasPrefix(fieldType, "decimal.Decimal"):
				filterType = string(FilterTypeRange)
			case strings.Contains(fieldType, "Time"):
				filterType = string(FilterTypeTimerange)
			case fieldType == "string":
				filterType = string(FilterTypeLike)
			case fieldType == "bool":
				filterType = string(FilterTypeSwitch)
			default:
				filterType = string(FilterTypeNone)
			}
		}

		result[propName] = map[string]interface{}{
			"filter_type": filterType,
			"sortable":    func() bool { val, ok := field.Tag.Lookup("sortable"); return !ok || val == "true" }(),
			"order":       *order,
			"title":       i18n.T___(langID, field.Tag.Get("title")),
			"display":     func() bool { val, ok := field.Tag.Lookup("display"); return !ok || val == "true" }(),
			"hidden":      field.Tag.Get("hidden") == "true",
		}
		*order++
	}
}
