package exec

import (
	"fmt"
	"reflect"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/workflow/run"
)

// ErrorCode is the stable, core-defined failure category. Handlers choose a
// Code from this set (they cannot invent new codes); the executor coerces an
// unknown/missing code to internal so unstructured failures never appear as a
// more specific category.
type ErrorCode string

const (
	// CodeValidation reports malformed or invalid input.
	CodeValidation ErrorCode = "validation"
	// CodeNotFound reports a missing referenced resource.
	CodeNotFound ErrorCode = "not_found"
	// CodePermissionDenied reports an authorization or scope refusal.
	CodePermissionDenied ErrorCode = "permission_denied"
	// CodeConflict reports stale revision, version, or idempotency state.
	CodeConflict ErrorCode = "conflict"
	// CodeAgentExtensionCatalogStale reports that the Server no longer accepts
	// the immutable Extension Catalog pinned to this process generation.
	CodeAgentExtensionCatalogStale ErrorCode = "agent_extension_catalog_stale"
	// CodeProviderAuthUnavailable reports that the selected provider connection
	// cannot currently supply credentials.
	CodeProviderAuthUnavailable ErrorCode = "provider_auth_unavailable"
	// CodeRateLimited reports that the selected provider rejected the
	// invocation because its request budget is exhausted.
	CodeRateLimited ErrorCode = "rate_limited"
	// CodeUpstreamFailure reports a provider invocation failure after local
	// authorization and validation succeeded.
	CodeUpstreamFailure ErrorCode = "upstream_failure"
	// CodeInternal reports an unexpected daemon or service failure.
	CodeInternal ErrorCode = "internal"
	// CodeResultBudgetExceeded reports that a complete success envelope could
	// not fit in the shared Exec response boundary. No partial typed result is
	// returned; callers should follow Hint/Next and narrow or continue the
	// action-specific request.
	CodeResultBudgetExceeded ErrorCode = "result_budget_exceeded"
)

// valid reports whether c is one of the core-defined codes.
func (c ErrorCode) valid() bool {
	switch c {
	case CodeValidation, CodeNotFound, CodePermissionDenied, CodeConflict,
		CodeAgentExtensionCatalogStale, CodeProviderAuthUnavailable,
		CodeRateLimited, CodeUpstreamFailure, CodeInternal, CodeResultBudgetExceeded:
		return true
	}
	return false
}

func (c ErrorCode) normalized() ErrorCode {
	if !c.valid() {
		return CodeInternal
	}
	return c
}

// httpStatus is the stable HTTP status for the category.
func (c ErrorCode) httpStatus() int {
	switch c.normalized() {
	case CodeValidation:
		return 400
	case CodeNotFound:
		return 404
	case CodePermissionDenied:
		return 403
	case CodeConflict, CodeAgentExtensionCatalogStale:
		return 409
	case CodeRateLimited:
		return 429
	case CodeProviderAuthUnavailable:
		return 503
	case CodeUpstreamFailure:
		return 502
	case CodeResultBudgetExceeded:
		return 413
	default:
		return 500
	}
}

// ErrorExitCode is the shared Cobra/process projection for one typed Exec
// error. The daemon HTTP status and CLI exit are both derived from this closed
// production mapping; audit contracts must use the same mapping.
func ErrorExitCode(code ErrorCode) int {
	switch code.normalized() {
	case CodeValidation:
		return 64 // EX_USAGE
	case CodeNotFound:
		return 2
	case CodePermissionDenied:
		return 77 // EX_NOPERM
	case CodeConflict, CodeAgentExtensionCatalogStale, CodeResultBudgetExceeded:
		return 1
	case CodeProviderAuthUnavailable, CodeRateLimited, CodeUpstreamFailure:
		return 69 // EX_UNAVAILABLE
	default:
		return 70 // EX_SOFTWARE
	}
}

// builtinErrorPolicyIdentity names one reviewed production error projection. The
// daemon classifier/writeError and Cobra ErrorExitCode projector are shared by
// handlers and audit validation; routes select an identity instead of copying
// broad error-code arrays beside their transport declaration.
type builtinErrorPolicyIdentity string

const (
	builtinErrorPolicyLocalRead        builtinErrorPolicyIdentity = "exec.local-read.v1"
	builtinErrorPolicyLocalGet         builtinErrorPolicyIdentity = "exec.local-get.v1"
	builtinErrorPolicyRemoteRead       builtinErrorPolicyIdentity = "exec.remote-read.v1"
	builtinErrorPolicyNaturalMutation  builtinErrorPolicyIdentity = "exec.natural-mutation.v1"
	builtinErrorPolicyReceiptMutation  builtinErrorPolicyIdentity = "exec.receipt-mutation.v1"
	builtinErrorPolicyWorkflowMutation builtinErrorPolicyIdentity = "exec.workflow-mutation.v1"
)

func (policy builtinErrorPolicyIdentity) valid() bool {
	switch policy {
	case builtinErrorPolicyLocalRead, builtinErrorPolicyLocalGet,
		builtinErrorPolicyRemoteRead, builtinErrorPolicyNaturalMutation,
		builtinErrorPolicyReceiptMutation, builtinErrorPolicyWorkflowMutation:
		return true
	default:
		return false
	}
}

// daemonProjectedErrorCodes derives the remote classes from the actual shared
// HTTP classifier rather than maintaining a route-policy code array. The
// result-budget class is emitted by ApplyErrorBudget after the daemon response
// classifier and is therefore added from that shared boundary explicitly.
func daemonProjectedErrorCodes() []ErrorCode {
	seen := make(map[ErrorCode]struct{})
	for status := 400; status <= 599; status++ {
		code := apiErrorCode(status)
		if code.valid() && ErrorExitCode(code) > 0 {
			seen[code] = struct{}{}
		}
	}
	seen[CodeResultBudgetExceeded] = struct{}{}
	codes := make([]ErrorCode, 0, len(seen))
	for _, code := range []ErrorCode{
		CodeValidation, CodeNotFound, CodePermissionDenied, CodeConflict,
		CodeRateLimited, CodeResultBudgetExceeded, CodeInternal,
	} {
		if _, ok := seen[code]; ok {
			codes = append(codes, code)
		}
	}
	return codes
}

func (policy builtinErrorPolicyIdentity) codes() []ErrorCode {
	projected := daemonProjectedErrorCodes()
	if policy == builtinErrorPolicyRemoteRead || policy == builtinErrorPolicyNaturalMutation ||
		policy == builtinErrorPolicyReceiptMutation || policy == builtinErrorPolicyWorkflowMutation {
		return projected
	}
	allowed := make(map[ErrorCode]struct{})
	if policy == builtinErrorPolicyLocalRead {
		for _, code := range projected {
			if code != CodeNotFound && code != CodeRateLimited {
				allowed[code] = struct{}{}
			}
		}
	} else if policy == builtinErrorPolicyLocalGet {
		for _, code := range []ErrorCode{CodePermissionDenied, CodeNotFound, CodeResultBudgetExceeded, CodeInternal} {
			allowed[code] = struct{}{}
		}
	}
	codes := make([]ErrorCode, 0, len(allowed))
	for _, code := range projected {
		if _, ok := allowed[code]; ok {
			codes = append(codes, code)
		}
	}
	return codes
}

// builtinErrorPolicyCodes returns a fresh slice for audit rendering and tests.
func builtinErrorPolicyCodes(policy builtinErrorPolicyIdentity) []ErrorCode {
	return policy.codes()
}

// Error is the typed failure returned by handlers and the executor. Handlers set
// Code (from the core set) and add Problem/Hint/Next plus an internal Cause.
// Only Code/Problem/Hint/Next and the allowlisted canonical Workflow contract
// fields (Stage/Path/Retryable/ServerCode/Diagnostics) are exposed to the model
// envelope; Cause never crosses that boundary.
type Error struct {
	Code    ErrorCode
	Problem string
	Hint    string
	Next    string
	Cause   error // internal; not serialized to the model envelope

	// Allowlisted canonical Workflow contract fields. Only Workflow action
	// handlers populate them from a structured server APIError; every other
	// domain leaves them empty so the envelope keeps the legacy shape.
	Stage       string
	Path        string
	Retryable   bool
	ServerCode  string
	Diagnostics []Diagnostic
	// Truncation carries the server-declared deterministic diagnostic budget
	// cuts when the server truncated its diagnostics. It is projected into the
	// envelope metadata so the model sees exactly what was omitted.
	Truncation *Truncation
	// Truncated reports that the server error body exceeded the apiclient read
	// limit and was not fully parsed; structured fields are not trustworthy and
	// fail closed to internal.
	Truncated bool

	// mutationApplied and mutationReceipt are kept out of the ordinary Error
	// schema and projected into bounded ErrorEnvelope metadata only for an
	// oversize mutation result. They are populated only from the closed local
	// Exec error contract; raw metadata maps never cross this boundary.
	mutationApplied bool
	mutationReceipt *mutationSuccessReceipt
}

// Diagnostic is the canonical public Workflow diagnostic projected through the
// Exec envelope. Field and Index are populated only when the server names a
// concrete field or array element; Line/Column/NodeID/EdgeID preserve the
// deterministic compiler/validator source location and graph identity so a
// model can map a finding back to exact source and node/edge.
type Diagnostic struct {
	Severity string `json:"severity"`
	Code     string `json:"code"`
	Path     string `json:"path,omitempty"`
	Field    string `json:"field,omitempty"`
	Index    *int   `json:"index,omitempty"`
	Line     int    `json:"line,omitempty"`
	Column   int    `json:"column,omitempty"`
	NodeID   string `json:"nodeId,omitempty"`
	EdgeID   string `json:"edgeId,omitempty"`
	Message  string `json:"message"`
}

// Truncation reports the server-declared deterministic diagnostic budget cuts
// carried through the Exec envelope so the model can see exactly what was
// omitted by the server instead of silently receiving a partial list.
type Truncation struct {
	DiagnosticsTruncated bool     `json:"diagnosticsTruncated,omitempty"`
	EnvelopeTruncated    bool     `json:"envelopeTruncated,omitempty"`
	OmittedCount         int      `json:"omittedCount,omitempty"`
	TruncatedFields      []string `json:"truncatedFields,omitempty"`
	SerializedBytes      int      `json:"serializedBytes,omitempty"`
}

// NewError builds a typed error. An invalid code is coerced to internal so a
// handler can never introduce an unregulated code.
func NewError(code ErrorCode, problem string) *Error {
	return &Error{Code: code.normalized(), Problem: problem}
}

// Error returns the stable category and bounded problem text.
func (e *Error) Error() string {
	if e == nil {
		return ""
	}
	if e.Problem == "" {
		return string(e.Code)
	}
	return fmt.Sprintf("%s: %s", e.Code, e.Problem)
}

// Unwrap returns the internal cause for errors.Is/errors.As.
func (e *Error) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.Cause
}

// Envelope bounds for the model-visible fields (UTF-8 safe, trimmed, and capped
// so a handler cannot push unbounded / API-leaking text into the envelope).
const (
	maxProblemRunes         = 1024
	maxHintRunes            = 256
	maxNextRunes            = 256
	maxStageRunes           = 64
	maxPathRunes            = 512
	maxServerCodeRunes      = 128
	maxDiagnostics          = 32
	maxDiagnosticFieldRunes = 256
)

// envelope returns the model-visible fields. Cause is excluded; Code is coerced
// to internal if invalid; Problem/Hint/Next are made valid UTF-8, trimmed and
// length-capped (the ellipsis counts within the cap); the allowlisted canonical
// contract fields are bounded the same way. ServerCode is only projected when it
// is an exact allowlisted server code (never trimmed/capped into a malformed
// accepted variant): a non-allowlisted or oversized ServerCode fails closed to
// empty.
func (e *Error) envelope() ErrorEnvelope {
	diagnostics := projectDiagnostics(e.Diagnostics)
	hint, next, retryable := e.Hint, e.Next, e.Retryable
	if e.mutationApplied && e.mutationReceipt != nil && e.mutationReceipt.Applied {
		hint, next, retryable = mutationAppliedHint, "", false
	}
	serverCode := e.ServerCode
	if !allowlistedWorkflowServerCode(serverCode) {
		serverCode = ""
	}
	envelope := ErrorEnvelope{
		Code:        e.Code.normalized(),
		Problem:     capField(e.Problem, maxProblemRunes),
		Hint:        capField(hint, maxHintRunes),
		Next:        capField(next, maxNextRunes),
		Stage:       capField(e.Stage, maxStageRunes),
		Path:        capField(e.Path, maxPathRunes),
		Retryable:   retryable,
		ServerCode:  serverCode,
		Diagnostics: diagnostics,
		Truncation:  e.Truncation,
		Truncated:   e.Truncated,
	}
	attachMutationReceiptMetadata(&envelope, e.mutationReceipt)
	return envelope
}

// projectDiagnostics bounds the typed diagnostic list deterministically:
// count capped, free-text fields (severity/message) made valid UTF-8/trimmed/
// capped, nil entries dropped, and the order preserved. The identity fields
// (code/path/field) are validated byte-for-byte against the shared canonical
// grammar and fail closed: a diagnostic whose code/path/field identity is
// malformed, spaced, or secret-bearing is dropped entirely rather than trimmed
// into a different valid identity.
func projectDiagnostics(diagnostics []Diagnostic) []Diagnostic {
	if len(diagnostics) == 0 {
		return nil
	}
	if len(diagnostics) > maxDiagnostics {
		diagnostics = diagnostics[:maxDiagnostics]
	}
	out := make([]Diagnostic, 0, len(diagnostics))
	for _, diagnostic := range diagnostics {
		if diagnostic.Code == "" && diagnostic.Message == "" {
			continue
		}
		if _, ok := run.PublicFailureCode(diagnostic.Code); !ok {
			continue
		}
		if diagnostic.Path != "" {
			if _, ok := run.PublicFailurePath(diagnostic.Path); !ok {
				continue
			}
		}
		if diagnostic.Field != "" {
			if _, ok := run.PublicDiagnosticField(diagnostic.Field); !ok {
				continue
			}
		}
		diagnostic.Severity = capField(diagnostic.Severity, maxDiagnosticFieldRunes)
		diagnostic.NodeID = capField(diagnostic.NodeID, maxDiagnosticFieldRunes)
		diagnostic.EdgeID = capField(diagnostic.EdgeID, maxDiagnosticFieldRunes)
		diagnostic.Message = capField(diagnostic.Message, maxProblemRunes)
		out = append(out, diagnostic)
	}
	return out
}

// ErrorEnvelope is the failure shape exposed to the model (no internal Cause).
type ErrorEnvelope struct {
	Code        ErrorCode      `json:"code"`
	Problem     string         `json:"problem"`
	Hint        string         `json:"hint,omitempty"`
	Next        string         `json:"next,omitempty"`
	Stage       string         `json:"stage,omitempty"`
	Path        string         `json:"path,omitempty"`
	Retryable   bool           `json:"retryable,omitempty"`
	ServerCode  string         `json:"serverCode,omitempty"`
	Diagnostics []Diagnostic   `json:"diagnostics,omitempty"`
	Truncation  *Truncation    `json:"truncation,omitempty"`
	Truncated   bool           `json:"truncated,omitempty"`
	Metadata    map[string]any `json:"metadata,omitempty"`
}

// classify converts a handler-returned error into a typed *Error:
//   - nil -> nil;
//   - a direct typed-nil *Error (non-nil interface) -> internal;
//   - exactly one distinct non-nil *Error in the graph and no anomaly -> a
//     normalized copy, preserving the full outer chain as Cause when wrapped;
//   - otherwise (raw error; >1 distinct *Error; a typed-nil *Error anywhere in
//     the graph; a non-comparable or cyclic/bounded node) -> internal, fail
//     closed — order- and shape-independent.
//
// It never panics on non-comparable error values and never mutates the caller.
func classify(err error) *Error {
	if err == nil {
		return nil
	}
	if e, ok := err.(*Error); ok && e == nil { // direct typed-nil: non-nil interface -> internal
		return &Error{Code: CodeInternal, Problem: "internal error", Cause: err}
	}
	typed, anomaly := collectTypedErrors(err)
	if !anomaly && len(typed) == 1 {
		e := typed[0]
		out := *e // copy; do not mutate the caller's value
		out.Code = out.Code.normalized()
		if err != error(e) { // wrapped: keep the full outer chain for logging/errors.Is
			out.Cause = err
		}
		return &out
	}
	return &Error{Code: CodeInternal, Problem: "internal error", Cause: err}
}

// maxErrorDepth bounds graph traversal so a cyclic or pathological error chain
// cannot hang or exhaust the stack; reaching it is treated as an anomaly.
const maxErrorDepth = 100

// collectTypedErrors walks the error graph (single Unwrap and errors.Join's
// multi-Unwrap), depth-bounded. It dedupes *Error BY POINTER (always comparable,
// so non-comparable sibling nodes never panic as map keys) and reports an
// anomaly if it sees a typed-nil *Error anywhere or hits the depth bound.
func collectTypedErrors(err error) (typed []*Error, anomaly bool) {
	seen := map[*Error]bool{}
	var walk func(e error, depth int)
	walk = func(e error, depth int) {
		if e == nil {
			return
		}
		if depth > maxErrorDepth {
			anomaly = true
			return
		}
		// Standard wrappers (*fmt.wrapError, *errors.joinError, *Error) are
		// pointers and thus comparable. A non-comparable value type (slice/map/
		// func-based custom error) cannot be deduped safely and is exotic, so it
		// fails closed to internal rather than risk misclassification.
		if t := reflect.TypeOf(e); !t.Comparable() {
			anomaly = true
			return
		}
		if ae, ok := e.(*Error); ok {
			if ae == nil {
				anomaly = true // typed-nil *Error in the graph -> fail closed
				return
			}
			if seen[ae] {
				return
			}
			seen[ae] = true
			typed = append(typed, ae)
		}
		switch u := e.(type) {
		case interface{ Unwrap() error }:
			walk(u.Unwrap(), depth+1)
		case interface{ Unwrap() []error }:
			for _, c := range u.Unwrap() {
				walk(c, depth+1)
			}
		}
	}
	walk(err, 0)
	return typed, anomaly
}

func capField(s string, max int) string {
	s = strings.ToValidUTF8(s, "�")
	s = strings.TrimSpace(s)
	return truncateRunes(s, max)
}

// truncateRunes caps s to max runes total; when truncating, the ellipsis counts
// within the cap (no off-by-one).
func truncateRunes(s string, max int) string {
	if max <= 0 {
		return ""
	}
	r := []rune(s)
	if len(r) <= max {
		return s
	}
	if max == 1 {
		return "…"
	}
	return string(r[:max-1]) + "…"
}
