package analytics

import (
	"context"
	"fmt"
	"strings"

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

// ValidateFormulaForObject applies schema-aware authoring validation for a
// candidate formula expression on objectName. Runtime-supported self-referential
// active-computed reads are accepted here; syntax parsing and generic compile
// diagnostics stay owned by the existing validate endpoint flow.
func (s *FormulaViewService) ValidateFormulaForObject(ctx context.Context, objectName, expr string) error {
	return s.ValidateFormulaForField(ctx, objectName, "", expr)
}

// ValidateFormulaForField validates a candidate formula against the same active
// formula set that runtime view rebuild will use. When fieldAPIName is set, the
// candidate expression replaces that field before the DAG is built.
func (s *FormulaViewService) ValidateFormulaForField(ctx context.Context, objectName, fieldAPIName, expr string) error {
	if s == nil || s.metadata == nil {
		return nil
	}
	schema := s.metadata.GetSchema(ctx, objectName)
	if schema == nil {
		// Preserve the historical validate endpoint behavior when schema metadata
		// is not yet available; this rollup only narrows the unsupported
		// self-referential active-computed case.
		return nil
	}
	if err := ValidateNoAggregateAuthoring(expr); err != nil {
		return err
	}
	ast, err := Parse(expr)
	if err != nil {
		return err
	}
	if err := s.validateFormulaReferences(ctx, schema, ast); err != nil {
		return err
	}
	if err := s.validateAggregateFormulaRefs(ctx, schema, expr); err != nil {
		return err
	}

	// Read-only formula validation with candidate (unsaved) field defs: never
	// publish into the live projection cache (gen 0, publish=false).
	_, err = s.buildViewFromDSL(ctx, objectName, schema, validationFormulaFields(schema, fieldAPIName, expr), 0, false)
	return err
}

// CompileFormulaPreview compiles a candidate formula expression for validate
// responses. Deep relationship references reuse the same join-chain resolver
// as runtime view generation so preview SQL matches the runtime compiler path.
func (s *FormulaViewService) CompileFormulaPreview(ctx context.Context, objectName, expr string) (string, error) {
	ast, err := Parse(expr)
	if err != nil {
		return "", err
	}
	functionExecutables, err := s.activeFunctionExecutables(ctx)
	if err != nil {
		return "", fmt.Errorf("resolve formula function authority: %w", err)
	}

	compileCtx := &CompileContext{
		TableAlias:          "t",
		JoinAliases:         map[string]string{},
		QualifyFields:       false,
		FunctionExecutables: functionExecutables,
	}
	if s == nil || s.metadata == nil {
		return Compile(ast, compileCtx)
	}

	schema := s.metadata.GetSchema(ctx, objectName)
	if schema == nil {
		return Compile(ast, compileCtx)
	}

	deepJoins, err := s.resolveDeepJoinChains(ctx, objectName, schema, []FormulaDefinition{{
		Name:       "__PreviewFormula__c",
		Expression: expr,
	}})
	if err != nil {
		return "", err
	}
	if len(deepJoins) > 0 {
		compileCtx.DeepJoinMap = buildDeepJoinMap(deepJoins)
		compileCtx.FieldTypes = s.buildFieldTypeMap(ctx, schema, nil, deepJoins, nil)
	}

	return Compile(ast, compileCtx)
}

func validationFormulaFields(schema *models.ObjectMetadata, fieldAPIName, expr string) []models.FieldMetadata {
	rt := models.FieldType(constants.FieldTypeString)
	mode := "active"
	target := strings.TrimSpace(fieldAPIName)
	replaced := false
	out := make([]models.FieldMetadata, 0, len(schema.Fields)+1)

	for _, f := range schema.Fields {
		if target != "" && strings.EqualFold(f.APIName, target) {
			candidate := f
			candidate.Type = constants.FieldTypeFormula
			candidate.Formula = &expr
			candidate.FormulaMode = &mode
			if candidate.ReturnType == nil {
				candidate.ReturnType = &rt
			}
			out = append(out, candidate)
			replaced = true
			continue
		}
		if formulastorage.IsActiveComputedField(&f) {
			out = append(out, f)
		}
	}

	if !replaced {
		name := target
		if name == "" {
			name = "__ValidationFormula__c"
		}
		out = append(out, models.FieldMetadata{
			APIName:     name,
			Type:        constants.FieldTypeFormula,
			Formula:     &expr,
			FormulaMode: &mode,
			ReturnType:  &rt,
		})
	}

	return out
}

func (s *FormulaViewService) validateFormulaReferences(ctx context.Context, baseSchema *models.ObjectMetadata, ast Node) error {
	relTargets := buildRelationshipTargetsFromSchema(baseSchema)
	baseObject := strings.TrimSpace(baseSchema.APIName)
	for _, ref := range ExtractCrossRefs(ast) {
		target := relTargets[strings.ToLower(ref.Object)]
		if target == "" {
			return fmt.Errorf("unresolved cross-reference %s.%s: missing metadata join", ref.Object, ref.Field)
		}
		targetSchema := s.schemaForValidationHop(ctx, baseSchema, baseObject, target)
		if targetSchema == nil {
			return fmt.Errorf("unresolved cross-reference %s.%s: target object %s not found", ref.Object, ref.Field, target)
		}
		if _, err := resolveReferencedField(targetSchema, ref.Field); err != nil {
			return fmt.Errorf("unresolved cross-reference %s.%s: field not found on %s", ref.Object, ref.Field, targetSchema.APIName)
		}
	}
	for _, ref := range extractDeepCrossRefs(ast) {
		if err := s.validateDeepFormulaReference(ctx, baseSchema, baseObject, ref); err != nil {
			return err
		}
	}
	return nil
}

func (s *FormulaViewService) validateDeepFormulaReference(ctx context.Context, baseSchema *models.ObjectMetadata, baseObject string, ref DeepCrossRef) error {
	if len(ref.Parts) < 3 {
		return nil
	}
	currentSchema := baseSchema
	for i := 0; i < len(ref.Parts)-1; i++ {
		relTargets := buildRelationshipTargetsFromSchema(currentSchema)
		target := relTargets[strings.ToLower(ref.Parts[i])]
		if target == "" {
			return fmt.Errorf("unresolved deep cross-reference %s: missing metadata join chain", strings.Join(ref.Parts, "."))
		}
		targetSchema := s.schemaForValidationHop(ctx, baseSchema, baseObject, target)
		if targetSchema == nil {
			return fmt.Errorf("unresolved deep cross-reference %s: target object %s not found", strings.Join(ref.Parts, "."), target)
		}
		if i == len(ref.Parts)-2 {
			if _, err := resolveReferencedField(targetSchema, ref.Parts[len(ref.Parts)-1]); err != nil {
				return fmt.Errorf("unresolved deep cross-reference %s: field not found on %s", strings.Join(ref.Parts, "."), targetSchema.APIName)
			}
			return nil
		}
		currentSchema = targetSchema
	}
	return nil
}

func (s *FormulaViewService) schemaForValidationHop(ctx context.Context, baseSchema *models.ObjectMetadata, baseObject, target string) *models.ObjectMetadata {
	if strings.EqualFold(target, baseObject) {
		return baseSchema
	}
	if s == nil || s.metadata == nil {
		return nil
	}
	return s.metadata.GetSchema(ctx, target)
}

func (s *FormulaViewService) validateAggregateFormulaRefs(ctx context.Context, schema *models.ObjectMetadata, expr string) error {
	if schema == nil {
		return nil
	}
	ast, err := Parse(expr)
	if err != nil {
		return err
	}
	for idx, agg := range extractAggregates(ast) {
		if _, err := s.resolveAggregateJoinSpec(ctx, schema, agg, idx); err != nil {
			return err
		}
	}
	return nil
}
