// Package httpclient provides a robust HTTP client with automatic retries and exponential backoff.
//
// This package wraps the standard http.Client to provide:
//   - Automatic retries with exponential backoff
//   - Circuit breaker protection for cascading failure prevention
//   - Request ID and trace ID propagation
//   - Configurable timeouts
//
// Usage:
//
//	// Create a client with default settings
//	client := httpclient.New(httpclient.Config{
//	    Timeout:    10 * time.Second,
//	    MaxRetries: 3,
//	})
//
//	// Create a client with circuit breaker
//	client := httpclient.New(httpclient.Config{
//	    Timeout:        10 * time.Second,
//	    MaxRetries:     3,
//	    CircuitBreaker: httpclient.NewCircuitBreaker(httpclient.CircuitBreakerConfig{}),
//	})
//
//	// Make requests
//	resp, err := client.Do(req)
//
//	// Or use convenience methods
//	resp, err := httpclient.Get(ctx, "https://api.example.com/users")
package httpclient

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"time"

	"{{GO_MODULE}}/pkg/httpcontext"
)

// Config holds configuration for the HTTP client.
type Config struct {
	// Timeout for individual HTTP requests (default: 10s)
	Timeout time.Duration

	// MaxRetries for failed requests (default: 3)
	MaxRetries int

	// Logger for structured logging (optional, defaults to slog.Default())
	Logger *slog.Logger

	// CircuitBreaker provides circuit breaker protection (optional).
	// When set, requests are blocked with ErrCircuitOpen if the circuit is open.
	CircuitBreaker *CircuitBreaker
}

// Client wraps http.Client to provide retry logic and request ID propagation.
type Client struct {
	httpClient     *http.Client
	logger         *slog.Logger
	config         Config
	circuitBreaker *CircuitBreaker
}

// New creates a new robust HTTP client.
func New(config Config) *Client {
	if config.Timeout == 0 {
		config.Timeout = 10 * time.Second
	}
	if config.MaxRetries == 0 {
		config.MaxRetries = 3
	}
	if config.Logger == nil {
		config.Logger = slog.Default()
	}

	return &Client{
		httpClient: &http.Client{
			Timeout: config.Timeout,
		},
		logger:         config.Logger,
		config:         config,
		circuitBreaker: config.CircuitBreaker,
	}
}

// Do executes an HTTP request with exponential backoff retry logic.
//
// Retries on transient errors:
// - HTTP 5xx server errors
// - HTTP 429 Too Many Requests
// - Connection errors (timeout, connection refused)
//
// Does NOT retry on:
// - HTTP 4xx client errors (except 429)
// - Context cancellation or deadline exceeded
// - Circuit breaker open (returns ErrCircuitOpen immediately)
func (c *Client) Do(req *http.Request) (*http.Response, error) {
	const (
		initialDelay = 100 * time.Millisecond
		maxDelay     = 2 * time.Second
	)

	// Check circuit breaker if configured
	if c.circuitBreaker != nil {
		if err := c.circuitBreaker.Allow(); err != nil {
			return nil, err
		}
	}

	// Propagate request ID if present in context
	if requestID, ok := httpcontext.GetRequestID(req.Context()); ok && requestID != "" {
		if req.Header.Get("X-Request-ID") == "" {
			req.Header.Set("X-Request-ID", requestID)
		}
	}

	// Propagate trace ID if present in context
	if traceID, ok := httpcontext.GetTraceID(req.Context()); ok && traceID != "" {
		if req.Header.Get("X-Trace-ID") == "" {
			req.Header.Set("X-Trace-ID", traceID)
		}
	}

	// Clone request body for retries (critical: POST/PUT bodies get exhausted)
	var bodyBytes []byte
	if req.Body != nil {
		var err error
		bodyBytes, err = io.ReadAll(req.Body)
		if err != nil {
			return nil, fmt.Errorf("read request body: %w", err)
		}
		_ = req.Body.Close()
	}

	var lastErr error
	maxRetries := c.config.MaxRetries
	ctx := req.Context()

	for attempt := 0; attempt <= maxRetries; attempt++ {
		// Check if context is already cancelled
		if err := ctx.Err(); err != nil {
			return nil, err
		}

		// Reset body for each attempt
		if bodyBytes != nil {
			req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
		}

		// Execute the request
		resp, err := c.httpClient.Do(req)

		// Network error
		if err != nil {
			lastErr = err
			if !isRetryableError(err, nil) {
				return nil, lastErr
			}
			// Continue to retry
		} else {
			// HTTP 429 - retry
			if resp.StatusCode == http.StatusTooManyRequests {
				_, _ = io.Copy(io.Discard, resp.Body)
				_ = resp.Body.Close()
				lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
				// Continue to retry
			} else if resp.StatusCode >= 400 && resp.StatusCode < 500 {
				// Other HTTP 4xx - return immediately (not transient)
				return resp, nil
			} else if resp.StatusCode >= 500 {
				// HTTP 5xx - retry
				_, _ = io.Copy(io.Discard, resp.Body)
				_ = resp.Body.Close()
				lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
				// Continue to retry
			} else {
				// HTTP 2xx/3xx - success
				c.recordSuccess()
				return resp, nil
			}
		}

		// Don't retry if we've exhausted attempts
		if attempt >= maxRetries {
			break
		}

		// Calculate exponential backoff delay using bit-shift
		delay := initialDelay << attempt
		if delay > maxDelay {
			delay = maxDelay
		}

		c.logger.Debug("retrying http request",
			"attempt", attempt+1,
			"max_retries", maxRetries,
			"delay_ms", delay.Milliseconds(),
			"url", req.URL.String(),
			"error", lastErr)

		// Wait with context awareness
		select {
		case <-time.After(delay):
			// Continue to next retry
		case <-ctx.Done():
			return nil, ctx.Err()
		}
	}

	c.recordFailure()
	return nil, fmt.Errorf("request failed after %d retries: %w", maxRetries, lastErr)
}

// recordSuccess records a successful request to the circuit breaker.
func (c *Client) recordSuccess() {
	if c.circuitBreaker != nil {
		c.circuitBreaker.RecordSuccess()
	}
}

// recordFailure records a failed request to the circuit breaker.
func (c *Client) recordFailure() {
	if c.circuitBreaker != nil {
		c.circuitBreaker.RecordFailure()
	}
}

// isRetryableError determines if an error or response should trigger a retry.
func isRetryableError(err error, resp *http.Response) bool {
	// Network/connection errors are retryable
	if err != nil {
		// Don't retry on context cancellation
		if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
			return false
		}
		// Retry on all other errors (connection refused, timeout, etc.)
		return true
	}

	// HTTP 5xx errors and 429 are retryable
	if resp != nil {
		if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500 {
			return true
		}
	}

	return false
}

// -----------------------------------------------------------------------------
// Convenience methods using a default client
// -----------------------------------------------------------------------------

// Default is a pre-configured client with 30s timeout and 3 retries.
var Default = New(Config{
	Timeout:    30 * time.Second,
	MaxRetries: 3,
})

// Do performs an HTTP request with retry logic using the default client.
func Do(req *http.Request) (*http.Response, error) {
	return Default.Do(req)
}

// Get performs a GET request with the default client.
func Get(ctx context.Context, url string) (*http.Response, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, err
	}
	return Default.Do(req)
}

// Post performs a POST request with the default client.
func Post(ctx context.Context, url, contentType string, body io.Reader) (*http.Response, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", contentType)
	return Default.Do(req)
}

// JSONPost performs a POST request with JSON content type.
func JSONPost(ctx context.Context, url string, body io.Reader) (*http.Response, error) {
	return Post(ctx, url, "application/json", body)
}
