package analytics

import (
	"context"
	"errors"
	"fmt"
	"sort"
	"strings"

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

// ErrFormulaViewMetadataNotReady is returned when an object's live schema
// cannot be loaded from MetadataService. This means the cache is cold or
// the object row is genuinely absent — not that the object has zero active
// formulas (that is not an error; see DerivedViewDependencies).
var ErrFormulaViewMetadataNotReady = errors.New("formula view metadata not ready")

// candidateFieldRead records a specific field read on a cross-object target.
// The dependency filter uses both pieces together: target B's v_B view is only
// required when the SPECIFIC field being read (fieldName) is active-computed on
// B. A field that lives in B's physical table (not the view) does not create a
// view-build dependency.
type candidateFieldRead struct {
	targetAPIName string
	fieldName     string // the specific field read on the target
}

// DerivedViewDependencies returns the API names of objects whose v_<obj>
// views must be built before objectAPIName's view, computed from LIVE metadata.
//
// Algorithm (field-level dependency — tightened from object-level per CI Codex):
//
//  1. Load schema for objectAPIName via s.metadata.GetSchema. If missing,
//     return ErrFormulaViewMetadataNotReady wrapping the object name.
//  2. Build relationship-target map for the schema: for each FK field,
//     map each traversal name (lower-cased) to the target object API name.
//     Traversal names: explicit RelationshipName, derived name (ParentId →
//     Parent, Foo__c → Foo__r), and ReferenceTo[0].
//  3. For each active formula field on the schema, parse the expression:
//     a. For each CrossRef, look up CrossRef.Object (lower-cased) in the
//     relationship-target map; if found, record (target, CrossRef.Field)
//     as a candidate read.
//     b. For each DeepCrossRef, resolve ONLY the LAST hop's target and the
//     final field name (DeepCrossRef.Parts[-1]). Intermediate hops join
//     physical tables and do NOT create view-build dependencies. Require
//     loading intermediate schemas to continue hop resolution; if a
//     schema is missing, return ErrFormulaViewMetadataNotReady.
//     c. Aggregates are NOT candidates. Aggregate subqueries emit
//     FROM <child_physical_table> (verified in formula_dsl_viewbuilder.go
//     lines 264-270, 470-474). They read physical columns, never v_<child>.
//     No view-build dependency arises from aggregate expressions.
//  4. Filter candidates: include ONLY if the SPECIFIC field read on the
//     candidate object is active-computed on that object (i.e. lives in
//     v_<obj>, not in the physical table). Per R3-F2: fields that exist on
//     the physical table only are NOT errors and NOT dependencies — skip.
//     Note: the pack-side analog (source_backed_pack.go) uses object-level
//     filtering for metadata registration order, which intentionally diverges
//     from view-build order. The two algorithms serve different purposes and
//     are not required to agree.
//  5. Dedupe (case-insensitive); exclude self.
//  6. Return sorted slice for deterministic output.
//
// Errors:
//   - ErrFormulaViewMetadataNotReady when objectAPIName's schema OR a
//     transitively-referenced schema needed to RESOLVE a hop is missing.
//     Distinguished from "object has no active formulas" — that returns
//     nil without error.
func (s *FormulaViewService) DerivedViewDependencies(
	ctx context.Context,
	objectAPIName string,
) ([]string, error) {
	schema := s.metadata.GetSchema(ctx, objectAPIName)
	if schema == nil {
		return nil, fmt.Errorf("%w: %s", ErrFormulaViewMetadataNotReady, objectAPIName)
	}

	relTargets := buildRelationshipTargetsFromSchema(schema)

	selfKey := strings.ToLower(objectAPIName)
	// candidates maps (lower(target) + "." + lower(field)) → candidateFieldRead.
	// Keyed by (target,field) pair to deduplicate identical reads across formula fields.
	candidates := make(map[string]candidateFieldRead)

	for _, f := range schema.Fields {
		if !formulastorage.IsActiveComputedField(&f) {
			continue
		}
		formula := ""
		if f.Formula != nil {
			formula = strings.TrimSpace(*f.Formula)
		}
		if formula == "" {
			// ProjectionConfig-only active fields have no formula AST to parse;
			// their dependencies follow LOOKUPVALUE raw-table semantics in
			// CompileContext and are intentionally outside this resolver's scope.
			// See design doc R2-4.
			continue
		}

		ast, err := Parse(formula)
		if err != nil {
			return nil, fmt.Errorf("parse active formula %s.%s: %w", objectAPIName, f.APIName, err)
		}

		for _, ref := range ExtractCrossRefs(ast) {
			target := relTargets[strings.ToLower(ref.Object)]
			if target == "" {
				continue
			}
			key := strings.ToLower(target)
			if key == selfKey {
				continue
			}
			pairKey := key + "." + strings.ToLower(ref.Field)
			candidates[pairKey] = candidateFieldRead{targetAPIName: target, fieldName: ref.Field}
		}

		for _, dcr := range extractDeepCrossRefs(ast) {
			// Parts[-1] is the field name; Parts[0..n-2] are relationship traversal tokens.
			// Only the LAST hop's target creates a view-build dependency because only
			// that hop may read a computed column. Intermediate hops join physical tables.
			if len(dcr.Parts) < 2 {
				continue
			}
			currentTargets := relTargets
			var lastTarget string
			for i := 0; i < len(dcr.Parts)-1; i++ {
				hopName := strings.ToLower(dcr.Parts[i])
				target := currentTargets[hopName]
				if target == "" {
					lastTarget = ""
					break
				}
				lastTarget = target
				if i < len(dcr.Parts)-2 {
					// Need to load the hop schema to continue resolving.
					hopSchema := s.metadata.GetSchema(ctx, target)
					if hopSchema == nil {
						return nil, fmt.Errorf("%w: %s (referenced from %s.%s via %s)",
							ErrFormulaViewMetadataNotReady, target, objectAPIName, f.APIName, strings.Join(dcr.Parts[:i+1], "."))
					}
					currentTargets = buildRelationshipTargetsFromSchema(hopSchema)
				}
			}
			// Only record the final-hop target with the specific field read.
			if lastTarget == "" {
				continue
			}
			key := strings.ToLower(lastTarget)
			if key == selfKey {
				continue
			}
			finalField := dcr.Parts[len(dcr.Parts)-1]
			pairKey := key + "." + strings.ToLower(finalField)
			candidates[pairKey] = candidateFieldRead{targetAPIName: lastTarget, fieldName: finalField}
		}

		// Aggregates: subqueries emit FROM <child_physical_table> (not v_<child>).
		// They read physical columns only, so aggregate expressions do NOT create
		// view-build dependencies. No candidate emitted here.
	}

	// Filter: keep only candidates where the SPECIFIC field read on the target is
	// active-computed (lives in v_<obj>, not in the physical table). Per R3-F2:
	// fields on the physical table are not dependencies — silently skip.
	var deps []string
	seen := make(map[string]bool)
	for _, c := range candidates {
		depSchema := s.metadata.GetSchema(ctx, c.targetAPIName)
		if depSchema == nil {
			// Cannot determine field status without schema — fail closed.
			return nil, fmt.Errorf("%w: %s (dependency of %s)",
				ErrFormulaViewMetadataNotReady, c.targetAPIName, objectAPIName)
		}
		resolvedField, err := resolveReferencedField(depSchema, c.fieldName)
		if err != nil {
			// Unknown target field cannot require a view-build dependency.
			continue
		}
		if !formulastorage.SchemaFieldIsActiveComputed(depSchema, resolvedField) {
			// Field is a physical column on the target — no v_<obj> needed. Skip silently.
			continue
		}
		targetKey := strings.ToLower(c.targetAPIName)
		if !seen[targetKey] {
			seen[targetKey] = true
			deps = append(deps, c.targetAPIName)
		}
	}

	sort.Strings(deps)
	return deps, nil
}

// buildRelationshipTargetsFromSchema builds a map from relationship traversal
// name (lower-cased) to target object API name for all FK fields in the schema.
//
// Mirrors buildRelationshipTargets in source_backed_pack.go, adapted for
// *models.ObjectMetadata. Traversal names added per field:
//   - explicit RelationshipName (if non-empty)
//   - derived name via deriveRelationshipName(fieldAPIName)
//   - ReferenceTo[0] (if present) — mirrors objectSpecRelationshipTraversalNames
func buildRelationshipTargetsFromSchema(schema *models.ObjectMetadata) map[string]string {
	targets := make(map[string]string)
	for _, f := range schema.Fields {
		if !constants.IsReferenceType(f.Type) {
			continue
		}
		target, ok := formulaFieldPrimaryTarget(f)
		if !ok {
			continue
		}
		// Build traversal names: explicit relationship name, derived name, and
		// ReferenceTo[0] itself (to match objectSpecRelationshipTraversalNames shape).
		seen := make(map[string]bool)
		add := func(name string) {
			name = strings.TrimSpace(name)
			if name == "" {
				return
			}
			lower := strings.ToLower(name)
			if seen[lower] {
				return
			}
			seen[lower] = true
			targets[lower] = target
		}
		for _, name := range relationshipTraversalNamesForField(f) {
			add(name)
		}
		add(target)
	}
	return targets
}
