package analytics

import (
	"context"
	"strings"
	"sync"

	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/formulastorage"
	"github.com/nexuscrm/shared/pkg/models"
)

// ValidationProjectionProvider is consumed by the validation service to obtain
// the precomputed set of active computed fields needed for validation on a
// given object. This avoids evaluating all active fields (e.g. 496 on Account)
// when only a handful are actually referenced by validation rules.
type ValidationProjectionProvider interface {
	ValidationProjectionFields(objectName string) ([]string, bool)
	ValidationProjectionFieldsOrCompute(ctx context.Context, objectName string) ([]string, bool)
}

// requestBoundMetadataSource is implemented by metadata readers whose facts
// come from an immutable view carried by the current request. A process-global
// object-name cache cannot safely represent those readers: two requests may
// legitimately bind different Catalog revisions at the same time.
type requestBoundMetadataSource interface {
	UsesRequestBoundRuntimeView() bool
}

// validationProjectionCache stores precomputed validation projection closures
// keyed by lowercase object API name. Each entry is the set of active computed
// field API names that must be projected for validation rules to evaluate
// correctly on that object.
type validationProjectionCache struct {
	mu         sync.RWMutex
	store      map[string][]string
	generation uint64
}

func newValidationProjectionCache() *validationProjectionCache {
	return &validationProjectionCache{
		store: make(map[string][]string),
	}
}

func (c *validationProjectionCache) get(objectName string) ([]string, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	fields, ok := c.store[strings.ToLower(objectName)]
	return fields, ok
}

// currentGeneration returns the current generation for callers that want to
// conditionally set only if no invalidation occurred in the meantime.
func (c *validationProjectionCache) currentGeneration() uint64 {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.generation
}

// setIfGeneration stores the closure only if the cache generation hasn't
// changed since the caller captured it. Prevents stale in-flight rebuilds
// from repopulating the cache after invalidation.
func (c *validationProjectionCache) setIfGeneration(objectName string, fields []string, gen uint64) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.generation != gen {
		return
	}
	c.store[strings.ToLower(objectName)] = fields
}

func (c *validationProjectionCache) remove(objectName string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	delete(c.store, strings.ToLower(objectName))
	c.generation++
}

// removeIfGeneration deletes the entry for objectName only if the cache
// generation is unchanged since the caller captured it, and does NOT advance the
// generation. This is the same-epoch counterpart to setIfGeneration: it lets a
// closure computation record "no closure for this object" (no rules / empty
// closure) without bumping the shared generation, which would otherwise drop
// every later generation-guarded store in the same batch (e.g. a single empty
// object mid-RecomputeAllValidationProjections suppressing rehydration for all
// subsequent objects). External invalidation that must drop in-flight rebuilds
// still uses remove()/clear(), which advance the generation.
func (c *validationProjectionCache) removeIfGeneration(objectName string, gen uint64) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.generation != gen {
		return
	}
	delete(c.store, strings.ToLower(objectName))
}

func (c *validationProjectionCache) clear() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.store = make(map[string][]string)
	c.generation++
}

// ValidationProjectionFields returns the precomputed set of active computed
// field API names that validation rules reference (directly or transitively)
// on the given object. Returns nil, false if no closure has been computed.
func (s *FormulaViewService) ValidationProjectionFields(objectName string) ([]string, bool) {
	if s.validationProjection == nil || s.usesRequestBoundMetadata() {
		return nil, false
	}
	return s.validationProjection.get(objectName)
}

// ValidationProjectionFieldsOrCompute returns the narrow validation-projection
// closure for objectName, computing it on the spot if the cache is cold instead
// of forcing the caller to fall back to evaluating ALL active computed fields.
//
// Why this exists: the cache is cleared on every refresh-cache / metadata change
// (InvalidateAllValidationProjections). Before this, a cold cache made the runtime
// evaluate the full set of active computed fields per write (each a query-backed
// projection eval), which after a deploy's refresh-cache turned every write into
// an O(active-fields) DB-bound projection storm. Computing the closure here is
// cheap (in-memory rules + parse-only formula defs), so we recover the narrow set
// directly rather than degrading into the expensive path.
//
// The store reuses the existing generation guard: a racing invalidation between
// the epoch capture and the store simply drops the store (the returned result is
// still correct for THIS request). The bool is true whenever a closure was
// computed (including a genuinely empty closure for rules that touch only plain
// fields); it is false only when no provider/metadata is wired or the object has
// no active computed fields, in which case the caller has nothing to project.
func (s *FormulaViewService) ValidationProjectionFieldsOrCompute(ctx context.Context, objectName string) ([]string, bool) {
	if s.validationProjection == nil {
		return nil, false
	}
	requestBound := s.usesRequestBoundMetadata()
	if !requestBound {
		if fields, ok := s.validationProjection.get(objectName); ok {
			return fields, true
		}
	}
	if s.metadata == nil {
		return nil, false
	}

	gen, schema := s.projectionEpochForObject(ctx, objectName)
	if schema == nil || !formulastorage.HasActiveComputedFields(schema) {
		return nil, false
	}

	formulaFields := formulastorage.ActiveComputedFields(schema)
	defs, err := s.buildFormulaDefs(ctx, schema.APIName, schema, formulaFields)
	if err != nil {
		// Compute failed (e.g. unparseable formula); let the caller decide its
		// own fallback rather than masking the error as an empty closure.
		return nil, false
	}

	rules := s.validationRulesForProjection(ctx, objectName)
	if requestBound {
		// The request context already owns the immutable revision. Computing from
		// that view is both revision-safe and bounded to this call; publishing it
		// into the legacy process cache would either mix revisions or require an
		// unbounded digest-keyed cache. Catalog may precompute this projection in
		// a future artifact version without changing the consumer contract.
		return buildValidationProjectionClosureResult(schema, defs, rules), true
	}
	result := s.computeValidationProjectionClosureResult(schema, defs, rules, gen)
	if len(result) == 0 {
		// A genuinely empty closure (rules touch only plain fields) is an
		// authoritative answer: project NOTHING for this object. The shared
		// compute core records empty as removeIfGeneration (no cache entry), which
		// would make every subsequent request recompute (re-parsing all formula
		// defs). Store an explicit empty entry so steady state is a map lookup.
		// Generation-guarded so a racing invalidation still drops it.
		s.validationProjection.setIfGeneration(schema.APIName, []string{}, gen)
	}
	return result, true
}

func (s *FormulaViewService) usesRequestBoundMetadata() bool {
	if s == nil || s.metadata == nil {
		return false
	}
	source, ok := s.metadata.(requestBoundMetadataSource)
	return ok && source.UsesRequestBoundRuntimeView()
}

// InvalidateValidationProjection removes the cached closure for the given
// object. Called when validation rules are created/updated/deleted so that
// the runtime falls back to evaluating all active fields until the next
// view rebuild recomputes the closure.
func (s *FormulaViewService) InvalidateValidationProjection(objectName string) {
	if s.validationProjection == nil {
		return
	}
	s.validationProjection.remove(objectName)
}

// InvalidateAllValidationProjections clears all cached closures. Called on
// full metadata cache refresh (RefreshCache) to prevent stale closures from
// persisting after out-of-process metadata changes (pack apply, control-plane
// writes to _System_ValidationRule, etc.).
func (s *FormulaViewService) InvalidateAllValidationProjections() {
	if s.validationProjection == nil {
		return
	}
	s.validationProjection.clear()
}

// computeValidationProjectionClosure builds the transitive closure of active
// computed fields referenced by validation rules on objectName. It uses the
// formula definitions (with ExtraDeps already attached) to expand dependencies
// that are not visible from the rule condition text alone.
//
// The closure covers:
//   - Direct field references in validation rule conditions
//   - Direct field references in record-set binding filters
//   - ExtraDeps of referenced formula fields (Name rewrite, cross-ref, deep cross-ref)
//   - Transitive expansion: if an ExtraDep is itself active-computed, its own deps are included
//
// gen is the projection generation captured by the caller BEFORE it read the
// schema. The closure is stored only if the generation is unchanged at store
// time, so an invalidation racing this rebuild causes a fall-back-to-all-fields
// drop rather than a stale store.
func (s *FormulaViewService) computeValidationProjectionClosure(
	ctx context.Context,
	objectName string,
	schema *models.ObjectMetadata,
	defs []FormulaDefinition,
	gen uint64,
) {
	if schema == nil || s.metadata == nil || s.validationProjection == nil {
		return
	}

	rules := s.validationRulesForProjection(ctx, objectName)
	s.computeValidationProjectionClosureFromRulesWithGen(schema, defs, rules, gen)
}

// projectionGeneration returns the current validation-projection cache
// generation, or 0 if no cache is configured. Callers capture this before
// reading the schema so a concurrent invalidation drops the stale rebuild.
func (s *FormulaViewService) projectionGeneration() uint64 {
	if s.validationProjection == nil {
		return 0
	}
	return s.validationProjection.currentGeneration()
}

// projectionEpochForObject atomically captures the projection-cache generation
// and THEN reads the schema for a single object, returning both. Every projection
// publish path must obtain its (generation, schema) pair through this helper (or
// projectionEpochForAll) rather than capturing the generation separately, so the
// "capture generation BEFORE the metadata read" invariant is enforced structurally
// instead of re-implemented per call site.
//
// Why the order is load-bearing: the closure is stored via setIfGeneration, which
// drops the store if the generation moved. If a caller read the schema first and
// captured the generation second, an invalidation landing in between would be
// missed, and a closure built from the now-stale schema/rules could be published
// (or could overwrite a fresher closure) under a generation that looks current.
// Capturing first makes a false-drop (harmless fall-back to all active fields) the
// only possible race outcome, never a false-store (a correctness regression).
func (s *FormulaViewService) projectionEpochForObject(ctx context.Context, objectName string) (uint64, *models.ObjectMetadata) {
	gen := s.projectionGeneration()
	if s.metadata == nil {
		return gen, nil
	}
	return gen, s.metadata.GetSchema(ctx, objectName)
}

// projectionEpochForAll atomically captures the projection-cache generation and
// THEN reads all schemas, returning both. See projectionEpochForObject for why
// the capture-before-read ordering is a correctness invariant; this is the
// all-objects variant used by the rehydration path.
func (s *FormulaViewService) projectionEpochForAll(ctx context.Context) (uint64, []*models.ObjectMetadata) {
	gen := s.projectionGeneration()
	if s.metadata == nil {
		return gen, nil
	}
	return gen, s.metadata.GetSchemas(ctx)
}

// computeValidationProjectionClosureFromRules is the core closure computation,
// separated for testability. It accepts rules directly instead of fetching them.
// Captures the current generation so the store is not dropped (no concurrent
// invalidation in the single-threaded test path).
func (s *FormulaViewService) computeValidationProjectionClosureFromRules(
	schema *models.ObjectMetadata,
	defs []FormulaDefinition,
	rules []*models.ValidationRule,
) {
	gen := uint64(0)
	if s.validationProjection != nil {
		gen = s.validationProjection.currentGeneration()
	}
	s.computeValidationProjectionClosureFromRulesWithGen(schema, defs, rules, gen)
}

// computeValidationProjectionClosureFromRulesWithGen is the generation-aware
// core. Only stores the computed closure if the cache generation has not
// changed since the caller captured it (prevents stale in-flight rebuilds
// from repopulating after invalidation).
func (s *FormulaViewService) computeValidationProjectionClosureFromRulesWithGen(
	schema *models.ObjectMetadata,
	defs []FormulaDefinition,
	rules []*models.ValidationRule,
	gen uint64,
) {
	s.computeValidationProjectionClosureResult(schema, defs, rules, gen)
}

// computeValidationProjectionClosureResult is the returning core of the closure
// computation. It returns the narrow set of active computed field API names that
// validation rules reference (possibly empty when rules touch only plain fields),
// regardless of whether the generation-guarded store succeeded. Callers that only
// publish into the cache can ignore the return; the compute-on-miss path uses it
// to answer the current request even if a racing invalidation dropped the store.
//
// The store side preserves the existing generation-guard contract exactly:
//   - rules empty / no active computed fields / empty closure -> removeIfGeneration
//     (do not bump generation; this is the same-epoch "no closure" record)
//   - non-empty closure -> setIfGeneration
func (s *FormulaViewService) computeValidationProjectionClosureResult(
	schema *models.ObjectMetadata,
	defs []FormulaDefinition,
	rules []*models.ValidationRule,
	gen uint64,
) []string {
	if schema == nil || s.validationProjection == nil {
		return nil
	}
	result := buildValidationProjectionClosureResult(schema, defs, rules)
	if len(result) == 0 {
		s.validationProjection.removeIfGeneration(schema.APIName, gen)
		return nil
	}
	s.validationProjection.setIfGeneration(schema.APIName, result, gen)
	return result
}

// buildValidationProjectionClosureResult is the pure revision-local closure
// calculation. Cache publication is deliberately outside this function so a
// request-bound immutable metadata reader can use the result without touching
// the legacy process-global cache.
func buildValidationProjectionClosureResult(
	schema *models.ObjectMetadata,
	defs []FormulaDefinition,
	rules []*models.ValidationRule,
) []string {
	if schema == nil {
		return nil
	}
	if len(rules) == 0 {
		return nil
	}

	activeFieldSet := make(map[string]bool)
	for i := range schema.Fields {
		if formulastorage.IsActiveComputedField(&schema.Fields[i]) {
			activeFieldSet[strings.ToLower(schema.Fields[i].APIName)] = true
		}
	}
	if len(activeFieldSet) == 0 {
		return nil
	}

	// Build ExtraDeps lookup from formula definitions
	extraDepsMap := make(map[string][]string, len(defs))
	for _, def := range defs {
		extraDepsMap[strings.ToLower(def.Name)] = def.ExtraDeps
	}

	// Build expression-level deps: bare FieldRef references to other active
	// computed fields within each formula's Expression. These are the standard
	// inter-formula dependencies that the DAG uses for topological ordering.
	// ExtraDeps only covers non-obvious deps (Name rewrite, cross-ref, deep
	// cross-ref); bare FieldRefs are the primary dependency surface.
	exprDepsMap := buildExpressionDeps(defs, activeFieldSet, schema)

	// Build source-deps map for projection-backed fields
	projectionSourceDeps := buildProjectionSourceDeps(schema)

	// Collect all field names directly referenced by active validation rules
	directRefs := extractValidationRuleFieldRefs(rules, schema)

	// Fixed-point transitive closure expansion
	closure := make(map[string]bool)
	queue := make([]string, 0, len(directRefs))
	for ref := range directRefs {
		lower := strings.ToLower(ref)
		if activeFieldSet[lower] {
			if !closure[lower] {
				closure[lower] = true
				queue = append(queue, lower)
			}
		}
	}

	for len(queue) > 0 {
		current := queue[0]
		queue = queue[1:]

		// Expand expression-level FieldRef deps (bare formula-to-formula references)
		for _, dep := range exprDepsMap[current] {
			lower := strings.ToLower(dep)
			if activeFieldSet[lower] && !closure[lower] {
				closure[lower] = true
				queue = append(queue, lower)
			}
		}

		// Expand ExtraDeps (Name rewrite, cross-ref, deep cross-ref dependencies)
		for _, dep := range extraDepsMap[current] {
			lower := strings.ToLower(dep)
			if activeFieldSet[lower] && !closure[lower] {
				closure[lower] = true
				queue = append(queue, lower)
			}
		}

		// Expand projection source dependencies (lookupValue match fields)
		for _, dep := range projectionSourceDeps[current] {
			lower := strings.ToLower(dep)
			if activeFieldSet[lower] && !closure[lower] {
				closure[lower] = true
				queue = append(queue, lower)
			}
		}
	}

	if len(closure) == 0 {
		return nil
	}

	// Convert closure to sorted slice of original-case API names
	result := make([]string, 0, len(closure))
	for i := range schema.Fields {
		if closure[strings.ToLower(schema.Fields[i].APIName)] {
			result = append(result, schema.Fields[i].APIName)
		}
	}
	return result
}

// extractValidationRuleFieldRefs extracts all field names referenced by active
// validation rule conditions and record-set binding filters.
func extractValidationRuleFieldRefs(rules []*models.ValidationRule, schema *models.ObjectMetadata) map[string]bool {
	refs := make(map[string]bool)
	schemaFieldSet := buildSchemaFieldLookup(schema)

	for _, rule := range rules {
		if rule == nil || !rule.Active {
			continue
		}
		addExpressionFieldRefs(refs, rule.Condition, schema, schemaFieldSet)
		for _, binding := range rule.RecordSetBindings {
			for _, filter := range binding.Filters {
				field := strings.TrimSpace(filter.Field)
				if field != "" {
					refs[field] = true
				}
				addRecordSetBindingValueRef(refs, filter, schema, schemaFieldSet)
			}
		}
	}
	return refs
}

// addExpressionFieldRefs extracts current-object field references from a
// validation rule condition using the runtime formula parser's exhaustive
// AST walk (same normalization + parser as the evaluator). Handles all
// runtime syntax: bare identifiers, $Record/$Record__Prior, PRIORVALUE,
// ISCHANGED, record/prior aliases, and relationship traversals. Tokens that
// don't match a direct field API name are resolved through ResolveRelationFK
// (same FK resolution the runtime uses). Fail-closed: unclassifiable tokens
// are included as potential field refs.
func addExpressionFieldRefs(refs map[string]bool, expr string, schema *models.ObjectMetadata, schemaFieldSet map[string]string) {
	if strings.TrimSpace(expr) == "" {
		return
	}
	for _, token := range formula.CollectValidationProjectionFieldRefs(expr) {
		if canonical, ok := schemaFieldSet[strings.ToLower(token)]; ok {
			refs[canonical] = true
			continue
		}
		// Token doesn't match a direct field — try relation→FK resolution.
		// Handles Parent__r→ParentId, PrimaryContact→PrimaryContact__c, etc.
		if fk := formula.ResolveRelationFK(schema, token); fk != "" {
			if canonical, ok := schemaFieldSet[strings.ToLower(fk)]; ok {
				refs[canonical] = true
				continue
			}
		}
		// Fail-closed: include unresolved tokens as-is. If the token happens
		// to be a field name with different casing not in the lookup, this
		// ensures it's not silently dropped.
		refs[token] = true
	}
}

// addRecordSetBindingValueRef extracts record field references from a
// record-set binding filter's Value based on ValueSource. The binding contract
// supports "literal" (no field ref), "recordPath" ($Record.FieldName or
// record.FieldName), and "userPath" (no record field ref). The root segment
// is resolved through ResolveRelationFK for relationship traversals.
func addRecordSetBindingValueRef(refs map[string]bool, filter models.ValidationRuleRecordSetFilter, schema *models.ObjectMetadata, schemaFieldSet map[string]string) {
	source := strings.ToLower(strings.TrimSpace(filter.ValueSource))
	source = strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(source, "_", ""), "-", ""), " ", "")
	if source != "recordpath" {
		return
	}
	path, ok := filter.Value.(string)
	if !ok || strings.TrimSpace(path) == "" {
		return
	}
	cleaned := strings.TrimSpace(path)
	cleaned = strings.TrimPrefix(cleaned, "$Record.")
	cleaned = strings.TrimPrefix(cleaned, "record.")
	parts := strings.SplitN(cleaned, ".", 2)
	root := strings.TrimSpace(parts[0])
	if root == "" {
		return
	}
	// Try direct field match first
	if _, ok := schemaFieldSet[strings.ToLower(root)]; ok {
		refs[root] = true
		return
	}
	// Try relation→FK resolution
	if fk := formula.ResolveRelationFK(schema, root); fk != "" {
		refs[fk] = true
		return
	}
	// Fail-closed: include root verbatim
	refs[root] = true
}

// buildSchemaFieldLookup creates a lowercase→canonical API name map for O(1) matching.
func buildSchemaFieldLookup(schema *models.ObjectMetadata) map[string]string {
	if schema == nil {
		return nil
	}
	lookup := make(map[string]string, len(schema.Fields))
	for _, f := range schema.Fields {
		lookup[strings.ToLower(f.APIName)] = f.APIName
	}
	// Include the logical Name field which may be rewritten
	lookup[strings.ToLower(constants.FieldName)] = constants.FieldName
	return lookup
}

// buildExpressionDeps parses each formula definition's Expression and returns
// the set of references that are themselves active computed fields. This
// captures the inter-formula dependency surface that the formula DAG uses for
// topological ordering — the same dependencies the runtime multi-pass loop
// resolves by iterating until convergence.
//
// Two reference shapes are unioned:
//   - bare FieldRef nodes (e.g. OtherFormula__c)
//   - relationship-traversal roots from CrossRef / DeepCrossRef nodes
//     (e.g. ComputedLookup.Name, Parent__r.Name, Parent.Manager.Name),
//     canonicalized to their FK field via formula.ResolveRelationFK
//
// Without the second shape, a formula that reads another active computed
// reference/lookup field only through a relationship traversal would never
// register that FK field as a dependency, so the BFS closure could omit it and
// validation would evaluate the FK as nil from the raw record (issue #2298
// current-head Codex blocker).
func buildExpressionDeps(defs []FormulaDefinition, activeFieldSet map[string]bool, schema *models.ObjectMetadata) map[string][]string {
	deps := make(map[string][]string, len(defs))
	for _, def := range defs {
		if strings.TrimSpace(def.Expression) == "" {
			continue
		}
		ast, err := Parse(def.Expression)
		if err != nil {
			continue
		}
		var fieldDeps []string
		seen := make(map[string]bool)
		addDep := func(ref string) {
			lower := strings.ToLower(ref)
			if activeFieldSet[lower] && !seen[lower] {
				seen[lower] = true
				fieldDeps = append(fieldDeps, ref)
			}
		}
		// Bare FieldRef references.
		for _, ref := range extractFieldRefs(ast) {
			addDep(ref)
		}
		// Relationship-traversal roots: resolve the relation segment to its FK
		// field on the current object so an active computed reference field
		// consumed via CrossRef/DeepCrossRef is captured as a dependency.
		for _, cr := range extractCrossRefs(ast) {
			addDep(cr.Object)
			if fk := formula.ResolveRelationFK(schema, cr.Object); fk != "" {
				addDep(fk)
			}
		}
		for _, dcr := range extractDeepCrossRefs(ast) {
			if len(dcr.Parts) == 0 {
				continue
			}
			root := dcr.Parts[0]
			addDep(root)
			if fk := formula.ResolveRelationFK(schema, root); fk != "" {
				addDep(fk)
			}
		}
		if len(fieldDeps) > 0 {
			deps[strings.ToLower(def.Name)] = fieldDeps
		}
	}
	return deps
}

// buildProjectionSourceDeps extracts source field dependencies from
// projection-backed active computed fields (lookupValue match source fields).
func buildProjectionSourceDeps(schema *models.ObjectMetadata) map[string][]string {
	deps := make(map[string][]string)
	for i := range schema.Fields {
		f := &schema.Fields[i]
		if !formulastorage.IsActiveComputedField(f) || f.ProjectionConfig == nil {
			continue
		}
		var sourceDeps []string
		for _, m := range f.ProjectionConfig.Matches {
			src := strings.TrimSpace(m.SourceField)
			if src != "" {
				sourceDeps = append(sourceDeps, src)
			}
		}
		if len(sourceDeps) > 0 {
			deps[strings.ToLower(f.APIName)] = sourceDeps
		}
	}
	return deps
}

// validationRulesForProjection safely fetches validation rules for the given
// object. Returns nil if the metadata service cannot provide rules (e.g. in
// test contexts where the repo is incomplete).
func (s *FormulaViewService) validationRulesForProjection(ctx context.Context, objectName string) (rules []*models.ValidationRule) {
	defer func() {
		if r := recover(); r != nil {
			rules = nil
		}
	}()
	return s.metadata.GetValidationRules(ctx, objectName)
}
