package daemon

import (
	"errors"
	"sort"
	"strings"
	"time"
)

type CommandDirection string

const (
	CommandDirectionServerToDesktop CommandDirection = "server_to_desktop"
	CommandDirectionDesktopToServer CommandDirection = "desktop_to_server"
)

type CommandDomain string

const (
	CommandDomainSession                 CommandDomain = "session"
	CommandDomainAgentLifecycle          CommandDomain = "agent.lifecycle"
	CommandDomainAgentDelivery           CommandDomain = "agent.delivery"
	CommandDomainAgentWorkspace          CommandDomain = "agent.workspace"
	CommandDomainMachineWorkspace        CommandDomain = "machine.workspace"
	CommandDomainReview                  CommandDomain = "review"
	CommandDomainRuntimeAccess           CommandDomain = "runtime_access"
	CommandDomainTransportElasticRequest CommandDomain = "transport.elastic_request"
)

type CommandKind string

const (
	CommandKindCommand  CommandKind = "command"
	CommandKindEvent    CommandKind = "event"
	CommandKindRequest  CommandKind = "request"
	CommandKindResponse CommandKind = "response"
)

type CommandLinkPolicy string

const (
	CommandLinkPolicySessionOnly      CommandLinkPolicy = "session_only"
	CommandLinkPolicySessionPreferred CommandLinkPolicy = "session_preferred"
	CommandLinkPolicyElasticPreferred CommandLinkPolicy = "elastic_preferred"
	CommandLinkPolicyElasticRequired  CommandLinkPolicy = "elastic_required"
)

type CommandDescriptor struct {
	Name             string
	WireType         string
	Domain           CommandDomain
	Kind             CommandKind
	Direction        CommandDirection
	SchemaVersion    int
	RequestType      any
	ResponseType     any
	CapabilityGate   string
	LinkPolicy       CommandLinkPolicy
	Timeout          time.Duration
	Fallback         string
	Critical         bool
	Owner            string
	MetricsName      string
	TestRequirements []string
}

type CommandRegistry struct {
	byWireType map[string]CommandDescriptor
}

var (
	ErrCommandDescriptorInvalid = errors.New("daemon command descriptor invalid")
	ErrCommandAlreadyRegistered = errors.New("daemon command already registered")
)

func NewCommandRegistry(descriptors []CommandDescriptor) (*CommandRegistry, error) {
	registry := &CommandRegistry{byWireType: make(map[string]CommandDescriptor, len(descriptors))}
	for _, descriptor := range descriptors {
		descriptor.Name = strings.TrimSpace(descriptor.Name)
		descriptor.WireType = strings.TrimSpace(descriptor.WireType)
		descriptor.CapabilityGate = strings.TrimSpace(descriptor.CapabilityGate)
		descriptor.Fallback = strings.TrimSpace(descriptor.Fallback)
		descriptor.Owner = strings.TrimSpace(descriptor.Owner)
		descriptor.MetricsName = strings.TrimSpace(descriptor.MetricsName)
		if err := validateCommandDescriptor(descriptor); err != nil {
			return nil, err
		}
		if _, exists := registry.byWireType[descriptor.WireType]; exists {
			return nil, ErrCommandAlreadyRegistered
		}
		registry.byWireType[descriptor.WireType] = descriptor
	}
	return registry, nil
}

func validateCommandDescriptor(descriptor CommandDescriptor) error {
	if descriptor.Name == "" || descriptor.WireType == "" || descriptor.Domain == "" || descriptor.Kind == "" || descriptor.Direction == "" || descriptor.SchemaVersion <= 0 || descriptor.CapabilityGate == "" || descriptor.LinkPolicy == "" || descriptor.Timeout <= 0 || descriptor.Fallback == "" || descriptor.Owner == "" || descriptor.MetricsName == "" || len(descriptor.TestRequirements) == 0 {
		return ErrCommandDescriptorInvalid
	}
	return nil
}

func (registry *CommandRegistry) Lookup(wireType string) (CommandDescriptor, bool) {
	if registry == nil {
		return CommandDescriptor{}, false
	}
	descriptor, ok := registry.byWireType[strings.TrimSpace(wireType)]
	return descriptor, ok
}

func (registry *CommandRegistry) Descriptors() []CommandDescriptor {
	if registry == nil || len(registry.byWireType) == 0 {
		return nil
	}
	descriptors := make([]CommandDescriptor, 0, len(registry.byWireType))
	for _, descriptor := range registry.byWireType {
		descriptors = append(descriptors, descriptor)
	}
	sort.Slice(descriptors, func(i, j int) bool {
		return descriptors[i].WireType < descriptors[j].WireType
	})
	return descriptors
}

func defaultSessionLinkCommandRegistry() *CommandRegistry {
	return defaultSessionLinkCommands
}

func mustCommandRegistry(descriptors []CommandDescriptor) *CommandRegistry {
	registry, err := NewCommandRegistry(descriptors)
	if err != nil {
		panic(err)
	}
	return registry
}

const (
	commandTimeoutSessionLink   = 10 * time.Second
	commandTimeoutRemoteRequest = 15 * time.Second
)

const (
	commandCapabilityNone                = "none"
	commandCapabilityOptionalRuntimeGate = "optional_runtime_capability_gate"
	commandFallbackNone                  = "none"
	commandFallbackLegacySessionLink     = "legacy_session_link"
	commandOwnerDaemonTransport          = "daemon-transport"
	commandOwnerAgentRuntime             = "agent-runtime"
	commandOwnerWorkspace                = "workspace"
	commandOwnerReview                   = "review"
	commandOwnerMachine                  = "machine"
	commandOwnerRuntimeAccess            = "runtime-access"
)

const (
	sessionLinkCommandReady                         = "ready"
	sessionLinkCommandServerCapabilities            = "server:capabilities"
	sessionLinkCommandAgentStart                    = "agent:start"
	sessionLinkCommandAgentStop                     = "agent:stop"
	sessionLinkCommandAgentUpdateIdentity           = "agent:update_identity"
	sessionLinkCommandAgentResetWorkspace           = "agent:reset-workspace"
	sessionLinkCommandAgentDeliver                  = "agent:deliver"
	sessionLinkCommandAgentDeliverAck               = "agent:deliver:ack"
	sessionLinkCommandAgentStatus                   = "agent:status"
	sessionLinkCommandAgentActivity                 = "agent:activity"
	sessionLinkCommandAgentSession                  = "agent:session"
	sessionLinkCommandAgentTurnState                = "agent:turn_state"
	sessionLinkCommandAgentWakeStatus               = "agent:wake_status"
	sessionLinkCommandAgentRuntimeEvent             = "agent:runtime_event"
	sessionLinkCommandAgentRecoveryPointPrepared    = "agent:recovery_point_prepared"
	sessionLinkCommandAgentTrajectory               = "agent:trajectory"
	sessionLinkCommandAgentTrajectoryHistory        = "agent:trajectory:history"
	sessionLinkCommandAgentTrajectoryHistoryResult  = "agent:trajectory:history_result"
	sessionLinkCommandAgentRestoreRecoveryPoint     = "agent:restore_recovery_point"
	sessionLinkCommandAgentRestoreRecoveryPointDone = "agent:restore_recovery_point:result"
	sessionLinkCommandAgentGCRecoveryPoints         = "agent:gc_recovery_points"
	sessionLinkCommandAgentMemoryAccessChanged      = "agent:memory:access-changed"
	sessionLinkCommandAgentAssetsAccessChanged      = "agent:assets:access-changed"
	sessionLinkCommandAgentWorkspaceList            = "agent:workspace:list"
	sessionLinkCommandAgentWorkspaceFileTree        = "agent:workspace:file_tree"
	sessionLinkCommandAgentWorkspaceRead            = "agent:workspace:read"
	sessionLinkCommandAgentWorkspaceFileContent     = "agent:workspace:file_content"
	sessionLinkCommandAgentWorkspaceApplySeedFiles  = "agent:workspace:apply_seed_files"
	sessionLinkCommandAgentWorkspaceApplySeedDone   = "agent:workspace:apply_seed_files_result"
	sessionLinkCommandAgentWorkspaceDelete          = "agent:workspace:delete"
	sessionLinkCommandAgentWorkspaceDeleteResult    = "agent:workspace:delete_result"
	sessionLinkCommandAgentReviewSummary            = "agent:review:summary"
	sessionLinkCommandAgentReviewSummaryResult      = "agent:review:summary_result"
	sessionLinkCommandAgentReviewFiles              = "agent:review:files"
	sessionLinkCommandAgentReviewFilesResult        = "agent:review:files_result"
	sessionLinkCommandAgentReviewDiff               = "agent:review:diff"
	sessionLinkCommandAgentReviewDiffResult         = "agent:review:diff_result"
	sessionLinkCommandOutputTargetValidate          = "agent:output-target:validate"
	sessionLinkCommandOutputTargetValidateResult    = "agent:output-target:validate_result"
	sessionLinkCommandMachineWorkspaceScan          = "machine:workspace:scan"
	sessionLinkCommandMachineWorkspaceScanResult    = "machine:workspace:scan_result"
	sessionLinkCommandMachineWorkspaceDelete        = "machine:workspace:delete"
	sessionLinkCommandMachineWorkspaceDeleteResult  = "machine:workspace:delete_result"
	sessionLinkCommandMachineWorkspacesList         = "machine:workspaces:list"
	sessionLinkCommandMachineWorkspacesListResponse = "machine:workspaces:list:response"
	sessionLinkCommandMachineAgentList              = "machine:agent:list"
	sessionLinkCommandMachineAgentListResponse      = "machine:agent:list:response"
	sessionLinkCommandElasticRequestOpen            = elasticRequestLinkOpenType
	sessionLinkCommandElasticRequestDrain           = elasticRequestLinkDrainType
	sessionLinkCommandElasticRequestCancel          = elasticRequestLinkCancelType
	sessionLinkCommandRemoteRequestAssign           = sessionLinkRemoteRequestAssignType
	sessionLinkCommandRemoteRequestResult           = sessionLinkRemoteRequestResultType
)

const (
	transientLegacyWakeStatusServerCapability = "transient_legacy_wake_status"
	transientWakeStatusDesktopCapability      = "wake:status-transient"
)

const (
	sessionLinkCompatibilityPing       = "ping"
	sessionLinkCompatibilityPong       = "pong"
	sessionLinkCompatibilityAgentSleep = "agent:sleep"
)

type sessionLinkCompatibilityException struct {
	WireType  string
	Domain    CommandDomain
	Direction CommandDirection
	Reason    string
}

// sessionLinkCompatibilityExceptions documents lifecycle/control messages that
// stay outside the typed Command Registry because they are not Server-owned
// request/response commands today. Keep this list small: entries here are
// compatibility exceptions, not an alternative registry.
var sessionLinkCompatibilityExceptions = []sessionLinkCompatibilityException{
	{
		WireType:  sessionLinkCompatibilityPing,
		Domain:    CommandDomainSession,
		Direction: CommandDirectionServerToDesktop,
		Reason:    "application heartbeat probe handled by the Desktop dispatch agentteam, alongside WebSocket ping control frames",
	},
	{
		WireType:  sessionLinkCompatibilityPong,
		Domain:    CommandDomainSession,
		Direction: CommandDirectionDesktopToServer,
		Reason:    "application heartbeat response handled by the Session Link read-deadline path rather than typed command dispatch",
	},
	{
		WireType:  sessionLinkCompatibilityAgentSleep,
		Domain:    CommandDomainAgentLifecycle,
		Direction: CommandDirectionServerToDesktop,
		Reason:    "Desktop-advertised lifecycle control capability without a current public Server sender; promote to registry when that sender exists",
	},
}

type commandDescriptorSpec struct {
	name           string
	wireType       string
	domain         CommandDomain
	kind           CommandKind
	direction      CommandDirection
	owner          string
	capabilityGate string
	linkPolicy     CommandLinkPolicy
	fallback       string
	critical       bool
	requestType    any
	responseType   any
}

func descriptor(spec commandDescriptorSpec) CommandDescriptor {
	metricsName := strings.NewReplacer(":", ".", "-", "_", " ", "_").Replace(spec.wireType)
	return CommandDescriptor{
		Name:             spec.name,
		WireType:         spec.wireType,
		Domain:           spec.domain,
		Kind:             spec.kind,
		Direction:        spec.direction,
		SchemaVersion:    1,
		RequestType:      spec.requestType,
		ResponseType:     spec.responseType,
		CapabilityGate:   spec.capabilityGate,
		LinkPolicy:       spec.linkPolicy,
		Timeout:          commandDescriptorTimeout(spec.kind, spec.linkPolicy),
		Fallback:         spec.fallback,
		Critical:         spec.critical,
		Owner:            spec.owner,
		MetricsName:      metricsName,
		TestRequirements: []string{"registry_guardrail", "legacy_session_link_compatibility"},
	}
}

func commandDescriptorTimeout(kind CommandKind, linkPolicy CommandLinkPolicy) time.Duration {
	switch {
	case linkPolicy == CommandLinkPolicySessionPreferred || linkPolicy == CommandLinkPolicyElasticPreferred || linkPolicy == CommandLinkPolicyElasticRequired:
		return commandTimeoutRemoteRequest
	case kind == CommandKindRequest || kind == CommandKindResponse:
		return commandTimeoutRemoteRequest
	default:
		return commandTimeoutSessionLink
	}
}
