package records

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/nexuscrm/audit"
	eventFeature "github.com/nexuscrm/events"
	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/schemautil"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/events"
	"github.com/nexuscrm/shared/pkg/models"
)

// PreviewBeforeUpdate runs the same before-save trigger dispatch and
// post-before-save validation path as a normal PATCH update, but stops before
// system field stamping and row write. Callers receive the post-before-save
// record image plus the effective diff that a protected finalization path can
// later resume from.
//
// internalStamps carries system-managed fields (e.g. ConvertedAccountId during
// Lead Convert) that must be visible in $Record when the RecordBeforeUpdate
// event fires, even though user-facing field-write guards would normally filter
// them out. Non-empty stamps are merged into both recordToValidate and
// effectiveUpdates before the event fires, and are stored in
// prepared.InternalUpdates so CommitPreparedUpdate can write them without
// re-routing through user permission guards.
func (ps *PersistenceService) PreviewBeforeUpdate(ctx context.Context, objectName string, id string, updates models.SObject, internalStamps models.SObject, currentUser *models.UserSession) (*models.PreparedRecordUpdate, error) {
	schema, err := ps.prepareOperation(ctx, objectName, constants.PermEdit, currentUser)
	if err != nil {
		return nil, err
	}
	updates, permChanges, err := ps.prepareUpdateRequest(ctx, objectName, id, schema, updates, currentUser)
	if err != nil {
		return nil, err
	}
	internalStamps, err = ps.normalizePreparedInternalUpdates(schema, internalStamps)
	if err != nil {
		return nil, err
	}
	var prepared *models.PreparedRecordUpdate
	previewCtx := constants.WithUpdatePreviewPreparation(ctx)
	err = ps.RunInTransaction(previewCtx, func(txCtx context.Context) error {
		preUpdate, err := ps.prepareUpdateBeforeSave(txCtx, objectName, id, schema, updates, permChanges, currentUser)
		if err != nil {
			return err
		}
		if preUpdate == nil {
			return nil
		}
		recordToValidate := cloneSObject(preUpdate.recordToValidate)
		effectiveUpdates := cloneSObject(preUpdate.effectiveUpdates)

		// Merge system-stamped internal fields into both views so that
		// RecordBeforeUpdate listeners (user-authored before-save flows) see them
		// on $Record. These fields bypass user permission guards — they are
		// system-managed and the caller is responsible for supplying correct values.
		for k, v := range internalStamps {
			recordToValidate[k] = v
			effectiveUpdates[k] = v
		}

		// If stamps introduced changes when the user-facing updates had none,
		// we must still fire the event so the before-save flow sees the stamps.
		hasChanges := preUpdate.hasChanges || len(internalStamps) > 0

		if !hasChanges {
			prepared = &models.PreparedRecordUpdate{
				Record:           recordToValidate,
				EffectiveUpdates: effectiveUpdates,
				InternalUpdates:  cloneSObject(internalStamps),
			}
			return nil
		}
		if err := ps.publishRecordEvent(txCtx, events.RecordBeforeUpdate, objectName, recordToValidate, &preUpdate.oldRecord, currentUser); err != nil {
			return err
		}
		normalizedRecord, err := ps.normalizeUpdateRecordPreservingUnchangedValues(schema, recordToValidate, preUpdate.oldRecord)
		if err != nil {
			return err
		}
		recordToValidate = normalizedRecord
		restorePolymorphicDiscriminatorsFromOldRecord(schema, recordToValidate, effectiveUpdates, preUpdate.oldRecord)
		if err := ps.runRecordWriteCanonicalizers(txCtx, recordWriteValidationBeforeUpdate, recordWriteCanonicalizationEvent{
			ObjectName:       objectName,
			RecordID:         id,
			Record:           recordToValidate,
			OldRecord:        preUpdate.oldRecord,
			EffectiveUpdates: effectiveUpdates,
			CurrentUser:      currentUser,
		}); err != nil {
			return err
		}
		ps.recalculateEffectiveUpdates(schema, recordToValidate, preUpdate.oldRecord, effectiveUpdates, true)
		if err := rejectWriteProtectedFieldWrites(txCtx, schema, effectiveUpdates, protectedFieldWriteOnUpdate); err != nil {
			return err
		}
		validationErrors := ps.newClientValidationErrorCollector()
		if err := ps.validateRecordTypeId(txCtx, objectName, effectiveUpdates, currentUser); err != nil {
			if terminal := validationErrors.add(err); terminal != nil {
				return terminal
			}
		}
		if err := ps.validateQueueAssignment(txCtx, objectName, effectiveUpdates, currentUser); err != nil {
			if terminal := validationErrors.add(err); terminal != nil {
				return terminal
			}
		}
		if err := ps.validateNonPolymorphicLookups(txCtx, effectiveUpdates, schema, currentUser); err != nil {
			return err
		}
		normalizeGroupUserRoleReferenceWriteImage(objectName, recordToValidate)
		if strings.EqualFold(objectName, constants.TableGroup) {
			ps.recalculateEffectiveUpdates(schema, recordToValidate, preUpdate.oldRecord, effectiveUpdates, true)
		}
		if err := ps.reconcilePolymorphicDiscriminatorUpdates(txCtx, schema, recordToValidate, effectiveUpdates, preUpdate.oldRecord); err != nil {
			return err
		}
		formulaFetchErrFields := computeFormulaFieldsWithPrior(txCtx, schema, recordToValidate, preUpdate.oldRecord, ps.repo, ps.metadata, objectName)
		injectComputedFormulaFields(schema, recordToValidate, preUpdate.oldRecord, effectiveUpdates, formulaFetchErrFields)
		writeValidationErr := ps.runRecordWriteValidators(txCtx, recordWriteValidationBeforeUpdate, recordWriteValidationEvent{
			ObjectName:  objectName,
			RecordID:    id,
			Record:      recordToValidate,
			OldRecord:   preUpdate.oldRecord,
			CurrentUser: currentUser,
		})
		if terminal := validationErrors.add(writeValidationErr); terminal != nil {
			return terminal
		}
		if err := ps.validateUpdate(txCtx, objectName, id, schema, recordToValidate, effectiveUpdates, preUpdate.oldRecord, currentUser, validationErrors); err != nil {
			return err
		}
		prepared = &models.PreparedRecordUpdate{
			Record:           cloneSObject(recordToValidate),
			EffectiveUpdates: cloneSObject(effectiveUpdates),
			InternalUpdates:  cloneSObject(internalStamps),
		}
		return nil
	})
	return prepared, err
}

// CommitPreparedUpdate resumes the shared update lifecycle from a prepared
// post-before-save image. User-authored final extraUpdates are re-routed
// through the normal update guards; only prepared.InternalUpdates may bypass
// those guards for the narrow system-owned finalization tail.
func (ps *PersistenceService) CommitPreparedUpdate(ctx context.Context, objectName string, id string, prepared *models.PreparedRecordUpdate, extraUpdates models.SObject, currentUser *models.UserSession) (err error) {
	if prepared == nil {
		return appErrors.NewInternalError("prepared update missing", nil)
	}

	// Rejected-save audit (#4261). CommitPreparedUpdate does NOT route through
	// updateWithPostLockDerivedValues — it resumes the update lifecycle from a
	// prepared post-before-save image — so it needs its own hook or the
	// prepared-commit surface stays a blind spot. The user-authored payload
	// here is extraUpdates; prepared.InternalUpdates is system-owned
	// finalization and is deliberately not reported as caller intent.
	ctx = withRejectedSaveClaim(ctx)
	commitRequestedFields := ps.rejectedSaveRequestedFields(extraUpdates)
	defer func() {
		if err == nil {
			return
		}
		ps.emitRejectedSaveAudit(ctx, objectName, id, audit.ActionUpdate,
			commitRequestedFields, err, currentUser)
	}()

	stageStart := time.Now()
	schema, err := ps.prepareOperation(ctx, objectName, constants.PermEdit, currentUser)
	logRecordStageTiming(ctx, "Update", "prepareOperation", objectName, id, stageStart, err, len(extraUpdates))
	if err != nil {
		return err
	}
	if schemaHasExclusiveSiblingFields(schema) && ps.txManager != nil && ps.txManager.IsTransactionActive(ctx) {
		return appErrors.NewValidationError("ExclusiveSiblingFields", "CommitPreparedUpdate does not support ExclusiveSiblingFields inside an existing transaction; call the prepared commit outside the preview transaction or use the direct Update path")
	}
	if err := ps.validateRecordCacheInvalidationConfiguration(ctx, objectName); err != nil {
		return err
	}

	var finalRecord models.SObject
	var oldRecordForInvalidation models.SObject
	boundaryUpdates, err := ps.preparedUpdateBoundaryData(schema, prepared, extraUpdates)
	if err != nil {
		return err
	}
	forcedParentPrelocks := parentLockForcedPrelocksFromContext(ctx)
	for attempt := 0; attempt < 3; attempt++ {
		err = ps.runRollupAwareUpdateTransaction(ctx, objectName, boundaryUpdates, func(txCtx context.Context) error {
			if err := requireInitialHistoryBusinessRowPrelocked(txCtx, objectName, id); err != nil {
				return err
			}
			parentPrelocks, preloadedOldRecord, err := ps.prelockInitialPreparedUpdateParents(txCtx, objectName, id, schema, prepared, extraUpdates, currentUser, forcedParentPrelocks)
			if err != nil {
				return err
			}

			oldRecord, err := ps.loadAndValidateAccess(txCtx, objectName, id, schema, currentUser)
			if err != nil {
				return err
			}
			if err := ps.ensureNonPolymorphicLockedOldParentsPrelocked(txCtx, schema, preloadedOldRecord, oldRecord, parentPrelocks); err != nil {
				return err
			}
			if err := ps.ensureExclusiveSiblingLockedOldParentsPrelocked(schema, oldRecord, parentPrelocks); err != nil {
				return err
			}

			effectiveUpdates := cloneSObject(prepared.EffectiveUpdates)
			if effectiveUpdates == nil {
				effectiveUpdates = models.SObject{}
			}
			// Prepared.Record is the full image observed by the preview. It is
			// useful to chained planning, but it is not write intent. Reusing it
			// here would turn any field changed after preview (including system
			// audit fields) into a stale write-back. Rebase only the preview's
			// effective delta onto the latest locked row, then run the normal
			// canonicalization and validation pipeline below.
			recordToValidate := ps.mergeRecords(oldRecord, effectiveUpdates)

			filteredExtra, err := ps.preparePreparedFinalExtraUpdates(txCtx, objectName, id, schema, extraUpdates, oldRecord, currentUser)
			if err != nil {
				return err
			}
			for k, v := range filteredExtra {
				effectiveUpdates[k] = v
				recordToValidate[k] = v
			}

			internalUpdates, err := ps.normalizePreparedInternalUpdates(schema, prepared.InternalUpdates)
			if err != nil {
				return err
			}
			for k, v := range internalUpdates {
				effectiveUpdates[k] = v
				recordToValidate[k] = v
			}
			if err := ps.runRecordWriteCanonicalizers(txCtx, recordWriteValidationBeforeUpdate, recordWriteCanonicalizationEvent{
				ObjectName:       objectName,
				RecordID:         id,
				Record:           recordToValidate,
				OldRecord:        oldRecord,
				EffectiveUpdates: effectiveUpdates,
				CurrentUser:      currentUser,
			}); err != nil {
				return err
			}
			ps.recalculateEffectiveUpdates(schema, recordToValidate, oldRecord, effectiveUpdates, false)

			// Slice 2 (#1362): stage OLD-record active-formula snapshot — same
			// helper as PersistenceService.Update so both Update entry points
			// emit RecordUpdated with $Record__Old active-formula values.
			// Without this, CommitPreparedUpdate would publish RecordUpdated
			// missing the old active snap (or inheriting an outer caller's
			// snap if ctx already carried one) — Codex #1403 R1 HIGH.
			txCtxForUpdate, err := ps.stageOldRecordActiveProjection(txCtx, schema, id, currentUser)
			if err != nil {
				return err
			}

			finalRecord, err = ps.completeUpdateAfterBeforeSave(txCtxForUpdate, ctx, objectName, id, schema, recordToValidate, effectiveUpdates, oldRecord, currentUser, nil, nil, parentPrelocks)
			if err != nil {
				return err
			}
			oldRecordForInvalidation = oldRecord
			return nil
		})
		retryKeys, shouldRetry := parentLockPrelockRetryKeys(err)
		if !shouldRetry || !ps.canRetryParentLockPrelock(ctx) {
			break
		}
		var changed bool
		forcedParentPrelocks, changed = mergeParentLockKeys(forcedParentPrelocks, retryKeys)
		if !changed {
			break
		}
	}
	if err != nil {
		return err
	}

	updatedID, _ := finalRecord[constants.FieldID].(string)
	if !permissionAuthoringAggregateOwnsRecordsConvergence(ctx) {
		ps.invalidateSecurityCaches(ctx, objectName, oldRecordForInvalidation, finalRecord)
	}
	ps.logger.Info(fmt.Sprintf("Updated record %s in %s (User: %s)", updatedID, objectName, getUserID(currentUser)))
	return nil
}

// stageOldRecordActiveProjection projects the pre-update active-formula
// snapshot for (objectName, id) and stamps it onto ctx so the downstream
// processPostUpdateHooks call can compose the outbox InternalMetadata for
// RecordUpdated. Used by both Update and CommitPreparedUpdate — they share
// completeUpdateAfterBeforeSave → processPostUpdateHooks, so both must
// stage the snapshot identically or after-save flows on Commit-path
// updates would observe a missing or stale $Record__Old.
//
// The sequence is:
//  1. Clear inherited active-projection ctx keys (nested-write defense:
//     outer after-save flow's snapshot must not survive onto this Update's
//     ctx when this record has no active fields).
//  2. Mark attempted=true (sentinel for "writer wired").
//  3. Project the OLD record under SystemContext elevation (write-side
//     bypass; the view still reflects pre-update physical values here
//     because repo.Update hasn't run yet).
//  4. Stamp WithActiveProjections(nil, oldSnap); processPostUpdateHooks
//     fills in the post-update new-snap on the enqueueCtx.
//
// When activeFormulaProjector is unwired (test harness / production
// without the adapter set), returns ctx unchanged — degraded behavior
// matches the no-active-fields case.
func (ps *PersistenceService) stageOldRecordActiveProjection(
	ctx context.Context,
	schema *models.ObjectMetadata,
	id string,
	currentUser *models.UserSession,
) (context.Context, error) {
	if ps.activeFormulaProjector == nil {
		return ctx, nil
	}
	ctx = eventFeature.ClearActiveProjections(ctx)
	ctx = eventFeature.MarkActiveProjectionAttempted(ctx)
	// Write-side SystemContext elevation — see analogous block in
	// persistence_insert.go for full rationale.
	projCtx := constants.WithSecurityContext(
		ctx, constants.SystemContext,
		"active-formula write-side projection: pre-update old record (issue #1362)",
	)
	oldSnap, projErr := ps.activeFormulaProjector.project(projCtx, currentUser, schema, id)
	if projErr != nil {
		return nil, appErrors.NewInternalError("active-formula projection (pre-update, old record) failed", projErr)
	}
	updatedCtx, encErr := eventFeature.WithActiveProjections(ctx, nil, oldSnap)
	if encErr != nil {
		return nil, appErrors.NewInternalError("encode active-projection old-record snapshot failed", encErr)
	}
	return updatedCtx, nil
}

func (ps *PersistenceService) preparePreparedFinalExtraUpdates(txCtx context.Context, objectName, id string, schema *models.ObjectMetadata, extraUpdates, oldRecord models.SObject, currentUser *models.UserSession) (models.SObject, error) {
	if len(extraUpdates) == 0 {
		return nil, nil
	}

	filteredInput, permChanges, err := ps.prepareUpdateRequest(txCtx, objectName, id, schema, extraUpdates, currentUser)
	if err != nil {
		return nil, err
	}
	if len(permChanges) > 0 {
		return nil, appErrors.NewValidationError("Permissions", "prepared final extra updates do not support permission virtual fields")
	}

	filteredExtra, _, err := ps.filterEditableFields(txCtx, objectName, schema, filteredInput, oldRecord, currentUser)
	if err != nil {
		return nil, err
	}
	return filteredExtra, nil
}

func (ps *PersistenceService) normalizePreparedInternalUpdates(schema *models.ObjectMetadata, internalUpdates models.SObject) (models.SObject, error) {
	if len(internalUpdates) == 0 {
		return nil, nil
	}

	normalized := schemautil.NormalizeSObject(schema, internalUpdates)
	scrubPolymorphicDiscriminators(schema, normalized)
	if err := ps.schemaValidator.ValidateSchemaCompliance(normalized, schema, true, false, false); err != nil {
		return nil, err
	}
	return normalized, nil
}
