package records

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/nexuscrm/shared/pkg/contract"
)

type recordCacheInvalidationHandler func(context.Context, recordCacheInvalidationEvent, *securityInvalidationPlan) error

type recordCacheInvalidationEvent struct {
	ObjectName string
	Changes    []securityRecordChange
}

type recordCacheInvalidations struct {
	capabilityHandlers map[string]recordCacheInvalidationHandler
}

type securityCacheInvalidationFallbackError struct {
	objectName string
	err        error
}

func (e *securityCacheInvalidationFallbackError) Error() string {
	if e == nil || e.err == nil {
		return "security cache invalidation fallback required"
	}
	return e.err.Error()
}

func (e *securityCacheInvalidationFallbackError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.err
}

func newSecurityCacheInvalidationFallbackError(objectName string, err error) error {
	if err == nil {
		err = fmt.Errorf("security cache invalidation fallback required for %s", objectName)
	}
	return &securityCacheInvalidationFallbackError{
		objectName: objectName,
		err:        err,
	}
}

func securityCacheInvalidationFallbackEligible(err error) bool {
	var fallbackErr *securityCacheInvalidationFallbackError
	return errors.As(err, &fallbackErr)
}

func newRecordCacheInvalidations() *recordCacheInvalidations {
	return &recordCacheInvalidations{
		capabilityHandlers: make(map[string]recordCacheInvalidationHandler),
	}
}

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

func (r *recordCacheInvalidations) tryRegisterCapability(capability string, handler recordCacheInvalidationHandler) error {
	if r == nil || handler == nil {
		return fmt.Errorf("record cache invalidation registration requires a non-nil registry and handler")
	}
	key := recordCacheInvalidationCapabilityKey(capability)
	if key == "" {
		return fmt.Errorf("record cache invalidation registration requires a capability")
	}
	if _, exists := r.capabilityHandlers[key]; exists {
		return fmt.Errorf("duplicate record cache invalidation handler for capability %q", capability)
	}
	r.capabilityHandlers[key] = handler
	return nil
}

func (r *recordCacheInvalidations) runCapabilities(ctx context.Context, event recordCacheInvalidationEvent, capabilities []string, plan *securityInvalidationPlan) error {
	if r == nil || len(capabilities) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(capabilities))
	for _, capability := range capabilities {
		key := recordCacheInvalidationCapabilityKey(capability)
		if key == "" {
			continue
		}
		if _, exists := seen[key]; exists {
			continue
		}
		seen[key] = struct{}{}
		handler := r.capabilityHandlers[key]
		if handler == nil {
			return fmt.Errorf("missing record cache invalidation handler for capability %q on %s", capability, event.ObjectName)
		}
		if err := handler(ctx, event, plan); err != nil {
			return err
		}
	}
	return nil
}

func (ps *PersistenceService) RegisterRecordCacheInvalidation(reg contract.RecordCacheInvalidationRegistration) error {
	if ps == nil {
		return fmt.Errorf("record cache invalidation registration requires a persistence service")
	}
	if reg.Handler == nil {
		return fmt.Errorf("record cache invalidation registration requires a non-nil handler")
	}
	return ps.registerRecordCacheInvalidation(reg.Capability, func(ctx context.Context, event recordCacheInvalidationEvent, plan *securityInvalidationPlan) error {
		next, err := reg.Handler(ctx, contract.RecordCacheInvalidationEvent{
			ObjectName: event.ObjectName,
			Changes:    contractCacheInvalidationChanges(event.Changes),
		})
		if err != nil {
			return err
		}
		mergeContractCacheInvalidationPlan(plan, next)
		return nil
	})
}

func (ps *PersistenceService) registerRecordCacheInvalidation(capability string, handler recordCacheInvalidationHandler) error {
	if ps == nil {
		return fmt.Errorf("record cache invalidation registration requires a persistence service")
	}
	if ps.cacheInvalidations == nil {
		ps.cacheInvalidations = newRecordCacheInvalidations()
	}
	return ps.cacheInvalidations.tryRegisterCapability(capability, handler)
}

func (ps *PersistenceService) runRecordCacheInvalidations(ctx context.Context, event recordCacheInvalidationEvent, plan *securityInvalidationPlan) error {
	if ps == nil {
		return nil
	}
	var capabilities []string
	if ps.metadata != nil {
		if schema := ps.metadata.GetSchema(ctx, event.ObjectName); schema != nil {
			capabilities = schema.RecordCacheInvalidationCapabilities()
		}
	}
	if len(capabilities) == 0 {
		if securityCacheInvalidationMetadataRequired(event.ObjectName) {
			return newSecurityCacheInvalidationFallbackError(event.ObjectName, fmt.Errorf("missing record cache invalidation metadata for security object %s", event.ObjectName))
		}
		return nil
	}
	invalidations := ps.cacheInvalidations
	if invalidations == nil {
		invalidations = newRecordCacheInvalidations()
	}
	if err := invalidations.runCapabilities(ctx, event, capabilities, plan); err != nil {
		if securityCacheInvalidationCapabilitiesFallbackEligible(capabilities) {
			return newSecurityCacheInvalidationFallbackError(event.ObjectName, err)
		}
		return err
	}
	return nil
}

func (ps *PersistenceService) validateRecordCacheInvalidationConfiguration(ctx context.Context, objectName string) error {
	if ps == nil {
		return nil
	}
	var capabilities []string
	if ps.metadata != nil {
		if schema := ps.metadata.GetSchema(ctx, objectName); schema != nil {
			capabilities = schema.RecordCacheInvalidationCapabilities()
		}
	}
	if len(capabilities) == 0 {
		return nil
	}
	invalidations := ps.cacheInvalidations
	if invalidations == nil {
		invalidations = newRecordCacheInvalidations()
	}
	seen := make(map[string]struct{}, len(capabilities))
	for _, capability := range capabilities {
		key := recordCacheInvalidationCapabilityKey(capability)
		if key == "" {
			continue
		}
		if _, exists := seen[key]; exists {
			continue
		}
		seen[key] = struct{}{}
		if invalidations.capabilityHandlers[key] == nil &&
			!securityCacheInvalidationCapabilityFallbackEligible(capability) {
			return fmt.Errorf("missing record cache invalidation handler for capability %q on %s", capability, objectName)
		}
	}
	return nil
}

func contractCacheInvalidationChanges(changes []securityRecordChange) []contract.RecordCacheInvalidationChange {
	if len(changes) == 0 {
		return nil
	}
	out := make([]contract.RecordCacheInvalidationChange, 0, len(changes))
	for _, change := range changes {
		out = append(out, contract.RecordCacheInvalidationChange{
			Before: contractRecordFromSObject(change.before),
			After:  contractRecordFromSObject(change.after),
		})
	}
	return out
}

func mergeContractCacheInvalidationPlan(plan *securityInvalidationPlan, next contract.RecordCacheInvalidationPlan) {
	if plan == nil {
		return
	}
	plan.invalidatePermissions = plan.invalidatePermissions || next.InvalidatePermissions
	plan.invalidateSharing = plan.invalidateSharing || next.InvalidateSharing
	plan.refreshRoleHierarchy = plan.refreshRoleHierarchy || next.RefreshRoleHierarchy
	for _, userID := range next.UserIDs {
		addUserID(plan.userIDs, userID)
	}
}
