package records

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

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

type preparedUpdateState struct {
	oldRecord        models.SObject
	recordToValidate models.SObject
	effectiveUpdates models.SObject
	permChanges      map[string]bool
	currentGrants    map[string]bool
	hasChanges       bool
}

type updateAuthorization struct {
	recordOperation                         string
	userDeactivationByDeletePermissionGrant bool
}

func editUpdateAuthorization() updateAuthorization {
	return updateAuthorization{recordOperation: constants.PermEdit}
}

func (ps *PersistenceService) prepareUpdateOperation(ctx context.Context, objectName string, updates models.SObject, currentUser *models.UserSession) (*models.ObjectMetadata, updateAuthorization, error) {
	schema, err := ps.prepareOperation(ctx, objectName, constants.PermEdit, currentUser)
	if err == nil {
		if ps.canAuthorizeUserDeactivationByDeleteGrant(ctx, objectName, schema, updates, currentUser) {
			return schema, userDeactivationByDeleteGrantAuthorization(constants.PermEdit), nil
		}
		return schema, editUpdateAuthorization(), nil
	}

	fallbackSchema, schemaErr := ps.metadata.GetSchemaOrError(ctx, objectName)
	if schemaErr != nil {
		return nil, editUpdateAuthorization(), err
	}
	if !ps.canAuthorizeUserDeactivationByDeleteGrant(ctx, objectName, fallbackSchema, updates, currentUser) {
		return nil, editUpdateAuthorization(), err
	}
	return fallbackSchema, userDeactivationByDeleteGrantAuthorization(constants.PermDelete), nil
}

func userDeactivationByDeleteGrantAuthorization(recordOperation string) updateAuthorization {
	return updateAuthorization{
		recordOperation:                         recordOperation,
		userDeactivationByDeletePermissionGrant: true,
	}
}

func (ps *PersistenceService) canAuthorizeUserDeactivationByDeleteGrant(ctx context.Context, objectName string, schema *models.ObjectMetadata, updates models.SObject, currentUser *models.UserSession) bool {
	if ps == nil || ps.permissions == nil || currentUser == nil {
		return false
	}
	if !isUserDeactivationUpdateRequest(objectName, schema, updates) {
		return false
	}
	return ps.permissions.CheckUserDeactivationPermissionWithUser(ctx, currentUser)
}

func isUserDeactivationUpdateRequest(objectName string, schema *models.ObjectMetadata, updates models.SObject) bool {
	if schema == nil || !strings.EqualFold(schema.APIName, constants.TableUser) || !strings.EqualFold(objectName, constants.TableUser) {
		return false
	}
	normalized := schemautil.NormalizeSObject(schema, updates)
	fieldCount := 0
	for key, value := range normalized {
		if models.IsRecordMetadataKey(key) {
			continue
		}
		fieldCount++
		if !isDeleteAuthorizedUserDeactivationField(key, value) {
			return false
		}
	}
	return fieldCount == 1
}

func (ps *PersistenceService) prepareUpdateRequest(ctx context.Context, objectName, id string, schema *models.ObjectMetadata, updates models.SObject, currentUser *models.UserSession) (models.SObject, map[string]bool, error) {
	// Normalize values to match schema types before validation.
	// Internal callers (e.g. ApprovalService FieldUpdate) pass all values as strings;
	// the bouncer expects native Go types (float64 for numbers, bool for booleans).
	// Virtual boolean fields go through the same schema-driven coercion as
	// physical booleans. Unknown types stay uncoerced; the virtual intercept
	// below fails loud on those, and physical booleans get rejected by
	// ValidateSchemaCompliance. Review #1831 P1: the previous behavior
	// silently coerced garbage to `false`, which turned into a silent
	// permission revoke.
	updates = stripRecordMetadataKeys(schemautil.NormalizeSObject(schema, updates))
	scrubPolymorphicDiscriminators(schema, updates)
	if err := rejectWriteProtectedFieldWrites(ctx, schema, updates, protectedFieldWriteOnUpdate); err != nil {
		return nil, nil, err
	}
	if !constants.GetSecurityContext(ctx).BypassesProcessFields() {
		if err := ps.rejectExplicitAutoNumberWrites(ctx, objectName, updates); err != nil {
			return nil, nil, err
		}
	}

	// Virtual-field intercept: strip schema-declared virtual permission keys
	// from the payload before the rest of the write pipeline runs. Fails
	// loud on non-bool values so garbage that strict Normalize left
	// uncoerced cannot become a silent revoke.
	var permChanges map[string]bool
	if ps.permissions != nil {
		extracted, extractErr := persistence.ExtractPermissionGrants(updates, schema)
		if extractErr != nil {
			return nil, nil, appErrors.NewValidationError("Permissions", extractErr.Error())
		}
		permChanges = extracted
	}

	// Strict Schema Validation (Bouncer). Physical fields only at this
	// point; virtual permission keys were stripped above.
	// allowDerivedRecompute is set when ctx carries the records-package-private
	// derived-recompute marker (only PersistenceService.UpdateDerived sets it),
	// so rollup-engine writes can pass the RollupSummary read-only check.
	if err := ps.schemaValidator.ValidateSchemaCompliance(updates, schema, true, false, isDerivedRecomputeWrite(ctx)); err != nil {
		return nil, nil, err
	}
	if err := ps.runRecordWriteCanonicalizers(ctx, recordWriteValidationBeforeUpdate, recordWriteCanonicalizationEvent{
		ObjectName:  objectName,
		Record:      updates,
		CurrentUser: currentUser,
	}); err != nil {
		return nil, nil, err
	}

	// Immutable field protection: blocks writes to security-critical metadata fields.
	if err := rejectUnbypassableMetadataFieldWrites(objectName, updates); err != nil {
		return nil, nil, err
	}
	// Most immutable metadata fields are still bypassable by trusted internal
	// writers. Executable declaration sidecars stay blocked above for every
	// generic-record writer, including SystemContext flow execution.
	if !constants.GetSecurityContext(ctx).BypassesCRUDandFLS() {
		if err := rejectImmutableMetadataFieldWrites(objectName, updates); err != nil {
			return nil, nil, err
		}
	}

	// Protected-records guard-field: blocks writes to the guard_field declared
	// by `protectedRecords` in system_tables.json. Gated at the same
	// `< SystemContext` threshold as the delete guard; ElevatedContext is
	// NOT allowed to flip the guard (it would defeat reject_delete).
	if constants.GetSecurityContext(ctx) < constants.SystemContext {
		if err := rejectProtectedGuardFieldWrites(objectName, updates); err != nil {
			return nil, nil, err
		}
	}

	if !constants.GetSecurityContext(ctx).BypassesProcessFields() {
		if err := ps.rejectUnauthorizedPasswordWrite(ctx, objectName, updates, currentUser); err != nil {
			return nil, nil, err
		}
	}

	return updates, permChanges, nil
}

func (ps *PersistenceService) prepareUpdateBeforeSave(txCtx context.Context, objectName, id string, schema *models.ObjectMetadata, updates models.SObject, permChanges map[string]bool, currentUser *models.UserSession) (*preparedUpdateState, error) {
	return ps.prepareUpdateBeforeSaveWithAuthorization(txCtx, objectName, id, schema, updates, permChanges, currentUser, editUpdateAuthorization())
}

func (ps *PersistenceService) prepareUpdateBeforeSaveWithAuthorization(txCtx context.Context, objectName, id string, schema *models.ObjectMetadata, updates models.SObject, permChanges map[string]bool, currentUser *models.UserSession, authorization updateAuthorization) (*preparedUpdateState, error) {
	oldRecord, err := ps.loadAndValidateAccessWithAuthorization(txCtx, objectName, id, schema, currentUser, authorization)
	if err != nil {
		return nil, err
	}
	return ps.prepareUpdateBeforeSaveFromLockedRecord(txCtx, objectName, id, schema, updates, permChanges, currentUser, authorization, oldRecord)
}

func (ps *PersistenceService) prepareUpdateBeforeSaveFromLockedRecord(txCtx context.Context, objectName, id string, schema *models.ObjectMetadata, updates models.SObject, permChanges map[string]bool, currentUser *models.UserSession, authorization updateAuthorization, oldRecord models.SObject) (*preparedUpdateState, error) {
	// validateNonPolymorphicLookups + validatePolymorphicLookups are
	// deferred to post-BeforeUpdate per PR #2057 round-6 F2; see the
	// full block below after publishRecordEvent + NormalizeSObject.

	effectiveUpdates, hasChanges, filterErr := ps.filterEditableFieldsWithAuthorization(txCtx, objectName, schema, updates, oldRecord, currentUser, authorization)
	if filterErr != nil {
		return nil, filterErr
	}

	var currentGrants map[string]bool
	var permDiff map[string]bool
	if len(permChanges) > 0 {
		var gErr error
		currentGrants, gErr = ps.permissions.GetPermissionSetGrants(txCtx, id)
		if gErr != nil {
			return nil, appErrors.NewInternalError("failed to load current grants", gErr)
		}
		permDiff = make(map[string]bool, len(permChanges))
		for apiName, enabled := range permChanges {
			if currentGrants[apiName] != enabled {
				permDiff[apiName] = enabled
			}
		}
	}

	recordToValidate := cloneSObject(oldRecord)
	if !hasChanges && len(permDiff) == 0 {
		return &preparedUpdateState{
			oldRecord:        oldRecord,
			recordToValidate: recordToValidate,
			effectiveUpdates: effectiveUpdates,
			permChanges:      permChanges,
			currentGrants:    currentGrants,
			hasChanges:       false,
		}, nil
	}

	recordToValidate = ps.mergeRecords(oldRecord, effectiveUpdates)
	if _, nameProvided := effectiveUpdates[constants.FieldName]; !nameProvided {
		if schemautil.HasCompoundMemberChange(schema, constants.FieldName, effectiveUpdates) {
			applyCompoundNameSynthesis(schema, recordToValidate, true)
		}
		if synthesizedName, ok := recordToValidate[constants.FieldName]; ok && !isBlankValue(synthesizedName) {
			effectiveUpdates[constants.FieldName] = synthesizedName
		}
	}
	if err := ps.runRecordWriteCanonicalizers(txCtx, recordWriteValidationBeforeUpdate, recordWriteCanonicalizationEvent{
		ObjectName:       objectName,
		RecordID:         id,
		Record:           recordToValidate,
		OldRecord:        oldRecord,
		EffectiveUpdates: effectiveUpdates,
		CurrentUser:      currentUser,
	}); err != nil {
		return nil, err
	}
	ps.recalculateEffectiveUpdates(schema, recordToValidate, oldRecord, effectiveUpdates, false)

	return &preparedUpdateState{
		oldRecord:        oldRecord,
		recordToValidate: recordToValidate,
		effectiveUpdates: effectiveUpdates,
		permChanges:      permChanges,
		currentGrants:    currentGrants,
		hasChanges:       true,
	}, nil
}

func (ps *PersistenceService) loadAndValidateAccess(txCtx context.Context, objectName, id string, schema *models.ObjectMetadata, currentUser *models.UserSession) (models.SObject, error) {
	return ps.loadAndValidateAccessForOperation(txCtx, objectName, id, schema, constants.PermEdit, currentUser, func() error {
		return appErrors.NewNotFoundError("Record", "")
	}, func() error {
		return appErrors.NewNotFoundError("Record", "")
	})
}

func (ps *PersistenceService) loadAndValidateAccessWithAuthorization(txCtx context.Context, objectName, id string, schema *models.ObjectMetadata, currentUser *models.UserSession, authorization updateAuthorization) (models.SObject, error) {
	if !authorization.userDeactivationByDeletePermissionGrant {
		return ps.loadAndValidateAccess(txCtx, objectName, id, schema, currentUser)
	}
	return ps.loadAndValidateAccessForUpdateAuthorization(txCtx, objectName, id, schema, currentUser, authorization, func() error {
		return appErrors.NewNotFoundError("Record", "")
	}, func() error {
		return appErrors.NewNotFoundError("Record", "")
	})
}

func (ps *PersistenceService) loadAndValidateAccessForOperation(
	txCtx context.Context,
	objectName, id string,
	schema *models.ObjectMetadata,
	operation string,
	currentUser *models.UserSession,
	missingRecord func() error,
	accessDenied func() error,
) (models.SObject, error) {
	oldRecord, err := ps.repo.GetLock(txCtx, objectName, id)
	if err != nil {
		return nil, appErrors.NewInternalError("failed to lock record", err)
	}
	if oldRecord == nil {
		if missingRecord != nil {
			return nil, missingRecord()
		}
		return nil, appErrors.NewNotFoundError("Record", "")
	}
	if err := ps.checkRecordMutationAccessForOperation(txCtx, objectName, id, schema, oldRecord, operation, currentUser, accessDenied); err != nil {
		return nil, err
	}
	return oldRecord, nil
}

func (ps *PersistenceService) loadAndValidateAccessForUpdateAuthorization(
	txCtx context.Context,
	objectName, id string,
	schema *models.ObjectMetadata,
	currentUser *models.UserSession,
	authorization updateAuthorization,
	missingRecord func() error,
	accessDenied func() error,
) (models.SObject, error) {
	oldRecord, err := ps.repo.GetLock(txCtx, objectName, id)
	if err != nil {
		return nil, appErrors.NewInternalError("failed to lock record", err)
	}
	if oldRecord == nil {
		if missingRecord != nil {
			return nil, missingRecord()
		}
		return nil, appErrors.NewNotFoundError("Record", "")
	}
	if err := ps.checkRecordMutationAccessForUpdateAuthorization(txCtx, objectName, id, schema, oldRecord, authorization, currentUser, accessDenied); err != nil {
		return nil, err
	}
	return oldRecord, nil
}

func (ps *PersistenceService) checkRecordMutationAccessForOperation(
	ctx context.Context,
	objectName, id string,
	schema *models.ObjectMetadata,
	record models.SObject,
	operation string,
	currentUser *models.UserSession,
	accessDenied func() error,
) error {
	if schema == nil ||
		constants.GetSecurityContext(ctx) >= constants.SystemContext ||
		constants.IsScopedInternalAccess(ctx, objectName, operation) {
		return nil
	}
	if accessDenied == nil {
		accessDenied = func() error { return appErrors.NewNotFoundError("Record", "") }
	}
	if !ps.permissions.CheckObjectPermissionOnRecord(ctx, schema, record, operation, currentUser) {
		return accessDenied()
	}
	if !ps.permissions.CheckRecordAccess(ctx, schema, record, operation, currentUser) {
		return accessDenied()
	}
	return nil
}

func (ps *PersistenceService) checkRecordMutationAccessForUpdateAuthorization(
	ctx context.Context,
	objectName, id string,
	schema *models.ObjectMetadata,
	record models.SObject,
	authorization updateAuthorization,
	currentUser *models.UserSession,
	accessDenied func() error,
) error {
	if !authorization.userDeactivationByDeletePermissionGrant {
		return ps.checkRecordMutationAccessForOperation(ctx, objectName, id, schema, record, constants.PermEdit, currentUser, accessDenied)
	}
	if schema == nil ||
		constants.GetSecurityContext(ctx) >= constants.SystemContext ||
		constants.IsScopedInternalAccess(ctx, objectName, constants.PermEdit) {
		return nil
	}
	if accessDenied == nil {
		accessDenied = func() error { return appErrors.NewNotFoundError("Record", "") }
	}
	if !ps.permissions.CheckUserDeactivationPermissionWithUser(ctx, currentUser) {
		return accessDenied()
	}
	if !ps.permissions.CheckRecordAccess(ctx, schema, record, authorization.recordOperation, currentUser) {
		return accessDenied()
	}
	return nil
}

func (ps *PersistenceService) filterEditableFields(txCtx context.Context, objectName string, schema *models.ObjectMetadata, updates, oldRecord models.SObject, currentUser *models.UserSession) (models.SObject, bool, error) {
	return ps.filterEditableFieldsWithAuthorization(txCtx, objectName, schema, updates, oldRecord, currentUser, editUpdateAuthorization())
}

func (ps *PersistenceService) filterEditableFieldsWithAuthorization(txCtx context.Context, objectName string, schema *models.ObjectMetadata, updates, oldRecord models.SObject, currentUser *models.UserSession, authorization updateAuthorization) (models.SObject, bool, error) {
	effectiveUpdates := make(models.SObject)
	hasChanges := false
	normalizedUpdates := schemautil.NormalizeSObject(schema, updates)
	bypassFLS := constants.GetSecurityContext(txCtx).BypassesCRUDandFLS() ||
		constants.IsScopedInternalAccess(txCtx, objectName, constants.PermEdit)

	// Collect ALL FLS-denied fields in a single pass so the error message and
	// `fields` array match SF v63.0 byte-for-byte. SF returns every denied
	// field in one error per record (verified against sandbox for PATCH/POST);
	// bailing on the first miss would understate the violation to the client.
	var deniedFields []string
	for key, newVal := range normalizedUpdates {
		if authorization.userDeactivationByDeletePermissionGrant && models.IsRecordMetadataKey(key) {
			continue
		}
		// Intrinsic capability check: silently drop non-updateable fields.
		// UpdateDerived writes are allowed to update derived fields (rollup
		// summary aggregates) so rollup recomputes can route through the full
		// save lifecycle. Gated on the private derived-recompute marker (set
		// only by PersistenceService.UpdateDerived), NOT on SystemContext at
		// large; so generic SystemContext emitters (flow execution, approval
		// ops, default child rows) cannot accidentally widen this surface.
		// Other readonly types (formula, autonumber, system-maintained) remain
		// dropped.
		if isFieldNonUpdateable(objectName, schema, key) &&
			!isDerivedFieldUnderRecompute(txCtx, schema, key) &&
			!isTrustedWriteProtectedFieldWrite(txCtx, schema, key) {
			continue
		}
		if authorization.userDeactivationByDeletePermissionGrant && !isDeleteAuthorizedUserDeactivationField(key, newVal) {
			deniedFields = append(deniedFields, key)
			continue
		}
		// Permission check: user-FLS Edit denial. Accumulate into deniedFields.
		if !bypassFLS && !authorizationAllowsFieldWithoutEditGrant(authorization, key, newVal) {
			editable, err := ps.permissions.CheckFieldEditabilityOnRecord(txCtx, objectName, key, oldRecord, currentUser)
			if err != nil {
				return nil, false, err
			}
			if !editable {
				deniedFields = append(deniedFields, key)
				continue
			}
		}
		if !ps.areFieldValuesEqual(fieldByKey(schema, key), oldRecord[key], newVal) {
			effectiveUpdates[key] = newVal
			hasChanges = true
		}
	}
	if len(deniedFields) > 0 {
		return nil, false, appErrors.NewFLSDeniedErrorForFields(deniedFields)
	}
	return effectiveUpdates, hasChanges, nil
}

func stripRecordMetadataKeys(record models.SObject) models.SObject {
	for key := range record {
		if models.IsRecordMetadataKey(key) {
			delete(record, key)
		}
	}
	return record
}

func authorizationAllowsFieldWithoutEditGrant(authorization updateAuthorization, fieldName string, value interface{}) bool {
	return authorization.userDeactivationByDeletePermissionGrant && isDeleteAuthorizedUserDeactivationField(fieldName, value)
}

func isDeleteAuthorizedUserDeactivationField(fieldName string, value interface{}) bool {
	if !strings.EqualFold(fieldName, constants.FieldIsActive) {
		return false
	}
	active, ok := value.(bool)
	return ok && !active
}

func (ps *PersistenceService) validateUpdate(txCtx context.Context, objectName, id string, schema *models.ObjectMetadata, recordToValidate, effectiveUpdates, oldRecord models.SObject, currentUser *models.UserSession, validationErrors *clientValidationErrorCollector) error {
	if validationErrors == nil {
		validationErrors = ps.newClientValidationErrorCollector()
	}
	validationRules, err := ps.metadata.GetValidationRulesForMutation(txCtx, objectName)
	if err != nil {
		return appErrors.NewInternalError("load mutation validation rules", err)
	}
	userCtx := ps.buildUserContext(txCtx, currentUser)
	if err := ps.validateStaticDependentPicklists(schema, recordToValidate, effectiveUpdates); err != nil {
		if terminal := validationErrors.add(err); terminal != nil {
			return terminal
		}
	}
	if err := ps.validateRuntimeScopedPicklists(txCtx, schema, recordToValidate, effectiveUpdates); err != nil {
		if terminal := validationErrors.add(err); terminal != nil {
			return terminal
		}
	}
	if err := ps.validateRuntimeScopedLookups(txCtx, schema, recordToValidate, effectiveUpdates); err != nil {
		if terminal := validationErrors.add(err); terminal != nil {
			return terminal
		}
	}
	if err := ps.validator.ValidateRecord(txCtx, recordToValidate, schema, validationRules, &oldRecord, userCtx); err != nil {
		if terminal := validationErrors.add(err); terminal != nil {
			return terminal
		}
	}
	if err := validationErrors.combined(); err != nil {
		return err
	}
	guardObservation := claimExternalIDUpsertAdoptionGuardMutation(
		txCtx,
		objectName,
		id,
		externalIDUpsertAdoptionGuardUniquenessPrecheck,
	)
	return wrapExternalIDUpsertAdoptionGuardMutationError(
		guardObservation,
		externalIDUpsertAdoptionGuardUniquenessPrecheck,
		ps.checkUniqueness(txCtx, objectName, effectiveUpdates, schema, id),
	)
}

// normalizeUpdateRecordPreservingUnchangedValues canonicalizes case-insensitive
// before-save writes, then normalizes the candidate without turning
// normalization-only representation changes into update intent. Candidate
// aliases override the canonical pre-flow key because request and locked-row
// fields were already canonical before automation ran. Conflicting aliases fail
// closed instead of making map iteration choose the stored value nondeterministically.
// If the canonical pre-normalization candidate still matches the locked value,
// retain that locked carrier byte-for-byte; payload and automation changes
// remain normalized and continue through validation and persistence.
func (ps *PersistenceService) normalizeUpdateRecordPreservingUnchangedValues(schema *models.ObjectMetadata, candidate, oldRecord models.SObject) (models.SObject, error) {
	canonicalCandidate := cloneSObject(candidate)
	type candidateAlias struct {
		key   string
		value interface{}
	}
	aliasesByField := make(map[string][]candidateAlias)
	for key, value := range candidate {
		field := schemautil.FindField(schema, key)
		if field == nil || key == field.APIName {
			continue
		}
		aliasesByField[field.APIName] = append(aliasesByField[field.APIName], candidateAlias{key: key, value: value})
	}
	for fieldAPIName, aliases := range aliasesByField {
		field := schemautil.FindField(schema, fieldAPIName)
		sort.Slice(aliases, func(i, j int) bool { return aliases[i].key < aliases[j].key })
		selected := aliases[0].value
		for _, alias := range aliases[1:] {
			if !ps.areFieldValuesEqual(field, selected, alias.value) {
				return nil, appErrors.NewValidationError(field.APIName, "has conflicting case-insensitive values")
			}
		}
		for _, alias := range aliases {
			delete(canonicalCandidate, alias.key)
		}
		canonicalCandidate[field.APIName] = selected
	}

	normalized := schemautil.NormalizeSObject(schema, canonicalCandidate)
	for key, oldValue := range oldRecord {
		candidateValue, exists := canonicalCandidate[key]
		if !exists {
			continue
		}
		field := fieldByKey(schema, key)
		if !ps.areFieldValuesEqual(field, oldValue, candidateValue) {
			continue
		}
		normalizedKey := key
		if field != nil {
			normalizedKey = field.APIName
		}
		normalized[normalizedKey] = oldValue
	}
	return normalized, nil
}

func (ps *PersistenceService) recalculateEffectiveUpdates(schema *models.ObjectMetadata, recordToValidate, oldRecord, effectiveUpdates models.SObject, evictStaleKeys bool) {
	for key, newVal := range recordToValidate {
		if !ps.areFieldValuesEqual(fieldByKey(schema, key), oldRecord[key], newVal) {
			effectiveUpdates[key] = newVal
		} else if evictStaleKeys {
			delete(effectiveUpdates, key)
		}
	}
}

func (ps *PersistenceService) completeUpdateAfterBeforeSaveWithPostWrite(txCtx context.Context, ctx context.Context, objectName, id string, schema *models.ObjectMetadata, recordToValidate, effectiveUpdates, oldRecord models.SObject, currentUser *models.UserSession, permChanges, currentGrants map[string]bool, parentPrelocks map[string]struct{}, onPostWrite func(context.Context) error) (models.SObject, error) {
	restorePolymorphicDiscriminatorsFromOldRecord(schema, recordToValidate, effectiveUpdates, oldRecord)
	if err := ps.runRecordWriteCanonicalizers(txCtx, recordWriteValidationBeforeUpdate, recordWriteCanonicalizationEvent{
		ObjectName:       objectName,
		RecordID:         id,
		Record:           recordToValidate,
		OldRecord:        oldRecord,
		EffectiveUpdates: effectiveUpdates,
		CurrentUser:      currentUser,
	}); err != nil {
		return nil, err
	}
	ps.recalculateEffectiveUpdates(schema, recordToValidate, oldRecord, effectiveUpdates, true)
	stageStart := time.Now()
	if err := rejectUnbypassableMetadataFieldWrites(objectName, effectiveUpdates); err != nil {
		logRecordStageTiming(txCtx, "Update", "complete.rejectUnbypassableMetadataFieldWrites", objectName, id, stageStart, err, len(effectiveUpdates))
		return nil, err
	}
	logRecordStageTiming(txCtx, "Update", "complete.rejectUnbypassableMetadataFieldWrites", objectName, id, stageStart, nil, len(effectiveUpdates))

	stageStart = time.Now()
	if err := rejectWriteProtectedFieldWrites(ctx, schema, effectiveUpdates, protectedFieldWriteOnUpdate); err != nil {
		logRecordStageTiming(txCtx, "Update", "complete.rejectWriteProtectedFieldWrites", objectName, id, stageStart, err, len(effectiveUpdates))
		return nil, err
	}
	logRecordStageTiming(txCtx, "Update", "complete.rejectWriteProtectedFieldWrites", objectName, id, stageStart, nil, len(effectiveUpdates))

	// State-dependent validators run on the FINAL post-flow DIFF
	// (PR #2057 round-6 F2 — Pass B blocker). A BeforeSave flow that
	// repointed RecordTypeId, OwnerId, an Order's QuoteId, or any
	// polymorphic FK gets re-validated here so the row that lands in
	// SQL has been checked against the validators it claims to satisfy.
	validationErrors := ps.newClientValidationErrorCollector()
	stageStart = time.Now()
	if err := ps.validateRecordTypeId(txCtx, objectName, effectiveUpdates, currentUser); err != nil {
		logRecordStageTiming(txCtx, "Update", "complete.validateRecordTypeId", objectName, id, stageStart, err, len(effectiveUpdates))
		if terminal := validationErrors.add(err); terminal != nil {
			return nil, terminal
		}
	} else {
		logRecordStageTiming(txCtx, "Update", "complete.validateRecordTypeId", objectName, id, stageStart, nil, len(effectiveUpdates))
	}
	stageStart = time.Now()
	if err := ps.validateQueueAssignment(txCtx, objectName, effectiveUpdates, currentUser); err != nil {
		logRecordStageTiming(txCtx, "Update", "complete.validateQueueAssignment", objectName, id, stageStart, err, len(effectiveUpdates))
		if terminal := validationErrors.add(err); terminal != nil {
			return nil, terminal
		}
	} else {
		logRecordStageTiming(txCtx, "Update", "complete.validateQueueAssignment", objectName, id, stageStart, nil, len(effectiveUpdates))
	}
	stageStart = time.Now()
	var lookupPrelockErr error
	if recycleBinRestoreFinalizing(ctx) {
		// Restore validates and prelocks required parents on the final
		// reactivation image before entering the shared update finalizer. The
		// historical deleted row may point at a deleted ancestor that must not
		// be resurrected or locked after a before-save repair repoints it.
		lookupPrelockErr = ps.ensureNonPolymorphicLookupParentsPrelocked(txCtx, effectiveUpdates, schema, parentPrelocks)
	} else {
		lookupPrelockErr = ps.ensureUpdateNonPolymorphicLookupParentsPrelocked(txCtx, schema, oldRecord, recordToValidate, parentPrelocks)
	}
	if lookupPrelockErr != nil {
		logRecordStageTiming(txCtx, "Update", "complete.ensureUpdateNonPolymorphicLookupParentsPrelocked", objectName, id, stageStart, lookupPrelockErr, len(effectiveUpdates))
		return nil, lookupPrelockErr
	}
	logRecordStageTiming(txCtx, "Update", "complete.ensureUpdateNonPolymorphicLookupParentsPrelocked", objectName, id, stageStart, nil, len(effectiveUpdates))
	stageStart = time.Now()
	if err := ps.ensureRollupParentsPrelocked(txCtx, objectName, parentPrelocks, oldRecord, recordToValidate); err != nil {
		logRecordStageTiming(txCtx, "Update", "complete.ensureRollupParentsPrelocked", objectName, id, stageStart, err, len(effectiveUpdates))
		return nil, err
	}
	logRecordStageTiming(txCtx, "Update", "complete.ensureRollupParentsPrelocked", objectName, id, stageStart, nil, len(effectiveUpdates))
	stageStart = time.Now()
	if err := ps.validateNonPolymorphicLookups(txCtx, effectiveUpdates, schema, currentUser); err != nil {
		logRecordStageTiming(txCtx, "Update", "complete.validateNonPolymorphicLookups", objectName, id, stageStart, err, len(effectiveUpdates))
		return nil, err
	}
	logRecordStageTiming(txCtx, "Update", "complete.validateNonPolymorphicLookups", objectName, id, stageStart, nil, len(effectiveUpdates))
	normalizeGroupUserRoleReferenceWriteImage(objectName, recordToValidate)
	if strings.EqualFold(objectName, constants.TableGroup) {
		ps.recalculateEffectiveUpdates(schema, recordToValidate, oldRecord, effectiveUpdates, true)
	}
	stageStart = time.Now()
	if err := ps.reconcilePolymorphicDiscriminatorUpdates(txCtx, schema, recordToValidate, effectiveUpdates, oldRecord); err != nil {
		logRecordStageTiming(txCtx, "Update", "complete.reconcilePolymorphicDiscriminatorUpdates", objectName, id, stageStart, err, len(effectiveUpdates))
		return nil, err
	}
	logRecordStageTiming(txCtx, "Update", "complete.reconcilePolymorphicDiscriminatorUpdates", objectName, id, stageStart, nil, len(effectiveUpdates))

	stageStart = time.Now()
	formulaFetchErrFields := computeFormulaFieldsWithPrior(txCtx, schema, recordToValidate, oldRecord, ps.repo, ps.metadata, objectName)
	injectComputedFormulaFields(schema, recordToValidate, oldRecord, effectiveUpdates, formulaFetchErrFields)
	logRecordStageTiming(txCtx, "Update", "complete.computeFormulaFields", objectName, id, stageStart, nil, len(recordToValidate))

	stageStart = time.Now()
	writeValidationErr := ps.runRecordWriteValidators(txCtx, recordWriteValidationBeforeUpdate, recordWriteValidationEvent{
		ObjectName:  objectName,
		RecordID:    id,
		Record:      recordToValidate,
		OldRecord:   oldRecord,
		CurrentUser: currentUser,
	})
	if writeValidationErr != nil {
		logRecordStageTiming(txCtx, "Update", "complete.runRecordWriteValidators", objectName, id, stageStart, writeValidationErr, len(recordToValidate))
	} else {
		logRecordStageTiming(txCtx, "Update", "complete.runRecordWriteValidators", objectName, id, stageStart, nil, len(recordToValidate))
	}
	if terminal := validationErrors.add(writeValidationErr); terminal != nil {
		return nil, terminal
	}
	stageStart = time.Now()
	if err := ps.validateUpdate(txCtx, objectName, id, schema, recordToValidate, effectiveUpdates, oldRecord, currentUser, validationErrors); err != nil {
		logRecordStageTiming(txCtx, "Update", "complete.validateUpdate", objectName, id, stageStart, err, len(effectiveUpdates))
		return nil, err
	}
	logRecordStageTiming(txCtx, "Update", "complete.validateUpdate", objectName, id, stageStart, nil, len(effectiveUpdates))
	// Atomic Permission Set authoring prepares Records mutations inside the
	// caller's already-open pessimistic transaction.  Its outcome is based on
	// the final business image, not on the presence of the original request or
	// on system timestamps that would be generated below.  Capture a true
	// lifecycle no-op here, after all before-save canonicalization and
	// validation, and before any write-side effect is produced.
	if preparation := permissionAuthoringRecordPreparationFromContext(ctx); preparation != nil &&
		len(filterToLogicalFields(schema, effectiveUpdates)) == 0 {
		if err := preparation.captureNoop(objectName, id, recordToValidate, oldRecord); err != nil {
			return nil, err
		}
		return recordToValidate, nil
	}
	var exclusiveSiblingPlans []exclusiveSiblingResetPlan
	if !exclusiveSiblingRestoreNoReset(ctx) {
		var err error
		stageStart = time.Now()
		exclusiveSiblingPlans, err = ps.prepareExclusiveSiblingUpdate(txCtx, schema, id, recordToValidate, effectiveUpdates, oldRecord, parentPrelocks)
		if err != nil {
			logRecordStageTiming(txCtx, "Update", "complete.prepareExclusiveSiblingUpdate", objectName, id, stageStart, err, len(effectiveUpdates))
			return nil, err
		}
		logRecordStageTiming(txCtx, "Update", "complete.prepareExclusiveSiblingUpdate", objectName, id, stageStart, nil, len(effectiveUpdates))
	}

	stageStart = time.Now()
	systemFieldsUpdate := ps.generateSystemFields(ctx, objectName, effectiveUpdates, currentUser, false)
	for k, v := range systemFieldsUpdate {
		effectiveUpdates[k] = v
	}
	logRecordStageTiming(txCtx, "Update", "complete.generateSystemFields", objectName, id, stageStart, nil, len(systemFieldsUpdate))

	stageStart = time.Now()
	if err := ps.rejectCurrentUserPasswordReuse(txCtx, objectName, effectiveUpdates, currentUser); err != nil {
		logRecordStageTiming(txCtx, "Update", "complete.rejectCurrentUserPasswordReuse", objectName, id, stageStart, err, len(effectiveUpdates))
		return nil, err
	}
	logRecordStageTiming(txCtx, "Update", "complete.rejectCurrentUserPasswordReuse", objectName, id, stageStart, nil, len(effectiveUpdates))

	if strings.EqualFold(objectName, constants.TableUser) {
		if pwd, ok := effectiveUpdates[constants.FieldPassword].(string); ok && pwd != "" {
			if strings.HasPrefix(pwd, "$2") {
				if err := passwordhash.ValidateCost(pwd); err != nil {
					return nil, err
				}
			} else {
				hashed, err := passwordhash.Hash(pwd)
				if err != nil {
					return nil, fmt.Errorf("failed to hash password: %w", err)
				}
				effectiveUpdates[constants.FieldPassword] = string(hashed)
			}
		}
	}

	finalRecord := ps.mergeRecords(recordToValidate, effectiveUpdates)
	// Guarded external-ID adoption must be decided from the same final,
	// post-before-save image that will reach SQL, while the canonical Update
	// path still owns the target-row lock. Run before permission state, audit,
	// hooks, and DML so a refused adoption leaves no durable side effects.
	if err := ps.validateExternalIDUpsertAdoptionGuard(txCtx, objectName, id, oldRecord, finalRecord); err != nil {
		return nil, err
	}
	logicalUpdates := filterToLogicalFields(schema, effectiveUpdates)
	if preparation := permissionAuthoringRecordPreparationFromContext(ctx); preparation != nil {
		if err := preparation.captureUpdate(&permissionAuthoringPreparedUpdate{
			objectName:            objectName,
			recordID:              id,
			schema:                schema,
			finalRecord:           cloneSObject(finalRecord),
			effectiveUpdates:      cloneSObject(effectiveUpdates),
			logicalUpdates:        cloneSObject(logicalUpdates),
			oldRecord:             cloneSObject(oldRecord),
			currentUser:           currentUser,
			exclusiveSiblingPlans: exclusiveSiblingPlans,
		}); err != nil {
			return nil, err
		}
		return finalRecord, nil
	}
	// Ordinary updates preserve the established audit/permission ordering.
	// Guarded external-ID adoption is the one exception: its final repository
	// UPDATE can still lose a concurrent unique guard claim after the in-memory
	// uniqueness precheck. Defer every mutation side effect until that exact DML
	// succeeds so a routed ownership fault cannot commit audit/permission rows
	// for an UPDATE that never landed.
	deferGuardedMutationSideEffects := len(logicalUpdates) > 0 &&
		externalIDUpsertAdoptionGuardDirectUpdateActive(txCtx, objectName, id)
	if deferGuardedMutationSideEffects && !ports.TransactionAbortSupported(txCtx) {
		return nil, appErrors.NewInternalError(
			"guarded external-ID adoption requires transaction abort support before repository update",
			nil,
		)
	}
	applyMutationSideEffects := func(sideEffectCtx context.Context) error {
		if err := ps.requestPermissionStateAdvanceForMutation(sideEffectCtx, objectName, oldRecord, finalRecord); err != nil {
			return appErrors.NewInternalError("permission state advance request failed", err)
		}
		if err := ps.emitUpdateAuditsForRecordUpdate(sideEffectCtx, schema, objectName, id, effectiveUpdates, oldRecord, currentUser); err != nil {
			return err
		}
		if len(permChanges) > 0 {
			stageStart = time.Now()
			if applyErr := ps.permissions.ApplyPermissionChanges(sideEffectCtx, id, currentGrants, permChanges, currentUser); applyErr != nil {
				logRecordStageTiming(sideEffectCtx, "Update", "complete.ApplyPermissionChanges", objectName, id, stageStart, applyErr, len(permChanges))
				if appErrors.IsInternal(applyErr) {
					return applyErr
				}
				return appErrors.NewValidationError("Permissions", applyErr.Error())
			}
			logRecordStageTiming(sideEffectCtx, "Update", "complete.ApplyPermissionChanges", objectName, id, stageStart, nil, len(permChanges))
		}
		return nil
	}
	if !deferGuardedMutationSideEffects {
		if err := applyMutationSideEffects(txCtx); err != nil {
			return nil, err
		}
	}
	if len(logicalUpdates) > 0 {
		// Capture the pre-update COUNT-rollup filter-match snapshot while the
		// OLD physical child row is still committed and locked. repo.Update
		// below overwrites it; ProcessRollupsForUpdate consumes this snapshot to
		// apply ±1 COUNT deltas instead of re-COUNTing every sibling. Staged on
		// txCtx so it flows through processPostUpdateHooks to the rollup engine.
		stageStart = time.Now()
		preimageCtx, preimageErr := ps.rollup.StageRollupCountUpdatePreimage(txCtx, objectName, oldRecord)
		logRecordStageTiming(txCtx, "Update", "complete.StageRollupCountUpdatePreimage", objectName, id, stageStart, preimageErr, len(logicalUpdates))
		if preimageErr != nil {
			return nil, wrapRollupHookError("failed to stage rollup count pre-update snapshot", preimageErr)
		}
		txCtx = preimageCtx
		stageStart = time.Now()
		guardObservation := claimExternalIDUpsertAdoptionGuardMutation(
			txCtx,
			objectName,
			id,
			externalIDUpsertAdoptionGuardRepositoryUpdate,
		)
		if err := ps.repo.Update(txCtx, objectName, id, logicalUpdates); err != nil {
			logRecordStageTiming(txCtx, "Update", "complete.repo.Update", objectName, id, stageStart, err, len(logicalUpdates))
			guardMutationErr := wrapExternalIDUpsertAdoptionGuardMutationError(
				guardObservation,
				externalIDUpsertAdoptionGuardRepositoryUpdate,
				err,
			)
			if guardObservation != nil &&
				ports.IsDatabaseDuplicateKey(err) &&
				exactAdoptionGuardUniqueConflict(err, objectName, schema, guardObservation.fields) {
				// This exact direct guard race is a proved business conflict, not
				// infrastructure. Preserve its private mutation provenance without
				// installing an InternalError that would override Flow routing.
				return nil, guardMutationErr
			}
			return nil, wrapRepositoryMutationError(
				"update failed",
				guardMutationErr,
			)
		}
		InvalidateRecordReadCache(txCtx, objectName, id)
		logRecordStageTiming(txCtx, "Update", "complete.repo.Update", objectName, id, stageStart, nil, len(logicalUpdates))
		if deferGuardedMutationSideEffects {
			if err := applyMutationSideEffects(txCtx); err != nil {
				if !ports.AbortTransaction(txCtx, err) {
					return nil, appErrors.NewInternalError(
						"failed to abort guarded external-ID adoption after post-DML side-effect failure",
						err,
					)
				}
				return nil, err
			}
		}
		stageStart = time.Now()
		if err := ps.applyExclusiveSiblingResets(txCtx, schema, exclusiveSiblingPlans); err != nil {
			logRecordStageTiming(txCtx, "Update", "complete.applyExclusiveSiblingResets", objectName, id, stageStart, err, len(logicalUpdates))
			return nil, err
		}
		logRecordStageTiming(txCtx, "Update", "complete.applyExclusiveSiblingResets", objectName, id, stageStart, nil, len(logicalUpdates))
		stageStart = time.Now()
		if err := ps.markGroupMembershipClosureDirtyAfterMutation(txCtx, objectName, groupMembershipClosureMutationUpdate, logicalUpdates); err != nil {
			logRecordStageTiming(txCtx, "Update", "complete.markGroupMembershipClosureDirtyAfterMutation", objectName, id, stageStart, err, len(logicalUpdates))
			return nil, appErrors.NewInternalError("group membership closure invalidation failed", err)
		}
		logRecordStageTiming(txCtx, "Update", "complete.markGroupMembershipClosureDirtyAfterMutation", objectName, id, stageStart, nil, len(logicalUpdates))
	}

	if onPostWrite != nil {
		if err := onPostWrite(txCtx); err != nil {
			return nil, err
		}
	}

	stageStart = time.Now()
	if err := ps.runRecordLifecycleHooks(txCtx, recordLifecycleAfterUpdateInTx, recordLifecycleEvent{
		ObjectName:  objectName,
		RecordID:    id,
		Record:      cloneSObject(finalRecord),
		OldRecord:   cloneSObject(oldRecord),
		CurrentUser: currentUser,
	}); err != nil {
		logRecordStageTiming(txCtx, "Update", "complete.runRecordLifecycleHooks.afterUpdate", objectName, id, stageStart, err, len(finalRecord))
		return nil, err
	}
	logRecordStageTiming(txCtx, "Update", "complete.runRecordLifecycleHooks.afterUpdate", objectName, id, stageStart, nil, len(finalRecord))
	stageStart = time.Now()
	if err := ps.processPostUpdateHooks(txCtx, objectName, finalRecord, oldRecord, currentUser); err != nil {
		logRecordStageTiming(txCtx, "Update", "complete.processPostUpdateHooks", objectName, id, stageStart, err, len(finalRecord))
		return nil, err
	}
	logRecordStageTiming(txCtx, "Update", "complete.processPostUpdateHooks", objectName, id, stageStart, nil, len(finalRecord))
	return finalRecord, nil
}

func (ps *PersistenceService) processPostUpdateHooks(txCtx context.Context, objectName string, finalRecord, oldRecord models.SObject, currentUser *models.UserSession) error {
	// Slice 2 (#1362): read the old-record active-formula snapshot stamped
	// on txCtx by the outer Update path INTO A LOCAL variable BEFORE we
	// dispatch any nested writes. The rollup processor below can recurse
	// into PersistenceService.UpdateDerived, which reuses ctx; without
	// clearing our snapshot before that recursion, the nested update's
	// outbox event would lift OUR outer-record snapshot into its own
	// InternalMetadata and FlowTriggerListener would overlay the wrong
	// record's $Record__Old. Codex Pass A HIGH on #1390 R2.
	var preStampedOldSnap models.SObject
	if ps.activeFormulaProjector != nil {
		s, decodeErr := eventFeature.ActiveProjectionOldRecordFromCtx(txCtx)
		if decodeErr != nil {
			return appErrors.NewInternalError("decode active-projection old-record snapshot (compose) failed", decodeErr)
		}
		preStampedOldSnap = s
	}

	// Process rollups via merged entry point: both old and new parent images
	// are deduplicated and sorted for deterministic lock acquisition order,
	// preventing deadlocks when concurrent transactions reparent in opposite
	// directions.
	//
	// Run on a ctx with active-projection metadata cleared so any nested
	// UpdateDerived call does NOT inherit our outer snapshot. The cleared
	// ctx becomes the base for our own enqueueCtx below; the local
	// preStampedOldSnap is re-stamped explicitly there.
	rollupCtx := eventFeature.ClearActiveProjections(txCtx)
	if err := ps.rollup.ProcessRollupsForUpdate(rollupCtx, objectName, finalRecord, oldRecord); err != nil {
		return wrapRollupHookError("failed to process rollups", err)
	}
	// Enqueue afterUpdate event to outbox.
	if ps.outbox != nil {
		enqueueCtx := rollupCtx
		if ps.activeFormulaProjector != nil {
			// Re-stamp attempted on the cleared enqueueCtx so the outbox
			// envelope still carries the marker for this event.
			enqueueCtx = eventFeature.MarkActiveProjectionAttempted(enqueueCtx)
			// Project the NEW record (view reflects post-update values).
			schema := ps.metadata.GetSchema(rollupCtx, objectName)
			recordID, _ := finalRecord[constants.FieldID].(string)
			// Write-side SystemContext elevation; see analogous block in
			// persistence_insert.go for full rationale.
			projCtx := constants.WithSecurityContext(
				rollupCtx, constants.SystemContext,
				"active-formula write-side projection: post-update new record (issue #1362)",
			)
			newSnap, newErr := ps.activeFormulaProjector.project(projCtx, currentUser, schema, recordID)
			if newErr != nil {
				return appErrors.NewInternalError("active-formula projection (update, new record) failed", newErr)
			}
			// Compose with the local preStampedOldSnap captured above (NOT
			// re-read from ctx, which is now cleared). Stamps both snapshots
			// onto enqueueCtx so OutboxService.EnqueueEvent lifts them into
			// THIS event's InternalMetadata.
			updatedCtx, encErr := eventFeature.WithActiveProjections(enqueueCtx, newSnap, preStampedOldSnap)
			if encErr != nil {
				return appErrors.NewInternalError("encode active-projection snapshots (update) failed", encErr)
			}
			enqueueCtx = updatedCtx
		}
		if err := ps.enqueueRecordEvent(enqueueCtx, events.RecordUpdated, eventFeature.RecordEventPayload{
			ObjectAPIName: objectName,
			Record:        finalRecord,
			OldRecord:     &oldRecord,
			CurrentUser:   currentUser,
		}); err != nil {
			return appErrors.NewInternalError("failed to enqueue record updated event", err)
		}
	}
	return nil
}
