package metadata

import (
	"context"
	"errors"
	"fmt"
	"log"
	"reflect"
	"strings"
	"time"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	domainSchema "github.com/nexuscrm/kernel/pkg/schema"
	"github.com/nexuscrm/shared/pkg/constants"
	sharedEvents "github.com/nexuscrm/shared/pkg/events"
	"github.com/nexuscrm/shared/pkg/formulastorage"
	"github.com/nexuscrm/shared/pkg/models"
)

// UpdateField updates an existing field.
func (ms *MetadataService) UpdateField(ctx context.Context, objectAPIName, fieldAPIName string, updates *models.FieldMetadata) error {
	return ms.updateField(ctx, objectAPIName, fieldAPIName, updates, true, true, false, false)
}

// UpdateFieldWithPatchPresence preserves PATCH omission semantics for the
// non-pointer bool fields used by the metadata PATCH API. Callers that decode
// partial JSON updates must tell the service which bool keys were present in
// the request body.
func (ms *MetadataService) UpdateFieldWithPatchPresence(
	ctx context.Context,
	objectAPIName, fieldAPIName string,
	updates *models.FieldMetadata,
	requiredProvided, isUniqueProvided, trackHistoryProvided, readProtectionPolicyProvided bool,
) error {
	return ms.updateField(ctx, objectAPIName, fieldAPIName, updates, requiredProvided, isUniqueProvided, trackHistoryProvided, readProtectionPolicyProvided)
}

func (ms *MetadataService) updateField(
	ctx context.Context,
	objectAPIName, fieldAPIName string,
	updates *models.FieldMetadata,
	requiredProvided, isUniqueProvided, trackHistoryProvided, readProtectionPolicyProvided bool,
) error {
	ms.mu.Lock()
	defer ms.mu.Unlock()

	// Get the object
	obj, err := ms.repo.GetSchemaByAPIName(ctx, objectAPIName)
	if err != nil || obj == nil {
		return apperrors.NewNotFoundError("Object", objectAPIName)
	}

	// Find the field
	var existingField *models.FieldMetadata
	for i := range obj.Fields {
		if obj.Fields[i].APIName == fieldAPIName {
			existingField = &obj.Fields[i]
			break
		}
	}
	if existingField == nil {
		return apperrors.NewNotFoundError("Field", fmt.Sprintf("%s.%s", objectAPIName, fieldAPIName))
	}
	if parent := models.PolymorphicDiscriminatorParentForField(obj.Fields, existingField.APIName); parent != "" {
		return apperrors.NewValidationError(fieldAPIName, fmt.Sprintf("cannot modify generated polymorphic discriminator field; update %s instead", parent))
	}
	existingWritePolicy := strings.TrimSpace(existingField.WriteProtectionPolicy)
	requestedWritePolicy := strings.TrimSpace(updates.WriteProtectionPolicy)
	if requestedWritePolicy != "" &&
		requestedWritePolicy != existingWritePolicy &&
		(existingWritePolicy == constants.FieldWriteProtectionAuthorityOnly ||
			requestedWritePolicy == constants.FieldWriteProtectionAuthorityOnly) {
		return apperrors.NewValidationError(
			constants.FieldSysField_WriteProtectionPolicy,
			fmt.Sprintf("field %s.%s cannot transition catalog-owned write protection through generic metadata authoring", objectAPIName, fieldAPIName),
		)
	}
	previousField := *existingField

	if trackHistoryProvided {
		if !isTrackHistoryOnlyPatch(updates, requiredProvided, isUniqueProvided, readProtectionPolicyProvided) {
			return apperrors.NewValidationError(constants.FieldSysField_TrackHistory, "trackHistory must be changed in a dedicated PATCH")
		}
		candidate := *existingField
		candidate.TrackHistory = updates.TrackHistory
		if ms.validationSvc != nil {
			if err := ms.validationSvc.ValidateFieldMetadataForUpdate(&candidate, &previousField); err != nil {
				return err
			}
		}
		return ms.updateHistoryTracking(ctx, obj, existingField, updates.TrackHistory)
	}

	if updates.ReferenceTo != nil &&
		isPhysicalCascadeReferenceField(existingField) &&
		!sameReferenceTargets(existingField.ReferenceTo, updates.ReferenceTo) {
		return apperrors.NewValidationError("referenceTo", "cannot change referenceTo for a field backed by a cascading foreign key")
	}

	// Don't allow editing system fields (except help text and default value which is safe)
	if existingField.IsSystem {
		if (updates.Type != "" && updates.Type != existingField.Type) ||
			(requiredProvided && updates.Required != existingField.Required) ||
			(isUniqueProvided && updates.IsUnique != existingField.IsUnique) {
			return apperrors.NewValidationError(fieldAPIName, "cannot modify structural properties of system field")
		}
	}

	// Apply updates
	if updates.Label != "" {
		existingField.Label = updates.Label
	}
	if requiredProvided {
		existingField.Required = updates.Required
	}
	if isUniqueProvided {
		existingField.IsUnique = updates.IsUnique
	}

	if updates.HelpText != nil {
		existingField.HelpText = updates.HelpText
	}
	if updates.DefaultValue != nil {
		existingField.DefaultValue = updates.DefaultValue
	}
	if updates.Options != nil {
		existingField.Options = updates.Options
	}
	if updates.MinLength != nil {
		existingField.MinLength = updates.MinLength
	}
	if updates.MaxLength != nil {
		existingField.MaxLength = updates.MaxLength
	}
	if updates.ReferenceTo != nil {
		existingField.ReferenceTo = updates.ReferenceTo
	}
	if updates.WritableReferenceTo != nil {
		existingField.WritableReferenceTo = updates.WritableReferenceTo
	}
	if updates.ChildRelationshipName != nil {
		existingField.ChildRelationshipName = updates.ChildRelationshipName
	}
	if updates.WriteProtectionPolicy != "" {
		existingField.WriteProtectionPolicy = updates.WriteProtectionPolicy
	}
	if updates.ReadProtectionPolicy != "" {
		existingField.ReadProtectionPolicy = updates.ReadProtectionPolicy
	} else if readProtectionPolicyProvided {
		existingField.ReadProtectionPolicy = ""
	}
	if updates.SiblingMaxParentField != nil {
		parentField := strings.TrimSpace(*updates.SiblingMaxParentField)
		if parentField == "" {
			existingField.SiblingMaxParentField = nil
		} else {
			existingField.SiblingMaxParentField = &parentField
		}
	}
	if updates.Formula != nil {
		existingField.Formula = updates.Formula
	}
	if updates.FormulaMode != nil {
		existingField.FormulaMode = updates.FormulaMode
	}
	if updates.FormulaStorageMode != nil {
		existingField.FormulaStorageMode = updates.FormulaStorageMode
	}
	if updates.ReturnType != nil {
		existingField.ReturnType = updates.ReturnType
	}
	if updates.Validator != nil {
		existingField.Validator = updates.Validator
	}
	if updates.ValidatorConfig != nil {
		existingField.ValidatorConfig = updates.ValidatorConfig
	}
	if updates.ProjectionConfig != nil {
		return apperrors.NewValidationError(fieldAPIName, "field projection config is internal and can only be declared by standard metadata")
	}

	targetFieldType := existingField.Type
	if updates.Type != "" {
		targetFieldType = updates.Type
	}

	// Handle RollupConfig updates: canonicalize before merge so the persisted
	// form always uses live API names from schema lookups, not the caller's
	// raw casing/whitespace variant.
	if updates.RollupConfig != nil {
		if !domainSchema.IsRollupSummaryType(targetFieldType) {
			return apperrors.NewValidationError("rollupConfig", "rollupConfig requires field type rollupsummary")
		}
		schemaLookup := ms.rollupSchemaLookupFunc(ctx)
		canonicalCfg := domainSchema.CanonicalizeRollupConfig(*updates.RollupConfig, schemaLookup)
		existingField.RollupConfig = &canonicalCfg
	}

	// Validate the fully merged candidate BEFORE any DDL or metadata save so
	// UpdateField cannot widen the shared formula/type contract or legacy
	// migration carve-out independently of ValidateFieldMetadataForUpdate.
	provisionalField := *existingField
	if updates.Type != "" {
		provisionalField.Type = updates.Type
	}
	provisionalObj := objectWithFieldCandidate(obj, &provisionalField)
	if err := validateSiblingMaxFieldsForObject(provisionalObj, nil); err != nil {
		return err
	}
	if ms.validationSvc != nil {
		if err := ms.validationSvc.ValidateFieldMetadataForUpdate(&provisionalField, &previousField); err != nil {
			return err
		}
		if isFormulaAuthoringMutation(&provisionalField, &previousField) {
			if err := ms.validateWriteMaterializedFormulaField(ctx, obj, &provisionalField); err != nil {
				return err
			}
		}
	}

	// Determine the type-change scenario for rollupsummary fields.
	// There are four cases:
	//   A) Promotion: non-rollupsummary → rollupsummary
	//   B) Demotion: rollupsummary → non-rollupsummary
	//   C) Config-only: rollupsummary → rollupsummary (type unchanged, config may differ)
	//   D) Unrelated: neither old nor new type is rollupsummary
	//
	// Cases A and C must bypass MapFieldTypeToSQL (which returns INVALID_TYPE_ERROR
	// for "rollupsummary") and use the canonical resolver instead.
	previousIsRollup := domainSchema.IsRollupSummaryType(previousField.Type)
	incomingIsRollup := updates.Type != "" && domainSchema.IsRollupSummaryType(updates.Type)
	typeChangingToRollup := !previousIsRollup && incomingIsRollup
	typeChangingFromRollup := previousIsRollup && updates.Type != "" && !incomingIsRollup
	rollupConfigEdited := previousIsRollup && updates.RollupConfig != nil

	// Case A: Promotion to rollupsummary.
	if typeChangingToRollup {
		if existingField.RollupConfig == nil {
			return apperrors.NewValidationError("rollupConfig", "rollupsummary fields require a rollupConfig")
		}
		// Validate the rollup graph before any DDL or metadata write.
		if err := ms.validateRollupGraphForField(ctx, obj, existingField); err != nil {
			return err
		}
		// Resolve via the single-fact authoring contract — canonical
		// config + resolved SQL type in one call, bypassing
		// MapFieldTypeToSQL which returns INVALID_TYPE_ERROR for
		// "rollupsummary".
		sourceFieldSQLType, err := ms.resolveRollupSourceFieldSQLType(ctx, existingField.RollupConfig)
		if err != nil {
			return apperrors.NewValidationError("rollupConfig", fmt.Sprintf("cannot resolve source field SQL type for rollupsummary: %v", err))
		}
		schemaLookup := ms.rollupSchemaLookupFunc(ctx)
		resolved, err := domainSchema.ResolveRollup(objectAPIName, *existingField.RollupConfig, schemaLookup, sourceFieldSQLType)
		if err != nil {
			return apperrors.NewValidationError("rollupConfig", fmt.Sprintf("invalid rollupsummary config: %v", err))
		}
		// Re-apply the canonical config from the resolver (it may have
		// further canonicalized names that the earlier merge missed).
		existingField.RollupConfig = &resolved.CanonicalConfig
		applyResolvedRollupResult(existingField, resolved)
		colDef := domainSchema.ColumnDefinition{
			Name:        fieldAPIName,
			Type:        resolved.SQLType,
			LogicalType: string(updates.Type),
			Nullable:    fieldPhysicalNullable(existingField),
		}
		if err := ms.schemaMgr.ModifyColumn(objectAPIName, fieldAPIName, colDef); err != nil {
			return apperrors.NewInternalError("failed to modify field type", err)
		}
		existingField.Type = updates.Type
	}

	// Case B: Demotion from rollupsummary to another type.
	if typeChangingFromRollup {
		newSQLType := ms.schemaMgr.MapFieldTypeToSQL(string(updates.Type))
		colDef := domainSchema.ColumnDefinition{
			Name:        fieldAPIName,
			Type:        newSQLType,
			LogicalType: string(updates.Type),
			Nullable:    fieldPhysicalNullable(existingField),
		}
		if err := ms.schemaMgr.ModifyColumn(objectAPIName, fieldAPIName, colDef); err != nil {
			return apperrors.NewInternalError("failed to modify field type", err)
		}
		existingField.Type = updates.Type
		// Clear stale rollup-derived metadata — the field is no longer a
		// rollup summary and must not retain its previous output contract.
		existingField.RollupConfig = nil
		existingField.ReturnType = nil
		existingField.Precision = nil
		existingField.Scale = nil
	}

	// Case C: Config-only edit on an existing rollupsummary field (type unchanged).
	// Compare the previous and next resolved SQL types through the same
	// authority. Only ALTER if the resolved SQL type actually changed
	// (e.g. CalcType changed from COUNT to MIN).
	if rollupConfigEdited && !typeChangingToRollup && !typeChangingFromRollup {
		// Validate the rollup graph with the updated config.
		if err := ms.validateRollupGraphForField(ctx, obj, existingField); err != nil {
			return err
		}

		schemaLookup := ms.rollupSchemaLookupFunc(ctx)

		// Resolve next config through the same authority.
		nextSourceSQLType, err := ms.resolveRollupSourceFieldSQLType(ctx, existingField.RollupConfig)
		if err != nil {
			return apperrors.NewValidationError("rollupConfig", fmt.Sprintf("cannot resolve rollupsummary source field SQL type: %v", err))
		}
		nextResolved, err := domainSchema.ResolveRollup(objectAPIName, *existingField.RollupConfig, schemaLookup, nextSourceSQLType)
		if err != nil {
			return apperrors.NewValidationError("rollupConfig", fmt.Sprintf("invalid rollupsummary config: %v", err))
		}

		// Persist the canonical form from the resolver.
		existingField.RollupConfig = &nextResolved.CanonicalConfig
		applyResolvedRollupResult(existingField, nextResolved)

		// Best-effort previous resolution: when the old config has already
		// drifted out of contract, still allow a valid PATCH to repair it.
		// Without direct access to the current physical column type, fail
		// safe by issuing an ALTER to the requested SQL type when the
		// previous config no longer resolves.
		needsAlter := true
		if previousField.RollupConfig != nil {
			prevSourceSQLType, prevErr := ms.resolveRollupSourceFieldSQLType(ctx, previousField.RollupConfig)
			if prevErr == nil {
				prevResolved, resolvePrevErr := domainSchema.ResolveRollup(objectAPIName, *previousField.RollupConfig, schemaLookup, prevSourceSQLType)
				if resolvePrevErr == nil {
					needsAlter = prevResolved.SQLType != nextResolved.SQLType
				}
			}
		}

		if needsAlter {
			colDef := domainSchema.ColumnDefinition{
				Name:        fieldAPIName,
				Type:        nextResolved.SQLType,
				LogicalType: string(existingField.Type),
				Nullable:    fieldPhysicalNullable(existingField),
			}
			if err := ms.schemaMgr.ModifyColumn(objectAPIName, fieldAPIName, colDef); err != nil {
				return apperrors.NewInternalError("failed to modify field type", err)
			}
		}
	}

	// Case D: Non-rollupsummary type change (original path).
	if updates.Type != "" && updates.Type != existingField.Type && !typeChangingToRollup && !typeChangingFromRollup {
		log.Printf("🔧 Field type change detected: %s.%s from %s to %s", objectAPIName, fieldAPIName, existingField.Type, updates.Type)

		// Build column definition for ALTER TABLE
		colDef := domainSchema.ColumnDefinition{
			Name:        fieldAPIName,
			Type:        ms.schemaMgr.MapFieldTypeToSQL(string(updates.Type)),
			LogicalType: string(updates.Type),
			Nullable:    fieldPhysicalNullable(existingField),
		}

		if err := ms.schemaMgr.ModifyColumn(objectAPIName, fieldAPIName, colDef); err != nil {
			return apperrors.NewInternalError("failed to modify field type", err)
		}

		existingField.Type = updates.Type
	}

	// Revalidate after merge so field-type-specific invariants (e.g. the
	// multi-picklist no-';' option-value constraint — #1686) apply to both
	// create and update paths. UpdateField was previously unguarded and would
	// silently accept option values that reopen the #1683 self-update loop.
	// The nil guard mirrors the existing convention in field_create.go +
	// field_batch.go (validationSvc is optional in some test wirings).
	//
	// Codex round-6 BLOCK on #964: use the update-aware entry point so the
	// non-formula-Type + non-empty-Formula guard does NOT regress metadata-
	// only edits on pre-existing pack-installed legacy fields (e.g.
	// CampaignMember.Title, OpportunityLineItem.TotalPrice). The exemption
	// only fires when the (Type, FormulaMode, Formula) triple is unchanged
	// from previousField; any mutation of those three falls back to the
	// create-strict reject.
	if err := validateSiblingMaxFieldsForObject(obj, nil); err != nil {
		return err
	}
	if ms.validationSvc != nil {
		if err := ms.validationSvc.ValidateFieldMetadataForUpdate(existingField, &previousField); err != nil {
			return err
		}
	}

	// Generate Field ID (reuse existing)
	fieldID := GenerateFieldID(obj.APIName, fieldAPIName)

	// Delegate to SchemaManager
	if err := ms.schemaMgr.SaveFieldMetadataWithIDs(ctx, existingField, obj.ID, obj.APIName, fieldID, nil); err != nil {
		return apperrors.NewInternalError("failed to update field metadata", err)
	}

	ms.invalidateCacheLocked()
	if err := ms.rebuildFormulaViewForUpdatedField(ctx, obj, fieldID, &previousField, existingField); err != nil {
		return err
	}

	// Publish Event
	ms.publishMetadataEvent(ctx, sharedEvents.FieldUpdated, sharedEvents.SchemaEventPayload{
		EventType:     sharedEvents.FieldUpdated,
		ObjectAPIName: objectAPIName,
		FieldAPIName:  fieldAPIName,
		Timestamp:     time.Now().Format(time.RFC3339),
	})

	return nil
}

func isTrackHistoryOnlyPatch(updates *models.FieldMetadata, requiredProvided, isUniqueProvided, readProtectionPolicyProvided bool) bool {
	if updates == nil || requiredProvided || isUniqueProvided || readProtectionPolicyProvided {
		return false
	}
	candidate := *updates
	candidate.TrackHistory = false
	return reflect.DeepEqual(candidate, models.FieldMetadata{})
}

func (ms *MetadataService) updateHistoryTracking(ctx context.Context, obj *models.ObjectMetadata, field *models.FieldMetadata, desired bool) error {
	if ms.txManager == nil || ms.schemaMgr == nil {
		return apperrors.NewInternalError("history tracking transaction owner is unavailable", nil)
	}
	fieldID := field.ID
	if fieldID == "" {
		fieldID = GenerateFieldID(obj.APIName, field.APIName)
	}
	err := ms.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
		if !ms.txManager.IsTransactionActive(txCtx) {
			return apperrors.NewInternalError("history tracking transition requires an active transaction", nil)
		}
		tx := ms.txManager.ExtractTx(txCtx)
		if tx == nil {
			return apperrors.NewInternalError("history tracking transition requires a transaction executor", nil)
		}
		if err := ms.schemaMgr.AdmitAdminFieldMetadataWrite(txCtx, obj.ID, obj.APIName, field.APIName, tx); err != nil {
			return err
		}
		changed, err := ms.schemaMgr.ApplyHistoryTrackingTransition(txCtx, fieldID, desired, tx, false, true)
		if err != nil {
			return err
		}
		if !changed {
			return nil
		}
		if !ports.RegisterAfterCommitHook(txCtx, func() {
			ms.invalidateCacheLocked()
			ms.publishMetadataEvent(ctx, sharedEvents.FieldUpdated, sharedEvents.SchemaEventPayload{
				EventType:     sharedEvents.FieldUpdated,
				ObjectAPIName: obj.APIName,
				FieldAPIName:  field.APIName,
				Timestamp:     time.Now().Format(time.RFC3339),
			})
		}) {
			return apperrors.NewInternalError("history tracking after-commit hook is unavailable", nil)
		}
		return nil
	})
	if errors.Is(err, ports.ErrHistoryTrackingConflict) || ports.IsHistorySerializationConflict(err) {
		return apperrors.NewConflictErrorWithMessage("Field", "history tracking state changed concurrently; retry with fresh metadata")
	}
	return err
}

func (ms *MetadataService) rebuildFormulaViewForUpdatedField(ctx context.Context, obj *models.ObjectMetadata, fieldID string, previousField, updatedField *models.FieldMetadata) error {
	if ms.formulaViewRebuilder == nil || obj == nil || previousField == nil || updatedField == nil {
		return nil
	}
	if !((previousField.Type == constants.FieldTypeFormula && formulastorage.IsActiveComputedField(previousField)) ||
		(updatedField.Type == constants.FieldTypeFormula && formulastorage.IsActiveComputedField(updatedField))) {
		return nil
	}

	if err := ms.formulaViewRebuilder.RebuildView(ctx, obj.APIName); err != nil {
		rollbackField := *previousField
		if rollbackErr := ms.schemaMgr.SaveFieldMetadataWithIDs(ctx, &rollbackField, obj.ID, obj.APIName, fieldID, nil); rollbackErr != nil {
			return apperrors.NewInternalError("failed to rebuild formula view after field update; rollback also failed", fmt.Errorf("rebuild: %w; rollback: %v", err, rollbackErr))
		}
		*updatedField = rollbackField
		ms.invalidateCacheLocked()

		if repairErr := ms.formulaViewRebuilder.RebuildView(ctx, obj.APIName); repairErr != nil {
			return apperrors.NewInternalError("failed to rebuild formula view after field update; rollback view repair failed", fmt.Errorf("rebuild: %w; repair: %v", err, repairErr))
		}

		return apperrors.NewInternalError("failed to rebuild formula view after field update", err)
	}

	return nil
}

func isPhysicalCascadeReferenceField(field *models.FieldMetadata) bool {
	if field == nil {
		return false
	}
	if field.IsMasterDetail || strings.EqualFold(string(field.Type), string(constants.FieldTypeMasterDetail)) {
		return true
	}
	return field.DeleteRule != nil && constants.NormalizeDeleteRule(*field.DeleteRule) == constants.DeleteRuleCascade
}

func sameReferenceTargets(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	counts := make(map[string]int, len(a))
	for _, target := range a {
		counts[strings.ToLower(strings.TrimSpace(target))]++
	}
	for _, target := range b {
		key := strings.ToLower(strings.TrimSpace(target))
		if counts[key] == 0 {
			return false
		}
		counts[key]--
	}
	return true
}

// validateRollupGraphForField builds the full metadata snapshot (all objects
// from the repo) and delegates to domain/schema.ValidateRollupGraph to check
// for cycles and depth violations. This ensures metadata CRUD and pack/apply
// enforce the same rollup graph validity rules — one SSoT.
func (ms *MetadataService) validateRollupGraphForField(ctx context.Context, obj *models.ObjectMetadata, field *models.FieldMetadata) error {
	if field.RollupConfig == nil {
		return nil
	}
	return ms.validateRollupGraphForObject(ctx, obj)
}
