package errors

import (
	"errors"
	"fmt"
	"log/slog"
)

var (
	codeUnknown Code = MustNewCode("unknown")
)

// base is the fundamental struct that implements the error interface.
// The order of the struct is 'TCMEUA'.
type base struct {
	// t denotes the custom type of the error.
	t typ
	// c denotes the short code for the error message.
	c Code
	// m contains error message passed through errors.New.
	m string
	// e is the actual error being wrapped.
	e error
	// u denotes the url for the documentation (if present) for the error.
	u string
	// a denotes any additional error messages (if present).
	a []string
}

func (b *base) LogValue() slog.Value {
	return slog.GroupValue(
		slog.String("type", b.t.s),
		slog.String("code", b.c.s),
		slog.String("message", b.m),
		slog.String("url", b.u),
		slog.Any("additional", b.a),
	)
}

// base implements Error interface.
func (b *base) Error() string {
	if b.e != nil {
		return b.e.Error()
	}

	return fmt.Sprintf("%s(%s): %s", b.t.s, b.c, b.m)
}

// New returns a base error. It requires type, code and message as input.
func New(t typ, code Code, message string) *base {
	return &base{
		t: t,
		c: code,
		m: message,
		e: nil,
		u: "",
		a: []string{},
	}
}

// Newf returns a new base by formatting the error message with the supplied format specifier.
func Newf(t typ, code Code, format string, args ...interface{}) *base {
	return &base{
		t: t,
		c: code,
		m: fmt.Sprintf(format, args...),
		e: nil,
	}
}

// Wrapf returns a new error by formatting the error message with the supplied format specifier
// and wrapping another error with base.
func Wrapf(cause error, t typ, code Code, format string, args ...interface{}) *base {
	return &base{
		t: t,
		c: code,
		m: fmt.Sprintf(format, args...),
		e: cause,
	}
}

// WithUrl adds a url to the base error and returns a new base error.
func (b *base) WithUrl(u string) *base {
	return &base{
		t: b.t,
		c: b.c,
		m: b.m,
		e: b.e,
		u: u,
		a: b.a,
	}
}

// WithUrl adds additional messages to the base error and returns a new base error.
func (b *base) WithAdditional(a ...string) *base {
	return &base{
		t: b.t,
		c: b.c,
		m: b.m,
		e: b.e,
		u: b.u,
		a: a,
	}
}

// Unwrapb is a combination of built-in errors.As and type casting.
// It finds the first error in cause that matches base,
// and if one is found, returns the individual fields of base.
// Otherwise, it returns TypeInternal, the original error string
// and the error itself.
//
//lint:ignore ST1008 we want to return arguments in the 'TCMEUA' order of the struct
func Unwrapb(cause error) (typ, Code, string, error, string, []string) {
	base, ok := cause.(*base)
	if ok {
		return base.t, base.c, base.m, base.e, base.u, base.a
	}

	return TypeInternal, codeUnknown, cause.Error(), cause, "", []string{}
}

// Ast checks if the provided error matches the specified custom error type.
func Ast(cause error, typ typ) bool {
	t, _, _, _, _, _ := Unwrapb(cause)

	return t == typ
}

// Ast checks if the provided error matches the specified custom error code.
func Asc(cause error, code Code) bool {
	_, c, _, _, _, _ := Unwrapb(cause)

	return c.s == code.s
}

// Join is a wrapper around errors.Join.
func Join(errs ...error) error {
	return errors.Join(errs...)
}

func As(err error, target any) bool {
	return errors.As(err, target)
}

func Is(err error, target error) bool {
	return errors.Is(err, target)
}

func WrapNotFoundf(cause error, code Code, format string, args ...interface{}) *base {
	return Wrapf(cause, TypeNotFound, code, format, args...)
}

func NewNotFoundf(code Code, format string, args ...interface{}) *base {
	return Newf(TypeNotFound, code, format, args...)
}

func WrapInternalf(cause error, code Code, format string, args ...interface{}) *base {
	return Wrapf(cause, TypeInternal, code, format, args...)
}

func NewInternalf(code Code, format string, args ...interface{}) *base {
	return Newf(TypeInternal, code, format, args...)
}

func WrapInvalidInputf(cause error, code Code, format string, args ...interface{}) *base {
	return Wrapf(cause, TypeInvalidInput, code, format, args...)
}

func NewInvalidInputf(code Code, format string, args ...interface{}) *base {
	return Newf(TypeInvalidInput, code, format, args...)
}
