package exec

import (
	"encoding/json"
	"fmt"
	"sort"
	"strings"
)

func builtinActionKey(domain, name string) string { return domain + " " + name }

func builtinActionPolicyWithAudit(policy builtinActionPolicy) (builtinActionPolicy, bool) {
	decision, ok := builtinActionAuditDecisions[builtinActionKey(policy.Domain, policy.Name)]
	if !ok {
		return builtinActionPolicy{}, false
	}
	policy.builtinActionAuditDecision = enrichBuiltinAudit(policy, decision)
	return policy, true
}

func builtinActionPolicyFor(domain, name string) (builtinActionPolicy, bool) {
	key := builtinActionKey(domain, name)
	for _, policy := range builtinActionInventory {
		if builtinActionKey(policy.Domain, policy.Name) == key {
			return builtinActionPolicyWithAudit(policy)
		}
	}
	return builtinActionPolicy{}, false
}

func builtinActionInventorySnapshot() []builtinActionPolicy {
	out := make([]builtinActionPolicy, 0, len(builtinActionInventory))
	for _, policy := range builtinActionInventory {
		if audited, ok := builtinActionPolicyWithAudit(policy); ok {
			out = append(out, audited)
		}
	}
	return out
}

func validateBuiltinActionInventory(descriptors []actionDescriptor) error {
	policies := make(map[string]builtinActionPolicy, len(builtinActionInventory))
	for _, policy := range builtinActionInventory {
		if err := validateName(policy.Domain); err != nil {
			return fmt.Errorf("exec: built-in inventory domain: %w", err)
		}
		if err := validateName(policy.Name); err != nil {
			return fmt.Errorf("exec: built-in inventory action: %w", err)
		}
		if policy.Continuation == "" {
			return fmt.Errorf("exec: built-in inventory %s %s lacks continuation policy", policy.Domain, policy.Name)
		}
		if policy.ResultPolicy != builtinResultBounded && policy.ResultPolicy != builtinResultCursor &&
			policy.ResultPolicy != builtinResultPage && policy.ResultPolicy != builtinResultNarrow &&
			policy.ResultPolicy != builtinResultWorkflow {
			return fmt.Errorf("exec: built-in inventory %s %s has unknown result policy %q", policy.Domain, policy.Name, policy.ResultPolicy)
		}
		key := builtinActionKey(policy.Domain, policy.Name)
		decision, ok := builtinActionAuditDecisions[key]
		if !ok {
			return fmt.Errorf("exec: built-in inventory %s lacks explicit audit decision", key)
		}
		decision = enrichBuiltinAudit(policy, decision)
		if err := validateBuiltinActionAuditDecision(policy, decision); err != nil {
			return err
		}
		if _, duplicate := policies[key]; duplicate {
			return fmt.Errorf("exec: duplicate built-in inventory entry %s %s", policy.Domain, policy.Name)
		}
		policy.builtinActionAuditDecision = enrichBuiltinAudit(policy, decision)
		policies[key] = policy
	}
	if len(builtinActionAuditDecisions) != len(policies) {
		stale := make([]string, 0)
		for key := range builtinActionAuditDecisions {
			if _, ok := policies[key]; !ok {
				stale = append(stale, key)
			}
		}
		sort.Strings(stale)
		return fmt.Errorf("exec: built-in inventory has stale audit decisions: %v", stale)
	}
	registeredCapabilities := builtinRouteCapabilities()
	descriptorCatalog, err := newDescriptorCatalog(descriptors...)
	if err != nil {
		return fmt.Errorf("exec: built-in descriptor catalog for audit validation: %w", err)
	}
	seen := make(map[string]struct{}, len(descriptors))
	for _, descriptor := range descriptors {
		key := descriptor.identity()
		policy, ok := policies[key]
		if !ok {
			return fmt.Errorf("exec: built-in action %s lacks risk/result-policy coverage", key)
		}
		if string(policy.Risk) != string(descriptor.Permission) {
			return fmt.Errorf("exec: built-in action %s risk %q disagrees with permission %q", key, policy.Risk, descriptor.Permission)
		}
		if err := validateBuiltinActionContinuationPolicy(policy, descriptor); err != nil {
			return err
		}
		capability, ok := registeredCapabilities[key]
		if !ok {
			return fmt.Errorf("exec: built-in action %s lacks registered transport capability", key)
		}
		if err := capability.validate(key); err != nil {
			return err
		}
		if err := validateBuiltinActionConcreteAudit(policy, descriptor, descriptorCatalog, capability); err != nil {
			return err
		}
		if _, duplicate := seen[key]; duplicate {
			return fmt.Errorf("exec: duplicate built-in action %s", key)
		}
		seen[key] = struct{}{}
	}
	if len(seen) != len(policies) {
		missing := make([]string, 0, len(policies)-len(seen))
		for key := range policies {
			if _, ok := seen[key]; !ok {
				missing = append(missing, key)
			}
		}
		sort.Strings(missing)
		return fmt.Errorf("exec: built-in inventory has unregistered actions: %v", missing)
	}
	if err := validateWorkflowOperationMatrix(descriptors); err != nil {
		return err
	}
	return nil
}

func validateBuiltinActionAuditDecision(policy builtinActionPolicy, decision builtinActionAuditDecision) error {
	key := builtinActionKey(policy.Domain, policy.Name)
	if len(decision.AllowedSensitiveFields) == 0 || len(decision.RedactedSensitiveFields) == 0 {
		return fmt.Errorf("exec: built-in action %s lacks explicit sensitive-field decisions", key)
	}
	if len(decision.StableSelectors) == 0 || strings.TrimSpace(decision.CanonicalFollowUp) == "" {
		return fmt.Errorf("exec: built-in action %s lacks stable selector/follow-up decisions", key)
	}
	if strings.TrimSpace(decision.ReplaySelector) == "" || strings.TrimSpace(decision.ReplayDigest) == "" || strings.TrimSpace(decision.Retryability) == "" {
		return fmt.Errorf("exec: built-in action %s lacks replay selector/digest/retryability decisions", key)
	}
	if strings.TrimSpace(decision.ExpectedErrorClass) == "" || strings.TrimSpace(decision.HelpExampleStatus) == "" || strings.TrimSpace(decision.CompatibilityDecision) == "" {
		return fmt.Errorf("exec: built-in action %s lacks error/help/compatibility decisions", key)
	}
	if len(strings.TrimSpace(decision.Rationale)) < 24 {
		return fmt.Errorf("exec: built-in action %s lacks a concrete audit rationale", key)
	}
	switch decision.PermissionExposure {
	case builtinExposureReadProjection, builtinExposureAgentMutation, builtinExposureExistingGate:
	default:
		return fmt.Errorf("exec: built-in action %s has unknown permission exposure policy %q", key, decision.PermissionExposure)
	}
	switch decision.StableIdentifiers {
	case builtinIdentifiersStable, builtinIdentifiersCursor, builtinIdentifiersPage, builtinIdentifiersNarrow:
	default:
		return fmt.Errorf("exec: built-in action %s has unknown stable identifier policy %q", key, decision.StableIdentifiers)
	}
	if decision.ErrorExit != builtinErrorTyped {
		return fmt.Errorf("exec: built-in action %s has unknown error/exit policy %q", key, decision.ErrorExit)
	}
	if decision.HelpSchemaCompatibility != builtinSurfaceClosed {
		return fmt.Errorf("exec: built-in action %s has unknown help/schema policy %q", key, decision.HelpSchemaCompatibility)
	}
	capability, capabilityOK := builtinCapabilityForAction(key)
	if !capabilityOK || capability.validate(key) != nil {
		return fmt.Errorf("exec: built-in action %s lacks a registered transport capability for audit review", key)
	}
	readTransport := capability.Transport == builtinTransportLocalRead || capability.Transport == builtinTransportRemoteRead
	if policy.Risk == builtinRiskReadOnly {
		if !readTransport || decision.PermissionExposure != builtinExposureReadProjection {
			return fmt.Errorf("exec: built-in action %s read-only audit decision disagrees with registered transport", key)
		}
	} else if readTransport || decision.PermissionExposure == builtinExposureReadProjection {
		return fmt.Errorf("exec: built-in action %s mutation audit decision disagrees with registered transport", key)
	}
	if policy.Risk == builtinRiskExistingGate && decision.PermissionExposure != builtinExposureExistingGate {
		return fmt.Errorf("exec: built-in action %s must use the existing approval gate exposure", key)
	}
	if policy.Risk == builtinRiskAgentScopedMutation && decision.PermissionExposure != builtinExposureAgentMutation {
		return fmt.Errorf("exec: built-in action %s must use the Agent-scoped mutation exposure", key)
	}
	if readTransport && !strings.HasPrefix(decision.ReplaySelector, "none:") {
		return fmt.Errorf("exec: built-in action %s read replay selector must explicitly say none", key)
	}
	if !readTransport && strings.HasPrefix(decision.ReplaySelector, "none:") {
		return fmt.Errorf("exec: built-in action %s mutation replay selector cannot be none", key)
	}
	return nil
}

func validateBuiltinActionContinuationPolicy(policy builtinActionPolicy, descriptor actionDescriptor) error {
	// Workflow action schemas intentionally contain several operation-specific
	// cursor/nextCursor branches and use their own closed workflow policy. The
	// ordinary collection inventory, however, is bidirectional: exposing a
	// public cursor pair is itself a requirement to classify the action as
	// cursor, not merely a reason for a cursor row to pass validation.
	inputProperties, err := contractObjectProperties(descriptor.InputSchema)
	if err != nil {
		if policy.ResultPolicy == builtinResultCursor || policy.ResultPolicy == builtinResultPage {
			return fmt.Errorf("exec: built-in action %s continuation input contract: %w", descriptor.identity(), err)
		}
		return nil
	}
	outputProperties, err := contractObjectProperties(descriptor.OutputSchema)
	if err != nil {
		if policy.ResultPolicy == builtinResultCursor || policy.ResultPolicy == builtinResultPage {
			return fmt.Errorf("exec: built-in action %s continuation output contract: %w", descriptor.identity(), err)
		}
		return nil
	}
	dataSchema, hasData := outputProperties["data"]
	if !hasData {
		if policy.ResultPolicy == builtinResultCursor || policy.ResultPolicy == builtinResultPage {
			return fmt.Errorf("exec: built-in action %s continuation output contract lacks data", descriptor.identity())
		}
		return nil
	}
	dataProperties, err := contractObjectProperties(dataSchema)
	if err != nil {
		if policy.ResultPolicy == builtinResultCursor || policy.ResultPolicy == builtinResultPage {
			return fmt.Errorf("exec: built-in action %s continuation output data contract: %w", descriptor.identity(), err)
		}
		return nil
	}

	hasInputCursor := hasContractProperty(inputProperties, "cursor")
	hasOutputCursor := hasContractProperty(dataProperties, "nextCursor")
	hasInputPage := hasContractProperty(inputProperties, "page")
	hasOutputPage := hasContractProperty(dataProperties, "page")
	if policy.ResultPolicy != builtinResultCursor && policy.ResultPolicy != builtinResultPage {
		if policy.ResultPolicy != builtinResultWorkflow {
			if hasInputCursor != hasOutputCursor {
				return fmt.Errorf("exec: built-in action %s cursor contract must expose both cursor and nextCursor", descriptor.identity())
			}
			if hasInputCursor {
				return fmt.Errorf("exec: built-in action %s exposes cursor/nextCursor and must use the cursor result policy", descriptor.identity())
			}
			if hasInputPage != hasOutputPage {
				return fmt.Errorf("exec: built-in action %s page contract must expose both page fields", descriptor.identity())
			}
			if hasInputPage {
				return fmt.Errorf("exec: built-in action %s exposes page continuation and must use the page result policy", descriptor.identity())
			}
		}
		return nil
	}

	var inputField, continuationField, continuationWord string
	switch policy.ResultPolicy {
	case builtinResultCursor:
		inputField, continuationField, continuationWord = "cursor", "nextCursor", "cursor"
	case builtinResultPage:
		inputField, continuationField, continuationWord = "page", "page", "page"
	}
	if policy.ResultPolicy == builtinResultCursor {
		if err := requireContractPropertyType(inputProperties, inputField, "string"); err != nil {
			return fmt.Errorf("exec: built-in action %s cursor input contract: %w", descriptor.identity(), err)
		}

		if err := requireContractPropertyType(dataProperties, "nextCursor", "string"); err != nil {
			return fmt.Errorf("exec: built-in action %s cursor output contract: %w", descriptor.identity(), err)
		}
		if _, hasMore := dataProperties["hasMore"]; hasMore {
			if err := requireContractPropertyType(dataProperties, "hasMore", "boolean"); err != nil {
				return fmt.Errorf("exec: built-in action %s cursor output contract: %w", descriptor.identity(), err)
			}
		}
	} else {
		if err := requireContractPropertyType(inputProperties, inputField, "integer"); err != nil {
			return fmt.Errorf("exec: built-in action %s page input contract: %w", descriptor.identity(), err)
		}
		if err := requireContractPropertyType(dataProperties, continuationField, "integer"); err != nil {
			return fmt.Errorf("exec: built-in action %s page output contract: %w", descriptor.identity(), err)
		}
	}
	if policy.ResultPolicy == builtinResultCursor && (hasInputPage || hasOutputPage) {
		return fmt.Errorf("exec: built-in action %s exposes page continuation and must use the page result policy", descriptor.identity())
	}
	if policy.ResultPolicy == builtinResultPage && (hasInputCursor || hasOutputCursor) {
		return fmt.Errorf("exec: built-in action %s exposes cursor/nextCursor and must use the cursor result policy", descriptor.identity())
	}
	if !strings.Contains(strings.ToLower(policy.Continuation), continuationWord) {
		return fmt.Errorf("exec: built-in action %s %s policy must mention the %s continuation", descriptor.identity(), policy.ResultPolicy, continuationWord)
	}
	return nil
}

func contractObjectProperties(raw json.RawMessage) (map[string]json.RawMessage, error) {
	var schema struct {
		Type       string                     `json:"type"`
		Properties map[string]json.RawMessage `json:"properties"`
	}
	if err := json.Unmarshal(raw, &schema); err != nil {
		return nil, fmt.Errorf("invalid JSON schema: %w", err)
	}
	if schema.Type != "object" {
		return nil, fmt.Errorf("expected object schema, got %q", schema.Type)
	}
	return schema.Properties, nil
}

func hasContractProperty(properties map[string]json.RawMessage, name string) bool {
	_, ok := properties[name]
	return ok
}

func requireContractPropertyType(properties map[string]json.RawMessage, name, wantType string) error {
	raw, ok := properties[name]
	if !ok {
		return fmt.Errorf("missing %s property", name)
	}
	var schema struct {
		Type string `json:"type"`
	}
	if err := json.Unmarshal(raw, &schema); err != nil {
		return fmt.Errorf("invalid %s property schema: %w", name, err)
	}
	if schema.Type != wantType {
		return fmt.Errorf("%s property type %q, want %q", name, schema.Type, wantType)
	}
	return nil
}

func validateBuiltinActionInvokers(catalog *Catalog, invokers map[string]builtinInvoker) error {
	if catalog == nil {
		return fmt.Errorf("exec: built-in invoker catalog is unavailable")
	}
	registeredCapabilities := builtinRouteCapabilities()
	for _, descriptor := range catalog.actionsSnapshot() {
		key := descriptor.identity()
		invoker, exists := invokers[key]
		if !exists || invoker.invoke == nil {
			return fmt.Errorf("exec: built-in action %s lacks route invoker", key)
		}
		if invoker.executable == nil {
			return fmt.Errorf("exec: built-in action %s route invoker lacks executable production transport", key)
		}
		attested := invoker.executable.capability(invoker.capability.SelectorPaths...).withRoute(strings.SplitN(key, " ", 2)[0], strings.SplitN(key, " ", 2)[1])
		if !capabilityEqual(invoker.capability, attested) {
			return fmt.Errorf("exec: built-in action %s executable transport capability disagrees with invoker metadata", key)
		}
		if err := invoker.capability.validate(key); err != nil {
			return err
		}
		expected, ok := registeredCapabilities[key]
		if !ok || !capabilityEqual(invoker.capability, expected) {
			return fmt.Errorf("exec: built-in action %s route invoker capability disagrees with registered production transport", key)
		}
	}
	if len(invokers) != catalog.len() {
		return fmt.Errorf("exec: built-in invoker count %d does not match catalog count %d", len(invokers), catalog.len())
	}
	return nil
}

func capabilityEqual(left, right builtinInvokerCapability) bool {
	return left.Transport == right.Transport && left.Retry == right.Retry &&
		left.Selector == right.Selector && left.Digest == right.Digest &&
		left.ErrorPolicy == right.ErrorPolicy && left.ErrorProjection == right.ErrorProjection && left.Route == right.Route &&
		equalAuditPaths(left.SelectorPaths, right.SelectorPaths)
}
