package dbs

import (
	"fmt"
	"reflect"
	"regexp"
	"runtime/debug"
	"strings"

	"go.mongodb.org/mongo-driver/bson"
)

type Operator int

const (
	LIKE Operator = iota
	EXISTS
	IN
	GTE
	LTE
	LT
	GT
	EQUAL
	NOT
	ELEMMATCH
	OR
)

var str = [...]string{
	"like",
	"exists",
	"in",
	"gte",
	"lte",
	"lt",
	"gt",
	"equal",
	"not",
	"elemMatch",
	"or",
}

func (m Operator) String() string {
	return str[m]
}

func (m Operator) ToMongoOperator(k string, value interface{}) bson.M {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered. Error:\n", r, debug.Stack())
		}
	}()
	defaultValue := bson.M{k: bson.M{"$regex": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
	switch m {
	case LIKE:
		return bson.M{k: bson.M{"$regex": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
	case EXISTS:
		return bson.M{k: bson.M{"$exists": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
	case IN:
		return bson.M{k: bson.M{"$in": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
	case GTE:
		return bson.M{k: bson.M{"$gte": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
	case GT:
		return bson.M{k: bson.M{"$gt": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
	case LTE:
		return bson.M{k: bson.M{"$lte": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
	case LT:
		return bson.M{k: bson.M{"$lt": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
	case ELEMMATCH:
		return bson.M{k: bson.M{"$elemMatch": m.ToValueOperator(StringToOperator(m.String()), value, false)}}
	case EQUAL:
		return bson.M{k: value}
	case NOT:
		return bson.M{"$not": m.ToValueOperator(StringToOperator(m.String()), value, false)}
	case OR:
		return bson.M{"$or": m.ToValueOperator(StringToOperator(m.String()), value, true)}
	default:
		return defaultValue
	}
}

func StringToOperator(s string) Operator {
	for i, v := range str {
		if v == s {
			return Operator(i)
		}
	}
	return LIKE
}

func GetBson(filters *Filters) bson.D {
	f := bson.D{}
	orList := bson.A{}
	andList := bson.A{}
	if filters != nil {
		for k, filter := range filters.Or {
			for _, ff := range filter {
				orList = append(orList, StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
			}
		}
		for k, filter := range filters.And {
			for _, ff := range filter {
				andList = append(andList, StringToOperator(ff.Operator).ToMongoOperator(k, ff.Value))
			}
		}
		if len(orList) > 0 && len(andList) == 0 {
			f = bson.D{{Key: "$or", Value: orList}}
		} else {
			if len(orList) > 0 {
				andList = append(andList, bson.M{"$or": orList})
			}
			f = bson.D{{Key: "$and", Value: andList}}
		}
	}
	return f
}

func (m Operator) ToValueOperator(operator Operator, value interface{}, or bool) interface{} {
	switch value := value.(type) {
	case *Filters:
		bson := GetBson(value)
		if or {
			for _, b := range bson {
				if b.Key == "$or" {
					return b.Value
				}
			}
		} else {
			return bson
		}
	default:
		if strings.TrimSpace(fmt.Sprintf("%v", value)) == "*" {
			value = ""
		}
		if operator == LIKE {
			return "(?i).*" + strings.TrimSpace(fmt.Sprintf("%v", value)) + ".*"
		}
	}

	return value
}

type Filters struct {
	And map[string][]Filter `json:"and"`
	Or  map[string][]Filter `json:"or"`
}

type Filter struct {
	Operator string      `json:"operator,omitempty"`
	Value    interface{} `json:"value,omitempty"`
}

// FiltersFromFlatMap builds a *Filters from a map[string]interface{} whose structure
// mirrors the JSON form of Filters:
//
//	{
//	  "and": { "name": [{"operator":"like","value":"foo"}] },
//	  "or":  { "source": [{"operator":"equal","value":"bar"}] }
//	}
//
// Keys inside "and"/"or" are json tag names; the function resolves each to its
// full dotted BSON path using the target struct. Unknown keys are kept as-is.
func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *Filters {
	defer func() {
		if r := recover(); r != nil {
			fmt.Printf("Panic recovered FiltersFromFlatMap: %v\n", r)
		}
	}()
	filters := &Filters{
		And: make(map[string][]Filter),
		Or:  make(map[string][]Filter),
	}
	paths := jsonToBsonPaths(reflect.TypeOf(target), "", "")
	resolve := func(jsonKey string) string {
		if p, ok := paths[jsonKey]; ok {
			return p
		}
		return jsonKey
	}
	parseFilters := func(raw interface{}) map[string][]Filter {
		out := make(map[string][]Filter)
		m, ok := raw.(map[string]interface{})
		if !ok {
			return out
		}
		for jsonKey, val := range m {
			bsonKey := resolve(jsonKey)
			//items, ok := val.([]interface{})
			fmt.Println(jsonKey, val, bsonKey)
			if !ok {
				continue
			}
			for _, item := range val.([]interface{}) {
				entry, ok := item.(map[string]interface{})
				if !ok {
					continue
				}
				f := Filter{}
				if op, ok := entry["operator"].(string); ok {
					f.Operator = op
				}
				if v, ok := entry["value"]; ok {
					f.Value = v
				}
				out[bsonKey] = append(out[bsonKey], f)
			}
		}
		return out
	}
	if and, ok := flatMap["and"]; ok {
		filters.And = parseFilters(and)
	}
	if or, ok := flatMap["or"]; ok {
		filters.Or = parseFilters(or)
	}
	return filters
}

// jsonToBsonPaths recursively walks a struct type and returns a map of
// json_name → dotted_bson_path for every field reachable from that type.
//
// Anonymous embedded fields without any tag follow the BSON convention of this
// codebase: they are stored as a nested sub-document whose key is the lowercased
// struct type name (e.g. utils.AbstractObject → "abstractobject"). Their JSON
// fields are promoted (flat), so bsonPrefix advances but jsonPrefix does not.
//
// For fields inside slices or maps, both the leaf json name and the full dotted
// json path (e.g. "instances.access_protocol") are registered as keys so callers
// can use either form unambiguously.
func jsonToBsonPaths(t reflect.Type, bsonPrefix string, jsonPrefix string) map[string]string {
	for t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice {
		t = t.Elem()
	}
	if t.Kind() == reflect.Map {
		t = t.Elem()
		for t.Kind() == reflect.Ptr {
			t = t.Elem()
		}
	}
	result := make(map[string]string)
	if t.Kind() != reflect.Struct {
		return result
	}
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)

		jsonTag := field.Tag.Get("json")
		bsonTag := field.Tag.Get("bson")
		jsonName := strings.Split(jsonTag, ",")[0]
		bsonName := strings.Split(bsonTag, ",")[0]

		// Anonymous embedded struct with no tags: use lowercase type name as BSON prefix.
		// JSON fields are promoted so jsonPrefix stays the same.
		if field.Anonymous && jsonName == "" && bsonName == "" {
			ft := field.Type
			for ft.Kind() == reflect.Ptr {
				ft = ft.Elem()
			}
			if ft.Kind() == reflect.Struct {
				embedBsonPrefix := strings.ToLower(ft.Name())
				re := regexp.MustCompile(`\[[^\]]*\]`)
				embedBsonPrefix = re.ReplaceAllString(embedBsonPrefix, "")
				embedBsonPrefix = strings.ReplaceAll(embedBsonPrefix, "*", "")
				if bsonPrefix != "" {
					embedBsonPrefix = bsonPrefix + "." + embedBsonPrefix
				}
				for k, v := range jsonToBsonPaths(ft, embedBsonPrefix, jsonPrefix) {
					if _, exists := result[k]; !exists {
						result[k] = v
					}
				}
			}
			continue
		}

		if jsonName == "" || jsonName == "-" {
			continue
		}
		if bsonName == "" || bsonName == "-" {
			bsonName = jsonName
		}

		fullBsonPath := bsonName
		if bsonPrefix != "" {
			fullBsonPath = bsonPrefix + "." + bsonName
		}
		fullJsonPath := jsonName
		if jsonPrefix != "" {
			fullJsonPath = jsonPrefix + "." + jsonName
		}

		result[jsonName] = fullBsonPath
		// Also register the full dotted JSON path so callers can use
		// "instances.access_protocol" instead of just "access_protocol".
		if fullJsonPath != jsonName {
			if _, exists := result[fullJsonPath]; !exists {
				result[fullJsonPath] = fullBsonPath
			}
		}

		ft := field.Type
		for ft.Kind() == reflect.Ptr || ft.Kind() == reflect.Slice {
			ft = ft.Elem()
		}
		if ft.Kind() == reflect.Map {
			ft = ft.Elem()
			for ft.Kind() == reflect.Ptr {
				ft = ft.Elem()
			}
		}
		if ft.Kind() == reflect.Struct {
			for k, v := range jsonToBsonPaths(ft, fullBsonPath, fullJsonPath) {
				if _, exists := result[k]; !exists {
					result[k] = v
				}
			}
		}
	}
	return result
}

type Input = map[string]interface{}

func InputToBson(i Input, isUpdate bool) bson.D {
	input := bson.D{}
	for k, v := range i {
		if k == "id" {
			input = append(input, bson.E{Key: "_id", Value: v})
		} else {
			input = append(input, bson.E{Key: k, Value: v})
		}
	}
	if isUpdate {
		return bson.D{{Key: "$set", Value: input}}
	}
	return input
}
