package analytics

import (
	"fmt"
	"sort"
	"strings"
)

// FormulaDefinition describes a single formula field for the DAG.
type FormulaDefinition struct {
	Name             string // Field API name (e.g. "Total_Amount__c")
	ObjectName       string // Owning object API name when known
	Expression       string // DSL formula text (e.g. "Amount * Quantity")
	ReturnType       string // "Number", "Text", "Percent", "Date", "Boolean", etc.
	LookupValueRules []LookupValueRule
	ExtraDeps        []string // synthetic dependencies not expressible as bare FieldRef nodes
}

// FormulaDAG represents a directed acyclic graph of formula field dependencies.
// It performs topological sorting to determine the correct evaluation order.
type FormulaDAG struct {
	formulas map[string]*dagNode
	order    []string // topologically sorted field names
}

type dagNode struct {
	def  FormulaDefinition
	ast  Node     // parsed AST
	deps []string // names of formula fields this formula depends on
}

// NewFormulaDAG builds a DAG from formula definitions.
// It parses each formula, extracts field references, identifies inter-formula
// dependencies, and produces a topological ordering.
func NewFormulaDAG(defs []FormulaDefinition) (*FormulaDAG, error) {
	dag := &FormulaDAG{
		formulas: make(map[string]*dagNode, len(defs)),
	}

	// Build name set for O(1) lookup
	nameSet := make(map[string]bool, len(defs))
	for _, d := range defs {
		nameSet[strings.ToLower(d.Name)] = true
	}

	// Parse all formulas and extract dependencies
	for _, d := range defs {
		ast, err := Parse(d.Expression)
		if err != nil {
			return nil, fmt.Errorf("parsing formula %q (%s): %w", qualifyFormulaName(d.ObjectName, d.Name), d.Expression, err)
		}

		refs := extractFieldRefs(ast)
		var deps []string
		for _, ref := range refs {
			if nameSet[strings.ToLower(ref)] {
				deps = append(deps, ref)
			}
		}
		for _, dep := range d.ExtraDeps {
			if dep == "" || !nameSet[strings.ToLower(dep)] {
				continue
			}
			deps = append(deps, dep)
		}
		deps = dedupePreserveOrder(deps)

		dag.formulas[d.Name] = &dagNode{
			def:  d,
			ast:  ast,
			deps: deps,
		}
	}

	// Topological sort
	order, err := dag.topoSort()
	if err != nil {
		return nil, err
	}
	dag.order = order

	return dag, nil
}

func dedupePreserveOrder(values []string) []string {
	if len(values) < 2 {
		return values
	}
	seen := make(map[string]bool, len(values))
	out := make([]string, 0, len(values))
	for _, value := range values {
		key := strings.ToLower(strings.TrimSpace(value))
		if key == "" || seen[key] {
			continue
		}
		seen[key] = true
		out = append(out, value)
	}
	return out
}

// Order returns the topologically sorted field names (dependencies first).
func (dag *FormulaDAG) Order() []string {
	return dag.order
}

// Layers groups formulas into dependency layers.
// Layer 0: formulas with no formula dependencies (only base fields).
// Layer N: formulas that depend on Layer N-1 formulas.
func (dag *FormulaDAG) Layers() [][]FormulaDefinition {
	depths := make(map[string]int)
	for name := range dag.formulas {
		dag.computeNodeDepth(name, depths, make(map[string]bool))
	}

	maxDepth := 0
	for _, d := range depths {
		if d > maxDepth {
			maxDepth = d
		}
	}

	// Emit each layer in lexicographic field order: map iteration order is
	// randomized per run, and letting it leak into the SELECT list makes
	// BuildViewDDL nondeterministic — rebuilt view definitions would churn
	// between identical rebuilds and the compiler-generation fingerprint
	// would flap, retriggering startup view rebuilds on every boot.
	names := make([]string, 0, len(depths))
	for name := range depths {
		names = append(names, name)
	}
	sort.Strings(names)
	layers := make([][]FormulaDefinition, maxDepth+1)
	for _, name := range names {
		layers[depths[name]] = append(layers[depths[name]], dag.formulas[name].def)
	}
	return layers
}

// AST returns the parsed AST for a formula field.
func (dag *FormulaDAG) AST(name string) Node {
	if n, ok := dag.formulas[name]; ok {
		return n.ast
	}
	return nil
}

// Dependencies returns the formula fields that the given field depends on.
func (dag *FormulaDAG) Dependencies(name string) []string {
	if n, ok := dag.formulas[name]; ok {
		return n.deps
	}
	return nil
}

// CrossTableRefs returns all unique cross-table references across all formulas.
// Each entry is {Object, Field} from CrossRef nodes.
func (dag *FormulaDAG) CrossTableRefs() []CrossRef {
	seen := make(map[string]bool)
	var refs []CrossRef
	for _, node := range dag.formulas {
		for _, cr := range extractCrossRefs(node.ast) {
			key := cr.Object + "." + cr.Field
			if !seen[key] {
				seen[key] = true
				refs = append(refs, cr)
			}
		}
	}
	return refs
}

// AggregateRefs returns all unique aggregate references across all formulas.
func (dag *FormulaDAG) AggregateRefs() []AggregateExpr {
	seen := make(map[string]bool)
	var aggs []AggregateExpr
	for _, node := range dag.formulas {
		for _, agg := range extractAggregates(node.ast) {
			key := fmt.Sprintf("%s.%s.%s", agg.Func, agg.ChildObject, agg.ChildField)
			if !seen[key] {
				seen[key] = true
				aggs = append(aggs, agg)
			}
		}
	}
	return aggs
}

func (dag *FormulaDAG) computeNodeDepth(name string, depths map[string]int, visiting map[string]bool) int {
	if d, ok := depths[name]; ok {
		return d
	}
	if visiting[name] {
		return 0 // cycle break
	}
	visiting[name] = true

	node, ok := dag.formulas[name]
	if !ok {
		return 0
	}

	maxDep := -1
	for _, dep := range node.deps {
		d := dag.computeNodeDepth(dep, depths, visiting)
		if d > maxDep {
			maxDep = d
		}
	}

	depth := 0
	if maxDep >= 0 {
		depth = maxDep + 1
	}
	depths[name] = depth
	return depth
}

// topoSort performs Kahn's algorithm for topological sorting.
func (dag *FormulaDAG) topoSort() ([]string, error) {
	// Kahn's algorithm with a lexicographic tie-break: every map iteration
	// below walks a sorted name slice so the emitted order is deterministic
	// (map range order is randomized per run and would otherwise leak into
	// generated view DDL).
	sortedNames := make([]string, 0, len(dag.formulas))
	for name := range dag.formulas {
		sortedNames = append(sortedNames, name)
	}
	sort.Strings(sortedNames)

	// Build in-degree map (only counting formula→formula edges)
	inDegree := make(map[string]int, len(dag.formulas))
	dependents := make(map[string][]string, len(dag.formulas))

	for _, name := range sortedNames {
		inDegree[name] = 0
	}

	for _, name := range sortedNames {
		for _, dep := range dag.formulas[name].deps {
			if _, ok := dag.formulas[dep]; ok {
				inDegree[name]++
				dependents[dep] = append(dependents[dep], name)
			}
		}
	}

	// Seed queue with zero in-degree nodes
	var queue []string
	for _, name := range sortedNames {
		if inDegree[name] == 0 {
			queue = append(queue, name)
		}
	}

	var order []string
	for len(queue) > 0 {
		// Pop
		name := queue[0]
		queue = queue[1:]
		order = append(order, name)

		// Reduce in-degree of dependents
		for _, dep := range dependents[name] {
			inDegree[dep]--
			if inDegree[dep] == 0 {
				queue = append(queue, dep)
			}
		}
	}

	if len(order) != len(dag.formulas) {
		// Collect the fields involved in the cycle for diagnostics
		resolved := make(map[string]bool, len(order))
		for _, name := range order {
			resolved[name] = true
		}
		var cycleMembers []string
		for name := range dag.formulas {
			if !resolved[name] {
				cycleMembers = append(cycleMembers, name)
			}
		}
		sort.Strings(cycleMembers)
		return nil, fmt.Errorf("circular dependency detected among formula fields: %s", strings.Join(cycleMembers, " → "))
	}

	return order, nil
}

// ExtractFieldRefs is the exported wrapper for extractFieldRefs.
func ExtractFieldRefs(node Node) []string { return extractFieldRefs(node) }

// ExtractCrossRefs is the exported wrapper for extractCrossRefs.
func ExtractCrossRefs(node Node) []CrossRef { return extractCrossRefs(node) }

// extractFieldRefs collects all FieldRef names from an AST.
func extractFieldRefs(node Node) []string {
	var refs []string
	walkAST(node, func(n Node) {
		if fr, ok := n.(*FieldRef); ok {
			refs = append(refs, fr.Name)
		}
	})
	return refs
}

// extractCrossRefs collects all CrossRef nodes from an AST.
func extractCrossRefs(node Node) []CrossRef {
	var refs []CrossRef
	walkAST(node, func(n Node) {
		if cr, ok := n.(*CrossRef); ok {
			refs = append(refs, *cr)
		}
	})
	return refs
}

// extractAggregates collects all AggregateExpr nodes from an AST.
func extractAggregates(node Node) []AggregateExpr {
	var aggs []AggregateExpr
	walkAST(node, func(n Node) {
		if ag, ok := n.(*AggregateExpr); ok {
			aggs = append(aggs, *ag)
		}
	})
	return aggs
}

// extractDeepCrossRefs collects all DeepCrossRef nodes from an AST.
func extractDeepCrossRefs(node Node) []DeepCrossRef {
	var refs []DeepCrossRef
	walkAST(node, func(n Node) {
		if dcr, ok := n.(*DeepCrossRef); ok {
			refs = append(refs, *dcr)
		}
	})
	return refs
}

// walkAST visits every node in the AST tree.
func walkAST(node Node, visit func(Node)) {
	if node == nil {
		return
	}
	visit(node)

	switch n := node.(type) {
	case *BinaryOp:
		walkAST(n.Left, visit)
		walkAST(n.Right, visit)
	case *UnaryOp:
		walkAST(n.Operand, visit)
	case *LogicalOp:
		for _, o := range n.Operands {
			walkAST(o, visit)
		}
	case *FuncCall:
		for _, a := range n.Args {
			walkAST(a, visit)
		}
	case *IfExpr:
		walkAST(n.Condition, visit)
		walkAST(n.Then, visit)
		walkAST(n.Else, visit)
	case *CaseExpr:
		walkAST(n.Expr, visit)
		for _, b := range n.Branches {
			walkAST(b.Value, visit)
			walkAST(b.Result, visit)
		}
		if n.Default != nil {
			walkAST(n.Default, visit)
		}
	case *DeepCrossRef:
		// leaf node, nothing to recurse into
	}
}
