package records

import (
	"context"
	"fmt"
	"strings"

	"github.com/nexuscrm/kernel/pkg/ports"
	domainSchema "github.com/nexuscrm/kernel/pkg/schema"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/persistence"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// DerivedUpdateHistoryFieldIDs returns the exact stable audit-field closure
// produced by the regular Records update lifecycle for the given derived
// fields. It intentionally reuses generateSystemFields and
// recordAuditFieldChanges rather than teaching Analytics which system stamps
// a save produces.
func (ps *PersistenceService) DerivedUpdateHistoryFieldIDs(ctx context.Context, objectName string, fieldNames ...string) ([]string, error) {
	if ps == nil || ps.metadata == nil {
		return nil, fmt.Errorf("derived update history planning requires metadata")
	}
	schema := ps.metadata.GetSchema(ctx, objectName)
	if schema == nil {
		return nil, fmt.Errorf("derived update history planning cannot resolve object %s", objectName)
	}
	updates := make(models.SObject, len(fieldNames))
	for _, fieldName := range fieldNames {
		field := models.FindField(schema, fieldName)
		if field == nil {
			return nil, fmt.Errorf("derived update history planning cannot resolve field %s.%s", objectName, fieldName)
		}
		updates[field.APIName] = nil
	}
	stamped := ps.generateSystemFields(ctx, objectName, updates, nil, false)
	changes := ps.recordAuditFieldChanges(ctx, schema, objectName, stamped, nil)
	fieldIDs := make([]string, 0, len(changes))
	for _, change := range changes {
		if strings.TrimSpace(change.fieldID) != "" {
			fieldIDs = append(fieldIDs, change.fieldID)
		}
	}
	return ports.OrderedHistorySequenceFields(fieldIDs), nil
}

type recordAuditFieldChange struct {
	fieldName            string
	fieldID              string
	fieldHistoryEligible *bool
	historySequence      *int64
	oldValue             string
	newValue             string
}

type recordAuditFieldMetadata struct {
	fieldID              string
	fieldHistoryEligible bool
}

func (ps *PersistenceService) recordAuditFieldChanges(txCtx context.Context, schema *models.ObjectMetadata, objectName string, effectiveUpdates, oldRecord models.SObject) []recordAuditFieldChange {
	if constants.IsSystemTable(objectName) && objectName != constants.TableUser {
		return nil
	}
	fieldMetadata := map[string]recordAuditFieldMetadata{}
	if schema != nil && strings.EqualFold(schema.APIName, objectName) {
		for i := range schema.Fields {
			field := &schema.Fields[i]
			fieldID := field.ID
			if fieldID == "" {
				fieldID = domainSchema.GenerateFieldID(schema.APIName, field.APIName)
			}
			fieldMetadata[strings.ToLower(field.APIName)] = recordAuditFieldMetadata{
				fieldID:              fieldID,
				fieldHistoryEligible: field.TrackHistory,
			}
		}
	}
	// The caller supplies the exact schema pointer admitted before the mutation's
	// retry loop. Runtime gets it from the immutable request-bound Mutation view;
	// the legacy graph captures the MetadataService COW snapshot once. Do not read
	// metadata again here: a second lookup could observe a later publication and
	// make audit eligibility disagree with the mutation that produced the diff.
	changes := make([]recordAuditFieldChange, 0, len(effectiveUpdates))
	for key, newVal := range effectiveUpdates {
		if permissions.IsFieldSystemReadOnly(txCtx, ps.metadata, objectName, key) {
			continue
		}
		if objectName == constants.TableFlowInterview && key == constants.FieldFlowInterview_ContextData {
			continue
		}
		metadata, resolved := fieldMetadata[strings.ToLower(key)]
		var fieldHistoryEligible *bool
		if resolved {
			eligible := metadata.fieldHistoryEligible
			fieldHistoryEligible = &eligible
		}
		changes = append(changes, recordAuditFieldChange{
			fieldName:            key,
			fieldID:              metadata.fieldID,
			fieldHistoryEligible: fieldHistoryEligible,
			oldValue:             ps.valToString(oldRecord[key]),
			newValue:             ps.valToString(newVal),
		})
	}
	return changes
}

func (ps *PersistenceService) reserveRecordAuditSequences(txCtx context.Context, changes []recordAuditFieldChange) error {
	// A producer that already carries an authoritative eligibility decision for
	// every history-addressable diff needs no second temporal authority. Skipping
	// on the transported fact itself (rather than graph identity) fails closed if
	// a stable field is ever unresolved or a non-authoritative producer reaches
	// this bridge. This intentionally leaves any Flow AtomicHistoryPrelocks
	// unconsumed: that metadata owns only legacy _System_FieldHistorySequence
	// rows, while AtomicParentPrelocks independently owns business-row locking.
	// Falling through for a provenance gap preserves both legacy contracts.
	if ps.fieldHistorySequenceReservationDisabled || historyAddressableAuditChangesCarryEligibility(changes) {
		return nil
	}
	fieldIDs := make([]string, 0, len(changes))
	for i := range changes {
		if changes[i].fieldID != "" {
			fieldIDs = append(fieldIDs, changes[i].fieldID)
		}
	}
	if len(fieldIDs) == 0 {
		return nil
	}
	// Complete the first-attempt plan with the current mutation's own audit
	// fields. Parent discovery may already have contributed transitive rollup
	// fields; adding both sets before ReserveHistorySequences makes the first
	// lock acquisition globally ordered. If an earlier reservation consumed the
	// plan, the existing typed replay path remains authoritative.
	ports.AugmentPlannedHistorySequenceLocks(txCtx, fieldIDs)
	tx := ps.txManager.ExtractTx(txCtx)
	if _, ok := tx.(persistence.Executor); !ok {
		// Lightweight transaction-manager fakes have no SQL executor. Production
		// owners always expose the active *sql.Tx and reserve below.
		return nil
	}
	reserved, err := persistence.ReserveHistorySequences(txCtx, tx, fieldIDs)
	if err != nil {
		return err
	}
	for i := range changes {
		sequence, ok := reserved[changes[i].fieldID]
		if !ok {
			continue
		}
		sequenceCopy := sequence
		changes[i].historySequence = &sequenceCopy
	}
	return nil
}

func historyAddressableAuditChangesCarryEligibility(changes []recordAuditFieldChange) bool {
	resolved := false
	for i := range changes {
		// A diff without a stable FieldId cannot participate in either the
		// legacy sequence predicate or the real-field history projection. Keep it
		// in Generic Audit, but do not let it force resolved siblings back onto
		// the shared sequence path.
		if changes[i].fieldID == "" {
			continue
		}
		resolved = true
		if changes[i].fieldHistoryEligible == nil {
			return false
		}
	}
	return resolved
}

func recordAuditChangedByID(currentUser *models.UserSession) string {
	if currentUser == nil {
		return constants.SystemUserID
	}
	return currentUser.ID
}
