package dbutil

import (
	. "kjol/basic"
	"net/http"
	"strconv"
	"strings"
)

const (
	PAGE_NUM_KEY             = "page_num"
	ORDER_BY_KEY             = "order_by"
	ITEMS_PER_PAGE_KEY       = "items_per_page"
	SEARCH_KEY_PREFIX        = "search_"
	FILTER_DEFAULT_MAX_ITEMS = 25
)

type Search struct {
	Values           []string
	Identifier       string
	CaseSensitive    bool
	IgnoreWhitespace bool
}

type OrderBy struct {
	Identifier string
	Descending bool
}

type Pagination struct {
	Disabled        bool
	CurrentPage     int
	NextPage        int
	PreviousPage    int
	TotalPages      int
	TotalItems      int
	MaxItemsPerPage int
	ItemsThisPage   int
	ViewRangeLower  int
	ViewRangeUpper  int
}

type Filter struct {
	Search     []Search
	Pagination Pagination
	OrderBy    OrderBy
}

// GetSearch returns the Search struct for the given identifier, or nil if not found.
func (f *Filter) GetSearch(identifier string) *Search {
	for i := range f.Search {
		if f.Search[i].Identifier == identifier {
			return &f.Search[i]
		}
	}

	return nil
}

// BindOrderBy applies an ORDER BY clause to the select builder if the filter's
// OrderBy identifier matches.
func BindOrderBy(identifier string, col Col, f Filter, sb *SelectBuilder) {
	if sb == nil {
		return
	}
	if identifier == f.OrderBy.Identifier {
		if f.OrderBy.Descending {
			sb.OrderBy(col.Desc())
		} else {
			sb.OrderBy(col.Asc())
		}
	}
}

// BindOrderByMultiCols applies ORDER BY with multiple columns if the filter's
// OrderBy identifier matches.
func BindOrderByMultiCols(identifier string, f Filter, sb *SelectBuilder, cols ...Col) {
	if sb == nil {
		return
	}
	if identifier == f.OrderBy.Identifier {
		exprs := make([]OrderExpr, len(cols))
		for i, c := range cols {
			if f.OrderBy.Descending {
				exprs[i] = c.Desc()
			} else {
				exprs[i] = c.Asc()
			}
		}
		sb.OrderBy(exprs...)
	}
}

// SetDefaultOrderBy applies the given order if no ORDER BY has been set yet.
func SetDefaultOrderBy(defaultExpr OrderExpr, sb *SelectBuilder) {
	if sb == nil {
		return
	}
	if !sb.HasOrderBy() {
		sb.OrderBy(defaultExpr)
	}
}

// ApplyPagination applies LIMIT and OFFSET to the select builder based on the filter.
func ApplyPagination(f Filter, sb *SelectBuilder) {
	if !f.Pagination.Disabled {
		if f.Pagination.MaxItemsPerPage > 0 {
			sb.Limit(int64(f.Pagination.MaxItemsPerPage))
			sb.Offset(int64((f.Pagination.CurrentPage - 1) * f.Pagination.MaxItemsPerPage))
		}
	}
}

func ParseFilterFromRequest(r *http.Request) Filter {
	if r.Body != nil {
		defer r.Body.Close()
	}

	r.ParseForm()

	filter := Filter{}
	filter.Pagination.MaxItemsPerPage = FILTER_DEFAULT_MAX_ITEMS
	filter.Pagination.CurrentPage = 1

	// Parse pagination
	if pageNum := r.FormValue(PAGE_NUM_KEY); pageNum != "" {
		if n, err := strconv.Atoi(pageNum); err == nil && n > 0 {
			filter.Pagination.CurrentPage = n
		}
	}
	if itemsPerPage := r.FormValue(ITEMS_PER_PAGE_KEY); itemsPerPage != "" {
		if n, err := strconv.Atoi(itemsPerPage); err == nil {
			if n > 0 {
				filter.Pagination.MaxItemsPerPage = n
			} else if n == -1 {
				filter.Pagination.MaxItemsPerPage = -1
			}
		}
	}

	// Parse order by
	if orderByValue := r.FormValue(ORDER_BY_KEY); orderByValue != "" {
		filter.OrderBy.Identifier = orderByValue
		filter.OrderBy.Descending = r.FormValue("order_desc") == "true"
	}

	// Parse search parameters (keys prefixed with search_)
	for key, values := range r.Form {
		if strings.HasPrefix(key, SEARCH_KEY_PREFIX) && len(values) > 0 {
			identifier := strings.TrimPrefix(key, SEARCH_KEY_PREFIX)

			if len(values) == 1 && values[0] == "__EMPTY_ARRAY__" {
				filter.Search = append(filter.Search, Search{
					Identifier: identifier,
					Values:     []string{},
				})
			} else {
				filter.Search = append(filter.Search, Search{
					Identifier: identifier,
					Values:     values,
				})
			}
		}
	}

	return filter
}

func (p *Pagination) GeneratePagination(totalItemsInSet int64, itemsDisplayedThisPage int) {
	p.TotalItems = int(totalItemsInSet)
	p.ItemsThisPage = itemsDisplayedThisPage

	if p.MaxItemsPerPage == 0 {
		p.MaxItemsPerPage = FILTER_DEFAULT_MAX_ITEMS
	}

	if p.MaxItemsPerPage == -1 {
		p.TotalPages = 1
		p.CurrentPage = 1
		p.PreviousPage = 1
		p.NextPage = 1
		if p.TotalItems != 0 {
			p.ViewRangeLower = 1
		} else {
			p.ViewRangeLower = 0
		}
		p.ViewRangeUpper = p.TotalItems
		return
	}

	if p.MaxItemsPerPage == 0 {
		p.TotalPages = 1
	} else {
		p.TotalPages = p.TotalItems / p.MaxItemsPerPage

		if p.TotalItems%p.MaxItemsPerPage != 0 {
			p.TotalPages++
		}
	}

	if p.TotalPages == 0 {
		p.TotalPages = 1
	}

	if p.CurrentPage < 1 {
		p.CurrentPage = 1
		p.PreviousPage = 1
	} else {
		p.PreviousPage = p.CurrentPage - 1
	}

	if p.TotalItems != 0 {
		p.ViewRangeLower = p.MaxItemsPerPage*p.CurrentPage - p.MaxItemsPerPage + 1
	} else {
		p.ViewRangeLower = 0
	}
	p.ViewRangeUpper = p.MaxItemsPerPage*p.CurrentPage - p.MaxItemsPerPage + p.ItemsThisPage

	if p.CurrentPage >= p.TotalPages {
		p.CurrentPage = p.TotalPages
		p.NextPage = p.TotalPages
	} else {
		p.NextPage = p.CurrentPage + 1
	}
}

// PaginateSlice performs in-memory pagination on a slice.
func PaginateSlice[T any](arr []T, f Filter) []T {
	if !f.Pagination.Disabled {
		if f.Pagination.CurrentPage <= 0 {
			f.Pagination.CurrentPage = 1
		}

		if f.Pagination.MaxItemsPerPage > 0 {
			offset := (f.Pagination.CurrentPage - 1) * f.Pagination.MaxItemsPerPage
			limit := f.Pagination.MaxItemsPerPage

			if offset > len(arr) {
				arr = []T{}
			} else if offset+limit > len(arr) {
				arr = arr[offset:]
			} else {
				arr = arr[offset : offset+limit]
			}
		}
	}

	return arr
}

// LikeNonAlphaNumeric creates a condition that strips non-alphanumeric (except space)
// characters from the column and matches against the sanitized search value.
func LikeNonAlphaNumeric(columnName string, searchValue string, cond Cond) Cond {
	searchSanitized := SanitizeAlphaNum(strings.ToLower(searchValue))

	return cond.And(RawCond(
		"regexp_replace(lower("+columnName+"), '[^a-zA-Z0-9 ]', '', 'g') LIKE ?",
		"%"+searchSanitized+"%",
	))
}

// LikeNonAlphaNumericStrict creates a condition that strips ALL non-alphanumeric
// characters (including spaces) from the column and matches against the sanitized search value.
func LikeNonAlphaNumericStrict(columnName string, searchValue string, cond Cond) Cond {
	searchSanitized := SanitizeAlphaNumStrict(strings.ToLower(searchValue))

	return cond.And(RawCond(
		"regexp_replace(lower("+columnName+"), '[^a-zA-Z0-9]', '', 'g') LIKE ?",
		"%"+searchSanitized+"%",
	))
}

// IsEmpty returns true if the filter has no search values and no order by set.
func (f *Filter) IsEmpty() bool {
	if f.OrderBy.Identifier != "" {
		return false
	}
	for _, s := range f.Search {
		if len(s.Values) > 0 && s.Values[0] != "" {
			return false
		}
	}
	return true
}

// ToQueryString converts the filter to a URL query string.
func (f *Filter) ToQueryString() string {
	params := make([]string, 0)

	for _, s := range f.Search {
		for _, v := range s.Values {
			if v != "" {
				params = append(params, SEARCH_KEY_PREFIX+s.Identifier+"="+v)
			}
		}
	}

	if f.OrderBy.Identifier != "" {
		params = append(params, ORDER_BY_KEY+"="+f.OrderBy.Identifier)
		if f.OrderBy.Descending {
			params = append(params, "order_desc=true")
		}
	}

	return strings.Join(params, "&")
}
