package records

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

	"github.com/nexuscrm/audit"
	"github.com/nexuscrm/kernel/pkg/perftiming"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/events"
	"github.com/nexuscrm/shared/pkg/models"
)

// recordValues is the app-neutral name for the existing engine record map.
// Keep new persistence mechanisms on this vocabulary instead of spreading the
// historical product-shaped type name through new L0 files.
type recordValues = models.SObject

// Update modifies an existing record with ACID transaction guarantees
func (ps *PersistenceService) Update(
	ctx context.Context,
	objectName string,
	id string,
	updates models.SObject,
	currentUser *models.UserSession,
) (err error) {
	ctx = enterExternalIDUpsertAdoptionGuardUpdate(ctx, objectName, id)
	return ps.updateWithPostLockDerivedValues(ctx, objectName, id, updates, currentUser, nil, nil, nil)
}

type postLockDerivedValueFunc func(context.Context) (models.SObject, error)

// UpdateGuarded re-evaluates guard against the row image loaded under the
// normal Update pipeline's child-row lock. A false guard is a successful
// zero-match outcome: no BeforeUpdate event, DML, lifecycle hook, cache
// invalidation, or post-upsert evidence is produced.
func (ps *PersistenceService) UpdateGuarded(
	ctx context.Context,
	objectName string,
	id string,
	updates models.SObject,
	currentUser *models.UserSession,
	guard func(context.Context, models.SObject) (bool, error),
) (bool, error) {
	if guard == nil {
		return false, fmt.Errorf("guarded update requires a current-row guard")
	}
	ctx = enterExternalIDUpsertAdoptionGuardUpdate(ctx, objectName, id)
	matched := false
	err := ps.updateWithPostLockDerivedValues(ctx, objectName, id, updates, currentUser, nil, guard, &matched)
	return matched, err
}

func (ps *PersistenceService) updateWithPostLockDerivedValues(
	ctx context.Context,
	objectName string,
	id string,
	updates models.SObject,
	currentUser *models.UserSession,
	postLockValues postLockDerivedValueFunc,
	guard func(context.Context, models.SObject) (bool, error),
	guardMatched *bool,
) (err error) {
	// Rejected-save audit (#4261). This function is the single funnel for both
	// public update entry points (Update and UpdateGuarded), and the deferred
	// hook on the named `err` return covers every rejection in the pipeline —
	// pre-transaction guards, FLS filtering, ValidateRecord, uniqueness, and
	// the post-BeforeSave re-validation pass, which re-enters the same
	// validateUpdate. See audit_rejected_save.go for why the hook lives at the
	// entry point rather than at the individual checks.
	ctx = withRejectedSaveClaim(ctx)
	updateRequestedFields := ps.rejectedSaveRequestedFields(updates)
	defer func() {
		if err == nil {
			return
		}
		ps.emitRejectedSaveAudit(ctx, objectName, id, audit.ActionUpdate,
			updateRequestedFields, err, currentUser)
	}()

	start := time.Now()
	defer func() {
		if !perftiming.Enabled(ctx) && err == nil {
			return
		}
		perftiming.Event(ctx, "record_timing", start, err).
			Str("operation", "Update").
			Str("object", objectName).
			Str("record_id", id).
			Bool("has_error", err != nil).
			Msg("Persistence timing")
	}()

	stageStart := time.Now()
	schema, updateAuth, err := ps.prepareUpdateOperation(ctx, objectName, updates, currentUser)
	logRecordStageTiming(ctx, "Update", "prepareOperation", objectName, id, stageStart, err, len(updates))
	if err != nil {
		return err
	}
	if schemaHasExclusiveSiblingFields(schema) {
		if err := ps.rejectExclusiveSiblingPlainOuterTransaction(ctx, "Update"); err != nil {
			return err
		}
	}
	if err := ps.validateRecordCacheInvalidationConfiguration(ctx, objectName); err != nil {
		return err
	}

	if !constants.GetSecurityContext(ctx).BypassesCRUDandFLS() {
		if err := rejectToolingOnlyTableWrites(objectName); err != nil {
			return err
		}
	}

	if strings.EqualFold(objectName, constants.TableEmailTemplate) {
		stageStart = time.Now()
		existing, findErr := ps.repo.FindOne(ctx, objectName, id)
		logRecordStageTiming(ctx, "Update", "preflight.FindOne.EmailTemplate", objectName, id, stageStart, findErr, len(updates))
		if findErr != nil {
			return fmt.Errorf("%s: failed to load row for pack ownership check: %w", constants.TableEmailTemplate, findErr)
		}
		if err := rejectManagedEmailTemplateGenericMutation(objectName, existing, "updated"); err != nil {
			return err
		}
	}

	// _System_RecordShare lock gate (Update): updating an existing share row's
	// AccessLevel mutates the parent record's ACL and must respect the same lock
	// check as Insert and Delete. Load the share row here (before the transaction)
	// so the parent's ObjectApiName and RecordId are available for the check.
	// Fail closed: if the preflight lookup errors we cannot evaluate the lock, so
	// we deny the update rather than silently skipping the check.
	if constants.GetSecurityContext(ctx) < constants.SystemContext &&
		strings.EqualFold(objectName, constants.TableRecordShare) {
		// Reject retargeting: changing ObjectApiName or RecordId would silently
		// redirect the share row onto a different (possibly locked) parent record
		// after the preflight check against the old parent. These fields are
		// immutable after creation so the caller must delete+insert to change target.
		if _, hasObjName := updates["ObjectApiName"]; hasObjName {
			return fmt.Errorf("_System_RecordShare: ObjectApiName is immutable after creation; delete and re-create the share row to change the target")
		}
		if _, hasRecordID := updates["RecordId"]; hasRecordID {
			return fmt.Errorf("_System_RecordShare: RecordId is immutable after creation; delete and re-create the share row to change the target")
		}
		stageStart = time.Now()
		shareRow, findErr := ps.repo.FindOne(ctx, objectName, id)
		logRecordStageTiming(ctx, "Update", "preflight.FindOne.RecordShare", objectName, id, stageStart, findErr, len(updates))
		if findErr != nil {
			return fmt.Errorf("_System_RecordShare: failed to load share row for lock check: %w", findErr)
		}
		if shareRow != nil {
			if lockErr := ps.checkRecordShareParentNotLocked(ctx, shareRow, currentUser); lockErr != nil {
				return lockErr
			}
		}
	}

	stageStart = time.Now()
	updates, permChanges, err := ps.prepareUpdateRequest(ctx, objectName, id, schema, updates, currentUser)
	logRecordStageTiming(ctx, "Update", "prepareUpdateRequest", objectName, id, stageStart, err, len(updates))
	if err != nil {
		return err
	}

	// State-dependent validators (validateRecordTypeId, validateQueueAssignment,
	// validateNonPolymorphicLookups, validatePolymorphicLookups) are deferred to
	// post-BeforeUpdate per PR #2057 round-6 F2: a BeforeSave flow can mutate
	// RecordTypeId, OwnerId, QuoteId, or any polymorphic FK, and pre-flow
	// validation would never see those mutations. SF order-of-execution puts
	// validation AFTER before-save flows, so we match that.

	var finalRecord models.SObject
	var oldRecordForInvalidation models.SObject

	// Execute Transactional Work
	forcedParentPrelocks := parentLockForcedPrelocksFromContext(ctx)
	for attempt := 0; attempt < 3; attempt++ {
		if guardMatched != nil {
			*guardMatched = false
		}
		stageStart = time.Now()
		err = ps.runRollupAwareUpdateTransaction(ctx, objectName, updates, func(txCtx context.Context) error {
			if err := requireInitialHistoryBusinessRowPrelocked(txCtx, objectName, id); err != nil {
				return err
			}
			stageStart := time.Now()
			parentPrelocks, preloadedOldRecord, err := ps.prelockInitialUpdateParents(txCtx, objectName, schema, id, updates, currentUser, forcedParentPrelocks)
			logRecordStageTiming(txCtx, "Update", "prelockInitialUpdateParents", objectName, id, stageStart, err, len(updates))
			if err != nil {
				return err
			}

			var lockedGuardRecord models.SObject
			if guard != nil {
				stageStart = time.Now()
				lockedGuardRecord, err = ps.loadAndValidateAccessWithAuthorization(txCtx, objectName, id, schema, currentUser, updateAuth)
				logRecordStageTiming(txCtx, "Update", "loadLockedCurrentForGuard", objectName, id, stageStart, err, len(lockedGuardRecord))
				if err != nil {
					return err
				}
				stageStart = time.Now()
				matched, guardErr := guard(txCtx, cloneSObject(lockedGuardRecord))
				logRecordStageTiming(txCtx, "Update", "lockedCurrentGuard", objectName, id, stageStart, guardErr, len(lockedGuardRecord))
				if guardErr != nil {
					return guardErr
				}
				if !matched {
					return nil
				}
				if guardMatched != nil {
					*guardMatched = true
				}
			}

			stageStart = time.Now()
			var preUpdate *preparedUpdateState
			if lockedGuardRecord != nil {
				preUpdate, err = ps.prepareUpdateBeforeSaveFromLockedRecord(txCtx, objectName, id, schema, updates, permChanges, currentUser, updateAuth, lockedGuardRecord)
			} else {
				preUpdate, err = ps.prepareUpdateBeforeSaveWithAuthorization(txCtx, objectName, id, schema, updates, permChanges, currentUser, updateAuth)
			}
			logRecordStageTiming(txCtx, "Update", "prepareUpdateBeforeSave", objectName, id, stageStart, err, len(updates))
			if err != nil {
				return err
			}
			if postLockValues != nil {
				stageStart = time.Now()
				lockedUpdates, calcErr := postLockValues(txCtx)
				logRecordStageTiming(txCtx, "Update", "postLockDerivedValues", objectName, id, stageStart, calcErr, len(updates))
				if calcErr != nil {
					return calcErr
				}
				updates = lockedUpdates
				if lockedGuardRecord != nil {
					preUpdate, err = ps.prepareUpdateBeforeSaveFromLockedRecord(txCtx, objectName, id, schema, updates, permChanges, currentUser, updateAuth, lockedGuardRecord)
				} else {
					preUpdate, err = ps.prepareUpdateBeforeSaveWithAuthorization(txCtx, objectName, id, schema, updates, permChanges, currentUser, updateAuth)
				}
				logRecordStageTiming(txCtx, "Update", "prepareUpdateBeforeSave.postLockDerivedValues", objectName, id, stageStart, err, len(updates))
				if err != nil {
					return err
				}
			}
			if preUpdate == nil || !preUpdate.hasChanges {
				if preUpdate != nil {
					stageStart = time.Now()
					if hookErr := ps.runRecordLifecycleHooks(txCtx, recordLifecycleAfterUpdateInTx, recordLifecycleEvent{
						ObjectName:  objectName,
						RecordID:    id,
						Record:      cloneSObject(preUpdate.recordToValidate),
						OldRecord:   cloneSObject(preUpdate.oldRecord),
						CurrentUser: currentUser,
					}); hookErr != nil {
						logRecordStageTiming(txCtx, "Update", "runRecordLifecycleHooks.noopAfterUpdate", objectName, id, stageStart, hookErr, len(preUpdate.recordToValidate))
						return hookErr
					}
					logRecordStageTiming(txCtx, "Update", "runRecordLifecycleHooks.noopAfterUpdate", objectName, id, stageStart, nil, len(preUpdate.recordToValidate))
				}
				return nil // Physical and virtual state unchanged — true no-op.
			}
			// Preserve the early generic-CRUD rejection, then prove it again from
			// the row image loaded under this update's lock. This closes the window
			// where a privileged ownership transition could make the preflight
			// image stale before ordinary DML.
			if err := rejectManagedEmailTemplateGenericMutation(objectName, preUpdate.oldRecord, "updated"); err != nil {
				return err
			}
			stageStart = time.Now()
			if err := ps.ensureNonPolymorphicLockedOldParentsPrelocked(txCtx, schema, preloadedOldRecord, preUpdate.oldRecord, parentPrelocks); err != nil {
				logRecordStageTiming(txCtx, "Update", "ensureNonPolymorphicLockedOldParentsPrelocked", objectName, id, stageStart, err, len(preUpdate.oldRecord))
				return err
			}
			logRecordStageTiming(txCtx, "Update", "ensureNonPolymorphicLockedOldParentsPrelocked", objectName, id, stageStart, nil, len(preUpdate.oldRecord))
			stageStart = time.Now()
			if err := ps.ensureExclusiveSiblingLockedOldParentsPrelocked(schema, preUpdate.oldRecord, parentPrelocks); err != nil {
				logRecordStageTiming(txCtx, "Update", "ensureExclusiveSiblingLockedOldParentsPrelocked", objectName, id, stageStart, err, len(preUpdate.oldRecord))
				return err
			}
			logRecordStageTiming(txCtx, "Update", "ensureExclusiveSiblingLockedOldParentsPrelocked", objectName, id, stageStart, nil, len(preUpdate.oldRecord))
			recordToValidate := cloneSObject(preUpdate.recordToValidate)
			effectiveUpdates := cloneSObject(preUpdate.effectiveUpdates)
			oldRecord := preUpdate.oldRecord

			// Salesforce order of execution (Apex Developer Guide):
			//   before-save record-triggered flows
			//   → custom validation rules
			//   → save (uniqueness check, then SQL UPDATE)
			//
			// PR #2057 IANTHEREAL blocker 1: pre-fix this code ran
			// compute(v1) → ValidateRecord → BeforeUpdate → compute(v2) → SQL,
			// so VRs saw the pre-flow formula snapshot while persistence wrote
			// the post-flow snapshot. Same admin-authored ValidationRule and
			// formula could disagree about what the row contains depending on
			// which engine evaluated it — Axiom 1.1 strict-parity drift on the
			// authoring model + R10 split read path on the formula fact.
			//
			// Fix: run BeforeUpdate first, then a SINGLE post-flow compute+inject,
			// then validateUpdate. VR and uniqueness now see the same record
			// image that SQL writes.

			// Publish beforeUpdate event so flows can mutate the in-flight record.
			stageStart = time.Now()
			if err := ps.publishRecordEvent(txCtx, events.RecordBeforeUpdate, objectName, recordToValidate, &oldRecord, currentUser); err != nil {
				logRecordStageTiming(txCtx, "Update", "publishRecordEvent.RecordBeforeUpdate", objectName, id, stageStart, err, len(recordToValidate))
				return err
			}
			logRecordStageTiming(txCtx, "Update", "publishRecordEvent.RecordBeforeUpdate", objectName, id, stageStart, nil, len(recordToValidate))

			// Re-normalize after BeforeSave flows may have written raw values
			// (e.g. plain strings into JSON/MultiPicklist columns).
			normalizedRecord, normalizeErr := ps.normalizeUpdateRecordPreservingUnchangedValues(schema, recordToValidate, oldRecord)
			if normalizeErr != nil {
				return normalizeErr
			}
			recordToValidate = normalizedRecord
			restorePolymorphicDiscriminatorsFromOldRecord(schema, recordToValidate, effectiveUpdates, oldRecord)

			// Re-calculate effectiveUpdates to capture any changes made by Flows/Triggers.
			ps.recalculateEffectiveUpdates(schema, recordToValidate, oldRecord, effectiveUpdates, true)

			// Slice 2: stage the OLD-record active-formula snapshot BEFORE
			// repo.Update so the view still reflects the pre-update physical
			// values. processPostUpdateHooks composes the outbox InternalMetadata
			// from this staged snap alongside the post-update new snap.
			stageStart = time.Now()
			txCtxForUpdate, err := ps.stageOldRecordActiveProjection(txCtx, schema, id, currentUser)
			logRecordStageTiming(txCtx, "Update", "stageOldRecordActiveProjection", objectName, id, stageStart, err, len(recordToValidate))
			if err != nil {
				return err
			}

			stageStart = time.Now()
			finalRecord, err = ps.completeUpdateAfterBeforeSave(txCtxForUpdate, ctx, objectName, id, schema, recordToValidate, effectiveUpdates, oldRecord, currentUser, preUpdate.permChanges, preUpdate.currentGrants, parentPrelocks)
			logRecordStageTiming(txCtxForUpdate, "Update", "completeUpdateAfterBeforeSave", objectName, id, stageStart, err, len(effectiveUpdates))
			if err != nil {
				return err
			}

			oldRecordForInvalidation = oldRecord
			return nil
		})
		logRecordStageTiming(ctx, "Update", "runRollupAwareUpdateTransaction", objectName, id, stageStart, err, len(updates))
		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
	}
	if guardMatched != nil && !*guardMatched {
		return nil
	}
	if permissionAuthoringRecordPreparationFromContext(ctx) != nil {
		return nil
	}

	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
}
