package packs

import (
	"fmt"
	"reflect"
	"strings"

	domainschema "github.com/nexuscrm/kernel/pkg/schema"
	"github.com/nexuscrm/shared/pkg/formulastorage"
)

// FormulaStorageTransitionKey identifies one storage-only formula transition
// in an immutable runtime declaration. The candidate planner uses these keys
// to bind stopped-Runtime execution authority to the exact candidate shape.
type FormulaStorageTransitionKey struct {
	ObjectAPIName string
	FieldAPIName  string
}

// FormulaStorageTransitionComparison compares one active pack declaration with
// its candidate successor. SemanticsEqual is true only when every runtime
// object declaration and formula-function bundle is unchanged after replacing
// the candidate transition fields' storage modes with their active values.
// This deliberately enforces a storage-only release: formula dependencies and
// function implementations cannot change under a backfill sourced from the
// active formula view.
type FormulaStorageTransitionComparison struct {
	Transitions    []FormulaStorageTransitionKey
	SemanticsEqual bool
}

// WriteMaterializedFormulaStorageFields returns only candidate fields that can
// participate in a view-only -> write-materialized transition. Callers use it
// to avoid touching installed-state authority for packs with no such surface.
func WriteMaterializedFormulaStorageFields(candidate Pack) ([]FormulaStorageTransitionKey, error) {
	if candidate == nil {
		return nil, nil
	}
	provider, ok := candidate.(runtimeMetadataProvider)
	if !ok {
		return nil, nil
	}
	specs, err := ParseObjectSpecsPayloads(provider.RuntimeObjectSpecPayloads(), "candidate-runtime-object-spec")
	if err != nil {
		return nil, fmt.Errorf("parse candidate object specs for %s: %w", candidate.Name(), err)
	}
	var fields []FormulaStorageTransitionKey
	for _, spec := range specs {
		for _, field := range spec.Fields {
			resolved, resolveErr := formulastorage.Resolve(formulastorage.Spec{
				FieldType:           field.Type,
				FormulaMode:         field.FormulaMode,
				FormulaStorageMode:  field.FormulaStorageMode,
				Formula:             field.Formula,
				HasFormula:          strings.TrimSpace(field.Formula) != "",
				HasReturnType:       strings.TrimSpace(field.ReturnType) != "",
				HasProjectionConfig: field.ProjectionConfig != nil,
			})
			if resolveErr != nil {
				return nil, fmt.Errorf("resolve candidate formula storage for %s.%s: %w", spec.APIName, field.APIName, resolveErr)
			}
			if resolved.IsWriteMaterialized() {
				fields = append(fields, FormulaStorageTransitionKey{
					ObjectAPIName: strings.TrimSpace(spec.APIName),
					FieldAPIName:  strings.TrimSpace(field.APIName),
				})
			}
		}
	}
	return fields, nil
}

func CompareFormulaStorageTransitionRuntimeDeclarations(
	activeObjectSpecsJSON string,
	activeFormulaBundlesJSON string,
	candidate Pack,
) (FormulaStorageTransitionComparison, error) {
	if candidate == nil {
		return FormulaStorageTransitionComparison{}, fmt.Errorf("formula storage transition candidate pack is unavailable")
	}
	activePayloads, err := unmarshalRuntimeObjectSpecPayloads(activeObjectSpecsJSON)
	if err != nil {
		return FormulaStorageTransitionComparison{}, fmt.Errorf("parse active runtime object declarations for %s: %w", candidate.Name(), err)
	}
	activeSpecs, err := ParseObjectSpecsPayloads(activePayloads, "active-runtime-object-spec")
	if err != nil {
		return FormulaStorageTransitionComparison{}, fmt.Errorf("parse active object specs for %s: %w", candidate.Name(), err)
	}
	candidateProvider, ok := candidate.(runtimeMetadataProvider)
	if !ok {
		return FormulaStorageTransitionComparison{}, fmt.Errorf("candidate pack %s has no runtime object declarations", candidate.Name())
	}
	candidateSpecs, err := ParseObjectSpecsPayloads(candidateProvider.RuntimeObjectSpecPayloads(), "candidate-runtime-object-spec")
	if err != nil {
		return FormulaStorageTransitionComparison{}, fmt.Errorf("parse candidate object specs for %s: %w", candidate.Name(), err)
	}

	comparison := FormulaStorageTransitionComparison{}
	if len(activeSpecs) != len(candidateSpecs) {
		return comparison, nil
	}
	normalizedCandidate := cloneObjectSpecs(candidateSpecs)
	for specIndex := range activeSpecs {
		activeSpec := activeSpecs[specIndex]
		candidateSpec := candidateSpecs[specIndex]
		if !strings.EqualFold(strings.TrimSpace(activeSpec.APIName), strings.TrimSpace(candidateSpec.APIName)) ||
			len(activeSpec.Fields) != len(candidateSpec.Fields) {
			return comparison, nil
		}
		transitionFields := make(map[string]struct{})
		for fieldIndex := range activeSpec.Fields {
			activeField := activeSpec.Fields[fieldIndex]
			candidateField := candidateSpec.Fields[fieldIndex]
			if !strings.EqualFold(strings.TrimSpace(activeField.APIName), strings.TrimSpace(candidateField.APIName)) {
				return comparison, nil
			}
			activeStorage, activeErr := formulastorage.Resolve(formulastorage.Spec{
				FormulaMode:         activeField.FormulaMode,
				FormulaStorageMode:  activeField.FormulaStorageMode,
				Formula:             activeField.Formula,
				HasFormula:          strings.TrimSpace(activeField.Formula) != "",
				HasReturnType:       strings.TrimSpace(activeField.ReturnType) != "",
				HasProjectionConfig: activeField.ProjectionConfig != nil,
			})
			candidateStorage, candidateErr := formulastorage.Resolve(formulastorage.Spec{
				FormulaMode:         candidateField.FormulaMode,
				FormulaStorageMode:  candidateField.FormulaStorageMode,
				Formula:             candidateField.Formula,
				HasFormula:          strings.TrimSpace(candidateField.Formula) != "",
				HasReturnType:       strings.TrimSpace(candidateField.ReturnType) != "",
				HasProjectionConfig: candidateField.ProjectionConfig != nil,
			})
			if activeErr != nil || candidateErr != nil ||
				!activeStorage.RequiresFormulaView() || !candidateStorage.IsWriteMaterialized() {
				continue
			}
			normalizedField := candidateField
			normalizedField.FormulaStorageMode = activeField.FormulaStorageMode
			if !reflect.DeepEqual(activeField, normalizedField) {
				continue
			}
			normalizedCandidate[specIndex].Fields[fieldIndex].FormulaStorageMode = activeField.FormulaStorageMode
			comparison.Transitions = append(comparison.Transitions, FormulaStorageTransitionKey{
				ObjectAPIName: strings.TrimSpace(candidateSpec.APIName),
				FieldAPIName:  strings.TrimSpace(candidateField.APIName),
			})
			transitionFields[strings.ToLower(strings.TrimSpace(candidateField.APIName))] = struct{}{}
		}
		if len(transitionFields) != 0 {
			// An index over the newly physical formula column is part of the
			// storage transition, not a change to the formula value being copied.
			// Keep every unrelated index exact, while allowing only indices that
			// actually touch a transitioning field to differ.
			if !reflect.DeepEqual(
				indicesWithoutFormulaStorageTransitions(activeSpec.Indices, transitionFields),
				indicesWithoutFormulaStorageTransitions(candidateSpec.Indices, transitionFields),
			) {
				continue
			}
			normalizedCandidate[specIndex].Indices = append([]domainschema.IndexDefinition(nil), activeSpec.Indices...)
		}
	}

	candidateDeclaration, err := BuildPersistedRuntimeDeclaration(candidate)
	if err != nil {
		return FormulaStorageTransitionComparison{}, err
	}
	objectSpecsEqual := len(activeSpecs) == 0 && len(normalizedCandidate) == 0
	if !objectSpecsEqual {
		objectSpecsEqual = reflect.DeepEqual(activeSpecs, normalizedCandidate)
	}
	comparison.SemanticsEqual = objectSpecsEqual &&
		persistedRuntimeJSONEqual(activeFormulaBundlesJSON, candidateDeclaration.FormulaFunctionBundlesJSON())
	return comparison, nil
}

func indicesWithoutFormulaStorageTransitions(
	indices []domainschema.IndexDefinition,
	transitionFields map[string]struct{},
) []domainschema.IndexDefinition {
	out := make([]domainschema.IndexDefinition, 0, len(indices))
	for _, index := range indices {
		touchesTransition := false
		for _, column := range index.Columns {
			if _, ok := transitionFields[strings.ToLower(strings.TrimSpace(column))]; ok {
				touchesTransition = true
				break
			}
		}
		if !touchesTransition {
			out = append(out, index)
		}
	}
	return out
}

func cloneObjectSpecs(specs []ObjectSpec) []ObjectSpec {
	out := make([]ObjectSpec, len(specs))
	copy(out, specs)
	for index := range out {
		out[index].Fields = append([]FieldSpec(nil), specs[index].Fields...)
	}
	return out
}
