package errors

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"

	"github.com/kataras/iris/v12/context"
	"github.com/kataras/iris/v12/x/client"
)

// LogErrorFunc is an alias of a function type which accepts the Iris request context and an error
// and it's fired whenever an error should be logged.
//
// See "OnErrorLog" variable to change the way an error is logged,
// by default the error is logged using the Application's Logger's Error method.
type LogErrorFunc = func(ctx *context.Context, err error)

// LogError can be modified to customize the way an error is logged to the server (most common: internal server errors, database errors et.c.).
// Can be used to customize the error logging, e.g. using Sentry (cloud-based error console).
var LogError LogErrorFunc = func(ctx *context.Context, err error) {
	ctx.Application().Logger().Error(err)
}

// SkipCanceled is a package-level setting which by default
// skips the logging of a canceled response or operation.
// See the "Context.IsCanceled()" method and "iris.IsCanceled()" function
// that decide if the error is caused by a canceled operation.
//
// Change of this setting MUST be done on initialization of the program.
var SkipCanceled = true

type (
	// ErrorCodeName is a custom string type represents canonical error names.
	//
	// It contains functionality for safe and easy error populating.
	// See its "Message", "Details", "Data" and "Log" methods.
	ErrorCodeName string

	// ErrorCode represents the JSON form ErrorCode of the Error.
	ErrorCode struct {
		CanonicalName ErrorCodeName `json:"canonical_name" yaml:"CanonicalName"`
		Status        int           `json:"status" yaml:"Status"`
	}
)

// A read-only map of valid http error codes.
var errorCodeMap = make(map[ErrorCodeName]ErrorCode)

// Deprecated: Use Register instead.
var E = Register

// Register registers a custom HTTP Error and returns its canonical name for future use.
// The method "New" is reserved and was kept as it is for compatibility
// with the standard errors package, therefore the "Register" name was chosen instead.
// The key stroke "e" is near and accessible while typing the "errors" word
// so developers may find it easy to use.
//
// See "RegisterErrorCode" and "RegisterErrorCodeMap" for alternatives.
//
// Example:
//
//		var (
//	   		NotFound = errors.Register("NOT_FOUND", http.StatusNotFound)
//		)
//		...
//		NotFound.Details(ctx, "resource not found", "user with id: %q was not found", userID)
//
// This method MUST be called on initialization, before HTTP server starts as
// the internal map is not protected by mutex.
func Register(httpErrorCanonicalName string, httpStatusCode int) ErrorCodeName {
	canonicalName := ErrorCodeName(httpErrorCanonicalName)
	RegisterErrorCode(canonicalName, httpStatusCode)
	return canonicalName
}

// RegisterErrorCode registers a custom HTTP Error.
//
// This method MUST be called on initialization, before HTTP server starts as
// the internal map is not protected by mutex.
func RegisterErrorCode(canonicalName ErrorCodeName, httpStatusCode int) {
	errorCodeMap[canonicalName] = ErrorCode{
		CanonicalName: canonicalName,
		Status:        httpStatusCode,
	}
}

// RegisterErrorCodeMap registers one or more custom HTTP Errors.
//
// This method MUST be called on initialization, before HTTP server starts as
// the internal map is not protected by mutex.
func RegisterErrorCodeMap(errorMap map[ErrorCodeName]int) {
	if len(errorMap) == 0 {
		return
	}

	for canonicalName, httpStatusCode := range errorMap {
		RegisterErrorCode(canonicalName, httpStatusCode)
	}
}

// List of default error codes a server should follow and send back to the client.
var (
	Cancelled          ErrorCodeName = Register("CANCELLED", context.StatusTokenRequired)
	Unknown            ErrorCodeName = Register("UNKNOWN", http.StatusInternalServerError)
	InvalidArgument    ErrorCodeName = Register("INVALID_ARGUMENT", http.StatusBadRequest)
	DeadlineExceeded   ErrorCodeName = Register("DEADLINE_EXCEEDED", http.StatusGatewayTimeout)
	NotFound           ErrorCodeName = Register("NOT_FOUND", http.StatusNotFound)
	AlreadyExists      ErrorCodeName = Register("ALREADY_EXISTS", http.StatusConflict)
	PermissionDenied   ErrorCodeName = Register("PERMISSION_DENIED", http.StatusForbidden)
	Unauthenticated    ErrorCodeName = Register("UNAUTHENTICATED", http.StatusUnauthorized)
	ResourceExhausted  ErrorCodeName = Register("RESOURCE_EXHAUSTED", http.StatusTooManyRequests)
	FailedPrecondition ErrorCodeName = Register("FAILED_PRECONDITION", http.StatusBadRequest)
	Aborted            ErrorCodeName = Register("ABORTED", http.StatusConflict)
	OutOfRange         ErrorCodeName = Register("OUT_OF_RANGE", http.StatusBadRequest)
	Unimplemented      ErrorCodeName = Register("UNIMPLEMENTED", http.StatusNotImplemented)
	Internal           ErrorCodeName = Register("INTERNAL", http.StatusInternalServerError)
	Unavailable        ErrorCodeName = Register("UNAVAILABLE", http.StatusServiceUnavailable)
	DataLoss           ErrorCodeName = Register("DATA_LOSS", http.StatusInternalServerError)
)

// errorFuncCodeMap is a read-only map of error code names and their error functions.
// See HandleError package-level function.
var errorFuncCodeMap = make(map[ErrorCodeName][]func(error) error)

// HandleError handles an error by sending it to the client
// based on the registered error code names and their error functions.
// Returns true if the error was handled, otherwise false.
// If the given "err" is nil then it returns false.
// If the given "err" is a type of validation error then it sends it to the client
// using the "Validation" method.
// If the given "err" is a type of client.APIError then it sends it to the client
// using the "HandleAPIError" function.
//
// See ErrorCodeName.MapErrorFunc and MapErrors methods too.
func HandleError(ctx *context.Context, err error) bool {
	if err == nil {
		return false
	}

	if ctx.IsStopped() {
		return false
	}

	for errorCodeName, errorFuncs := range errorFuncCodeMap {
		for _, errorFunc := range errorFuncs {
			if errToSend := errorFunc(err); errToSend != nil {
				errorCodeName.Err(ctx, errToSend)
				return true
			}
		}
	}

	// Unwrap and collect the errors slice so the error result doesn't contain the ErrorCodeName type
	// and fire the error status code and title based on this error code name itself.
	var asErrCode ErrorCodeName
	if As(err, &asErrCode) {
		if unwrapJoined, ok := err.(joinedErrors); ok {
			errs := unwrapJoined.Unwrap()
			errsToKeep := make([]error, 0, len(errs)-1)
			for _, src := range errs {
				if _, isErrorCodeName := src.(ErrorCodeName); !isErrorCodeName {
					errsToKeep = append(errsToKeep, src)
				}
			}

			if len(errsToKeep) > 0 {
				err = errors.Join(errsToKeep...)
			}
		}

		asErrCode.Err(ctx, err)
		return true
	}

	if handleJSONError(ctx, err) {
		return true
	}

	if vErr, ok := err.(ValidationError); ok {
		if vErr == nil {
			return false // consider as not error for any case, this should never happen.
		}

		InvalidArgument.Validation(ctx, vErr)
		return true
	}

	if vErrs, ok := err.(ValidationErrors); ok {
		if len(vErrs) == 0 {
			return false // consider as not error for any case, this should never happen.
		}

		InvalidArgument.Validation(ctx, vErrs...)
		return true
	}

	if apiErr, ok := client.GetError(err); ok {
		handleAPIError(ctx, apiErr)
		return true
	}

	Internal.LogErr(ctx, err)
	return true
}

// Error returns an empty string, it is only declared as a method of ErrorCodeName type in order
// to be a compatible error to be joined within other errors:
//
//	err = fmt.Errorf("%w%w", errors.InvalidArgument, err) OR
//	err = errors.InvalidArgument.Wrap(err)
func (e ErrorCodeName) Error() string {
	return ""
}

type joinedErrors interface{ Unwrap() []error }

// Wrap wraps the given error with this ErrorCodeName.
// It calls the standard errors.Join package-level function.
// See HandleError function for more.
func (e ErrorCodeName) Wrap(err error) error {
	return errors.Join(e, err)
}

// MapErrorFunc registers a function which will validate the incoming error and
// return the same error or overriden in order to be sent to the client, wrapped by this ErrorCodeName "e".
//
// This method MUST be called on initialization, before HTTP server starts as
// the internal map is not protected by mutex.
//
// Example Code:
//
//	errors.InvalidArgument.MapErrorFunc(func(err error) error {
//		stripeErr, ok := err.(*stripe.Error)
//		if !ok {
//			return nil
//		}
//
//		return &errors.Error{
//			Message: stripeErr.Msg,
//			Details: stripeErr.DocURL,
//		}
//	})
func (e ErrorCodeName) MapErrorFunc(fn func(error) error) {
	errorFuncCodeMap[e] = append(errorFuncCodeMap[e], fn)
}

// MapError registers one or more errors which will be sent to the client wrapped by this "e" ErrorCodeName
// when the incoming error matches to at least one of the given "targets" one.
//
// This method MUST be called on initialization, before HTTP server starts as
// the internal map is not protected by mutex.
func (e ErrorCodeName) MapErrors(targets ...error) {
	e.MapErrorFunc(func(err error) error {
		for _, target := range targets {
			if Is(err, target) {
				return err
			}
		}

		return nil
	})
}

// Message sends an error with a simple message to the client.
func (e ErrorCodeName) Message(ctx *context.Context, format string, args ...interface{}) {
	fail(ctx, e, sprintf(format, args...), "", nil, nil)
}

// Details sends an error with a message and details to the client.
func (e ErrorCodeName) Details(ctx *context.Context, msg, details string, detailsArgs ...interface{}) {
	fail(ctx, e, msg, sprintf(details, detailsArgs...), nil, nil)
}

// Data sends an error with a message and json data to the client.
func (e ErrorCodeName) Data(ctx *context.Context, msg string, data interface{}) {
	fail(ctx, e, msg, "", nil, data)
}

// DataWithDetails sends an error with a message, details and json data to the client.
func (e ErrorCodeName) DataWithDetails(ctx *context.Context, msg, details string, data interface{}) {
	fail(ctx, e, msg, details, nil, data)
}

// Validation sends an error which renders the invalid fields to the client.
func (e ErrorCodeName) Validation(ctx *context.Context, validationErrors ...ValidationError) {
	e.validation(ctx, validationErrors)
}

func (e ErrorCodeName) validation(ctx *context.Context, validationErrors interface{}) {
	fail(ctx, e, "validation failure", "fields were invalid", validationErrors, nil)
}

// Err sends the error's text as a message to the client.
// In exception, if the given "err" is a type of validation error
// then the Validation method is called instead.
func (e ErrorCodeName) Err(ctx *context.Context, err error) {
	if err == nil {
		return
	}

	if vErr, ok := err.(ValidationError); ok {
		if vErr == nil {
			return // consider as not error for any case, this should never happen.
		}

		e.Validation(ctx, vErr)
	}

	if vErrs, ok := err.(ValidationErrors); ok {
		if len(vErrs) == 0 {
			return // consider as not error for any case, this should never happen.
		}

		e.Validation(ctx, vErrs...)
	}

	// If it's already an Error type then send it directly.
	if httpErr, ok := err.(*Error); ok {
		if errorCode, ok := errorCodeMap[e]; ok {
			httpErr.ErrorCode = errorCode
			ctx.StopWithJSON(errorCode.Status, httpErr) // here we override the fail function and send the error as it is.
			return
		}
	}

	e.Message(ctx, err.Error())
}

// Log sends an error of "format" and optional "args" to the client and prints that
// error using the "LogError" package-level function, which can be customized.
//
// See "LogErr" too.
func (e ErrorCodeName) Log(ctx *context.Context, format string, args ...interface{}) {
	if SkipCanceled {
		if ctx.IsCanceled() {
			return
		}

		for _, arg := range args {
			if err, ok := arg.(error); ok {
				if context.IsErrCanceled(err) {
					return
				}
			}
		}
	}

	err := fmt.Errorf(format, args...)
	e.LogErr(ctx, err)
}

// LogErr sends the given "err" as message to the client and prints that
// error to using the "LogError" package-level function, which can be customized.
func (e ErrorCodeName) LogErr(ctx *context.Context, err error) {
	if SkipCanceled && (ctx.IsCanceled() || context.IsErrCanceled(err)) {
		return
	}

	LogError(ctx, err)

	e.Message(ctx, "server error")
}

// HandleAPIError handles remote server errors.
// Optionally, use it when you write your server's HTTP clients using the the /x/client package.
// When the HTTP Client sends data to a remote server but that remote server
// failed to accept the request as expected, then the error will be proxied
// to this server's end-client.
//
// When the given "err" is not a type of client.APIError then
// the error will be sent using the "Internal.LogErr" method which sends
// HTTP internal server error to the end-client and
// prints the "err" using the "LogError" package-level function.
func HandleAPIError(ctx *context.Context, err error) {
	// Error expected and came from the external server,
	// save its body so we can forward it to the end-client.
	if apiErr, ok := client.GetError(err); ok {
		handleAPIError(ctx, apiErr)
		return
	}

	Internal.LogErr(ctx, err)
}

func handleAPIError(ctx *context.Context, apiErr client.APIError) {
	// Error expected and came from the external server,
	// save its body so we can forward it to the end-client.
	statusCode := apiErr.Response.StatusCode
	if statusCode >= 400 && statusCode < 500 {
		InvalidArgument.DataWithDetails(ctx, "remote server error", "invalid client request", apiErr.Body)
	} else {
		Internal.Data(ctx, "remote server error", apiErr.Body)
	}

	// Unavailable.DataWithDetails(ctx, "remote server error", "unavailable", apiErr.Body)
}

func handleJSONError(ctx *context.Context, err error) bool {
	var syntaxErr *json.SyntaxError
	if errors.As(err, &syntaxErr) {
		InvalidArgument.Details(ctx, "unable to parse body", "syntax error at byte offset %d", syntaxErr.Offset)
		return true
	}

	var unmarshalErr *json.UnmarshalTypeError
	if errors.As(err, &unmarshalErr) {
		InvalidArgument.Details(ctx, "unable to parse body", "unmarshal error for field %q", unmarshalErr.Field)
		return true
	}

	return false
	// else {
	// 	InvalidArgument.Details(ctx, "unable to parse body", err.Error())
	// }
}

var (
	// ErrUnexpected is the HTTP error which sent to the client
	// when server fails to send an error, it's a fallback error.
	// The server fails to send an error on two cases:
	// 1. when the provided error code name is not registered (the error value is the ErrUnexpectedErrorCode)
	// 2. when the error contains data but cannot be encoded to json (the value of the error is the result error of json.Marshal).
	ErrUnexpected = Register("UNEXPECTED_ERROR", http.StatusInternalServerError)
	// ErrUnexpectedErrorCode is the error which logged
	// when the given error code name is not registered.
	ErrUnexpectedErrorCode = New("unexpected error code name")
)

// Error represents the JSON form of "http wire errors".
//
// Examples can be found at:
//
//	https://github.com/kataras/iris/tree/main/_examples/routing/http-wire-errors.
type Error struct {
	ErrorCode  ErrorCode       `json:"http_error_code" yaml:"HTTPErrorCode"`
	Message    string          `json:"message,omitempty" yaml:"Message"`
	Details    string          `json:"details,omitempty" yaml:"Details"`
	Validation interface{}     `json:"validation,omitempty" yaml:"Validation,omitempty"`
	Data       json.RawMessage `json:"data,omitempty" yaml:"Data,omitempty"` // any other custom json data.
}

// Error method completes the error interface. It just returns the canonical name, status code, message and details.
func (err *Error) Error() string {
	if err.Message == "" {
		err.Message = "<empty>"
	}

	if err.Details == "" {
		err.Details = "<empty>"
	}

	if err.ErrorCode.CanonicalName == "" {
		err.ErrorCode.CanonicalName = ErrUnexpected
	}

	if err.ErrorCode.Status <= 0 {
		err.ErrorCode.Status = http.StatusInternalServerError
	}

	return sprintf("iris http wire error: canonical name: %s, http status code: %d, message: %s, details: %s", err.ErrorCode.CanonicalName, err.ErrorCode.Status, err.Message, err.Details)
}

func fail(ctx *context.Context, codeName ErrorCodeName, msg, details string, validationErrors interface{}, dataValue interface{}) {
	errorCode, ok := errorCodeMap[codeName]
	if !ok {
		// This SHOULD NEVER happen, all ErrorCodeNames MUST be registered.
		LogError(ctx, ErrUnexpectedErrorCode)
		fail(ctx, ErrUnexpected, msg, details, validationErrors, dataValue)
		return
	}

	var data json.RawMessage
	if dataValue != nil {
		switch v := dataValue.(type) {
		case json.RawMessage:
			data = v
		case []byte:
			data = v
		case error:
			if msg == "" {
				msg = v.Error()
			} else if details == "" {
				details = v.Error()
			} else {
				data = json.RawMessage(v.Error())
			}
		default:
			b, err := json.Marshal(v)
			if err != nil {
				LogError(ctx, err)
				fail(ctx, ErrUnexpected, err.Error(), "", nil, nil)
				return
			}
			data = b

		}
	}

	err := Error{
		ErrorCode:  errorCode,
		Message:    msg,
		Details:    details,
		Data:       data,
		Validation: validationErrors,
	}

	// ctx.SetErr(&err)
	ctx.StopWithJSON(errorCode.Status, err)
}
