package execution

import (
	"context"
	"errors"
	"math"
	"time"
)

// DefaultResponseStallTimeoutMS is the default response-stall timeout in
// milliseconds. It is used when no provider-configured value is available
// (zero wire value, omitted config, direct/legacy dispatch).
const DefaultResponseStallTimeoutMS = 60000

// maxSafeStallTimeoutMS is the largest millisecond value that can safely
// become a time.Duration without overflow. Values above this bound are
// rejected by the config validator and treated as invalid on the wire.
const maxSafeStallTimeoutMS = math.MaxInt64 / int64(time.Millisecond)

// ResolveStallTimeoutMS validates and normalizes a raw response-stall timeout
// value in one pass. It is the single validate-then-normalize entry point used
// by config load and both Node wire boundaries: zero maps to the documented
// default, safe positive values pass through unchanged, and negative or
// duration-overflow values return a StallTimeoutValidationError before any
// router or provider invocation. It never silently converts an invalid value.
func ResolveStallTimeoutMS(ms int64) (int64, error) {
	if err := ValidateStallTimeoutMS(ms); err != nil {
		return 0, err
	}
	if ms == 0 {
		return DefaultResponseStallTimeoutMS, nil
	}
	return ms, nil
}

// ValidateStallTimeoutMS returns nil when ms is zero (use default) or a
// positive value that can safely become a time.Duration in milliseconds.
// Negative values and values exceeding the safe duration bound are rejected.
// It is the single validation entry point used by config and the wire boundary.
func ValidateStallTimeoutMS(ms int64) error {
	if ms < 0 {
		return &StallTimeoutValidationError{
			Value: ms,
			Msg:   "response_stall_timeout_ms must be non-negative",
		}
	}
	if ms > maxSafeStallTimeoutMS {
		return &StallTimeoutValidationError{
			Value: ms,
			Msg:   "response_stall_timeout_ms exceeds safe duration bound",
		}
	}
	return nil
}

// StallTimeoutValidationError is returned when a response_stall_timeout_ms
// value is negative or exceeds the safe duration bound.
type StallTimeoutValidationError struct {
	Value int64
	Msg   string
}

func (e *StallTimeoutValidationError) Error() string {
	if e.Msg != "" {
		return e.Msg
	}
	return "invalid response_stall_timeout_ms"
}

// ProviderActivityDisposition classifies a provider output signal for the
// watchdog. The classifier is the single source of truth for progress and
// terminal decisions; handlers never switch on kind independently.
type ProviderActivityDisposition string

const (
	// DispositionNone means the signal carries no provider progress
	// information and must not reset the watchdog timer.
	DispositionNone ProviderActivityDisposition = "none"
	// DispositionStart establishes the initial baseline for the watchdog.
	// It is emitted once per run before any progress signals and lets the
	// observer record a known starting point without conflating that
	// transition with later progress resets.
	DispositionStart ProviderActivityDisposition = "start"
	// DispositionProgress means the provider is actively making progress
	// and must reset the watchdog timer.
	DispositionProgress ProviderActivityDisposition = "progress"
	// DispositionTerminal means the provider has produced a terminal
	// signal (complete, error, cancelled, end). The watchdog must stop
	// observing this run.
	DispositionTerminal ProviderActivityDisposition = "terminal"
)

// ClassifyRuntimeEvent classifies a RuntimeEvent into a ProviderActivityDisposition.
// Terminality is decided by the event type, never by token counts.
//
// Rules:
//   - start → DispositionStart
//   - complete/error/cancelled → DispositionTerminal (takes precedence over any payload/usage)
//   - non-terminal delta/reasoning_delta with non-empty delta/message or a usage observation → DispositionProgress
//   - empty/unknown type → DispositionNone
func ClassifyRuntimeEvent(event RuntimeEvent) ProviderActivityDisposition {
	switch event.Type {
	case EventTypeStart:
		return DispositionStart
	case EventTypeComplete, EventTypeError, EventTypeCancelled:
		return DispositionTerminal
	case EventTypeDelta, EventTypeReasoningDelta:
		if event.Delta != "" || event.Message != "" || event.Usage != nil {
			return DispositionProgress
		}
		return DispositionNone
	default:
		return DispositionNone
	}
}

// ClassifyProviderTunnelFrame classifies a ProviderTunnelFrame into a
// ProviderActivityDisposition.
//
// Rules:
//   - response_start (including headers) → DispositionProgress
//   - non-empty body → DispositionProgress
//   - usage frame → DispositionProgress
//   - end/error → DispositionTerminal (takes precedence over payload)
//   - empty/unknown kind → DispositionNone
func ClassifyProviderTunnelFrame(frame ProviderTunnelFrame) ProviderActivityDisposition {
	switch frame.Kind {
	case ProviderTunnelFrameKindEnd, ProviderTunnelFrameKindError:
		return DispositionTerminal
	case ProviderTunnelFrameKindResponseStart:
		// response_start with or without headers is progress.
		return DispositionProgress
	case ProviderTunnelFrameKindBody:
		if len(frame.Body) > 0 {
			return DispositionProgress
		}
		return DispositionNone
	case ProviderTunnelFrameKindUsage:
		// A usage frame is always progress for the tunnel path; the watchdog
		// observes token consumption as active provider work.
		return DispositionProgress
	default:
		return DispositionNone
	}
}

// ErrProbeUnsupported is carried in a ProbeOutcome when an adapter does not
// implement active provider probing. It is one of the inconclusive outcomes
// the probe normalizer collapses to HealthUnknown rather than treating as a
// definitive available or exact-target-unavailable result.
var ErrProbeUnsupported = errors.New("execution: adapter does not support provider probing")

// ProviderHealth is the stable, fail-closed classification of a provider's
// health as observed by a single bounded exact-target probe. It is the only
// value terminal assembly consumes from a probe: probe completion is evidence
// only and must never reset original request progress, change the attempt
// fence, or authorize retry.
type ProviderHealth string

const (
	// HealthUnknown is the fail-closed default. The probe could not establish
	// a definitive available or exact-target-unavailable result. Every error,
	// timeout, unsupported adapter, unknown status, and identity mismatch maps
	// here.
	HealthUnknown ProviderHealth = "health_unknown"
	// ProviderUnhealthy means a valid probe positively reported the exact
	// target as absent.
	ProviderUnhealthy ProviderHealth = "provider_unhealthy"
	// RequestStalled means a valid probe positively reported the exact target
	// as available, corroborating that the stalled request targets a live
	// target rather than a missing endpoint.
	RequestStalled ProviderHealth = "request_stalled"
)

// LivenessClassification is the stable, observable category a bounded
// exact-target probe outcome reduces to before it becomes a ProviderHealth.
// It exists so every fail-closed branch is independently testable; the
// normalizer is the single mapping from classification to health.
type LivenessClassification string

const (
	// LivenessAvailable means a valid probe reported the exact target present.
	LivenessAvailable LivenessClassification = "available"
	// LivenessUnavailable means a valid probe reported the exact target absent.
	LivenessUnavailable LivenessClassification = "unavailable"
	// LivenessTimeout means the bounded probe context expired before a result.
	LivenessTimeout LivenessClassification = "timeout"
	// LivenessError means the probe returned a transport, protocol, or decode
	// error that is not itself a definitive target-absent result.
	LivenessError LivenessClassification = "error"
	// LivenessUnsupported means the adapter does not implement active probing.
	LivenessUnsupported LivenessClassification = "unsupported"
	// LivenessUnknown means the probe returned an unrecognized status.
	LivenessUnknown LivenessClassification = "unknown"
	// LivenessIdentityMismatch means the probe identity did not match the
	// requested adapter or target identity.
	LivenessIdentityMismatch LivenessClassification = "identity_mismatch"
)

// ProbeOutcome is the typed, target-aware input to the fail-closed probe
// outcome normalizer. The coordinator validates and populates every field
// from a single bounded exact-target probe attempt; the normalizer never
// copies arbitrary provider metadata from it.
type ProbeOutcome struct {
	// AdapterName is the adapter identity reported by the probe result.
	AdapterName string
	// InstanceKey is the stable registry instance key reported by the probe.
	InstanceKey string
	// Target is the exact target reported by the probe result.
	Target string
	// Status is the normalized provider status reported by the probe.
	Status ProviderStatus
	// Err is the inconclusive error returned by the probe, if any.
	Err error
	// ExpectedAdapter is the adapter identity the caller required.
	ExpectedAdapter string
	// ExpectedInstance is the instance key the caller required; empty means the
	// caller does not pin a specific registry instance.
	ExpectedInstance string
	// ExpectedTarget is the exact target the caller required.
	ExpectedTarget string
}

// ClassifyProbeOutcome reduces a bounded exact-target probe outcome to its
// stable liveness classification. It is pure and fail-closed: any error, probe
// expiry, unsupported adapter, unknown status, or identity mismatch is an
// inconclusive classification rather than a definitive one. A returned error
// takes precedence over any reported status.
func ClassifyProbeOutcome(outcome ProbeOutcome) LivenessClassification {
	if outcome.Err != nil {
		if errors.Is(outcome.Err, context.Canceled) || errors.Is(outcome.Err, context.DeadlineExceeded) {
			return LivenessTimeout
		}
		if errors.Is(outcome.Err, ErrProbeUnsupported) {
			return LivenessUnsupported
		}
		return LivenessError
	}
	if !probeIdentityValid(outcome) {
		return LivenessIdentityMismatch
	}
	switch outcome.Status {
	case ProviderStatusAvailable:
		return LivenessAvailable
	case ProviderStatusUnavailable:
		return LivenessUnavailable
	default:
		return LivenessUnknown
	}
}

// HealthFromClassification maps a liveness classification to its stable
// ProviderHealth value. Available yields RequestStalled, unavailable yields
// ProviderUnhealthy, and every inconclusive classification yields
// HealthUnknown.
func HealthFromClassification(classification LivenessClassification) ProviderHealth {
	switch classification {
	case LivenessAvailable:
		return RequestStalled
	case LivenessUnavailable:
		return ProviderUnhealthy
	default:
		return HealthUnknown
	}
}

// NormalizeProbeOutcome maps a bounded exact-target probe outcome to its
// stable fail-closed ProviderHealth value. It is the composition of
// ClassifyProbeOutcome and HealthFromClassification: a validated matching
// available result yields RequestStalled, a validated matching unavailable
// result yields ProviderUnhealthy, and every error, timeout, unsupported
// adapter, unknown status, and identity mismatch yields HealthUnknown. It is
// pure and side-effect free.
func NormalizeProbeOutcome(outcome ProbeOutcome) ProviderHealth {
	return HealthFromClassification(ClassifyProbeOutcome(outcome))
}

// probeIdentityValid reports whether a probe result's adapter and target
// identity is non-empty and exactly matches what the caller required. When the
// caller pins an instance key, the probe must confirm it. An empty or
// mismatched identity is inconclusive and must fail closed.
func probeIdentityValid(outcome ProbeOutcome) bool {
	if outcome.AdapterName == "" || outcome.ExpectedAdapter == "" {
		return false
	}
	if outcome.Target == "" || outcome.ExpectedTarget == "" {
		return false
	}
	if outcome.AdapterName != outcome.ExpectedAdapter || outcome.Target != outcome.ExpectedTarget {
		return false
	}
	if outcome.ExpectedInstance != "" && outcome.InstanceKey != outcome.ExpectedInstance {
		return false
	}
	return true
}
