package queue

import (
	"hash/fnv"
	"math"
	"time"
)

type RetryAction string

const (
	RetryActionRetry      RetryAction = "retry"
	RetryActionDeadLetter RetryAction = "dead_letter"
	RetryActionAck        RetryAction = "ack"
)

type RetryDecision struct {
	Action RetryAction
	Delay  time.Duration
	Reason string
}

type RetryPolicy interface {
	Decide(delivery *Delivery, envelope *Envelope, err error) RetryDecision
}

type ExponentialBackoffPolicy struct {
	minDelay              time.Duration
	maxDelay              time.Duration
	permanentFailureDelay time.Duration
}

func NewExponentialBackoffPolicy(minDelay, maxDelay, permanentFailureDelay time.Duration) *ExponentialBackoffPolicy {
	if minDelay <= 0 {
		minDelay = time.Second
	}
	if maxDelay < minDelay {
		maxDelay = minDelay
	}
	if permanentFailureDelay <= 0 {
		permanentFailureDelay = 30 * time.Second
	}
	return &ExponentialBackoffPolicy{
		minDelay:              minDelay,
		maxDelay:              maxDelay,
		permanentFailureDelay: permanentFailureDelay,
	}
}

func (p *ExponentialBackoffPolicy) Decide(delivery *Delivery, _ *Envelope, err error) RetryDecision {
	if err == nil {
		return RetryDecision{Action: RetryActionAck}
	}
	if IsPermanent(err) {
		return RetryDecision{
			Action: RetryActionDeadLetter,
			Delay:  p.permanentFailureDelay,
			Reason: err.Error(),
		}
	}
	attempt := 1
	if delivery != nil && delivery.ReceiveCount > 0 {
		attempt = delivery.ReceiveCount
	}
	delay := p.backoffWithJitter(deliveryID(delivery), attempt)
	return RetryDecision{
		Action: RetryActionRetry,
		Delay:  delay,
		Reason: err.Error(),
	}
}

func (p *ExponentialBackoffPolicy) backoffWithJitter(seed string, attempt int) time.Duration {
	if attempt < 1 {
		attempt = 1
	}
	power := math.Pow(2, float64(attempt-1))
	delay := time.Duration(float64(p.minDelay) * power)
	if delay > p.maxDelay {
		delay = p.maxDelay
	}
	if delay <= 0 {
		return p.minDelay
	}
	jitter := deterministicJitter(seed, delay/2)
	return delay/2 + jitter
}

func deterministicJitter(seed string, max time.Duration) time.Duration {
	if max <= 0 {
		return 0
	}
	h := fnv.New64a()
	_, _ = h.Write([]byte(seed))
	return time.Duration(h.Sum64() % uint64(max))
}

func deliveryID(delivery *Delivery) string {
	if delivery == nil {
		return "unknown"
	}
	if delivery.ID != "" {
		return delivery.ID
	}
	return "unknown"
}
