// Package retryx provides shared retry configuration for goreleaser.
package retryx

import (
	"context"
	"errors"
	"io"
	"net/http"
	"strings"
	"time"

	retry "github.com/avast/retry-go/v4"
	"github.com/caarlos0/log"
	"github.com/goreleaser/goreleaser/v2/pkg/config"
)

// HTTPError carries an HTTP status code alongside the original error.
// RetryAfter, when non-zero, is a server-suggested wait duration (e.g. from a
// rate-limit response). Errors that carry one are always retriable, and the
// retry loop honors the duration before the next attempt.
type HTTPError struct {
	Err        error
	Status     int
	RetryAfter time.Duration
}

func (e HTTPError) Error() string { return e.Err.Error() }
func (e HTTPError) Unwrap() error { return e.Err }

// HTTP wraps err with the status code from resp.
// A nil resp yields Status 0 (network-level failure).
func HTTP(err error, resp *http.Response) error {
	if err == nil {
		return nil
	}
	status := 0
	if resp != nil {
		status = resp.StatusCode
	}
	return HTTPError{Err: err, Status: status}
}

type retriableError struct{ error }

func (e retriableError) Unwrap() error { return e.error }

// Retriable wraps err so IsRetriable returns true unconditionally.
func Retriable(err error) error {
	if err == nil {
		return nil
	}
	return retriableError{err}
}

// IsRetriable returns true if the error represents a transient failure worth
// retrying: network errors, 5xx, 429, or explicitly marked retriable.
func IsRetriable(err error) bool {
	if IsNetworkError(err) {
		return true
	}
	if _, ok := errors.AsType[retriableError](err); ok {
		return true
	}
	if he, ok := errors.AsType[HTTPError](err); ok {
		return he.RetryAfter > 0 ||
			he.Status >= 500 ||
			he.Status == http.StatusTooManyRequests
	}
	return false
}

// DoWithData retries the given retryableFunc with the given configuration,
// following retryIf, and returns the data from retryableFunc.
func DoWithData[T any](ctx context.Context, c config.Retry, retryableFunc func() (T, error), retryIf func(error) bool) (T, error) {
	return retry.DoWithData(retryableFunc, opts(ctx, c, retryIf)...)
}

// Do retries the given retryableFunc with the given configuration, following retryIf.
func Do(ctx context.Context, c config.Retry, retryableFunc func() error, retryIf func(error) bool) error {
	return retry.Do(retryableFunc, opts(ctx, c, retryIf)...)
}

func opts(ctx context.Context, c config.Retry, retryIf func(error) bool) []retry.Option {
	opts := []retry.Option{
		retry.Context(ctx),
		retry.Attempts(max(c.Attempts, 1)),
		retry.DelayType(delay),
		retry.Delay(c.Delay),
		retry.MaxDelay(c.MaxDelay),
		retry.LastErrorOnly(true),
		retry.OnRetry(func(n uint, err error) {
			log.WithError(err).WithField("try", n+1).Warn("retrying")
		}),
	}
	if retryIf != nil {
		opts = append(opts, retry.RetryIf(retryIf))
	}
	return opts
}

// delay honors a server-suggested RetryAfter when the error carries one,
// otherwise falls back to exponential backoff. retry-go applies MaxDelay on top
// of whatever this returns.
func delay(n uint, err error, c *retry.Config) time.Duration {
	if he, ok := errors.AsType[HTTPError](err); ok && he.RetryAfter > 0 {
		return he.RetryAfter
	}
	return retry.BackOffDelay(n, err, c)
}

// IsNetworkError returns true if the error looks like a transient network error.
func IsNetworkError(err error) bool {
	if err == nil {
		return false
	}
	if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
		return true
	}
	s := strings.ToLower(err.Error())
	return strings.Contains(s, "connection reset") ||
		strings.Contains(s, "network is unreachable") ||
		strings.Contains(s, "connection closed") ||
		strings.Contains(s, "connection refused") ||
		strings.Contains(s, "tls handshake timeout") ||
		strings.Contains(s, "i/o timeout") ||
		strings.Contains(s, "broken pipe") ||
		strings.Contains(s, "timeout awaiting response headers") ||
		strings.Contains(s, "context deadline exceeded")
}

// Unrecoverable wraps an error so that the retry loop stops immediately.
func Unrecoverable(err error) error {
	return retry.Unrecoverable(err)
}
