package fetcher

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"

	"github.com/imgproxy/imgproxy/v3/errctx"
	"github.com/imgproxy/imgproxy/v3/fetcher/transport/generichttp"
)

const msgSourceIsUnreachable = "Source is unreachable"

type (
	RequestError          struct{ *errctx.WrappedError }
	RequstSchemeError     struct{ *errctx.TextError }
	PartialResponseError  struct{ *errctx.TextError }
	ResponseStatusError   struct{ *errctx.TextError }
	TooManyRedirectsError struct{ *errctx.TextError }
	RequestCanceledError  struct{ *errctx.WrappedError }
	RequestTimeoutError   struct{ *errctx.WrappedError }

	NotModifiedError struct {
		*errctx.TextError

		headers http.Header
	}
)

func newRequestError(err error) error {
	return RequestError{errctx.NewWrappedError(
		err,
		1,
		errctx.WithStatusCode(http.StatusNotFound),
		errctx.WithPublicMessage(msgSourceIsUnreachable),
		errctx.WithShouldReport(false),
	)}
}

var schemeEnvVars = map[string]string{
	"local": "IMGPROXY_LOCAL_FILESYSTEM_ROOT",
	"s3":    "IMGPROXY_USE_S3",
	"gs":    "IMGPROXY_USE_GCS",
	"abs":   "IMGPROXY_USE_ABS",
	"swift": "IMGPROXY_USE_SWIFT",
}

const schemeDocsUrl = "https://docs.imgproxy.net/configuration/options#"

func newRequestSchemeError(scheme string) error {
	msg := fmt.Sprintf("unknown scheme: %s", scheme)

	opts := []errctx.Option{
		errctx.WithStatusCode(http.StatusNotFound),
		errctx.WithPublicMessage(msgSourceIsUnreachable),
		errctx.WithShouldReport(false),
	}

	if envVar, ok := schemeEnvVars[scheme]; ok {
		msg = fmt.Sprintf("unknown scheme: %s (set %s to enable)", scheme, envVar)
		opts = append(opts, errctx.WithDocsURL(schemeDocsUrl+envVar))
	}

	return RequstSchemeError{errctx.NewTextError(msg, 1, opts...)}
}

func newPartialResponseError(msg string) error {
	return PartialResponseError{errctx.NewTextError(
		msg,
		1,
		errctx.WithStatusCode(http.StatusNotFound),
		errctx.WithPublicMessage(msgSourceIsUnreachable),
		errctx.WithShouldReport(false),
	)}
}

func newResponseStatusError(status int, body string) error {
	var msg string

	if len(body) > 0 {
		msg = fmt.Sprintf("status: %d; %s", status, body)
	} else {
		msg = fmt.Sprintf("status: %d", status)
	}

	statusCode := 404
	if status >= 400 && status < 500 {
		statusCode = status
	} else if status >= 500 {
		statusCode = http.StatusBadGateway
	}

	return ResponseStatusError{errctx.NewTextError(
		msg,
		1,
		errctx.WithStatusCode(statusCode),
		errctx.WithPublicMessage(msgSourceIsUnreachable),
		errctx.WithShouldReport(false),
	)}
}

func newTooManyRedirectsError(n int) error {
	return TooManyRedirectsError{errctx.NewTextError(
		fmt.Sprintf("stopped after %d redirects", n),
		1,
		errctx.WithStatusCode(http.StatusNotFound),
		errctx.WithPublicMessage(msgSourceIsUnreachable),
		errctx.WithShouldReport(false),
	)}
}

func newRequestCanceledError(err error) error {
	return RequestCanceledError{errctx.NewWrappedError(
		err,
		2,
		errctx.WithPrefix("source request is cancelled"),
		errctx.WithStatusCode(499),
		errctx.WithPublicMessage(msgSourceIsUnreachable),
		errctx.WithShouldReport(false),
	)}
}

func newRequestTimeoutError(err error) error {
	return RequestTimeoutError{errctx.NewWrappedError(
		err,
		2,
		errctx.WithPrefix("source request timed out"),
		errctx.WithStatusCode(http.StatusGatewayTimeout),
		errctx.WithPublicMessage(msgSourceIsUnreachable),
		errctx.WithShouldReport(false),
	)}
}

func newNotModifiedError(headers http.Header) error {
	return NotModifiedError{
		errctx.NewTextError(
			"not modified",
			1,
			errctx.WithStatusCode(http.StatusNotModified),
			errctx.WithPublicMessage("Not modified"),
			errctx.WithShouldReport(false),
		),
		headers,
	}
}

func (e NotModifiedError) Headers() http.Header {
	return e.headers
}

// WrapError wraps generic error into fetcher-specific error types
func WrapError(err error, skipStack int) error {
	type httpError interface {
		Timeout() bool
	}

	var srcErr generichttp.SourceAddressError

	switch {
	case errors.Is(err, context.DeadlineExceeded):
		return newRequestTimeoutError(err)
	case errors.Is(err, context.Canceled):
		return newRequestCanceledError(err)
	case errors.Is(err, io.ErrUnexpectedEOF):
		return PartialResponseError{errctx.NewTextError(
			"response is incomplete",
			1,
			errctx.WithStatusCode(http.StatusUnprocessableEntity),
			errctx.WithPublicMessage("Source response is incomplete"),
			errctx.WithShouldReport(false),
		)}
	case errors.As(err, &srcErr):
		return srcErr
	default:
		if httpErr, ok := err.(httpError); ok && httpErr.Timeout() {
			return newRequestTimeoutError(err)
		}

		//nolint:errorlint // Check for *url.Error itself, not wrapped error
		if _, ok := err.(*url.Error); ok {
			return newRequestError(err)
		}
	}

	return errctx.WrapWithStackSkip(err, skipStack+1)
}
