package packs

import (
	"errors"
	"fmt"
	"strings"

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

// ErrFlowFieldReferenceMissing is wrapped by every error returned from
// validateFlowFieldReferences so callers can use errors.Is for classification.
var ErrFlowFieldReferenceMissing = errors.New("flow field reference: field absent from pack schema")

// buildFlowObjectFieldGraph converts pack ObjectSpecs into a two-level
// lookup map: lowercase(objectAPIName) → set of lowercase(fieldAPIName).
//
// Fields are unioned across multiple specs for the same object (the second
// spec's fields are added, not used to overwrite). Platform system fields
// (Id, OwnerId, CreatedDate, …) are always included; RecordTypeId is added
// for objects with TableType == "custom_object". For each polymorphic
// reference field, the runtime-generated discriminator companion
// (<field>_type) is also added, matching the field created by platform_core
// applyObjectSpec for polymorphic FK columns.
func buildFlowObjectFieldGraph(specs []ObjectSpec) map[string]map[string]struct{} {
	graph := make(map[string]map[string]struct{}, len(specs))
	for _, spec := range specs {
		key := strings.ToLower(strings.TrimSpace(spec.APIName))
		if key == "" {
			continue
		}
		if graph[key] == nil {
			graph[key] = make(map[string]struct{})
		}
		fields := graph[key]
		for _, f := range flowPlatformFields(spec) {
			fields[strings.ToLower(f)] = struct{}{}
		}
		for _, fs := range spec.Fields {
			fn := strings.TrimSpace(fs.APIName)
			if fn == "" {
				continue
			}
			fields[strings.ToLower(fn)] = struct{}{}
			if fs.isPolymorphicReference() {
				fields[strings.ToLower(fn+constants.PolymorphicTypeSuffix)] = struct{}{}
			}
		}
	}
	return graph
}

// flowPlatformFields returns the set of platform-owned field names that are
// always present on an object at runtime, regardless of its ObjectSpec.Fields.
func flowPlatformFields(spec ObjectSpec) []string {
	base := []string{
		constants.FieldID,
		constants.FieldOwnerID,
		constants.FieldCreatedDate,
		constants.FieldCreatedByID,
		constants.FieldLastModifiedDate,
		constants.FieldLastModifiedByID,
		constants.FieldIsDeleted,
		constants.FieldSystemModstamp,
	}
	if constants.TableType(spec.TableType) == constants.TableTypeCustomObject {
		base = append(base, constants.FieldRecordTypeID)
	}
	return base
}

// validateFlowFieldReferences walks RecordCreate, RecordUpdate, RecordLookup,
// RecordDelete, Decision, start.filters, and Wait event conditions in meta and
// checks each (object, field) pair against graph.
//
// Only objects present in graph are validated — unknown objects (dep-pack
// declarations) are silently skipped. Custom fields (__c suffix) are out of
// scope for construction-time validation; peer-pack declarations cannot be
// resolved without peer-pack specs (see issue #1018 for install-time gate).
// For Decision and Wait elements, only direct "$Record.<field>" and
// "$Record__Old.<field>" two-part references against the flow's trigger object
// (meta.Start.Object) are checked; multi-segment and non-$Record roots are skipped.
//
// Returns an error wrapping ErrFlowFieldReferenceMissing for each missing
// non-__c field, naming the flow, element, object, and field.
func validateFlowFieldReferences(flowName string, meta models.FlowMetadata, graph map[string]map[string]struct{}) error {
	var errs []error

	for _, rc := range meta.RecordCreates {
		obj := strings.TrimSpace(rc.Object)
		if obj == "" {
			continue
		}
		errs = append(errs, checkFlowElementFields(flowName, rc.Name, obj, inputAssignmentFields(rc.InputAssignments), graph)...)
	}

	for _, ru := range meta.RecordUpdates {
		obj := strings.TrimSpace(ru.Object)
		if obj == "" {
			// Variable-reference update (InputReference set): cannot derive object statically.
			continue
		}
		var fields []string
		fields = append(fields, inputAssignmentFields(ru.InputAssignments)...)
		for _, f := range ru.Filters {
			if fn := strings.TrimSpace(f.Field); fn != "" {
				fields = append(fields, fn)
			}
		}
		errs = append(errs, checkFlowElementFields(flowName, ru.Name, obj, fields, graph)...)
	}

	for _, rl := range meta.RecordLookups {
		obj := strings.TrimSpace(rl.Object)
		if obj == "" {
			continue
		}
		var fields []string
		for _, f := range rl.Filters {
			if fn := strings.TrimSpace(f.Field); fn != "" {
				fields = append(fields, fn)
			}
		}
		for _, o := range rl.OutputAssignments {
			if fn := strings.TrimSpace(o.Field); fn != "" {
				fields = append(fields, fn)
			}
		}
		if sf := strings.TrimSpace(rl.SortField); sf != "" {
			fields = append(fields, sf)
		}
		errs = append(errs, checkFlowElementFields(flowName, rl.Name, obj, fields, graph)...)
	}

	for _, rd := range meta.RecordDeletes {
		obj := strings.TrimSpace(rd.Object)
		if obj == "" {
			// Variable-reference delete (InputReference set): cannot derive object statically.
			continue
		}
		var fields []string
		for _, f := range rd.Filters {
			if fn := strings.TrimSpace(f.Field); fn != "" {
				fields = append(fields, fn)
			}
		}
		errs = append(errs, checkFlowElementFields(flowName, rd.Name, obj, fields, graph)...)
	}

	if meta.Start != nil && strings.TrimSpace(meta.Start.Object) != "" {
		triggerObj := strings.TrimSpace(meta.Start.Object)

		var startFields []string
		for _, f := range meta.Start.Filters {
			if fn := strings.TrimSpace(f.Field); fn != "" {
				startFields = append(startFields, fn)
			}
		}
		errs = append(errs, checkFlowElementFields(flowName, "start", triggerObj, startFields, graph)...)

		for _, dec := range meta.Decisions {
			errs = append(errs, checkDecisionFieldRefs(flowName, dec.Name, triggerObj, dec.Rules, graph)...)
		}

		for _, w := range meta.Waits {
			for _, e := range w.WaitEvents {
				errs = append(errs, checkFlowConditionRefs(flowName, w.Name+"."+e.Name, triggerObj, e.Conditions, graph)...)
			}
		}
	}

	return errors.Join(errs...)
}

// checkFlowElementFields checks that each field in fields exists on object in
// graph. Object lookup is case-insensitive; objects absent from graph are skipped.
// Custom fields (__c suffix) are out of scope for construction-time validation.
func checkFlowElementFields(flowName, elementName, object string, fields []string, graph map[string]map[string]struct{}) []error {
	objKey := strings.ToLower(strings.TrimSpace(object))
	knownFields, inGraph := graph[objKey]
	if !inGraph {
		return nil
	}
	var errs []error
	for _, field := range fields {
		if strings.TrimSpace(field) == "" {
			continue
		}
		fieldKey := strings.ToLower(strings.TrimSpace(field))
		// Custom fields (__c suffix) are out of scope for construction-time validation.
		// Cross-pack custom field references cannot be verified without peer-pack specs,
		// which are not available at this call site. Install-time validation (issue #1018)
		// is the authoritative gate for __c field declarations across pack boundaries.
		if strings.HasSuffix(fieldKey, "__c") {
			continue
		}
		if _, ok := knownFields[fieldKey]; !ok {
			errs = append(errs, fmt.Errorf("flow %q element %q: field %q is absent from object %q in pack schema: %w",
				flowName, elementName, field, object, ErrFlowFieldReferenceMissing))
		}
	}
	return errs
}

// checkFlowConditionRefs validates "$Record.<field>" and "$Record__Old.<field>"
// LeftValueReferences in a []FlowCondition slice against the trigger object.
// Multi-segment refs and non-$Record roots are skipped.
// Custom fields (__c suffix) are out of scope for construction-time validation.
func checkFlowConditionRefs(flowName, elementName, triggerObject string, conditions []models.FlowCondition, graph map[string]map[string]struct{}) []error {
	objKey := strings.ToLower(strings.TrimSpace(triggerObject))
	knownFields, inGraph := graph[objKey]
	if !inGraph {
		return nil
	}
	var errs []error
	for _, cond := range conditions {
		lvr := strings.TrimSpace(cond.LeftValueReference)
		if lvr == "" {
			continue
		}
		parts := strings.SplitN(lvr, ".", 3)
		if len(parts) != 2 {
			// Multi-segment or bare variable: cannot resolve statically.
			continue
		}
		root := parts[0]
		if root != "$Record" && root != "$Record__Old" {
			continue
		}
		field := parts[1]
		if strings.TrimSpace(field) == "" {
			continue
		}
		fieldKey := strings.ToLower(strings.TrimSpace(field))
		// Custom fields (__c suffix) are out of scope for construction-time validation.
		// Cross-pack custom field references cannot be verified without peer-pack specs,
		// which are not available at this call site. Install-time validation (issue #1018)
		// is the authoritative gate for __c field declarations across pack boundaries.
		if strings.HasSuffix(fieldKey, "__c") {
			continue
		}
		if _, ok := knownFields[fieldKey]; !ok {
			errs = append(errs, fmt.Errorf("flow %q decision %q: field %q (from %q) is absent from trigger object %q in pack schema: %w",
				flowName, elementName, field, lvr, triggerObject, ErrFlowFieldReferenceMissing))
		}
	}
	return errs
}

// checkDecisionFieldRefs validates "$Record.<field>" and "$Record__Old.<field>"
// LeftValueReferences in Decision rules against the trigger object.
func checkDecisionFieldRefs(flowName, decisionName, triggerObject string, rules []models.FlowDecisionRule, graph map[string]map[string]struct{}) []error {
	var errs []error
	for _, rule := range rules {
		errs = append(errs, checkFlowConditionRefs(flowName, decisionName, triggerObject, rule.Conditions, graph)...)
	}
	return errs
}

// inputAssignmentFields extracts field names from FlowInputAssignment slices.
func inputAssignmentFields(assignments []models.FlowInputAssignment) []string {
	if len(assignments) == 0 {
		return nil
	}
	fields := make([]string, 0, len(assignments))
	for _, a := range assignments {
		if fn := strings.TrimSpace(a.Field); fn != "" {
			fields = append(fields, fn)
		}
	}
	return fields
}

// ErrFlowFilterFormulaInvalid is wrapped by every error returned from
// validateFlowFilterFormulaSyntax so callers can use errors.Is for classification.
var ErrFlowFilterFormulaInvalid = errors.New("flow filter formula: invalid expression")

// validateFlowFilterFormulaSyntax performs a syntax + global-allowlist check
// (Tier 1 + Tier 2) and, when the trigger object's schema is resolvable from
// packObjectSpecs, also a boolean-result + bare/merge-field ref check (Tier 3)
// on meta.Start.FilterFormula.
//
// Tier behaviour:
//   - Tier 1+2 (always): parse errors, unknown functions, unsupported $-globals
//     ($Flow, $Setup, $Label, …) are rejected; supported globals ($Permission,
//     $User, $Record, …) are accepted.
//   - Tier 3 (when trigger object is in packObjectSpecs): boolean-result check
//     and bare + merge-field ({!$Record.X}) field-ref validation against the
//     trigger object schema.  __c fields and __r relationship paths are tolerated
//     per the same #1018/#1724 deferrals applied across the rest of this package.
//   - Tier 3 nil-fallback: if the trigger object is NOT in packObjectSpecs (e.g.
//     it is declared by a dependency pack whose specs are not available here), a
//     nil schema is passed and only Tier 1+2 run.  This matches the existing
//     "unknown objects are silently skipped" policy for validateFlowFieldReferences.
//
// Empty filterFormula is a no-op (skip).  Returns an error wrapping
// ErrFlowFilterFormulaInvalid on any failure, naming the flow.
func validateFlowFilterFormulaSyntax(flowName string, meta models.FlowMetadata, packObjectSpecs []ObjectSpec) error {
	return validateFlowFilterFormulaSyntaxWithFunctionBundles(flowName, meta, packObjectSpecs, nil)
}

func validateFlowFilterFormulaSyntaxWithFunctionBundles(flowName string, meta models.FlowMetadata, packObjectSpecs []ObjectSpec, formulaFunctionBundles []formula.FunctionBundle) error {
	if meta.Start == nil {
		return nil
	}
	expr := strings.TrimSpace(meta.Start.FilterFormula)
	if expr == "" {
		return nil
	}

	// Resolve trigger object schema and build the full cross-object index for Tier 3.
	// BuildObjectMetadataIndex returns a map keyed by lowercase APIName.  If the
	// trigger object is absent (dep-pack declaration), triggerSchema is nil and
	// only Tier 1+2 run.  extraSchemas carries all pack objects so that
	// multi-hop relationship paths (e.g. $Record.Parent__r.Name) can be validated
	// when the target object's schema is available in the pack.
	var triggerSchema *models.ObjectMetadata
	var extraSchemas map[string]*models.ObjectMetadata
	if len(packObjectSpecs) > 0 && meta.Start.Object != "" {
		idx := BuildObjectMetadataIndex(packObjectSpecs)
		// filterFormula Tier-3 must resolve platform-owned fields (Id/OwnerId/
		// CreatedDate/...) that are always present at runtime but absent from raw
		// ObjectSpec.Fields. Augment LOCALLY here so BuildObjectMetadataIndex stays
		// raw for its other consumers (child-rows engine, insert-rules loader,
		// tenant-overlay pack) — only filterFormula validation sees the platform fields.
		for k, v := range idx {
			idx[k] = augmentObjectMetadataWithPlatformFields(v)
		}
		triggerKey := strings.ToLower(strings.TrimSpace(meta.Start.Object))
		triggerSchema = idx[triggerKey] // nil when absent → Tier 1+2 only
		extraSchemas = idx              // full index for cross-object path validation
	}

	engine := formula.NewEngine(formula.WithoutRuntimeFunctionBundles())
	if len(formulaFunctionBundles) > 0 {
		if err := engine.RegisterFunctionBundles(formula.CloneFunctionBundles(formulaFunctionBundles)); err != nil {
			return fmt.Errorf("flow %q start.filterFormula %q: %w: %v", flowName, expr, ErrFlowFilterFormulaInvalid, err)
		}
	}
	// ValidateFilterFormulaExpressionWithSchemas runs Tier 1+2 always; Tier 3
	// when triggerSchema != nil; extraSchemas enables full-path __r validation.
	if err := engine.ValidateFilterFormulaExpressionWithSchemas(expr, triggerSchema, extraSchemas); err != nil {
		return fmt.Errorf("flow %q start.filterFormula %q: %w: %v",
			flowName, expr, ErrFlowFilterFormulaInvalid, err)
	}
	return nil
}
