package records

import (
	"context"
	"fmt"
	"strings"

	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/kernel/pkg/query"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/formulastorage"
	"github.com/nexuscrm/shared/pkg/models"
)

// derivedRecomputeCtxKey marks a ctx as originating from
// PersistenceService.UpdateDerived. The intrinsic-readonly carve-out in
// filterEditableFields uses this - NOT plain SystemContext - so that other
// SystemContext call sites (flow execution, approval ops, default child
// rows, etc.) cannot accidentally write to a derived field even if their
// payload happens to include one.
type derivedRecomputeCtxKey struct{}

func withDerivedRecompute(ctx context.Context) context.Context {
	ctx = context.WithValue(ctx, derivedRecomputeCtxKey{}, true)
	return constants.WithFormulaDerivedRecompute(ctx)
}

func isDerivedRecomputeWrite(ctx context.Context) bool {
	v, _ := ctx.Value(derivedRecomputeCtxKey{}).(bool)
	return v
}

// UpdateDerived writes a derived/computed field value (e.g. rollup aggregate)
// to a parent record through the full save lifecycle (validation rules, audit,
// outbox). SystemContext is set on ctx so the write bypasses FLS/CRUD checks;
// additionally a private derived-recompute marker is set so filterEditableFields
// lets the derived field through the intrinsic-readonly gate (see
// isDerivedFieldUnderRecompute below). The marker is private to this package
// so only UpdateDerived can trigger the carve-out - generic SystemContext
// emitters do NOT widen this surface.
//
// Returns the underlying Update error. Callers should treat this as a
// platform-substrate write - it is not a user-driven update path.
func (ps *PersistenceService) UpdateDerived(ctx context.Context, objectName, recordID string, updates models.SObject, currentUser *models.UserSession) error {
	derivedCtx := constants.WithSecurityContext(ctx, constants.SystemContext, "derived_field_recompute")
	derivedCtx = withDerivedRecompute(derivedCtx)
	return ps.Update(derivedCtx, objectName, recordID, updates, currentUser)
}

func (ps *PersistenceService) UpdateDerivedCountRollupValues(ctx context.Context, objectName, recordID string, rollupFieldNames []string, currentUser *models.UserSession, recalculate func(context.Context) (models.SObject, error)) error {
	if recalculate == nil {
		return appErrors.NewInternalError("derived rollup value recompute callback missing", nil)
	}
	placeholder := make(models.SObject, len(rollupFieldNames))
	for _, fieldName := range rollupFieldNames {
		placeholder[fieldName] = int64(0)
	}
	if err := ps.validateDerivedCountRollupValues(ctx, objectName, placeholder); err != nil {
		return err
	}
	derivedCtx := constants.WithSecurityContext(ctx, constants.SystemContext, "derived_rollup_field_reconcile")
	derivedCtx = withDerivedRecompute(derivedCtx)
	return ps.updateWithPostLockDerivedValues(derivedCtx, objectName, recordID, placeholder, currentUser, func(lockedCtx context.Context) (models.SObject, error) {
		values, err := recalculate(lockedCtx)
		if err != nil {
			return nil, err
		}
		if err := ps.validateDerivedCountRollupValues(lockedCtx, objectName, values); err != nil {
			return nil, err
		}
		return values, nil
	}, nil, nil)
}

// ApplyDerivedCountRollupDeltas applies COUNT rollup increments with one
// narrow SQL update. Broader derived writes stay on UpdateDerived so validation,
// audit, outbox, and recursive rollups remain the default.
func (ps *PersistenceService) ApplyDerivedCountRollupDeltas(ctx context.Context, objectName, recordID string, deltas map[string]int64, currentUser *models.UserSession) error {
	if err := ps.validateDerivedCountRollupDeltas(ctx, objectName, deltas); err != nil {
		return err
	}
	canApply, err := ps.CanApplyDerivedCountRollupDeltas(ctx, objectName, derivedCountRollupDeltaFieldNames(deltas)...)
	if err != nil {
		return err
	}
	if !canApply {
		return appErrors.NewInternalError(fmt.Sprintf("derived rollup fast path rejected unsafe parent lifecycle for %s", objectName), nil)
	}
	return ps.applyDerivedCountRollupDeltasDirect(ctx, objectName, recordID, deltas, currentUser)
}

func (ps *PersistenceService) CanApplyDerivedCountRollupDeltas(ctx context.Context, objectName string, rollupFieldNames ...string) (bool, error) {
	if ps == nil || ps.metadata == nil {
		return false, nil
	}
	schema := ps.metadata.GetSchema(ctx, objectName)
	if schema == nil {
		return false, nil
	}
	safe, err := schemaCanApplyDerivedCountRollupDeltas(schema, rollupFieldNames...)
	if err != nil || !safe {
		return safe, err
	}
	validationRules, err := ps.metadata.GetValidationRulesForMutation(ctx, objectName)
	if err != nil {
		return false, appErrors.NewInternalError("load mutation validation rules", err)
	}
	for _, rule := range validationRules {
		if rule != nil && rule.Active {
			return false, nil
		}
	}
	if len(schema.RecordLifecycleCapabilitiesForPhase(string(recordLifecycleAfterUpdateInTx))) > 0 {
		return false, nil
	}
	beforeUpdate := string(recordWriteValidationBeforeUpdate)
	if len(schema.RecordWriteValidationCapabilitiesForPhase(beforeUpdate)) > 0 {
		return false, nil
	}
	if len(schema.RecordWriteCanonicalizationCapabilitiesForPhase(beforeUpdate)) > 0 {
		return false, nil
	}
	if len(schema.RecordWriteValidationRequiredValidationRulesForPhase(beforeUpdate)) > 0 {
		return false, nil
	}
	if len(schema.RecordCacheInvalidationCapabilities()) > 0 {
		return false, nil
	}
	for _, eventType := range []string{"RecordBeforeSave", "RecordAfterSave"} {
		for _, recordTriggerType := range []string{"Update", "CreateAndUpdate"} {
			if ps.hasActiveRecordTriggerSubscription(ctx, objectName, eventType, recordTriggerType) {
				return false, nil
			}
		}
	}
	return true, nil
}

func countRollupFieldsDeclareEventualMaintenance(schema *models.ObjectMetadata, rollupFields map[string]bool) bool {
	if schema == nil || len(rollupFields) == 0 {
		return false
	}
	declared := 0
	for i := range schema.Fields {
		field := &schema.Fields[i]
		fieldName := strings.ToLower(strings.TrimSpace(field.APIName))
		if !rollupFields[fieldName] {
			continue
		}
		declared++
		if field.RollupConfig == nil || !strings.EqualFold(strings.TrimSpace(field.RollupConfig.MaintenanceMode), "eventual") {
			return false
		}
	}
	return declared == len(rollupFields)
}

func (ps *PersistenceService) CountRollupFieldsDeclareEventual(ctx context.Context, objectName string, rollupFieldNames ...string) (bool, error) {
	if ps == nil || ps.metadata == nil {
		return false, nil
	}
	schema := ps.metadata.GetSchema(ctx, objectName)
	if schema == nil {
		return false, nil
	}
	rollupFields := countRollupFieldSet(schema, rollupFieldNames...)
	return countRollupFieldsDeclareEventualMaintenance(schema, rollupFields), nil
}

func derivedCountRollupDeltaFieldNames(deltas map[string]int64) []string {
	if len(deltas) == 0 {
		return nil
	}
	fields := make([]string, 0, len(deltas))
	for fieldName := range deltas {
		fields = append(fields, fieldName)
	}
	return fields
}

func schemaCanApplyDerivedCountRollupDeltas(schema *models.ObjectMetadata, rollupFieldNames ...string) (bool, error) {
	if schema == nil {
		return false, nil
	}
	rollupFields := countRollupFieldSet(schema, rollupFieldNames...)
	if len(rollupFields) == 0 {
		return true, nil
	}
	for i := range schema.Fields {
		field := &schema.Fields[i]
		resolved, err := formulastorage.Resolve(formulastorage.SpecFromField(field))
		if err != nil {
			return false, err
		}
		if !resolved.IsWriteMaterialized() {
			continue
		}
		formulaExpr := ""
		if field.Formula != nil {
			formulaExpr = strings.TrimSpace(*field.Formula)
		}
		if formulaExpr == "" {
			return false, nil
		}
		refs, err := formula.CollectCurrentObjectFieldReferences(formulaExpr)
		if err != nil {
			return false, err
		}
		for _, ref := range refs {
			if rollupFields[strings.ToLower(strings.TrimSpace(ref))] {
				return false, nil
			}
		}
	}
	return true, nil
}

func countRollupFieldSet(schema *models.ObjectMetadata, rollupFieldNames ...string) map[string]bool {
	out := make(map[string]bool)
	if schema == nil {
		return out
	}
	if len(rollupFieldNames) > 0 {
		for _, fieldName := range rollupFieldNames {
			if strings.TrimSpace(fieldName) != "" {
				out[strings.ToLower(strings.TrimSpace(fieldName))] = true
			}
		}
		return out
	}
	for i := range schema.Fields {
		field := &schema.Fields[i]
		if strings.EqualFold(string(field.Type), string(constants.FieldTypeRollupSummary)) {
			out[strings.ToLower(field.APIName)] = true
		}
	}
	return out
}

func (ps *PersistenceService) validateDerivedCountRollupDeltas(ctx context.Context, objectName string, deltas map[string]int64) error {
	schema := ps.metadata.GetSchema(ctx, objectName)
	if schema == nil {
		return appErrors.NewNotFoundError("Object", objectName)
	}
	for fieldName := range deltas {
		field := models.FindField(schema, fieldName)
		if field == nil || field.RollupConfig == nil || !strings.EqualFold(string(field.Type), string(constants.FieldTypeRollupSummary)) || !strings.EqualFold(strings.TrimSpace(field.RollupConfig.CalcType), "COUNT") {
			return appErrors.NewInternalError(fmt.Sprintf("derived rollup fast path rejected non-rollup field %s.%s", objectName, fieldName), nil)
		}
	}
	return nil
}

func (ps *PersistenceService) applyDerivedCountRollupDeltasDirect(ctx context.Context, objectName, recordID string, deltas map[string]int64, currentUser *models.UserSession) error {
	schema := ps.metadata.GetSchema(ctx, objectName)
	if schema == nil {
		return appErrors.NewNotFoundError("Object", objectName)
	}
	updates := make(models.SObject, len(deltas)+3)
	for fieldName, delta := range deltas {
		if delta == 0 {
			continue
		}
		field := models.FindField(schema, fieldName)
		column := storageColumnNameForField(*field)
		updates[fieldName] = query.Raw(fmt.Sprintf("COALESCE(`%s`, 0) + (%d)", column, delta))
	}
	if len(updates) == 0 {
		return nil
	}

	derivedCtx := constants.WithSecurityContext(ctx, constants.SystemContext, "derived_rollup_field_update")
	stampedUpdates := ps.generateSystemFields(derivedCtx, objectName, updates, currentUser, false)
	logicalUpdates := filterToLogicalFields(schema, stampedUpdates)
	if len(logicalUpdates) == 0 {
		return nil
	}
	if err := ps.repo.Update(derivedCtx, objectName, recordID, logicalUpdates); err != nil {
		return appErrors.NewInternalError("derived rollup field update failed", err)
	}
	InvalidateRecordReadCache(derivedCtx, objectName, recordID)
	return nil
}

func (ps *PersistenceService) validateDerivedCountRollupValues(ctx context.Context, objectName string, values models.SObject) error {
	schema := ps.metadata.GetSchema(ctx, objectName)
	if schema == nil {
		return appErrors.NewNotFoundError("Object", objectName)
	}
	for fieldName := range values {
		field := models.FindField(schema, fieldName)
		if field == nil || field.RollupConfig == nil || !strings.EqualFold(string(field.Type), string(constants.FieldTypeRollupSummary)) || !strings.EqualFold(strings.TrimSpace(field.RollupConfig.CalcType), "COUNT") {
			return appErrors.NewInternalError(fmt.Sprintf("derived rollup reconcile rejected non-rollup field %s.%s", objectName, fieldName), nil)
		}
	}
	return nil
}

func (ps *PersistenceService) applyDerivedCountRollupValuesDirect(ctx context.Context, objectName, recordID string, values models.SObject, currentUser *models.UserSession) error {
	if err := ps.validateDerivedCountRollupValues(ctx, objectName, values); err != nil {
		return err
	}
	schema := ps.metadata.GetSchema(ctx, objectName)
	updates := make(models.SObject, len(values)+3)
	for fieldName, value := range values {
		updates[fieldName] = value
	}
	if len(updates) == 0 {
		return nil
	}

	derivedCtx := constants.WithSecurityContext(ctx, constants.SystemContext, "derived_rollup_field_reconcile")
	stampedUpdates := ps.generateSystemFields(derivedCtx, objectName, updates, currentUser, false)
	logicalUpdates := filterToLogicalFields(schema, stampedUpdates)
	if len(logicalUpdates) == 0 {
		return nil
	}
	if err := ps.repo.Update(derivedCtx, objectName, recordID, logicalUpdates); err != nil {
		return appErrors.NewInternalError("derived rollup field reconcile failed", err)
	}
	InvalidateRecordReadCache(derivedCtx, objectName, recordID)
	return nil
}

func storageColumnNameForField(field models.FieldMetadata) string {
	physicalName := strings.TrimSpace(field.PhysicalName)
	if physicalName == "" || (strings.HasPrefix(physicalName, "`") && strings.HasSuffix(physicalName, "`")) {
		return field.APIName
	}
	return physicalName
}
