package client

import (
	"context"
	"errors"
	"math/rand"
	"net/http"
	"strconv"
	"strings"
	"time"
)

// RetryConfig holds retry configuration used across all client implementations.
type RetryConfig struct {
	MaxRetries int           // Maximum number of retry attempts
	RetryDelay time.Duration // Initial delay between retries
	MaxDelay   time.Duration // Maximum backoff delay (cap)
}

// DefaultRetryConfig returns sensible retry defaults.
func DefaultRetryConfig() RetryConfig {
	return RetryConfig{
		MaxRetries: 10,
		RetryDelay: 1 * time.Second,
		MaxDelay:   30 * time.Second,
	}
}

// StreamRetryPolicy defines provider-agnostic retry behavior for stream failures.
type StreamRetryPolicy struct {
	// MaxRetries applies to cold stream idle timeouts and generic retryable errors.
	MaxRetries int
	// MaxPartialRetries applies when stream idle happened after partial output.
	MaxPartialRetries int
	// BaseDelay is the initial backoff delay before retry.
	BaseDelay time.Duration
	// MaxDelay caps exponential backoff growth.
	MaxDelay time.Duration
}

// StreamRetryOptions controls policy behavior for a specific call site.
type StreamRetryOptions struct {
	// AllowPartial enables retries for partial stream-idle errors.
	AllowPartial bool
}

// StreamRetryDecision is the result of retry policy evaluation.
type StreamRetryDecision struct {
	ShouldRetry bool
	Delay       time.Duration
	Reason      string
	Partial     bool
}

// DefaultStreamRetryPolicy returns defaults shared across providers and runtimes.
func DefaultStreamRetryPolicy() StreamRetryPolicy {
	return StreamRetryPolicy{
		MaxRetries:        2,
		MaxPartialRetries: 1,
		BaseDelay:         2 * time.Second,
		MaxDelay:          30 * time.Second,
	}
}

func normalizeStreamRetryPolicy(policy StreamRetryPolicy) StreamRetryPolicy {
	def := DefaultStreamRetryPolicy()

	// Backward-compatible defaulting for zero-value policy.
	// This keeps existing callers that pass StreamRetryPolicy{} working.
	if policy == (StreamRetryPolicy{}) {
		return def
	}

	if policy.MaxRetries < 0 {
		policy.MaxRetries = 0
	}
	if policy.MaxPartialRetries < 0 {
		policy.MaxPartialRetries = 0
	}
	if policy.BaseDelay <= 0 {
		policy.BaseDelay = def.BaseDelay
	}
	if policy.MaxDelay <= 0 {
		policy.MaxDelay = def.MaxDelay
	}
	return policy
}

// DecideStreamRetry evaluates whether a failed stream request should be retried.
// retryCount and partialRetryCount are already-used retries for their categories.
func DecideStreamRetry(
	policy StreamRetryPolicy,
	err error,
	retryCount int,
	partialRetryCount int,
	ctx context.Context,
	options StreamRetryOptions,
) StreamRetryDecision {
	if err == nil {
		return StreamRetryDecision{}
	}
	if ctx != nil && ContextErr(ctx) != nil {
		return StreamRetryDecision{}
	}
	if errors.Is(err, context.Canceled) || errors.Is(err, ErrModelRoundTimeout) {
		return StreamRetryDecision{}
	}

	policy = normalizeStreamRetryPolicy(policy)

	var sitErr *ErrStreamIdleTimeout
	if errors.As(err, &sitErr) {
		// Partial stream-idle retries are expensive, make them explicit and capped.
		if sitErr.Partial {
			if !options.AllowPartial || partialRetryCount >= policy.MaxPartialRetries {
				return StreamRetryDecision{}
			}
			return StreamRetryDecision{
				ShouldRetry: true,
				Delay:       CalculateBackoff(policy.BaseDelay, partialRetryCount, policy.MaxDelay),
				Reason:      string(FailureReasonStreamIdleTimeout),
				Partial:     true,
			}
		}

		if retryCount >= policy.MaxRetries {
			return StreamRetryDecision{}
		}
		return StreamRetryDecision{
			ShouldRetry: true,
			Delay:       CalculateBackoff(policy.BaseDelay, retryCount, policy.MaxDelay),
			Reason:      string(FailureReasonStreamIdleTimeout),
		}
	}

	if !IsRetryableError(err) || retryCount >= policy.MaxRetries {
		return StreamRetryDecision{}
	}

	reason := DetectFailureTelemetry(err).Reason
	if reason == "" {
		reason = string(FailureReasonOther)
	}
	return StreamRetryDecision{
		ShouldRetry: true,
		Delay:       CalculateBackoff(policy.BaseDelay, retryCount, policy.MaxDelay),
		Reason:      reason,
	}
}

// AdaptiveRetryConfig returns a retry config adjusted by the provider's health score.
// Healthy providers (score > 0) get shorter delays and more retries.
// Unhealthy providers (score < 0) get longer delays and fewer retries.
func AdaptiveRetryConfig(provider string) RetryConfig {
	base := DefaultRetryConfig()
	h := getProviderHealth(provider)

	switch {
	case h.Score >= 5:
		// Very healthy — trust this provider, retry aggressively
		base.RetryDelay = 500 * time.Millisecond
		base.MaxDelay = 15 * time.Second
	case h.Score >= 0:
		// Healthy or new — normal retries
		// keep defaults
	case h.Score >= -5:
		// Slightly unhealthy — back off a bit
		base.RetryDelay = 2 * time.Second
		base.MaxDelay = 45 * time.Second
		base.MaxRetries = 7
	case h.Score >= -10:
		// Unhealthy — longer backoff, fewer retries
		base.RetryDelay = 4 * time.Second
		base.MaxDelay = 60 * time.Second
		base.MaxRetries = 5
	default:
		// Very unhealthy (score < -10) — minimal retries, long delays
		base.RetryDelay = 8 * time.Second
		base.MaxDelay = 90 * time.Second
		base.MaxRetries = 3
	}

	if h.FailureStreak >= 5 {
		base.RetryDelay = max(base.RetryDelay, 5*time.Second)
		base.MaxRetries = min(base.MaxRetries, 4)
	}

	return base
}

// AdaptiveStreamRetryPolicy returns a stream retry policy adjusted by provider health.
func AdaptiveStreamRetryPolicy(provider string) StreamRetryPolicy {
	base := DefaultStreamRetryPolicy()
	h := getProviderHealth(provider)

	switch {
	case h.Score >= 5:
		base.MaxRetries = 3
		base.BaseDelay = 1 * time.Second
	case h.Score >= 0:
		// keep defaults
	case h.Score >= -5:
		base.BaseDelay = 3 * time.Second
		base.MaxDelay = 45 * time.Second
	default:
		base.MaxRetries = 1
		base.BaseDelay = 5 * time.Second
		base.MaxDelay = 60 * time.Second
	}

	// Kimi's Coding Plan endpoint is prone to long silent reasoning/tool phases.
	// Give healthy/new Kimi sessions one extra cold retry and one extra partial
	// retry so transient stream stalls don't surface to the user immediately.
	if strings.EqualFold(strings.TrimSpace(provider), "kimi") && h.Score >= 0 {
		if base.MaxRetries < 3 {
			base.MaxRetries = 3
		}
		if base.MaxPartialRetries < 2 {
			base.MaxPartialRetries = 2
		}
		if h.Score < 5 {
			if base.BaseDelay < 3*time.Second {
				base.BaseDelay = 3 * time.Second
			}
			if base.MaxDelay < 45*time.Second {
				base.MaxDelay = 45 * time.Second
			}
		}
	}

	return base
}

// CalculateBackoff calculates exponential backoff with jitter.
// This prevents thundering herd problem when many clients retry simultaneously.
//
// Robust to hostile inputs (it's exported — callers' bounds can't be assumed):
//   - a negative attempt would wrap uint() to a huge shift count;
//   - a large attempt would overflow 1<<attempt past int64 into a negative or
//     zero delay that both slips past the maxDelay cap AND makes the jitter
//     argument non-positive — and rand.Int63n panics on n <= 0.
//
// The exponent is clamped (well past the point the result saturates at
// maxDelay), any non-positive delay falls back to maxDelay, and the jitter is
// skipped when delay/4 rounds to zero — so this can neither panic nor return a
// delay that bypasses the cap.
func CalculateBackoff(baseDelay time.Duration, attempt int, maxDelay time.Duration) time.Duration {
	if attempt < 0 {
		attempt = 0
	}
	const maxShift = 20 // 2^20 × any sane baseDelay already dwarfs maxDelay
	if attempt > maxShift {
		attempt = maxShift
	}

	// Exponential backoff: baseDelay * 2^attempt
	delay := baseDelay * time.Duration(1<<uint(attempt))
	if delay <= 0 || delay > maxDelay { // <= 0 catches overflow / zero baseDelay
		delay = maxDelay
	}

	// Add jitter: random value between 0 and 25% of delay. rand.Int63n panics on
	// n <= 0, so skip jitter when the quarter rounds to zero (sub-4ns or a
	// degenerate maxDelay).
	if quarter := int64(delay / 4); quarter > 0 {
		delay += time.Duration(rand.Int63n(quarter))
	}
	return delay
}

// cappedRetryDelay returns how long to wait before the next retry. It honors a
// server-supplied Retry-After when it exceeds the computed backoff, but never
// beyond maxRetryAfter — a buggy provider/proxy sending an absurd Retry-After
// (e.g. a unix timestamp landing in the header → ~decades) must not park the
// client far past its backoff policy. A non-positive retryAfter falls back to
// the backoff.
func cappedRetryDelay(backoff, retryAfter, maxRetryAfter time.Duration) time.Duration {
	if retryAfter > backoff {
		return min(retryAfter, maxRetryAfter)
	}
	return backoff
}

// ParseRetryAfter extracts Retry-After duration from an HTTP response.
// Supports both seconds (integer) and HTTP-date formats.
// Returns 0 if the header is absent or unparseable.
func ParseRetryAfter(resp *http.Response) time.Duration {
	if resp == nil {
		return 0
	}
	header := resp.Header.Get("Retry-After")
	if header == "" {
		return 0
	}

	// Try parsing as integer seconds first (most common for APIs)
	if seconds, err := strconv.Atoi(header); err == nil && seconds > 0 {
		return time.Duration(seconds) * time.Second
	}

	// Try parsing as HTTP-date
	if t, err := http.ParseTime(header); err == nil {
		d := time.Until(t)
		if d > 0 {
			return d
		}
	}

	return 0
}

// ParseHeaderInt64 parses an integer value from the specified header.
// Returns 0 if the header is absent or unparseable.
func ParseHeaderInt64(resp *http.Response, headerName string) int64 {
	if resp == nil {
		return 0
	}
	val := resp.Header.Get(headerName)
	if val == "" {
		return 0
	}
	if i, err := strconv.ParseInt(val, 10, 64); err == nil {
		return i
	}
	return 0
}

// ParseHeaderDuration parses a duration from the specified header.
// Supports seconds (integer) and standard duration strings (e.g., "1m30s", "45s").
// Returns 0 if the header is absent or unparseable.
func ParseHeaderDuration(resp *http.Response, headerName string) time.Duration {
	if resp == nil {
		return 0
	}
	val := resp.Header.Get(headerName)
	if val == "" {
		return 0
	}

	// Try parsing as integer seconds first
	if seconds, err := strconv.Atoi(val); err == nil && seconds > 0 {
		return time.Duration(seconds) * time.Second
	}

	// Try parsing as Go duration string (e.g. from some proxies or Kimi)
	if d, err := time.ParseDuration(val); err == nil {
		return d
	}

	// Try parsing Anthropic format (e.g. "45s", "1m30s") which might not have "s" sometimes
	// or might have different separators. Go's ParseDuration is quite robust for suffix-based strings.
	if !strings.HasSuffix(val, "s") && !strings.Contains(val, "m") && !strings.Contains(val, "h") {
		// Bare number, assume seconds if the previous strconv.Atoi failed (e.g. "45.5")
		if f, err := strconv.ParseFloat(val, 64); err == nil {
			return time.Duration(f * float64(time.Second))
		}
	}

	return 0
}

// extractAnthropicRateLimits extracts Anthropic rate limit headers from response.
func extractAnthropicRateLimits(resp *http.Response) *RateLimitMetadata {
	if resp == nil {
		return nil
	}

	metadata := &RateLimitMetadata{
		RequestsLimit:     ParseHeaderInt64(resp, "anthropic-ratelimit-requests-limit"),
		RequestsRemaining: ParseHeaderInt64(resp, "anthropic-ratelimit-requests-remaining"),
		RequestsReset:     ParseHeaderDuration(resp, "anthropic-ratelimit-requests-reset"),
		TokensLimit:       ParseHeaderInt64(resp, "anthropic-ratelimit-tokens-limit"),
		TokensRemaining:   ParseHeaderInt64(resp, "anthropic-ratelimit-tokens-remaining"),
		TokensReset:       ParseHeaderDuration(resp, "anthropic-ratelimit-tokens-reset"),
	}

	// If no headers were found (all 0), return nil
	if metadata.RequestsLimit == 0 && metadata.TokensLimit == 0 {
		return nil
	}

	return metadata
}

// extractOpenAIRateLimits extracts OpenAI rate limit headers from response.
func extractOpenAIRateLimits(resp *http.Response) *RateLimitMetadata {
	if resp == nil {
		return nil
	}

	metadata := &RateLimitMetadata{
		RequestsLimit:     ParseHeaderInt64(resp, "x-ratelimit-limit-requests"),
		RequestsRemaining: ParseHeaderInt64(resp, "x-ratelimit-remaining-requests"),
		RequestsReset:     ParseHeaderDuration(resp, "x-ratelimit-reset-requests"),
		TokensLimit:       ParseHeaderInt64(resp, "x-ratelimit-limit-tokens"),
		TokensRemaining:   ParseHeaderInt64(resp, "x-ratelimit-remaining-tokens"),
		TokensReset:       ParseHeaderDuration(resp, "x-ratelimit-reset-tokens"),
	}

	// If no headers were found (all 0), return nil
	if metadata.RequestsLimit == 0 && metadata.TokensLimit == 0 {
		return nil
	}

	return metadata
}

