// Package fserrors provides errors and error handling
package fserrors

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

	liberrors "github.com/rclone/rclone/lib/errors"
)

// Must be satisfied for errors.Is/errors.As/Errors.Unwrap
type unwrapper interface {
	Unwrap() error
}

// Retrier is an optional interface for error as to whether the
// operation should be retried at a high level.
//
// This should be returned from Update or Put methods as required
type Retrier interface {
	error
	Retry() bool
}

// retryError is a type of error
type retryError string

// Error interface
func (r retryError) Error() string {
	return string(r)
}

// Retry interface
func (r retryError) Retry() bool {
	return true
}

// Check interfaces
var _ Retrier = retryError("")

// RetryErrorf makes an error which indicates it would like to be retried
func RetryErrorf(format string, a ...any) error {
	return retryError(fmt.Sprintf(format, a...))
}

// wrappedRetryError is an error wrapped so it will satisfy the
// Retrier interface and return true
type wrappedRetryError struct {
	error
}

// Retry interface
func (err wrappedRetryError) Retry() bool {
	return true
}

// Check interfaces
var _ Retrier = wrappedRetryError{error(nil)}
var _ unwrapper = wrappedRetryError{}

// RetryError makes an error which indicates it would like to be retried
func RetryError(err error) error {
	if err == nil {
		err = errors.New("needs retry")
	}
	return wrappedRetryError{err}
}

func (err wrappedRetryError) Unwrap() error {
	return err.error
}

// IsRetryError returns true if err conforms to the Retry interface
// and calling the Retry method returns true.
func IsRetryError(err error) (isRetry bool) {
	liberrors.Walk(err, func(err error) bool {
		if r, ok := err.(Retrier); ok {
			isRetry = r.Retry()
			return true
		}
		return false
	})
	return
}

// Fataler is an optional interface for error as to whether the
// operation should cause the entire operation to finish immediately.
//
// This should be returned from Update or Put methods as required
type Fataler interface {
	error
	Fatal() bool
}

// wrappedFatalError is an error wrapped so it will satisfy the
// Retrier interface and return true
type wrappedFatalError struct {
	error
}

// Fatal interface
func (err wrappedFatalError) Fatal() bool {
	return true
}

// Check interfaces
var _ Fataler = wrappedFatalError{error(nil)}
var _ unwrapper = wrappedFatalError{}

// FatalError makes an error which indicates it is a fatal error and
// the sync should stop.
func FatalError(err error) error {
	if err == nil {
		err = errors.New("fatal error")
	}
	return wrappedFatalError{err}
}

func (err wrappedFatalError) Unwrap() error {
	return err.error
}

// IsFatalError returns true if err conforms to the Fatal interface
// and calling the Fatal method returns true.
func IsFatalError(err error) (isFatal bool) {
	liberrors.Walk(err, func(err error) bool {
		if r, ok := err.(Fataler); ok {
			isFatal = r.Fatal()
			return true
		}
		return false
	})
	return
}

// NoRetrier is an optional interface for error as to whether the
// operation should not be retried at a high level.
//
// If only NoRetry errors are returned in a sync then the sync won't
// be retried.
//
// This should be returned from Update or Put methods as required
type NoRetrier interface {
	error
	NoRetry() bool
}

// wrappedNoRetryError is an error wrapped so it will satisfy the
// Retrier interface and return true
type wrappedNoRetryError struct {
	error
}

// NoRetry interface
func (err wrappedNoRetryError) NoRetry() bool {
	return true
}

// Check interfaces
var _ NoRetrier = wrappedNoRetryError{error(nil)}
var _ unwrapper = wrappedNoRetryError{}

// NoRetryError makes an error which indicates the sync shouldn't be
// retried.
func NoRetryError(err error) error {
	return wrappedNoRetryError{err}
}

func (err wrappedNoRetryError) Unwrap() error {
	return err.error
}

// IsNoRetryError returns true if err conforms to the NoRetry
// interface and calling the NoRetry method returns true.
func IsNoRetryError(err error) (isNoRetry bool) {
	liberrors.Walk(err, func(err error) bool {
		if r, ok := err.(NoRetrier); ok {
			isNoRetry = r.NoRetry()
			return true
		}
		return false
	})
	return
}

// NoLowLevelRetrier is an optional interface for error as to whether
// the operation should not be retried at a low level.
//
// NoLowLevelRetry errors won't be retried by low level retry loops.
type NoLowLevelRetrier interface {
	error
	NoLowLevelRetry() bool
}

// wrappedNoLowLevelRetryError is an error wrapped so it will satisfy the
// NoLowLevelRetrier interface and return true
type wrappedNoLowLevelRetryError struct {
	error
}

// NoLowLevelRetry interface
func (err wrappedNoLowLevelRetryError) NoLowLevelRetry() bool {
	return true
}

// Check interfaces
var _ NoLowLevelRetrier = wrappedNoLowLevelRetryError{error(nil)}
var _ unwrapper = wrappedNoLowLevelRetryError{}

// NoLowLevelRetryError makes an error which indicates the sync
// shouldn't be low level retried.
func NoLowLevelRetryError(err error) error {
	return wrappedNoLowLevelRetryError{err}
}

// Unwrap returns the underlying error
func (err wrappedNoLowLevelRetryError) Unwrap() error {
	return err.error
}

// IsNoLowLevelRetryError returns true if err conforms to the NoLowLevelRetry
// interface and calling the NoLowLevelRetry method returns true.
func IsNoLowLevelRetryError(err error) (isNoLowLevelRetry bool) {
	liberrors.Walk(err, func(err error) bool {
		if r, ok := err.(NoLowLevelRetrier); ok {
			isNoLowLevelRetry = r.NoLowLevelRetry()
			return true
		}
		return false
	})
	return
}

// RetryAfter is an optional interface for error as to whether the
// operation should be retried after a given delay
//
// This should be returned from Update or Put methods as required and
// will cause the entire sync to be retried after a delay.
type RetryAfter interface {
	error
	RetryAfter() time.Time
}

// ErrorRetryAfter is an error which expresses a time that should be
// waited for until trying again
type ErrorRetryAfter time.Time

// NewErrorRetryAfter returns an ErrorRetryAfter with the given
// duration as an endpoint
func NewErrorRetryAfter(d time.Duration) ErrorRetryAfter {
	return ErrorRetryAfter(time.Now().Add(d))
}

// Error returns the textual version of the error
func (e ErrorRetryAfter) Error() string {
	return fmt.Sprintf("try again after %v (%v)", time.Time(e).Format(time.RFC3339Nano), time.Until(time.Time(e)))
}

// RetryAfter returns the time the operation should be retried at or
// after
func (e ErrorRetryAfter) RetryAfter() time.Time {
	return time.Time(e)
}

// Check interfaces
var _ RetryAfter = ErrorRetryAfter{}

// RetryAfterErrorTime returns the time that the RetryAfter error
// indicates or a Zero time.Time
func RetryAfterErrorTime(err error) (retryAfter time.Time) {
	liberrors.Walk(err, func(err error) bool {
		if r, ok := err.(RetryAfter); ok {
			retryAfter = r.RetryAfter()
			return true
		}
		return false
	})
	return
}

// IsRetryAfterError returns true if err is an ErrorRetryAfter
func IsRetryAfterError(err error) bool {
	return !RetryAfterErrorTime(err).IsZero()
}

// CountableError is an optional interface for error. It stores a boolean
// which signifies if the error has already been counted or not
type CountableError interface {
	error
	Count()
	IsCounted() bool
}

// wrappedCountableError is an error wrapped so it will satisfy the
// Retrier interface and return true
type wrappedCountableError struct {
	error
	isCounted bool
}

// CountableError interface
func (err *wrappedCountableError) Count() {
	err.isCounted = true
}

// CountableError interface
func (err *wrappedCountableError) IsCounted() bool {
	return err.isCounted
}

func (err wrappedCountableError) Unwrap() error {
	return err.error
}

// IsCounted returns true if err conforms to the CountableError interface
// and has already been counted
func IsCounted(err error) bool {
	if r, ok := err.(CountableError); ok {
		return r.IsCounted()
	}
	return false
}

// Count sets the isCounted variable on the error if it conforms to the
// CountableError interface
func Count(err error) {
	if r, ok := err.(CountableError); ok {
		r.Count()
	}
}

// Check interface
var _ CountableError = &wrappedCountableError{error: error(nil)}
var _ unwrapper = wrappedCountableError{}

// FsError makes an error which can keep a record that it is already counted
// or not
func FsError(err error) error {
	if err == nil {
		err = errors.New("countable error")
	}
	return &wrappedCountableError{error: err}
}

// Cause is a souped up errors.Cause which can unwrap some standard
// library errors too.  It returns true if any of the intermediate
// errors had a Timeout() or Temporary() method which returned true.
func Cause(cause error) (retriable bool, err error) {
	liberrors.Walk(cause, func(c error) bool {
		// Check for net error Timeout()
		if x, ok := c.(interface {
			Timeout() bool
		}); ok && x.Timeout() {
			retriable = true
		}

		// Check for net error Temporary()
		if x, ok := c.(interface {
			Temporary() bool
		}); ok && x.Temporary() {
			retriable = true
		}
		err = c
		return false
	})
	return
}

// retriableErrorStrings is a list of phrases which when we find it
// in an error, we know it is a networking error which should be
// retried.
//
// This is incredibly ugly - if only errors.Cause worked for all
// errors and all errors were exported from the stdlib.
var retriableErrorStrings = []string{
	"use of closed network connection", // internal/poll/fd.go
	"unexpected EOF reading trailer",   // net/http/transfer.go
	"transport connection broken",      // net/http/transport.go
	"http: ContentLength=",             // net/http/transfer.go
	"server closed idle connection",    // net/http/transport.go
	"bad record MAC",                   // crypto/tls/alert.go
	"stream error:",                    // net/http/h2_bundle.go
	"tls: use of closed connection",    // crypto/tls/conn.go
}

// Errors which indicate networking errors which should be retried
//
// These are added to in retriable_errors*.go
var retriableErrors = []error{
	io.EOF,
	io.ErrUnexpectedEOF,
}

// ShouldRetry looks at an error and tries to work out if retrying the
// operation that caused it would be a good idea. It returns true if
// the error implements Timeout() or Temporary() or if the error
// indicates a premature closing of the connection.
func ShouldRetry(err error) bool {
	if err == nil {
		return false
	}

	// If error has been marked to NoLowLevelRetry then don't retry
	if IsNoLowLevelRetryError(err) {
		return false
	}

	// Find root cause if available
	retriable, err := Cause(err)
	if retriable {
		return true
	}

	// Check if it is a retriable error
	if slices.Contains(retriableErrors, err) {
		return true
	}

	// Check error strings (yuch!) too
	errString := err.Error()
	for _, phrase := range retriableErrorStrings {
		if strings.Contains(errString, phrase) {
			return true
		}
	}

	return false
}

// ShouldRetryHTTP returns a boolean as to whether this resp deserves.
// It checks to see if the HTTP response code is in the slice
// retryErrorCodes.
func ShouldRetryHTTP(resp *http.Response, retryErrorCodes []int) bool {
	if resp == nil {
		return false
	}
	return slices.Contains(retryErrorCodes, resp.StatusCode)
}

// ContextError checks to see if ctx is in error.
//
// If it is in error then it overwrites *perr with the context error
// if *perr was nil and returns true.
//
// Otherwise it returns false.
func ContextError(ctx context.Context, perr *error) bool {
	if ctxErr := ctx.Err(); ctxErr != nil {
		if *perr == nil {
			*perr = ctxErr
		}
		return true
	}
	return false
}
