package exerr

import (
	"fmt"
	"github.com/rs/xid"
	"github.com/rs/zerolog"
	"gogs.mikescher.com/BlackForestBytes/goext/langext"
	"reflect"
	"strings"
	"time"
)

type ExErr struct {
	UniqueID string `json:"uniqueID"`

	Timestamp time.Time     `json:"timestamp"`
	Category  ErrorCategory `json:"category"`
	Severity  ErrorSeverity `json:"severity"`
	Type      ErrorType     `json:"type"`

	StatusCode *int `json:"statusCode"`

	Message        string `json:"message"`
	WrappedErrType string `json:"wrappedErrType"`
	WrappedErr     any    `json:"-"`
	Caller         string `json:"caller"`

	OriginalError *ExErr `json:"originalError"`

	Extra map[string]any `json:"extra"`
	Meta  MetaMap        `json:"meta"`
}

func (ee *ExErr) Error() string {
	return ee.RecursiveMessage()
}

// Unwrap must be implemented so that some error.XXX methods work
func (ee *ExErr) Unwrap() error {
	if ee.OriginalError == nil {

		if ee.WrappedErr != nil {
			if werr, ok := ee.WrappedErr.(error); ok {
				return werr
			}
		}

		return nil // this is neccessary - otherwise we return a wrapped nil and the `x == nil` comparison fails (= panic in errors.Is and other failures)
	}
	return ee.OriginalError
}

// Is must be implemented so that error.Is(x) works
func (ee *ExErr) Is(e error) bool {
	return IsFrom(ee, e)
}

// As must be implemented so that error.As(x) works
//
//goland:noinspection GoTypeAssertionOnErrors
func (ee *ExErr) As(target any) bool {
	if dstErr, ok := target.(*ExErr); ok {

		if dst0, ok := ee.contains(dstErr); ok {
			dstErr = dst0
			return true
		} else {
			return false
		}

	} else {

		val := reflect.ValueOf(target)

		typStr := val.Type().Elem().String()

		for curr := ee; curr != nil; curr = curr.OriginalError {
			if curr.Category == CatForeign && curr.WrappedErrType == typStr && curr.WrappedErr != nil {
				val.Elem().Set(reflect.ValueOf(curr.WrappedErr))
				return true
			}
		}

		return false
	}
}

func (ee *ExErr) Log(evt *zerolog.Event) {
	evt.Msg(ee.FormatLog(LogPrintFull))
}

func (ee *ExErr) FormatLog(lvl LogPrintLevel) string {

	// [LogPrintShort]
	//
	// - Only print message and type
	// - Used e.g. for logging to the console when Build is called
	// - also used in Print() if level == Warn/Info
	//
	// [LogPrintOverview]
	//
	// - print message, extra and errortrace
	//
	// [LogPrintFull]
	//
	// - print full error, with meta and extra, and trace, etc
	// - Used in Output() and Print()
	//

	if lvl == LogPrintShort {

		msg := ee.Message
		if msg == "" {
			msg = ee.RecursiveMessage()
		}
		if ee.OriginalError != nil && ee.OriginalError.Category == CatForeign {
			msg = msg + " (" + strings.ReplaceAll(ee.OriginalError.Message, "\n", " ") + ")"
		}

		if ee.Type != TypeWrap {
			return "[" + ee.Type.Key + "] " + msg
		} else {
			return msg
		}

	} else if lvl == LogPrintOverview {

		str := "[" + ee.RecursiveType().Key + "] <" + ee.UniqueID + "> " + strings.ReplaceAll(ee.RecursiveMessage(), "\n", " ") + "\n"

		for exk, exv := range ee.Extra {
			str += fmt.Sprintf(" # [[[ %s ==> %v ]]]\n", exk, exv)
		}

		indent := ""
		for curr := ee; curr != nil; curr = curr.OriginalError {
			indent += "  "

			str += indent
			str += "-> "
			strmsg := strings.Trim(curr.Message, " \r\n\t")
			if lbidx := strings.Index(curr.Message, "\n"); lbidx >= 0 {
				strmsg = strmsg[0:lbidx]
			}
			strmsg = langext.StrLimit(strmsg, 61, "...")
			str += strmsg
			str += "\n"

		}
		return str

	} else if lvl == LogPrintFull {

		str := "[" + ee.RecursiveType().Key + "] <" + ee.UniqueID + "> " + strings.ReplaceAll(ee.RecursiveMessage(), "\n", " ") + "\n"

		for exk, exv := range ee.Extra {
			str += fmt.Sprintf(" # [[[ %s ==> %v ]]]\n", exk, exv)
		}

		indent := ""
		for curr := ee; curr != nil; curr = curr.OriginalError {
			indent += "  "

			etype := ee.Type.Key
			if ee.Type == TypeWrap {
				etype = "~"
			}

			str += indent
			str += "-> ["
			str += etype
			if curr.Category == CatForeign {
				str += "|Foreign"
			}
			str += "] "
			str += strings.ReplaceAll(curr.Message, "\n", " ")
			if curr.Caller != "" {
				str += " (@ "
				str += curr.Caller
				str += ")"
			}
			str += "\n"

			if curr.Meta.Any() {
				meta := indent + "   {" + curr.Meta.FormatOneLine(240) + "}"
				if len(meta) < 200 {
					str += meta
					str += "\n"
				} else {
					str += curr.Meta.FormatMultiLine(indent+"   ", "  ", 1024)
					str += "\n"
				}
			}
		}
		return str

	} else {

		return "[?[" + ee.UniqueID + "]?]"

	}
}

func (ee *ExErr) ShortLog(evt *zerolog.Event) {
	ee.Meta.Apply(evt, langext.Ptr(240)).Msg(ee.FormatLog(LogPrintShort))
}

// RecursiveMessage returns the message to show
// = first error (top-down) that is not wrapping/foreign/empty
// = lowest level error (that is not empty)
// = fallback to self.message
func (ee *ExErr) RecursiveMessage() string {

	// ==== [1] ==== first error (top-down) that is not wrapping/foreign/empty

	for curr := ee; curr != nil; curr = curr.OriginalError {
		if curr.Message != "" && curr.Category != CatWrap && curr.Category != CatForeign {
			return curr.Message
		}
	}

	// ==== [2] ==== lowest level error (that is not empty)

	deepestMsg := ""
	for curr := ee; curr != nil; curr = curr.OriginalError {
		if curr.Message != "" {
			deepestMsg = curr.Message
		}
	}
	if deepestMsg != "" {
		return deepestMsg
	}

	// ==== [3] ==== fallback to self.message

	return ee.Message
}

// RecursiveType returns the statuscode to use
// = first error (top-down) that is not wrapping/empty
func (ee *ExErr) RecursiveType() ErrorType {
	for curr := ee; curr != nil; curr = curr.OriginalError {
		if curr.Type != TypeWrap {
			return curr.Type
		}

	}

	// fallback to self
	return ee.Type
}

// RecursiveStatuscode returns the HTTP Statuscode to use
// = first error (top-down) that has a statuscode set
func (ee *ExErr) RecursiveStatuscode() *int {
	for curr := ee; curr != nil; curr = curr.OriginalError {
		if curr.StatusCode != nil {
			return langext.Ptr(*curr.StatusCode)
		}
	}

	return nil
}

// RecursiveCategory returns the ErrorCategory to use
// = first error (top-down) that has a statuscode set
func (ee *ExErr) RecursiveCategory() ErrorCategory {
	for curr := ee; curr != nil; curr = curr.OriginalError {
		if curr.Category != CatWrap {
			return curr.Category
		}
	}

	// fallback to <empty>
	return ee.Category
}

// RecursiveMeta searches (top-down) for teh first error that has a meta value with teh specified key
// and returns its value (or nil)
func (ee *ExErr) RecursiveMeta(key string) *MetaValue {
	for curr := ee; curr != nil; curr = curr.OriginalError {
		if metaval, ok := curr.Meta[key]; ok {
			return langext.Ptr(metaval)
		}
	}

	return nil
}

// Depth returns the depth of recursively contained errors
func (ee *ExErr) Depth() int {
	if ee.OriginalError == nil {
		return 1
	} else {
		return ee.OriginalError.Depth() + 1
	}
}

// GetMeta returns the meta value with the specified key
// this method recurses through all wrapped errors and returns the first matching meta value
func (ee *ExErr) GetMeta(key string) (any, bool) {
	for curr := ee; curr != nil; curr = curr.OriginalError {
		if v, ok := curr.Meta[key]; ok {
			return v.Value, true
		}
	}

	return nil, false
}

// GetMetaString functions the same as GetMeta, but returns false if the type does not match
func (ee *ExErr) GetMetaString(key string) (string, bool) {
	if v1, ok := ee.GetMeta(key); ok {
		if v2, ok := v1.(string); ok {
			return v2, true
		}
	}
	return "", false
}

func (ee *ExErr) GetMetaBool(key string) (bool, bool) {
	if v1, ok := ee.GetMeta(key); ok {
		if v2, ok := v1.(bool); ok {
			return v2, true
		}
	}
	return false, false
}

func (ee *ExErr) GetMetaInt(key string) (int, bool) {
	if v1, ok := ee.GetMeta(key); ok {
		if v2, ok := v1.(int); ok {
			return v2, true
		}
	}
	return 0, false
}

func (ee *ExErr) GetMetaFloat32(key string) (float32, bool) {
	if v1, ok := ee.GetMeta(key); ok {
		if v2, ok := v1.(float32); ok {
			return v2, true
		}
	}
	return 0, false
}

func (ee *ExErr) GetMetaFloat64(key string) (float64, bool) {
	if v1, ok := ee.GetMeta(key); ok {
		if v2, ok := v1.(float64); ok {
			return v2, true
		}
	}
	return 0, false
}

func (ee *ExErr) GetMetaTime(key string) (time.Time, bool) {
	if v1, ok := ee.GetMeta(key); ok {
		if v2, ok := v1.(time.Time); ok {
			return v2, true
		}
	}
	return time.Time{}, false
}

func (ee *ExErr) GetExtra(key string) (any, bool) {
	if v, ok := ee.Extra[key]; ok {
		return v, true
	}

	return nil, false
}

// contains test if the supplied error is contained in this error (anywhere in the chain)
func (ee *ExErr) contains(original *ExErr) (*ExErr, bool) {
	if original == nil {
		return nil, false
	}

	if ee == original {
		return ee, true
	}

	for curr := ee; curr != nil; curr = curr.OriginalError {
		if curr.equalsDirectProperties(curr) {
			return curr, true
		}
	}

	return nil, false
}

// equalsDirectProperties tests if ee and other are equals, but only looks at primary properties (not `OriginalError` or `Meta`)
func (ee *ExErr) equalsDirectProperties(other *ExErr) bool {

	if ee.UniqueID != other.UniqueID {
		return false
	}
	if ee.Timestamp != other.Timestamp {
		return false
	}
	if ee.Category != other.Category {
		return false
	}
	if ee.Severity != other.Severity {
		return false
	}
	if ee.Type != other.Type {
		return false
	}
	if ee.StatusCode != other.StatusCode {
		return false
	}
	if ee.Message != other.Message {
		return false
	}
	if ee.WrappedErrType != other.WrappedErrType {
		return false
	}
	if ee.Caller != other.Caller {
		return false
	}

	return true
}

func newID() string {
	return xid.New().String()
}