package search

import (
	"errors"
	"fmt"
	"strings"

	"github.com/ganigeorgiev/fexpr"
	"github.com/pocketbase/dbx"
	"github.com/pocketbase/pocketbase/tools/security"
	"github.com/pocketbase/pocketbase/tools/store"
	"github.com/spf13/cast"
)

// FilterData is a filter expession string following the `fexpr` package grammar.
//
// Example:
//	var filter FilterData = "id = null || (name = 'test' && status = true)"
//	resolver := search.NewSimpleFieldResolver("id", "name", "status")
//	expr, err := filter.BuildExpr(resolver)
type FilterData string

// parsedFilterData holds a cache with previously parsed filter data expressions
// (initialized with some prealocated empty data map)
var parsedFilterData = store.New(make(map[string][]fexpr.ExprGroup, 50))

// BuildExpr parses the current filter data and returns a new db WHERE expression.
func (f FilterData) BuildExpr(fieldResolver FieldResolver) (dbx.Expression, error) {
	raw := string(f)
	if parsedFilterData.Has(raw) {
		return f.build(parsedFilterData.Get(raw), fieldResolver)
	}
	data, err := fexpr.Parse(raw)
	if err != nil {
		return nil, err
	}
	// store in cache
	// (the limit size is arbitrary and it is there to prevent the cache growing too big)
	parsedFilterData.SetIfLessThanLimit(raw, data, 500)
	return f.build(data, fieldResolver)
}

func (f FilterData) build(data []fexpr.ExprGroup, fieldResolver FieldResolver) (dbx.Expression, error) {
	if len(data) == 0 {
		return nil, errors.New("Empty filter expression.")
	}

	var result dbx.Expression

	for _, group := range data {
		var expr dbx.Expression
		var exprErr error

		switch item := group.Item.(type) {
		case fexpr.Expr:
			expr, exprErr = f.resolveTokenizedExpr(item, fieldResolver)
		case fexpr.ExprGroup:
			expr, exprErr = f.build([]fexpr.ExprGroup{item}, fieldResolver)
		case []fexpr.ExprGroup:
			expr, exprErr = f.build(item, fieldResolver)
		default:
			exprErr = errors.New("Unsupported expression item.")
		}

		if exprErr != nil {
			return nil, exprErr
		}

		if group.Join == fexpr.JoinAnd {
			result = dbx.And(result, expr)
		} else {
			result = dbx.Or(result, expr)
		}
	}

	return result, nil
}

func (f FilterData) resolveTokenizedExpr(expr fexpr.Expr, fieldResolver FieldResolver) (dbx.Expression, error) {
	lName, lParams, lErr := f.resolveToken(expr.Left, fieldResolver)
	if lName == "" || lErr != nil {
		return nil, fmt.Errorf("Invalid left operand %q - %v.", expr.Left.Literal, lErr)
	}

	rName, rParams, rErr := f.resolveToken(expr.Right, fieldResolver)
	if rName == "" || rErr != nil {
		return nil, fmt.Errorf("Invalid right operand %q - %v.", expr.Right.Literal, rErr)
	}

	// merge both operands parameters (if any)
	params := dbx.Params{}
	for k, v := range lParams {
		params[k] = v
	}
	for k, v := range rParams {
		params[k] = v
	}

	switch expr.Op {
	case fexpr.SignEq:
		op := "="
		if strings.ToLower(lName) == "null" || strings.ToLower(rName) == "null" {
			op = "IS"
		}
		return dbx.NewExp(fmt.Sprintf("%s %s %s", lName, op, rName), params), nil
	case fexpr.SignNeq:
		op := "!="
		if strings.ToLower(lName) == "null" || strings.ToLower(rName) == "null" {
			op = "IS NOT"
		}
		return dbx.NewExp(fmt.Sprintf("%s %s %s", lName, op, rName), params), nil
	case fexpr.SignLike:
		// normalize operands and switch sides if the left operand is a number or text
		if len(lParams) > 0 {
			return dbx.NewExp(fmt.Sprintf("%s LIKE %s", rName, lName), f.normalizeLikeParams(params)), nil
		}
		return dbx.NewExp(fmt.Sprintf("%s LIKE %s", lName, rName), f.normalizeLikeParams(params)), nil
	case fexpr.SignNlike:
		// normalize operands and switch sides if the left operand is a number or text
		if len(lParams) > 0 {
			return dbx.NewExp(fmt.Sprintf("%s NOT LIKE %s", rName, lName), f.normalizeLikeParams(params)), nil
		}
		return dbx.NewExp(fmt.Sprintf("%s NOT LIKE %s", lName, rName), f.normalizeLikeParams(params)), nil
	case fexpr.SignLt:
		return dbx.NewExp(fmt.Sprintf("%s < %s", lName, rName), params), nil
	case fexpr.SignLte:
		return dbx.NewExp(fmt.Sprintf("%s <= %s", lName, rName), params), nil
	case fexpr.SignGt:
		return dbx.NewExp(fmt.Sprintf("%s > %s", lName, rName), params), nil
	case fexpr.SignGte:
		return dbx.NewExp(fmt.Sprintf("%s >= %s", lName, rName), params), nil
	}

	return nil, fmt.Errorf("Unknown expression operator %q", expr.Op)
}

func (f FilterData) resolveToken(token fexpr.Token, fieldResolver FieldResolver) (name string, params dbx.Params, err error) {
	switch token.Type {
	case fexpr.TokenIdentifier:
		name, params, err := fieldResolver.Resolve(token.Literal)

		if name == "" || err != nil {
			m := map[string]string{
				// if `null` field is missing, treat `null` identifier as NULL token
				"null": "NULL",
				// if `true` field is missing, treat `true` identifier as TRUE token
				"true": "1",
				// if `false` field is missing, treat `false` identifier as FALSE token
				"false": "0",
			}
			if v, ok := m[strings.ToLower(token.Literal)]; ok {
				return v, nil, nil
			}
			return "", nil, err
		}

		return name, params, err
	case fexpr.TokenNumber, fexpr.TokenText:
		placeholder := "t" + security.RandomString(7)
		name := fmt.Sprintf("{:%s}", placeholder)
		params := dbx.Params{placeholder: token.Literal}

		return name, params, nil
	}

	return "", nil, errors.New("Unresolvable token type.")
}

func (f FilterData) normalizeLikeParams(params dbx.Params) dbx.Params {
	result := dbx.Params{}

	for k, v := range params {
		vStr := cast.ToString(v)
		if !strings.Contains(vStr, "%") {
			vStr = "%" + vStr + "%"
		}
		result[k] = vStr
	}

	return result
}
