package gateway

import (
	"context"
	"errors"
	"io"
	"net/http"
	"sync"
	"time"
)

var ErrCircuitOpen = errors.New("provider circuit is open")

type ResiliencePolicy struct {
	ResponseHeaderTimeout time.Duration
	MaxRetries            int
	RetryBackoff          time.Duration
	CircuitThreshold      int
	CircuitOpenDuration   time.Duration
}

func DefaultResiliencePolicy() ResiliencePolicy {
	return ResiliencePolicy{
		ResponseHeaderTimeout: 60 * time.Second, MaxRetries: 2, RetryBackoff: 50 * time.Millisecond,
		CircuitThreshold: 5, CircuitOpenDuration: 30 * time.Second,
	}
}

type circuitBreaker struct {
	mu          sync.Mutex
	failures    int
	threshold   int
	openFor     time.Duration
	openUntil   time.Time
	halfOpenRun bool
}

func newCircuitBreaker(policy ...ResiliencePolicy) *circuitBreaker {
	settings := DefaultResiliencePolicy()
	if len(policy) > 0 {
		settings = policy[0]
	}
	return &circuitBreaker{threshold: settings.CircuitThreshold, openFor: settings.CircuitOpenDuration}
}

func (c *circuitBreaker) allow(now time.Time) bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.openUntil.IsZero() {
		return true
	}
	if now.Before(c.openUntil) || c.halfOpenRun {
		return false
	}
	c.halfOpenRun = true
	return true
}

func (c *circuitBreaker) success() {
	c.mu.Lock()
	c.failures = 0
	c.openUntil = time.Time{}
	c.halfOpenRun = false
	c.mu.Unlock()
}

func (c *circuitBreaker) failure(now time.Time) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.halfOpenRun = false
	c.failures++
	if c.failures >= c.threshold || !c.openUntil.IsZero() {
		c.openUntil = now.Add(c.openFor)
	}
}

type resilientTransport struct {
	base       http.RoundTripper
	circuit    *circuitBreaker
	maxRetries int
	backoff    time.Duration
}

func (t *resilientTransport) RoundTrip(request *http.Request) (*http.Response, error) {
	if !t.circuit.allow(time.Now()) {
		return nil, ErrCircuitOpen
	}
	replayable := request.Method == http.MethodGet || request.Method == http.MethodHead ||
		request.Header.Get("Idempotency-Key") != "" && request.GetBody != nil
	attempts := 1
	if replayable {
		attempts += t.maxRetries
	}
	var response *http.Response
	var err error
	for attempt := 0; attempt < attempts; attempt++ {
		current := request
		if attempt > 0 {
			if waitErr := waitBackoff(request.Context(), t.backoff*time.Duration(attempt)); waitErr != nil {
				err = waitErr
				break
			}
			current = request.Clone(request.Context())
			if request.Body != nil && request.Body != http.NoBody {
				body, bodyErr := request.GetBody()
				if bodyErr != nil {
					err = bodyErr
					break
				}
				current.Body = body
			}
		}
		response, err = t.base.RoundTrip(current)
		if !retryableResult(response, err) || attempt == attempts-1 {
			break
		}
		if response != nil {
			_, _ = io.CopyN(io.Discard, response.Body, 4096)
			_ = response.Body.Close()
		}
	}
	if retryableResult(response, err) {
		t.circuit.failure(time.Now())
	} else {
		t.circuit.success()
	}
	return response, err
}

func retryableResult(response *http.Response, err error) bool {
	if err != nil {
		return !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded)
	}
	return response != nil && (response.StatusCode == http.StatusBadGateway || response.StatusCode == http.StatusServiceUnavailable || response.StatusCode == http.StatusGatewayTimeout)
}

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