package exerr

import (
	"encoding/json"
	"fmt"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"gogs.mikescher.com/BlackForestBytes/goext/langext"
	"reflect"
	"time"
)

var reflectTypeStr = reflect.TypeOf("")

func FromError(err error) *ExErr {
	if verr, ok := err.(*ExErr); ok {
		// A simple ExErr
		return verr
	}

	// A foreign error (eg a MongoDB exception)
	return &ExErr{
		UniqueID:       newID(),
		Category:       CatForeign,
		Type:           TypeInternal,
		Severity:       SevErr,
		Timestamp:      time.Time{},
		StatusCode:     nil,
		Message:        err.Error(),
		WrappedErrType: fmt.Sprintf("%T", err),
		WrappedErr:     err,
		Caller:         "",
		OriginalError:  nil,
		Meta:           getForeignMeta(err),
	}
}

func newExErr(cat ErrorCategory, errtype ErrorType, msg string) *ExErr {
	return &ExErr{
		UniqueID:       newID(),
		Category:       cat,
		Type:           errtype,
		Severity:       SevErr,
		Timestamp:      time.Now(),
		StatusCode:     nil,
		Message:        msg,
		WrappedErrType: "",
		WrappedErr:     nil,
		Caller:         callername(2),
		OriginalError:  nil,
		Meta:           make(map[string]MetaValue),
	}
}

func wrapExErr(e *ExErr, msg string, cat ErrorCategory, stacktraceskip int) *ExErr {
	return &ExErr{
		UniqueID:       newID(),
		Category:       cat,
		Type:           TypeWrap,
		Severity:       SevErr,
		Timestamp:      time.Now(),
		StatusCode:     e.StatusCode,
		Message:        msg,
		WrappedErrType: "",
		WrappedErr:     nil,
		Caller:         callername(1 + stacktraceskip),
		OriginalError:  e,
		Meta:           make(map[string]MetaValue),
	}
}

func getForeignMeta(err error) (mm MetaMap) {
	mm = make(map[string]MetaValue)

	defer func() {
		if panicerr := recover(); panicerr != nil {
			New(TypePanic, "Panic while trying to get foreign meta").
				Str("source", err.Error()).
				Interface("panic-object", panicerr).
				Stack().
				Print()
		}
	}()

	rval := reflect.ValueOf(err)
	if rval.Kind() == reflect.Interface || rval.Kind() == reflect.Ptr {
		rval = reflect.ValueOf(err).Elem()
	}

	mm.add("foreign.errortype", MDTString, rval.Type().String())

	for k, v := range addMetaPrefix("foreign", getReflectedMetaValues(err, 8)) {
		mm[k] = v
	}

	return mm
}

func getReflectedMetaValues(value interface{}, remainingDepth int) map[string]MetaValue {

	if remainingDepth <= 0 {
		return map[string]MetaValue{}
	}

	if langext.IsNil(value) {
		return map[string]MetaValue{"": {DataType: MDTNil, Value: nil}}
	}

	rval := reflect.ValueOf(value)

	if rval.Type().Kind() == reflect.Ptr {

		if rval.IsNil() {
			return map[string]MetaValue{"*": {DataType: MDTNil, Value: nil}}
		}

		elem := rval.Elem()

		return addMetaPrefix("*", getReflectedMetaValues(elem.Interface(), remainingDepth-1))
	}

	if !rval.CanInterface() {
		return map[string]MetaValue{"": {DataType: MDTString, Value: "<<no-interface>>"}}
	}

	raw := rval.Interface()

	switch ifraw := raw.(type) {
	case time.Time:
		return map[string]MetaValue{"": {DataType: MDTTime, Value: ifraw}}
	case time.Duration:
		return map[string]MetaValue{"": {DataType: MDTDuration, Value: ifraw}}
	case int:
		return map[string]MetaValue{"": {DataType: MDTInt, Value: ifraw}}
	case int8:
		return map[string]MetaValue{"": {DataType: MDTInt8, Value: ifraw}}
	case int16:
		return map[string]MetaValue{"": {DataType: MDTInt16, Value: ifraw}}
	case int32:
		return map[string]MetaValue{"": {DataType: MDTInt32, Value: ifraw}}
	case int64:
		return map[string]MetaValue{"": {DataType: MDTInt64, Value: ifraw}}
	case string:
		return map[string]MetaValue{"": {DataType: MDTString, Value: ifraw}}
	case bool:
		return map[string]MetaValue{"": {DataType: MDTBool, Value: ifraw}}
	case []byte:
		return map[string]MetaValue{"": {DataType: MDTBytes, Value: ifraw}}
	case float32:
		return map[string]MetaValue{"": {DataType: MDTFloat32, Value: ifraw}}
	case float64:
		return map[string]MetaValue{"": {DataType: MDTFloat64, Value: ifraw}}
	case []int:
		return map[string]MetaValue{"": {DataType: MDTIntArray, Value: ifraw}}
	case []int32:
		return map[string]MetaValue{"": {DataType: MDTInt32Array, Value: ifraw}}
	case primitive.ObjectID:
		return map[string]MetaValue{"": {DataType: MDTObjectID, Value: ifraw}}
	case []string:
		return map[string]MetaValue{"": {DataType: MDTStringArray, Value: ifraw}}
	}

	if rval.Type().Kind() == reflect.Struct {
		m := make(map[string]MetaValue)
		for i := 0; i < rval.NumField(); i++ {
			fieldtype := rval.Type().Field(i)

			fieldname := fieldtype.Name

			if fieldtype.IsExported() {
				for k, v := range addMetaPrefix(fieldname, getReflectedMetaValues(rval.Field(i).Interface(), remainingDepth-1)) {
					m[k] = v
				}
			}
		}
		return m
	}

	if rval.Type().ConvertibleTo(reflectTypeStr) {
		return map[string]MetaValue{"": {DataType: MDTString, Value: rval.Convert(reflectTypeStr).String()}}
	}

	jsonval, err := json.Marshal(value)
	if err != nil {
		panic(err) // gets recovered later up
	}

	return map[string]MetaValue{"": {DataType: MDTString, Value: string(jsonval)}}
}

func addMetaPrefix(prefix string, m map[string]MetaValue) map[string]MetaValue {
	if len(m) == 1 {
		for k, v := range m {
			if k == "" {
				return map[string]MetaValue{prefix: v}
			}
		}
	}

	r := make(map[string]MetaValue, len(m))
	for k, v := range m {
		r[prefix+"."+k] = v
	}
	return r
}