package rewriter

import (
	"fmt"
	"math"
	"sort"
	"strings"

	"github.com/samber/lo"

	"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
	"github.com/milvus-io/milvus/pkg/v3/proto/planpb"
	"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)

func columnKey(c *planpb.ColumnInfo) string {
	var b strings.Builder
	fmt.Fprintf(&b, "%d|%t|%d|",
		c.GetFieldId(),
		c.GetIsElementLevel(),
		len(c.GetNestedPath()))
	for _, p := range c.GetNestedPath() {
		fmt.Fprintf(&b, "%d:%s|", len(p), p)
	}
	return b.String()
}

// effectiveDataType returns the real scalar type to be used for comparisons.
// For JSON/Array columns with a concrete element_type, use element_type;
// otherwise fall back to the column data_type.
func effectiveDataType(c *planpb.ColumnInfo) schemapb.DataType {
	if c == nil {
		return schemapb.DataType_None
	}
	dt := c.GetDataType()
	if dt == schemapb.DataType_JSON || dt == schemapb.DataType_Array {
		et := c.GetElementType()
		// Treat 0 (None/Invalid) as not specified; otherwise use element type.
		if et != schemapb.DataType_None {
			return et
		}
	}
	return dt
}

func valueCase(v *planpb.GenericValue) string {
	switch v.GetVal().(type) {
	case *planpb.GenericValue_BoolVal:
		return "bool"
	case *planpb.GenericValue_Int64Val:
		return "int64"
	case *planpb.GenericValue_FloatVal:
		return "float"
	case *planpb.GenericValue_StringVal:
		return "string"
	case *planpb.GenericValue_ArrayVal:
		return "array"
	default:
		return "other"
	}
}

func valueCaseWithNil(v *planpb.GenericValue) string {
	if v == nil || v.GetVal() == nil {
		return "nil"
	}
	return valueCase(v)
}

func isNumericCase(k string) bool {
	return k == "int64" || k == "float"
}

func areComparableCases(a, b string) bool {
	if a == "nil" || b == "nil" {
		return false
	}
	if isNumericCase(a) && isNumericCase(b) {
		return true
	}
	return a == b && (a == "bool" || a == "string")
}

func isNumericType(dt schemapb.DataType) bool {
	if typeutil.IsBoolType(dt) || typeutil.IsStringType(dt) || typeutil.IsJSONType(dt) {
		return false
	}
	return typeutil.IsArithmetic(dt)
}

func sortTermValues(term *planpb.TermExpr) {
	if term == nil || len(term.GetValues()) <= 1 {
		return
	}
	term.Values = sortGenericValues(term.Values)
}

// sort and deduplicate a list of generic values.
func sortGenericValues(values []*planpb.GenericValue) []*planpb.GenericValue {
	if len(values) <= 1 {
		return values
	}
	var kind string
	for _, v := range values {
		if v == nil || v.GetVal() == nil {
			continue
		}
		kind = valueCase(v)
		if kind != "" && kind != "other" && kind != "array" {
			break
		}
	}
	switch kind {
	case "bool":
		sort.Slice(values, func(i, j int) bool {
			return !values[i].GetBoolVal() && values[j].GetBoolVal()
		})
		values = lo.UniqBy(values, func(v *planpb.GenericValue) bool { return v.GetBoolVal() })
	case "int64":
		sort.Slice(values, func(i, j int) bool {
			return values[i].GetInt64Val() < values[j].GetInt64Val()
		})
		values = lo.UniqBy(values, func(v *planpb.GenericValue) int64 { return v.GetInt64Val() })
	case "float":
		sort.Slice(values, func(i, j int) bool {
			a, b := values[i].GetFloatVal(), values[j].GetFloatVal()
			// NaN sorts last to maintain strict weak ordering required by sort.Slice
			if math.IsNaN(a) {
				return false
			}
			if math.IsNaN(b) {
				return true
			}
			return a < b
		})
		values = lo.UniqBy(values, func(v *planpb.GenericValue) float64 { return v.GetFloatVal() })
	case "string":
		sort.Slice(values, func(i, j int) bool {
			return values[i].GetStringVal() < values[j].GetStringVal()
		})
		values = lo.UniqBy(values, func(v *planpb.GenericValue) string { return v.GetStringVal() })
	}
	return values
}

func newTermExpr(col *planpb.ColumnInfo, values []*planpb.GenericValue) *planpb.Expr {
	return &planpb.Expr{
		Expr: &planpb.Expr_TermExpr{
			TermExpr: &planpb.TermExpr{
				ColumnInfo: col,
				Values:     values,
			},
		},
	}
}

func newUnaryRangeExpr(col *planpb.ColumnInfo, op planpb.OpType, val *planpb.GenericValue) *planpb.Expr {
	return &planpb.Expr{
		Expr: &planpb.Expr_UnaryRangeExpr{
			UnaryRangeExpr: &planpb.UnaryRangeExpr{
				ColumnInfo: col,
				Op:         op,
				Value:      val,
			},
		},
	}
}

func newBoolConstExpr(v bool) *planpb.Expr {
	return &planpb.Expr{
		Expr: &planpb.Expr_ValueExpr{
			ValueExpr: &planpb.ValueExpr{
				Value: &planpb.GenericValue{
					Val: &planpb.GenericValue_BoolVal{
						BoolVal: v,
					},
				},
			},
		},
	}
}

func newNullExpr(col *planpb.ColumnInfo, op planpb.NullExpr_NullOp) *planpb.Expr {
	return &planpb.Expr{
		Expr: &planpb.Expr_NullExpr{
			NullExpr: &planpb.NullExpr{
				ColumnInfo: col,
				Op:         op,
			},
		},
	}
}

func newAlwaysTrueExpr() *planpb.Expr {
	return &planpb.Expr{
		Expr: &planpb.Expr_AlwaysTrueExpr{
			AlwaysTrueExpr: &planpb.AlwaysTrueExpr{},
		},
	}
}

func hasNullableFieldSemantics(col *planpb.ColumnInfo) bool {
	return col != nil && col.GetNullable()
}

func hasMissingPathSemantics(col *planpb.ColumnInfo) bool {
	return col != nil && len(col.GetNestedPath()) > 0
}

// only non-nullable fields that don't need the true/false/null semantics can be folded to a bool constant.
func canFoldPredicateToBoolConstant(col *planpb.ColumnInfo) bool {
	return col != nil && col.GetDataType() != schemapb.DataType_JSON &&
		!hasNullableFieldSemantics(col) && !hasMissingPathSemantics(col)
}

// canRewriteNotEqual reports whether NOT(col == value) can be rewritten as
// col != value without changing UNKNOWN results. JSON scalar and array
// comparisons preserve missing paths, nulls, and type mismatches as UNKNOWN.
// Keep the existing conservative behavior for non-JSON nested access.
func canRewriteNotEqual(col *planpb.ColumnInfo, value *planpb.GenericValue) bool {
	if col == nil {
		return false
	}
	switch valueCaseWithNil(value) {
	case "nil", "other":
		return false
	}
	if col.GetDataType() == schemapb.DataType_JSON {
		return true
	}
	return !hasMissingPathSemantics(col)
}

// canBuildTermExpr reports whether values can be represented by one segcore
// TermExpr. TermExpr selects one scalar executor for the entire value list, so
// the values must be concrete, homogeneous scalars; array-valued literals are
// lowered to equality expressions instead.
func canBuildTermExpr(values ...*planpb.GenericValue) bool {
	if len(values) == 0 {
		return false
	}
	kind := valueCaseWithNil(values[0])
	if kind == "nil" || kind == "other" || kind == "array" {
		return false
	}
	for _, value := range values[1:] {
		if valueCaseWithNil(value) != kind {
			return false
		}
	}
	return true
}

func newAlwaysFalseExpr() *planpb.Expr {
	return &planpb.Expr{
		Expr: &planpb.Expr_UnaryExpr{
			UnaryExpr: &planpb.UnaryExpr{
				Op:    planpb.UnaryExpr_Not,
				Child: newAlwaysTrueExpr(),
			},
		},
	}
}

// IsAlwaysTrueExpr checks if the expression is an AlwaysTrueExpr
func IsAlwaysTrueExpr(e *planpb.Expr) bool {
	if e == nil {
		return false
	}
	return e.GetAlwaysTrueExpr() != nil
}

func IsAlwaysFalseExpr(e *planpb.Expr) bool {
	if e == nil {
		return false
	}
	ue := e.GetUnaryExpr()
	if ue == nil || ue.GetOp() != planpb.UnaryExpr_Not {
		return false
	}
	return IsAlwaysTrueExpr(ue.GetChild())
}

// equalsGeneric compares two GenericValue by content (bool/int/float/string).
func equalsGeneric(a, b *planpb.GenericValue) bool {
	if a.GetVal() == nil || b.GetVal() == nil {
		return false
	}

	switch a.GetVal().(type) {
	case *planpb.GenericValue_BoolVal:
		if _, ok := b.GetVal().(*planpb.GenericValue_BoolVal); ok {
			return a.GetBoolVal() == b.GetBoolVal()
		}
	case *planpb.GenericValue_Int64Val:
		if _, ok := b.GetVal().(*planpb.GenericValue_Int64Val); ok {
			return a.GetInt64Val() == b.GetInt64Val()
		}
	case *planpb.GenericValue_FloatVal:
		if _, ok := b.GetVal().(*planpb.GenericValue_FloatVal); ok {
			return a.GetFloatVal() == b.GetFloatVal()
		}
	case *planpb.GenericValue_StringVal:
		if _, ok := b.GetVal().(*planpb.GenericValue_StringVal); ok {
			return a.GetStringVal() == b.GetStringVal()
		}
	}
	return false
}

func satisfiesLower(dt schemapb.DataType, v, lower *planpb.GenericValue, inclusive bool) bool {
	c := cmpGeneric(dt, v, lower)
	if inclusive {
		return c >= 0
	}
	return c > 0
}

func satisfiesUpper(dt schemapb.DataType, v, upper *planpb.GenericValue, inclusive bool) bool {
	c := cmpGeneric(dt, v, upper)
	if inclusive {
		return c <= 0
	}
	return c < 0
}

func filterValuesByRange(dt schemapb.DataType, values []*planpb.GenericValue, lower *planpb.GenericValue, lowerInc bool, upper *planpb.GenericValue, upperInc bool) []*planpb.GenericValue {
	out := make([]*planpb.GenericValue, 0, len(values))
	for _, v := range values {
		pass := true
		if lower != nil && !satisfiesLower(dt, v, lower, lowerInc) {
			pass = false
		}
		if pass && upper != nil && !satisfiesUpper(dt, v, upper, upperInc) {
			pass = false
		}
		if pass {
			out = append(out, v)
		}
	}
	return out
}
