package records

import (
	"context"
	"fmt"
	"strings"

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

type recordLifecyclePhase = contract.RecordLifecyclePhase

const (
	recordLifecycleAfterCreateInTx recordLifecyclePhase = contract.RecordLifecycleAfterCreateInTx
	recordLifecycleAfterUpdateInTx recordLifecyclePhase = contract.RecordLifecycleAfterUpdateInTx
	recordLifecycleAfterDeleteInTx recordLifecyclePhase = contract.RecordLifecycleAfterDeleteInTx
)

type recordLifecycleEvent struct {
	ObjectName  string
	RecordID    string
	Record      models.SObject
	OldRecord   models.SObject
	CurrentUser *models.UserSession
}

type recordLifecycleHook func(context.Context, recordLifecycleEvent) error

type recordLifecycleBatchEvent struct {
	phase  recordLifecyclePhase
	event  recordLifecycleEvent
	policy *models.RecordLifecycleAccessPolicy
}

type recordLifecycleBatchHook func(context.Context, []recordLifecycleBatchEvent) error

type recordLifecycleBatchBinding struct {
	capability string
	hook       recordLifecycleBatchHook
}

type recordLifecycleBinding struct {
	hook                           recordLifecycleHook
	batch                          *recordLifecycleBatchBinding
	requiresPessimisticTransaction bool
}

type recordLifecycleHooks struct {
	capabilityAfterCreate map[string]recordLifecycleBinding
	capabilityAfterUpdate map[string]recordLifecycleBinding
	capabilityAfterDelete map[string]recordLifecycleBinding
}

func newRecordLifecycleHooks() *recordLifecycleHooks {
	return &recordLifecycleHooks{
		capabilityAfterCreate: make(map[string]recordLifecycleBinding),
		capabilityAfterUpdate: make(map[string]recordLifecycleBinding),
		capabilityAfterDelete: make(map[string]recordLifecycleBinding),
	}
}

func recordLifecycleCapabilityKey(capability string) string {
	return strings.ToLower(strings.TrimSpace(capability))
}

func (h *recordLifecycleHooks) tryRegisterCapability(capability string, phase recordLifecyclePhase, requiresPessimisticTransaction bool, hook recordLifecycleHook) error {
	if h == nil || hook == nil {
		return fmt.Errorf("record lifecycle hook registration requires a non-nil registry and hook")
	}
	key := recordLifecycleCapabilityKey(capability)
	if key == "" {
		return fmt.Errorf("record lifecycle hook registration requires a capability")
	}
	for _, handlers := range []map[string]recordLifecycleBinding{h.capabilityAfterCreate, h.capabilityAfterUpdate, h.capabilityAfterDelete} {
		if existing, ok := handlers[key]; ok && existing.batch != nil {
			return fmt.Errorf("record lifecycle capability %q is already registered in batch mode", capability)
		}
	}
	registerOne := func(handlers map[string]recordLifecycleBinding) error {
		if _, exists := handlers[key]; exists {
			return fmt.Errorf("duplicate record lifecycle hook for %s capability %q", phase, capability)
		}
		handlers[key] = recordLifecycleBinding{
			hook:                           hook,
			requiresPessimisticTransaction: requiresPessimisticTransaction,
		}
		return nil
	}
	switch phase {
	case recordLifecycleAfterCreateInTx:
		return registerOne(h.capabilityAfterCreate)
	case recordLifecycleAfterUpdateInTx:
		return registerOne(h.capabilityAfterUpdate)
	case recordLifecycleAfterDeleteInTx:
		return registerOne(h.capabilityAfterDelete)
	default:
		return fmt.Errorf("unsupported record lifecycle phase %q for capability %q", phase, capability)
	}
}

func (h *recordLifecycleHooks) tryRegisterBatchCapability(capability string, phases []recordLifecyclePhase, requiresPessimisticTransaction bool, hook recordLifecycleBatchHook) error {
	if h == nil || hook == nil {
		return fmt.Errorf("record lifecycle batch hook registration requires a non-nil registry and hook")
	}
	key := recordLifecycleCapabilityKey(capability)
	if key == "" {
		return fmt.Errorf("record lifecycle batch hook registration requires a capability")
	}
	if len(phases) == 0 {
		return fmt.Errorf("record lifecycle batch hook registration requires at least one phase")
	}
	for _, handlers := range []map[string]recordLifecycleBinding{h.capabilityAfterCreate, h.capabilityAfterUpdate, h.capabilityAfterDelete} {
		if _, exists := handlers[key]; exists {
			return fmt.Errorf("record lifecycle capability %q is already registered", capability)
		}
	}

	type phaseHandlers struct {
		handlers map[string]recordLifecycleBinding
	}
	selected := make([]phaseHandlers, 0, len(phases))
	seen := make(map[recordLifecyclePhase]struct{}, len(phases))
	for _, phase := range phases {
		if _, duplicate := seen[phase]; duplicate {
			return fmt.Errorf("record lifecycle batch hook registration repeats phase %q for capability %q", phase, capability)
		}
		seen[phase] = struct{}{}
		var handlers map[string]recordLifecycleBinding
		switch phase {
		case recordLifecycleAfterCreateInTx:
			handlers = h.capabilityAfterCreate
		case recordLifecycleAfterUpdateInTx:
			handlers = h.capabilityAfterUpdate
		case recordLifecycleAfterDeleteInTx:
			handlers = h.capabilityAfterDelete
		default:
			return fmt.Errorf("unsupported record lifecycle phase %q for capability %q", phase, capability)
		}
		if _, exists := handlers[key]; exists {
			return fmt.Errorf("duplicate record lifecycle hook for %s capability %q", phase, capability)
		}
		selected = append(selected, phaseHandlers{handlers: handlers})
	}

	batch := &recordLifecycleBatchBinding{capability: key, hook: hook}
	for _, target := range selected {
		target.handlers[key] = recordLifecycleBinding{
			batch:                          batch,
			requiresPessimisticTransaction: requiresPessimisticTransaction,
		}
	}
	return nil
}

func (h *recordLifecycleHooks) runCapabilities(ctx context.Context, phase recordLifecyclePhase, event recordLifecycleEvent, capabilities []string, policy *models.RecordLifecycleAccessPolicy) error {
	if h == nil || len(capabilities) == 0 {
		return nil
	}
	var handlers map[string]recordLifecycleBinding
	switch phase {
	case recordLifecycleAfterCreateInTx:
		handlers = h.capabilityAfterCreate
	case recordLifecycleAfterUpdateInTx:
		handlers = h.capabilityAfterUpdate
	case recordLifecycleAfterDeleteInTx:
		handlers = h.capabilityAfterDelete
	default:
		return fmt.Errorf("unsupported record lifecycle phase %q for %s", phase, event.ObjectName)
	}
	seen := make(map[string]struct{}, len(capabilities))
	for _, capability := range capabilities {
		key := recordLifecycleCapabilityKey(capability)
		if key == "" {
			continue
		}
		if _, exists := seen[key]; exists {
			continue
		}
		seen[key] = struct{}{}
		binding := handlers[key]
		if binding.hook == nil && binding.batch == nil {
			return fmt.Errorf("missing record lifecycle hook for %s capability %q on %s", phase, capability, event.ObjectName)
		}
		if binding.batch != nil {
			if err := stageRecordLifecycleBatchEvent(ctx, binding.batch, recordLifecycleBatchEvent{phase: phase, event: event, policy: policy}); err != nil {
				return err
			}
			continue
		}
		if err := binding.hook(ctx, event); err != nil {
			return err
		}
	}
	return nil
}

func (h *recordLifecycleHooks) requiresPessimisticTransaction(phase recordLifecyclePhase, capabilities []string) bool {
	if h == nil || len(capabilities) == 0 {
		return false
	}
	var handlers map[string]recordLifecycleBinding
	switch phase {
	case recordLifecycleAfterCreateInTx:
		handlers = h.capabilityAfterCreate
	case recordLifecycleAfterUpdateInTx:
		handlers = h.capabilityAfterUpdate
	case recordLifecycleAfterDeleteInTx:
		handlers = h.capabilityAfterDelete
	default:
		return false
	}
	for _, capability := range capabilities {
		if handlers[recordLifecycleCapabilityKey(capability)].requiresPessimisticTransaction {
			return true
		}
	}
	return false
}

// RegisterRecordLifecycleHook registers a transaction-scoped hook supplied by
// the composition root. This is the seam L1 app packs use to bind app-owned
// lifecycle behavior without importing backend/internal records types. The
// binding is capability-driven: object metadata declares which lifecycle
// capabilities an object needs, and registration supplies one handler per
// capability/phase.
func (ps *PersistenceService) RegisterRecordLifecycleHook(reg contract.RecordLifecycleRegistration) error {
	if ps == nil {
		return fmt.Errorf("record lifecycle hook registration requires a persistence service")
	}
	if reg.Hook == nil {
		return fmt.Errorf("record lifecycle hook registration requires a non-nil hook")
	}
	if ps.lifecycleHooks == nil {
		ps.lifecycleHooks = newRecordLifecycleHooks()
	}
	return ps.lifecycleHooks.tryRegisterCapability(reg.Capability, reg.Phase, reg.RequiresPessimisticTransaction, func(ctx context.Context, event recordLifecycleEvent) error {
		return reg.Hook(ctx, contract.RecordLifecycleEvent{
			ObjectName:  event.ObjectName,
			RecordID:    event.RecordID,
			Record:      contractRecordFromSObject(event.Record),
			OldRecord:   contractRecordFromSObject(event.OldRecord),
			CurrentUser: event.CurrentUser,
		})
	})
}

// RegisterRecordLifecycleBatchHook registers one capability handler that sees
// the complete ordered event set for a records-owned transaction attempt. The
// batch is flushed before commit, so its writes remain atomic with the source
// record mutations and a hook error rolls the attempt back.
func (ps *PersistenceService) RegisterRecordLifecycleBatchHook(reg contract.RecordLifecycleBatchRegistration) error {
	if ps == nil {
		return fmt.Errorf("record lifecycle batch hook registration requires a persistence service")
	}
	if reg.Hook == nil {
		return fmt.Errorf("record lifecycle batch hook registration requires a non-nil hook")
	}
	if ps.lifecycleHooks == nil {
		ps.lifecycleHooks = newRecordLifecycleHooks()
	}
	phases := append([]recordLifecyclePhase(nil), reg.Phases...)
	return ps.lifecycleHooks.tryRegisterBatchCapability(reg.Capability, phases, reg.RequiresPessimisticTransaction, func(ctx context.Context, events []recordLifecycleBatchEvent) error {
		batch := make([]contract.RecordLifecycleBatchEvent, len(events))
		for i, event := range events {
			batch[i] = contract.RecordLifecycleBatchEvent{
				Phase:  event.phase,
				Policy: event.policy,
				Event: contract.RecordLifecycleEvent{
					ObjectName:  event.event.ObjectName,
					RecordID:    event.event.RecordID,
					Record:      contractRecordFromSObject(event.event.Record),
					OldRecord:   contractRecordFromSObject(event.event.OldRecord),
					CurrentUser: event.event.CurrentUser,
				},
			}
		}
		return reg.Hook(ctx, batch)
	})
}

func (ps *PersistenceService) recordLifecyclePhaseRequiresPessimisticTransaction(ctx context.Context, schema *models.ObjectMetadata, phase recordLifecyclePhase) bool {
	if ps == nil || schema == nil || contract.RecordLifecycleHooksSuppressed(ctx) {
		return false
	}
	capabilities := schema.RecordLifecycleCapabilitiesForPhase(string(phase))
	return ps.lifecycleHooks.requiresPessimisticTransaction(phase, capabilities)
}

func (ps *PersistenceService) PublishRecordLifecycleEvent(ctx context.Context, eventType events.EventType, objectName string, record contract.Record, oldRecord *contract.Record, user *models.UserSession) error {
	if ps == nil || ps.eventBus == nil {
		return nil
	}
	var oldSObject *models.SObject
	if oldRecord != nil {
		old := contractRecordToSObject(*oldRecord)
		oldSObject = &old
	}
	return ps.publishRecordEvent(ctx, eventType, objectName, contractRecordToSObject(record), oldSObject, user)
}

func (ps *PersistenceService) RunRecordLifecycleHooks(ctx context.Context, phase contract.RecordLifecyclePhase, event contract.RecordLifecycleEvent) error {
	if ps == nil {
		return nil
	}
	return ps.runRecordLifecycleHooks(ctx, phase, recordLifecycleEvent{
		ObjectName:  event.ObjectName,
		RecordID:    event.RecordID,
		Record:      contractRecordToSObject(event.Record),
		OldRecord:   contractRecordToSObject(event.OldRecord),
		CurrentUser: event.CurrentUser,
	})
}

func (ps *PersistenceService) StageRecordAfterSaveOldProjection(ctx context.Context, objectName string, recordID string, user *models.UserSession) (context.Context, error) {
	if ps == nil || ps.activeFormulaProjector == nil {
		return ctx, nil
	}
	var schema *models.ObjectMetadata
	if ps.metadata != nil {
		schema = ps.metadata.GetSchema(ctx, objectName)
	}
	return ps.stageOldRecordActiveProjection(ctx, schema, recordID, user)
}

func (ps *PersistenceService) PublishRecordAfterSaveEvent(ctx context.Context, eventType events.EventType, objectName string, record contract.Record, oldRecord *contract.Record, user *models.UserSession) error {
	if ps == nil || ps.outbox == nil {
		return nil
	}
	recordSObject := contractRecordToSObject(record)
	var oldSObject *models.SObject
	if oldRecord != nil {
		old := contractRecordToSObject(*oldRecord)
		oldSObject = &old
	}
	enqueueCtx, err := ps.composeAfterSaveActiveProjectionCtx(ctx, eventType, objectName, recordSObject, oldSObject, user)
	if err != nil {
		return err
	}
	if err := ps.enqueueRecordEvent(enqueueCtx, eventType, eventFeature.RecordEventPayload{
		ObjectAPIName: objectName,
		Record:        recordSObject,
		OldRecord:     oldSObject,
		CurrentUser:   user,
	}); err != nil {
		return appErrors.NewInternalError("failed to enqueue record after-save event", err)
	}
	return nil
}

func (ps *PersistenceService) composeAfterSaveActiveProjectionCtx(ctx context.Context, eventType events.EventType, objectName string, record models.SObject, oldRecord *models.SObject, user *models.UserSession) (context.Context, error) {
	if ps == nil || ps.activeFormulaProjector == nil {
		return ctx, nil
	}
	if eventType != events.RecordCreated && eventType != events.RecordUpdated {
		return ctx, nil
	}
	recordID, _ := record[constants.FieldID].(string)
	var schema *models.ObjectMetadata
	if ps.metadata != nil {
		schema = ps.metadata.GetSchema(ctx, objectName)
	}
	var oldSnap models.SObject
	if eventType == events.RecordUpdated {
		var decodeErr error
		oldSnap, decodeErr = eventFeature.ActiveProjectionOldRecordFromCtx(ctx)
		if decodeErr != nil {
			return nil, appErrors.NewInternalError("decode active-projection old-record snapshot (after-save) failed", decodeErr)
		}
		if oldRecord == nil || !activeProjectionSnapshotMatchesRecord(oldSnap, *oldRecord) {
			oldSnap = nil
		}
	}

	enqueueCtx := eventFeature.ClearActiveProjections(ctx)
	enqueueCtx = eventFeature.MarkActiveProjectionAttempted(enqueueCtx)
	projCtx := constants.WithSecurityContext(
		enqueueCtx, constants.SystemContext,
		"active-formula write-side projection for record lifecycle after-save event",
	)
	newSnap, projErr := ps.activeFormulaProjector.project(projCtx, user, schema, recordID)
	if projErr != nil {
		return nil, appErrors.NewInternalError("active-formula projection (after-save, new record) failed", projErr)
	}
	updatedCtx, encErr := eventFeature.WithActiveProjections(enqueueCtx, newSnap, oldSnap)
	if encErr != nil {
		return nil, appErrors.NewInternalError("encode active-projection snapshots (after-save) failed", encErr)
	}
	return updatedCtx, nil
}

func activeProjectionSnapshotMatchesRecord(snapshot models.SObject, record models.SObject) bool {
	if snapshot == nil {
		return false
	}
	snapshotID, _ := snapshot[constants.FieldID].(string)
	recordID, _ := record[constants.FieldID].(string)
	return snapshotID != "" && snapshotID == recordID
}

func (ps *PersistenceService) runRecordLifecycleHooks(ctx context.Context, phase recordLifecyclePhase, event recordLifecycleEvent) error {
	if ps == nil {
		return nil
	}
	if contract.RecordLifecycleHooksSuppressed(ctx) {
		return nil
	}
	var capabilities []string
	var lifecyclePolicy *models.RecordLifecycleAccessPolicy
	if ps.metadata != nil {
		if schema := ps.metadata.GetSchema(ctx, event.ObjectName); schema != nil {
			if schema.AccessPolicy != nil {
				lifecyclePolicy = schema.AccessPolicy.Lifecycle
			}
			capabilities = schema.RecordLifecycleCapabilitiesForPhase(string(phase))
		}
	}
	if len(capabilities) == 0 {
		return nil
	}
	hooks := ps.lifecycleHooks
	if hooks == nil {
		hooks = newRecordLifecycleHooks()
	}
	if hooks.requiresPessimisticTransaction(phase, capabilities) &&
		(ps.txManager == nil || !ps.txManager.IsTransactionActive(ctx) || !ps.txManager.IsPessimisticTx(ctx)) {
		return appErrors.NewInternalError("record lifecycle hook transaction invariant violated",
			fmt.Errorf("%s capability on %s requires an active pessimistic transaction", phase, event.ObjectName))
	}
	return hooks.runCapabilities(ctx, phase, event, capabilities, lifecyclePolicy)
}

func (ps *PersistenceService) registerBuiltInRecordLifecycleHooks() {
	ps.registerProfileLifecycleHooks()
}
