package apiclient

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

var defaultRetryDelays = []time.Duration{
	250 * time.Millisecond,
	500 * time.Millisecond,
	time.Second,
	2 * time.Second,
	4 * time.Second,
}

const maxReplaySafeAttempts = 6

// maxAuthRefreshRounds bounds how many times one execute may rotate the Agent
// auth token after HTTP 401. The second round exists because the daemon-side
// refresh may hand back another already-stale cached token: the server keeps a
// single mutable agent token hash, so any out-of-band rotation invalidates
// every token the daemon still holds. Two bounded rounds recover from one dead
// cached token without unbounded rotation churn.
const maxAuthRefreshRounds = 2

// transportError marks a failure returned directly by http.Client.Do. Local
// request construction, response reading, and response decoding errors are
// deliberately not wrapped as transport failures so they remain terminal.
type transportError struct {
	err error
}

func (e *transportError) Error() string {
	if e == nil || e.err == nil {
		return "http transport error"
	}
	return e.err.Error()
}

func (e *transportError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.err
}

type retryConfig struct {
	delays []time.Duration
	now    func() time.Time
	jitter func(time.Duration) time.Duration
	wait   func(context.Context, time.Duration) error
}

type attemptMode uint8

const (
	attemptAuthRefreshOnly attemptMode = iota
	attemptReplaySafe
)

func defaultRetryConfig() retryConfig {
	return retryConfig{
		delays: append([]time.Duration(nil), defaultRetryDelays...),
		now:    time.Now,
		jitter: fullJitter,
		wait:   waitForRetry,
	}
}

func (a *Client) retryConfig() retryConfig {
	config := defaultRetryConfig()
	if a == nil || a.retry == nil {
		return config
	}
	if a.retry.delays != nil {
		config.delays = append([]time.Duration(nil), a.retry.delays...)
	}
	if a.retry.now != nil {
		config.now = a.retry.now
	}
	if a.retry.jitter != nil {
		config.jitter = a.retry.jitter
	}
	if a.retry.wait != nil {
		config.wait = a.retry.wait
	}
	return config
}

func fullJitter(max time.Duration) time.Duration {
	if max <= 0 {
		return 0
	}
	return time.Duration(rand.Int64N(int64(max) + 1))
}

func waitForRetry(ctx context.Context, delay time.Duration) error {
	if delay <= 0 {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
			return nil
		}
	}
	timer := time.NewTimer(delay)
	defer timer.Stop()
	select {
	case <-timer.C:
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

func parseRetryAfter(value string, now time.Time) (time.Duration, bool) {
	value = strings.TrimSpace(value)
	if value == "" {
		return 0, false
	}
	if seconds, err := strconv.ParseInt(value, 10, 64); err == nil {
		const maxDuration = time.Duration(1<<63 - 1)
		if seconds < 0 || seconds > int64(maxDuration/time.Second) {
			return 0, false
		}
		return time.Duration(seconds) * time.Second, true
	}
	retryAt, err := http.ParseTime(value)
	if err != nil {
		return 0, false
	}
	delay := retryAt.Sub(now)
	if delay < 0 {
		delay = 0
	}
	return delay, true
}

func (config retryConfig) delayFor(err error, retryIndex int) time.Duration {
	if retryIndex < 0 || retryIndex >= len(config.delays) {
		return 0
	}
	delay := config.jitter(config.delays[retryIndex])
	var apiErr *APIError
	if !errors.As(err, &apiErr) ||
		(apiErr.StatusCode != http.StatusTooManyRequests && apiErr.StatusCode != http.StatusServiceUnavailable) {
		return delay
	}
	if retryAfter, ok := parseRetryAfter(apiErr.retryAfter, config.now()); ok && retryAfter > delay {
		return retryAfter
	}
	return delay
}

// execute coordinates re-attempt policy around one-attempt HTTP functions.
// Authentication refresh is bounded by maxAuthRefreshRounds in both modes and
// remains independent from the transient retry budget reserved for explicitly
// replay-safe calls. Replay-safe execution still has one hard cap across every
// HTTP attempt.
func (a *Client) execute(ctx context.Context, operation string, mode attemptMode, attempt func() error) error {
	var config retryConfig
	if mode == attemptReplaySafe {
		config = a.retryConfig()
	}
	transientRetries := 0
	authRefreshRounds := 0
	totalAttempts := 0

	for {
		totalAttempts++
		err := attempt()
		if err == nil {
			return nil
		}
		if mode == attemptReplaySafe {
			if contextErr := ctx.Err(); contextErr != nil {
				return contextErr
			}
		}

		canReplayAfterRefresh := mode != attemptReplaySafe || totalAttempts < maxReplaySafeAttempts
		if authRefreshRounds < maxAuthRefreshRounds && canReplayAfterRefresh {
			staleToken := strings.TrimSpace(a.AuthToken)
			refreshed, refreshErr := a.refreshAuthTokenAfterUnauthorized(ctx, err)
			if refreshErr != nil {
				slog.Warn("agentteam api: agent auth token refresh failed",
					"operation", operation,
					"agent_id", a.AgentID,
					"round", authRefreshRounds+1,
					"error", refreshErr,
				)
				return refreshErr
			}
			// A refresh that hands back the very token that just failed cannot
			// make the next attempt succeed; stop instead of replaying it.
			if refreshed && (strings.TrimSpace(a.AuthToken) == "" || strings.TrimSpace(a.AuthToken) == staleToken) {
				refreshed = false
			}
			if refreshed {
				authRefreshRounds++
				slog.Warn("agentteam api: agent auth token stale; refreshed and retrying",
					"operation", operation,
					"agent_id", a.AgentID,
					"round", authRefreshRounds,
				)
				continue
			}
		}

		if mode != attemptReplaySafe ||
			totalAttempts >= maxReplaySafeAttempts ||
			transientRetries >= len(config.delays) ||
			!a.shouldRetry(err) {
			if isUnauthorizedAPIError(err) {
				slog.Warn("agentteam api: authenticated request rejected (unauthorized)",
					"operation", operation,
					"agent_id", a.AgentID,
					"auth_refresh_rounds", authRefreshRounds,
					"error", err,
				)
			}
			return err
		}
		delay := config.delayFor(err, transientRetries)
		slog.Debug("agentteam api: retrying replay-safe request",
			"operation", operation,
			"retry", transientRetries+1,
			"delay", delay,
			"error", err,
		)
		if waitErr := config.wait(ctx, delay); waitErr != nil {
			return waitErr
		}
		transientRetries++
		if a.RecordAPIRetry != nil {
			a.RecordAPIRetry()
		}
	}
}

// isUnauthorizedAPIError reports whether err is an upstream HTTP 401 so silent
// authentication failures stay operator-visible regardless of error body shape.
func isUnauthorizedAPIError(err error) bool {
	var apiErr *APIError
	return errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusUnauthorized
}
