package execution

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
)

const failureSchemaVersion = 1

// FailureCode is the stable, provider-neutral classification of an execution
// failure. Provider-specific diagnostics belong in Message or Metadata.
type FailureCode string

const (
	FailureCodeUnknown          FailureCode = "unknown"
	FailureCodeCancelled        FailureCode = "cancelled"
	FailureCodeDeadlineExceeded FailureCode = "deadline_exceeded"
	FailureCodeInvalidRequest   FailureCode = "invalid_request"
	FailureCodeUnavailable      FailureCode = "unavailable"
	FailureCodeQuotaExhausted   FailureCode = "quota_exhausted"
	FailureCodeProvider         FailureCode = "provider_error"
	FailureCodeResponseStalled  FailureCode = "response_stalled"
	FailureCodeInternal         FailureCode = "internal"
)

// Failure is the public typed failure value shared by runtime hosts.
type Failure struct {
	Code      FailureCode       `json:"code"`
	Message   string            `json:"message,omitempty"`
	Retryable bool              `json:"retryable"`
	Metadata  map[string]string `json:"metadata,omitempty"`
}

func (f *Failure) Error() string {
	if f == nil {
		return ""
	}
	if f.Message != "" {
		return f.Message
	}
	return string(f.Code)
}

type failureEnvelope struct {
	SchemaVersion int      `json:"schema_version"`
	Failure       *Failure `json:"failure"`
}

// EncodeFailure serializes a typed failure for durable host boundaries.
func EncodeFailure(f *Failure) (string, error) {
	if f == nil {
		return "", errors.New("execution: nil failure")
	}
	normalized := normalizeFailure(*f)
	payload, err := json.Marshal(failureEnvelope{
		SchemaVersion: failureSchemaVersion,
		Failure:       &normalized,
	})
	if err != nil {
		return "", fmt.Errorf("execution: encode failure: %w", err)
	}
	return string(payload), nil
}

// DecodeFailure parses a durable failure envelope. Unknown future codes are
// preserved in metadata and normalized to FailureCodeUnknown.
func DecodeFailure(payload string) (*Failure, error) {
	var envelope failureEnvelope
	if err := json.Unmarshal([]byte(payload), &envelope); err != nil {
		return nil, fmt.Errorf("execution: decode failure: %w", err)
	}
	if envelope.SchemaVersion != failureSchemaVersion {
		return nil, fmt.Errorf("execution: unsupported failure schema version %d", envelope.SchemaVersion)
	}
	if envelope.Failure == nil {
		return nil, errors.New("execution: failure payload is missing")
	}
	normalized := normalizeFailure(*envelope.Failure)
	return &normalized, nil
}

// FailureFromError maps cancellation boundaries and otherwise returns a
// provider-neutral unknown failure without guessing provider policy.
func FailureFromError(err error) *Failure {
	if err == nil {
		return nil
	}
	switch {
	case errors.Is(err, ErrRunCancelled), errors.Is(err, context.Canceled):
		return &Failure{Code: FailureCodeCancelled, Message: err.Error()}
	case errors.Is(err, context.DeadlineExceeded):
		return &Failure{Code: FailureCodeDeadlineExceeded, Message: err.Error(), Retryable: true}
	default:
		return &Failure{Code: FailureCodeUnknown, Message: err.Error()}
	}
}

func normalizeFailure(f Failure) Failure {
	if isKnownFailureCode(f.Code) {
		return f
	}
	metadata := cloneStringMap(f.Metadata)
	if metadata == nil {
		metadata = make(map[string]string, 1)
	}
	if f.Code != "" {
		metadata["original_code"] = string(f.Code)
	}
	f.Code = FailureCodeUnknown
	f.Metadata = metadata
	return f
}

func isKnownFailureCode(code FailureCode) bool {
	switch code {
	case FailureCodeUnknown,
		FailureCodeCancelled,
		FailureCodeDeadlineExceeded,
		FailureCodeInvalidRequest,
		FailureCodeUnavailable,
		FailureCodeQuotaExhausted,
		FailureCodeProvider,
		FailureCodeResponseStalled,
		FailureCodeInternal:
		return true
	default:
		return false
	}
}

func cloneStringMap(input map[string]string) map[string]string {
	if len(input) == 0 {
		return nil
	}
	cloned := make(map[string]string, len(input))
	for key, value := range input {
		cloned[key] = value
	}
	return cloned
}
