package exec

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"reflect"
	"sort"
	"strings"
	"sync"
)

// errorProjectorDescriptor is the closed, mechanically-derived contract shared
// by daemon status classification, writeError's public JSON envelope, the
// envelope budget, and Cobra's rendered stderr/exit projection. Its digest is
// attached to every executable invoker instead of a hand-maintained identity
// string.
type errorProjectorDescriptor struct {
	SchemaVersion             uint16                       `json:"schemaVersion"`
	HTTPStatuses              []errorProjectorHTTPStatus   `json:"httpStatuses"`
	EnvelopeKeys              []string                     `json:"envelopeKeys"`
	TopLevelKeys              []string                     `json:"topLevelKeys"`
	DiagnosticKeys            []string                     `json:"diagnosticKeys"`
	MutationAppliedHint       string                       `json:"mutationAppliedHint"`
	MutationAppliedClearsNext bool                         `json:"mutationAppliedClearsNext"`
	MutationAppliedRetryable  bool                         `json:"mutationAppliedRetryable"`
	Budget                    errorProjectorBudget         `json:"budget"`
	ExitCodes                 []errorProjectorExitCode     `json:"exitCodes"`
	StderrSamples             []errorProjectorStderrSample `json:"stderrSamples"`
}

type errorProjectorHTTPStatus struct {
	Status int       `json:"status"`
	Code   ErrorCode `json:"code"`
}

type errorProjectorBudget struct {
	ProblemRunes         int `json:"problemRunes"`
	HintRunes            int `json:"hintRunes"`
	NextRunes            int `json:"nextRunes"`
	StageRunes           int `json:"stageRunes"`
	PathRunes            int `json:"pathRunes"`
	ServerCodeRunes      int `json:"serverCodeRunes"`
	Diagnostics          int `json:"diagnostics"`
	DiagnosticFieldRunes int `json:"diagnosticFieldRunes"`
	SerializedBytes      int `json:"serializedBytes"`
}

type errorProjectorExitCode struct {
	Code ErrorCode `json:"code"`
	Exit int       `json:"exit"`
}

type errorProjectorStderrSample struct {
	Code ErrorCode `json:"code"`
	Text string    `json:"text"`
}

const (
	// These pins are intentionally independent of buildErrorProjectorDescriptor.
	// A daemon/Cobra behavior change requires an explicit reviewed update; a
	// rebuilt descriptor cannot silently bless the changed projection.
	reviewedErrorProjectorSchemaVersion uint16 = 1
	reviewedErrorProjectorDigest               = "sha256:1aea9b86f9120916289b46abb444de879086f8c56c87f100c50fafb3cf119611"
)

var errorProjectorCodes = []ErrorCode{
	CodeValidation, CodeNotFound, CodePermissionDenied, CodeConflict,
	CodeAgentExtensionCatalogStale, CodeProviderAuthUnavailable,
	CodeRateLimited, CodeUpstreamFailure, CodeInternal, CodeResultBudgetExceeded,
}

func buildErrorProjectorDescriptor() errorProjectorDescriptor {
	descriptor := errorProjectorDescriptor{
		SchemaVersion:             reviewedErrorProjectorSchemaVersion,
		TopLevelKeys:              jsonObjectKeys(errorResponsePayload(ErrorEnvelope{})),
		DiagnosticKeys:            jsonFieldNames(reflect.TypeOf(Diagnostic{})),
		MutationAppliedHint:       mutationAppliedHint,
		MutationAppliedClearsNext: true,
		MutationAppliedRetryable:  false,
		Budget: errorProjectorBudget{
			ProblemRunes: maxProblemRunes, HintRunes: maxHintRunes, NextRunes: maxNextRunes,
			StageRunes: maxStageRunes, PathRunes: maxPathRunes, ServerCodeRunes: maxServerCodeRunes,
			Diagnostics: maxDiagnostics, DiagnosticFieldRunes: maxDiagnosticFieldRunes, SerializedBytes: maxSerializedResultBytes,
		},
	}
	for status := 400; status <= 599; status++ {
		descriptor.HTTPStatuses = append(descriptor.HTTPStatuses, errorProjectorHTTPStatus{
			Status: status,
			Code:   apiErrorCode(status),
		})
	}
	descriptor.EnvelopeKeys = jsonFieldNames(reflect.TypeOf(ErrorEnvelope{}))
	for _, code := range errorProjectorCodes {
		descriptor.ExitCodes = append(descriptor.ExitCodes, errorProjectorExitCode{Code: code, Exit: ErrorExitCode(code)})
	}
	for _, envelope := range []ErrorEnvelope{
		{Code: CodeValidation, Problem: "invalid input", Hint: "fix the input"},
		{Code: CodeInternal, Problem: "internal error"},
		{Code: CodeConflict, Problem: "conflict", Retryable: true, Stage: "run", Path: "$.input", ServerCode: "run_conflict", Diagnostics: []Diagnostic{{Severity: "error", Code: "conflict", Path: "$.input", Field: "input", Message: "conflict"}}},
	} {
		descriptor.StderrSamples = append(descriptor.StderrSamples, errorProjectorStderrSample{Code: envelope.Code, Text: RenderErrorText(envelope)})
	}
	return descriptor
}

func jsonObjectKeys(value any) []string {
	encoded, err := json.Marshal(value)
	if err != nil {
		return nil
	}
	var fields map[string]json.RawMessage
	if err := json.Unmarshal(encoded, &fields); err != nil {
		return nil
	}
	keys := make([]string, 0, len(fields))
	for key := range fields {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	return keys
}

func jsonFieldNames(typ reflect.Type) []string {
	fields := make([]string, 0, typ.NumField())
	for index := 0; index < typ.NumField(); index++ {
		field := typ.Field(index)
		tag := strings.Split(field.Tag.Get("json"), ",")[0]
		if tag == "" || tag == "-" {
			continue
		}
		fields = append(fields, tag)
	}
	return fields
}

func validateErrorProjectorDescriptor(candidate errorProjectorDescriptor) error {
	expected := buildErrorProjectorDescriptor()
	if !reflect.DeepEqual(candidate, expected) {
		return fmt.Errorf("exec: error projector descriptor diverges from shared daemon/Cobra implementation")
	}
	return nil
}

var (
	errorProjectorDigestOnce  sync.Once
	errorProjectorDigestValue string
)

func builtinErrorProjectorDigest() string {
	errorProjectorDigestOnce.Do(func() {
		descriptor := buildErrorProjectorDescriptor()
		if err := validateErrorProjectorDescriptor(descriptor); err != nil {
			return
		}
		encoded, err := json.Marshal(descriptor)
		if err != nil {
			return
		}
		digest := sha256.Sum256(encoded)
		actual := "sha256:" + hex.EncodeToString(digest[:])
		if descriptor.SchemaVersion != reviewedErrorProjectorSchemaVersion || actual != reviewedErrorProjectorDigest {
			return
		}
		errorProjectorDigestValue = actual
	})
	return errorProjectorDigestValue
}

// ErrorProjectorDigest exposes the exact descriptor digest for governance
// diagnostics and integration tests. Callers cannot provide a replacement
// descriptor to production route construction.
func ErrorProjectorDigest() string { return builtinErrorProjectorDigest() }

// RenderErrorText is the shared Cobra stderr text projection. Keeping it next
// to ErrorExitCode makes descriptor samples mechanically reflect the renderer
// used by the command rather than a copied audit string.
func RenderErrorText(envelope ErrorEnvelope) string {
	message := string(envelope.Code)
	if envelope.Problem != "" {
		message += ": " + envelope.Problem
	}
	if hint := strings.TrimSpace(envelope.Hint); hint != "" {
		message += " — " + hint
	}
	return message
}
