package filters

import (
	"fmt"

	"github.com/samber/lo"
	"gorm.io/gorm"
)

// Use one of declared in the package constants to instantiate the type.
type filterCategory = string

// Enumaration of known types of filters. The assumption is that all filters
// belonging to a single category (type) can be used together at a particular
// step in the query process.
const (
	// Should be safe to use at any step of longer query series to reduce the
	// number of results. If it is not, choose a different filter type
	WHERE_FILTER filterCategory = "where"

	// An like filter
	LIKE_FILTER filterCategory = "where"

	// An order by clause which can be used at any final step of a complex query
	// to change the order of results.
	ORDER_FILTER filterCategory = "order"
	// TODO: document the special case of filters on products
	FEAT_VAL_PRODUCT_FILTER filterCategory = "featval_product"
)

type Filter struct {
	category filterCategory
	filter   func(*gorm.DB) *gorm.DB
}

func NewFilter(category filterCategory, filter func(*gorm.DB) *gorm.DB) Filter {
	return Filter{
		category: category,
		filter:   filter,
	}
}

type FiltersList struct {
	filters []Filter
}

func NewFiltersList() FiltersList {
	return FiltersList{
		// we allocate some extra space beforehand to reduce the overhead of resizing
		filters: make([]Filter, 0, 3),
	}
}

func NewListWithFilter(filt Filter) FiltersList {
	l := NewFiltersList()
	l.filters = append(l.filters, filt)
	return l
}

func (f *FiltersList) NewFilter(category filterCategory, filter func(*gorm.DB) *gorm.DB) {
	f.filters = append(f.filters, NewFilter(category, filter))
}

func (f *FiltersList) Append(filter ...Filter) {
	f.filters = append(f.filters, filter...)
}

// Return all stored filters as []func(*gorm.DB)*gorm.DB
func (f *FiltersList) All() []func(*gorm.DB) *gorm.DB {
	return lo.Map(f.filters, func(filt Filter, _ int) func(*gorm.DB) *gorm.DB {
		return filt.filter
	})
}

func (f *FiltersList) OfCategory(cat filterCategory) []func(*gorm.DB) *gorm.DB {
	return lo.Map(lo.Filter(f.filters, func(v Filter, _ int) bool {
		return v.category == cat
	}), func(el Filter, _ int) func(*gorm.DB) *gorm.DB {
		return el.filter
	})
}

func (f *FiltersList) ApplyAll(d *gorm.DB) {
	d.Scopes(f.All()...)
}

func (f *FiltersList) Apply(d *gorm.DB, cat filterCategory) {
	d.Scopes(f.OfCategory(cat)...)
}

func (f *FiltersList) Merge(another FiltersList) {
	f.filters = append(f.filters, another.filters...)
}

// An implementation of stringer on FiltersList that is meant rather to be used
// for debug display
func (f FiltersList) String() string {
	groupMap := lo.GroupBy(f.filters, func(t Filter) string {
		return t.category
	})
	res := "FiltersList{"
	for key := range groupMap {
		res += fmt.Sprintf(" \"%s\": %d filters", key, len(groupMap[key]))
	}
	res += " }"
	return res
}
