package flowactions

import (
	"context"
	"fmt"
	"os"
	"sort"
	"strings"
	"sync"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

type AtomicPolicy string

const (
	AtomicPolicySafe   AtomicPolicy = "safe"
	AtomicPolicyReject AtomicPolicy = "reject"
)

type PreviewPolicy string

const (
	PreviewPolicyReject                      PreviewPolicy = "reject"
	PreviewPolicyAllow                       PreviewPolicy = "allow"
	PreviewPolicyAllowWithPreviewPersistence PreviewPolicy = "allow_with_preview_persistence"
	PreviewPolicyRejectEmailIO               PreviewPolicy = "reject_email_io"
	PreviewPolicyInvokeHandler               PreviewPolicy = "invoke_handler"
)

type ContextRequirement string

const (
	ContextRequirementNone                    ContextRequirement = "none"
	ContextRequirementStandardActionProtected ContextRequirement = "standard_action_protected"
)

const (
	ActionTypeFlow                                   = "flow"
	ActionTypeSendEmail                              = "sendEmail"
	ActionTypeSendNotification                       = "sendNotification"
	ActionTypeResolveEmailRecipients                 = "resolveEmailRecipients"
	ActionTypeEnqueueOutboundMail                    = "enqueueOutboundMail"
	ActionTypeEnqueueOutboundMailV3                  = "enqueueOutboundMailV3"
	ActionTypePushToConnector                        = constants.ActionTypePushToConnector
	ActionTypeApplyConnectorFieldMappings            = "applyConnectorFieldMappings"
	ActionTypeRegisterConnectorInboundAuthorship     = "registerConnectorInboundRemoteAuthorship"
	ActionTypeFinalizeConnectorInboundRemoteIdentity = "finalizeConnectorInboundRemoteIdentity"
	ActionTypeResolveConnectorRemoteIdentity         = "resolveConnectorRemoteIdentity"
	ActionTypeTransferConnectorRemoteIdentity        = "transferConnectorRemoteIdentity"
	ActionTypeSetRetriable                           = "setRetriable"
	ActionTypeSetIdentityConvergenceDependency       = "setIdentityConvergenceDependency"
	ActionTypeRequestConnectorRemoteIdentityRecovery = "requestConnectorRemoteIdentityRecovery"
	ActionTypePreviewRecordUpdate                    = "previewRecordUpdate"
	ActionTypeCommitPreparedRecordUpdate             = "commitPreparedRecordUpdate"
	ActionTypeTransferRecordOwner                    = "transferRecordOwner"
	ActionTypeSubmitForApproval                      = constants.ActionTypeSubmitForApproval
)

type Handler func(context.Context, Invocation) (string, error)

type Invocation interface {
	Interview() *models.FlowInterview
	Metadata() *models.FlowMetadata
	Action() *models.FlowActionCall
	User() *models.UserSession
}

type Registration struct {
	ActionType       string
	HandlerRef       string
	Handler          Handler
	SendsEmail       bool
	AtomicPolicy     AtomicPolicy
	AtomicRejectText func(*models.FlowActionCall) string
	PreviewPolicy    PreviewPolicy
	RequiresContext  ContextRequirement
}

type Descriptor struct {
	ActionType      string
	HandlerRef      string
	SendsEmail      bool
	AtomicPolicy    string
	PreviewPolicy   string
	RequiresContext string
}

var globalRegistry = struct {
	sync.RWMutex
	byType   map[string]Registration
	reserved map[string]struct{}
	sealed   bool
}{
	byType:   map[string]Registration{},
	reserved: map[string]struct{}{},
}

var platformReservedActionTypeSet = normalizedActionTypeSet(PlatformReservedActionTypes())

// PlatformReservedActionTypes returns the generic L0/platform actionCall types
// reserved by the runtime. App-specific transitional handlers are intentionally
// excluded from this public contract.
func PlatformReservedActionTypes() []string {
	return []string{
		ActionTypeFlow,
		ActionTypeSendEmail,
		ActionTypeSendNotification,
		ActionTypeResolveEmailRecipients,
		ActionTypeEnqueueOutboundMail,
		ActionTypeEnqueueOutboundMailV3,
		ActionTypePushToConnector,
		ActionTypeApplyConnectorFieldMappings,
		ActionTypeRegisterConnectorInboundAuthorship,
		ActionTypeFinalizeConnectorInboundRemoteIdentity,
		ActionTypeResolveConnectorRemoteIdentity,
		ActionTypeTransferConnectorRemoteIdentity,
		ActionTypeSetRetriable,
		ActionTypeSetIdentityConvergenceDependency,
		ActionTypeRequestConnectorRemoteIdentityRecovery,
		ActionTypePreviewRecordUpdate,
		ActionTypeCommitPreparedRecordUpdate,
		ActionTypeTransferRecordOwner,
		ActionTypeSubmitForApproval,
	}
}

// IsPlatformReservedActionType reports whether actionType is owned by the
// platform runtime even when the selected host composition has not installed
// its executable handler. Dispatchers use this to fail closed instead of
// falling through to a metadata-backed action with the same authored name.
func IsPlatformReservedActionType(actionType string) bool {
	_, ok := platformReservedActionTypeSet[normalizeActionType(actionType)]
	return ok
}

func normalizedActionTypeSet(actionTypes []string) map[string]struct{} {
	out := make(map[string]struct{}, len(actionTypes))
	for _, actionType := range actionTypes {
		normalized := normalizeActionType(actionType)
		if normalized == "" {
			continue
		}
		out[normalized] = struct{}{}
	}
	return out
}

func MustRegister(registrations ...Registration) {
	if err := Register(registrations...); err != nil {
		panic(err)
	}
}

func MustReserveActionTypes(actionTypes ...string) {
	if err := ReserveActionTypes(actionTypes...); err != nil {
		panic(err)
	}
}

func ReserveActionTypes(actionTypes ...string) error {
	globalRegistry.Lock()
	defer globalRegistry.Unlock()

	normalizedActionTypes := make([]string, 0, len(actionTypes))
	batch := map[string]struct{}{}
	for _, actionType := range actionTypes {
		normalized := normalizeActionType(actionType)
		if normalized == "" {
			return fmt.Errorf("flow action-call reservation requires actionType")
		}
		if _, exists := batch[normalized]; exists {
			return fmt.Errorf("duplicate flow action-call reservation for %q (normalized actionType %q)", actionType, normalized)
		}
		if _, exists := globalRegistry.byType[normalized]; exists {
			return fmt.Errorf("flow action-call reservation for %q conflicts with registered actionType %q", actionType, normalized)
		}
		batch[normalized] = struct{}{}
		normalizedActionTypes = append(normalizedActionTypes, normalized)
	}
	if globalRegistry.sealed {
		for _, normalized := range normalizedActionTypes {
			if _, exists := globalRegistry.reserved[normalized]; !exists {
				return fmt.Errorf("flow action-call registrations are sealed")
			}
		}
		return nil
	}
	for _, normalized := range normalizedActionTypes {
		globalRegistry.reserved[normalized] = struct{}{}
	}
	return nil
}

func Register(registrations ...Registration) error {
	globalRegistry.Lock()
	defer globalRegistry.Unlock()

	if globalRegistry.sealed {
		return fmt.Errorf("flow action-call registrations are sealed")
	}

	normalizedRegistrations := make([]Registration, 0, len(registrations))
	batch := map[string]struct{}{}
	for _, registration := range registrations {
		normalized, err := normalizeRegistration(registration)
		if err != nil {
			return err
		}
		if _, exists := batch[normalized.ActionType]; exists {
			return fmt.Errorf("duplicate flow action-call registration for %q (normalized actionType %q)", registration.ActionType, normalized.ActionType)
		}
		if _, exists := globalRegistry.byType[normalized.ActionType]; exists {
			return fmt.Errorf("duplicate flow action-call registration for %q (normalized actionType %q)", registration.ActionType, normalized.ActionType)
		}
		if _, reserved := globalRegistry.reserved[normalized.ActionType]; reserved {
			return fmt.Errorf("flow action-call registration for %q conflicts with reserved actionType %q", registration.ActionType, normalized.ActionType)
		}
		if _, platformReserved := platformReservedActionTypeSet[normalized.ActionType]; platformReserved {
			return fmt.Errorf("flow action-call registration for %q conflicts with platform-reserved actionType %q", registration.ActionType, normalized.ActionType)
		}
		batch[normalized.ActionType] = struct{}{}
		normalizedRegistrations = append(normalizedRegistrations, normalized)
	}
	for _, normalized := range normalizedRegistrations {
		globalRegistry.byType[normalized.ActionType] = normalized
	}
	return nil
}

func Seal() {
	globalRegistry.Lock()
	globalRegistry.sealed = true
	globalRegistry.Unlock()
}

func Registrations() []Registration {
	globalRegistry.RLock()
	defer globalRegistry.RUnlock()
	actionTypes := make([]string, 0, len(globalRegistry.byType))
	for actionType := range globalRegistry.byType {
		actionTypes = append(actionTypes, actionType)
	}
	sort.Strings(actionTypes)
	out := make([]Registration, 0, len(actionTypes))
	for _, actionType := range actionTypes {
		out = append(out, globalRegistry.byType[actionType])
	}
	return out
}

func Descriptors() []Descriptor {
	registrations := Registrations()
	out := make([]Descriptor, 0, len(registrations))
	for _, registration := range registrations {
		out = append(out, registration.Descriptor())
	}
	return out
}

func (r Registration) Descriptor() Descriptor {
	return Descriptor{
		ActionType:      normalizeActionType(r.ActionType),
		HandlerRef:      strings.TrimSpace(r.HandlerRef),
		SendsEmail:      r.SendsEmail,
		AtomicPolicy:    string(r.AtomicPolicy),
		PreviewPolicy:   string(defaultPreviewPolicy(r.PreviewPolicy)),
		RequiresContext: string(defaultContextRequirement(r.RequiresContext)),
	}
}

// ResetForTest clears process-local registrations for tests. It panics outside
// Go test binaries so production consumers cannot desynchronize validation and
// executor snapshots through the public L1 registration seam.
func ResetForTest() func() {
	if !runningUnderGoTest() {
		panic("flowactions.ResetForTest is only available during go test")
	}
	globalRegistry.Lock()
	previous := make(map[string]Registration, len(globalRegistry.byType))
	for actionType, registration := range globalRegistry.byType {
		previous[actionType] = registration
	}
	previousReserved := make(map[string]struct{}, len(globalRegistry.reserved))
	for actionType := range globalRegistry.reserved {
		previousReserved[actionType] = struct{}{}
	}
	previousSealed := globalRegistry.sealed
	globalRegistry.byType = map[string]Registration{}
	globalRegistry.reserved = map[string]struct{}{}
	globalRegistry.sealed = false
	globalRegistry.Unlock()
	return func() {
		globalRegistry.Lock()
		globalRegistry.byType = previous
		globalRegistry.reserved = previousReserved
		globalRegistry.sealed = previousSealed
		globalRegistry.Unlock()
	}
}

func runningUnderGoTest() bool {
	return len(os.Args) > 0 && isGoTestExecutable(os.Args[0])
}

func isGoTestExecutable(executable string) bool {
	return strings.HasSuffix(executable, ".test")
}

func normalizeRegistration(registration Registration) (Registration, error) {
	registration.ActionType = normalizeActionType(registration.ActionType)
	registration.HandlerRef = strings.TrimSpace(registration.HandlerRef)
	registration.PreviewPolicy = defaultPreviewPolicy(registration.PreviewPolicy)
	registration.RequiresContext = defaultContextRequirement(registration.RequiresContext)
	switch {
	case registration.ActionType == "":
		return Registration{}, fmt.Errorf("flow action-call registration requires actionType")
	case strings.Contains(registration.ActionType, "/"):
		return Registration{}, fmt.Errorf("flow action-call registration actionType %q must not contain /", registration.ActionType)
	case registration.HandlerRef == "":
		return Registration{}, fmt.Errorf("flow action-call registration %q requires handlerRef", registration.ActionType)
	case registration.Handler == nil:
		return Registration{}, fmt.Errorf("flow action-call registration %q requires handler", registration.ActionType)
	case registration.AtomicPolicy != AtomicPolicySafe && registration.AtomicPolicy != AtomicPolicyReject:
		return Registration{}, fmt.Errorf("flow action-call registration %q requires supported atomicPolicy", registration.ActionType)
	case !validPreviewPolicy(registration.PreviewPolicy):
		return Registration{}, fmt.Errorf("flow action-call registration %q has unsupported previewPolicy %q", registration.ActionType, registration.PreviewPolicy)
	case !validContextRequirement(registration.RequiresContext):
		return Registration{}, fmt.Errorf("flow action-call registration %q has unsupported requiresContext %q", registration.ActionType, registration.RequiresContext)
	}
	return registration, nil
}

func normalizeActionType(actionType string) string {
	return strings.ToLower(strings.TrimSpace(actionType))
}

func defaultPreviewPolicy(policy PreviewPolicy) PreviewPolicy {
	if policy == "" {
		return PreviewPolicyReject
	}
	return policy
}

func defaultContextRequirement(requirement ContextRequirement) ContextRequirement {
	if requirement == "" {
		return ContextRequirementNone
	}
	return requirement
}

func validPreviewPolicy(policy PreviewPolicy) bool {
	switch policy {
	case PreviewPolicyReject,
		PreviewPolicyAllow,
		PreviewPolicyAllowWithPreviewPersistence,
		PreviewPolicyRejectEmailIO,
		PreviewPolicyInvokeHandler:
		return true
	default:
		return false
	}
}

func validContextRequirement(requirement ContextRequirement) bool {
	switch requirement {
	case ContextRequirementNone, ContextRequirementStandardActionProtected:
		return true
	default:
		return false
	}
}
