package exerr

import (
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/rs/zerolog"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"gogs.mikescher.com/BlackForestBytes/goext/langext"
	"math"
	"strconv"
	"strings"
	"time"
)

// This is a buffed up map[string]any
// we also save type information of the map-values
// which allows us to deserialize them back into te correct types later

type MetaMap map[string]MetaValue

type metaDataType string

const (
	MDTString      metaDataType = "String"
	MDTStringPtr   metaDataType = "StringPtr"
	MDTInt         metaDataType = "Int"
	MDTInt8        metaDataType = "Int8"
	MDTInt16       metaDataType = "Int16"
	MDTInt32       metaDataType = "Int32"
	MDTInt64       metaDataType = "Int64"
	MDTFloat32     metaDataType = "Float32"
	MDTFloat64     metaDataType = "Float64"
	MDTBool        metaDataType = "Bool"
	MDTBytes       metaDataType = "Bytes"
	MDTObjectID    metaDataType = "ObjectID"
	MDTTime        metaDataType = "Time"
	MDTDuration    metaDataType = "Duration"
	MDTStringArray metaDataType = "StringArr"
	MDTIntArray    metaDataType = "IntArr"
	MDTInt32Array  metaDataType = "Int32Arr"
	MDTID          metaDataType = "ID"
	MDTAny         metaDataType = "Interface"
	MDTNil         metaDataType = "Nil"
	MDTEnum        metaDataType = "Enum"
)

type MetaValue struct {
	DataType metaDataType `json:"dataType"`
	Value    interface{}  `json:"value"`
}

type metaValueSerialization struct {
	DataType metaDataType `bson:"dataType"`
	Value    string       `bson:"value"`
	Raw      interface{}  `bson:"raw"`
}

func (v MetaValue) SerializeValue() (string, error) {
	switch v.DataType {
	case MDTString:
		return v.Value.(string), nil
	case MDTID:
		return v.Value.(IDWrap).Serialize(), nil
	case MDTAny:
		return v.Value.(AnyWrap).Serialize(), nil
	case MDTStringPtr:
		if langext.IsNil(v.Value) {
			return "#", nil
		}
		r := v.Value.(*string)
		if r != nil {
			return "*" + *r, nil
		} else {
			return "#", nil
		}
	case MDTInt:
		return strconv.Itoa(v.Value.(int)), nil
	case MDTInt8:
		return strconv.FormatInt(int64(v.Value.(int8)), 10), nil
	case MDTInt16:
		return strconv.FormatInt(int64(v.Value.(int16)), 10), nil
	case MDTInt32:
		return strconv.FormatInt(int64(v.Value.(int32)), 10), nil
	case MDTInt64:
		return strconv.FormatInt(v.Value.(int64), 10), nil
	case MDTFloat32:
		return strconv.FormatFloat(float64(v.Value.(float32)), 'X', -1, 32), nil
	case MDTFloat64:
		return strconv.FormatFloat(v.Value.(float64), 'X', -1, 64), nil
	case MDTBool:
		if v.Value.(bool) {
			return "true", nil
		} else {
			return "false", nil
		}
	case MDTBytes:
		return hex.EncodeToString(v.Value.([]byte)), nil
	case MDTObjectID:
		return v.Value.(primitive.ObjectID).Hex(), nil
	case MDTTime:
		return strconv.FormatInt(v.Value.(time.Time).Unix(), 10) + "|" + strconv.FormatInt(int64(v.Value.(time.Time).Nanosecond()), 10), nil
	case MDTDuration:
		return v.Value.(time.Duration).String(), nil
	case MDTStringArray:
		if langext.IsNil(v.Value) {
			return "#", nil
		}
		r, err := json.Marshal(v.Value.([]string))
		if err != nil {
			return "", err
		}
		return string(r), nil
	case MDTIntArray:
		if langext.IsNil(v.Value) {
			return "#", nil
		}
		r, err := json.Marshal(v.Value.([]int))
		if err != nil {
			return "", err
		}
		return string(r), nil
	case MDTInt32Array:
		if langext.IsNil(v.Value) {
			return "#", nil
		}
		r, err := json.Marshal(v.Value.([]int32))
		if err != nil {
			return "", err
		}
		return string(r), nil
	case MDTNil:
		return "", nil
	case MDTEnum:
		return v.Value.(EnumWrap).Serialize(), nil
	}
	return "", errors.New("Unknown type: " + string(v.DataType))
}

func (v MetaValue) ShortString(lim int) string {
	switch v.DataType {
	case MDTString:
		r := strings.ReplaceAll(v.Value.(string), "\r", "")
		r = strings.ReplaceAll(r, "\n", "\\n")
		r = strings.ReplaceAll(r, "\t", "\\t")
		return langext.StrLimit(r, lim, "...")
	case MDTID:
		return v.Value.(IDWrap).String()
	case MDTAny:
		return v.Value.(AnyWrap).String()
	case MDTStringPtr:
		if langext.IsNil(v.Value) {
			return "<<null>>"
		}
		r := langext.CoalesceString(v.Value.(*string), "<<null>>")
		r = strings.ReplaceAll(r, "\r", "")
		r = strings.ReplaceAll(r, "\n", "\\n")
		r = strings.ReplaceAll(r, "\t", "\\t")
		return langext.StrLimit(r, lim, "...")
	case MDTInt:
		return strconv.Itoa(v.Value.(int))
	case MDTInt8:
		return strconv.FormatInt(int64(v.Value.(int8)), 10)
	case MDTInt16:
		return strconv.FormatInt(int64(v.Value.(int16)), 10)
	case MDTInt32:
		return strconv.FormatInt(int64(v.Value.(int32)), 10)
	case MDTInt64:
		return strconv.FormatInt(v.Value.(int64), 10)
	case MDTFloat32:
		return strconv.FormatFloat(float64(v.Value.(float32)), 'g', 4, 32)
	case MDTFloat64:
		return strconv.FormatFloat(v.Value.(float64), 'g', 4, 64)
	case MDTBool:
		return fmt.Sprintf("%v", v.Value.(bool))
	case MDTBytes:
		return langext.StrLimit(hex.EncodeToString(v.Value.([]byte)), lim, "...")
	case MDTObjectID:
		return v.Value.(primitive.ObjectID).Hex()
	case MDTTime:
		return v.Value.(time.Time).Format(time.RFC3339)
	case MDTDuration:
		return v.Value.(time.Duration).String()
	case MDTStringArray:
		if langext.IsNil(v.Value) {
			return "<<null>>"
		}
		r, err := json.Marshal(v.Value.([]string))
		if err != nil {
			return "(err)"
		}
		return langext.StrLimit(string(r), lim, "...")
	case MDTIntArray:
		if langext.IsNil(v.Value) {
			return "<<null>>"
		}
		r, err := json.Marshal(v.Value.([]int))
		if err != nil {
			return "(err)"
		}
		return langext.StrLimit(string(r), lim, "...")
	case MDTInt32Array:
		if langext.IsNil(v.Value) {
			return "<<null>>"
		}
		r, err := json.Marshal(v.Value.([]int32))
		if err != nil {
			return "(err)"
		}
		return langext.StrLimit(string(r), lim, "...")
	case MDTNil:
		return "<<null>>"
	case MDTEnum:
		return v.Value.(EnumWrap).String()
	}
	return "(err)"
}

func (v MetaValue) Apply(key string, evt *zerolog.Event, limitLen *int) *zerolog.Event {
	switch v.DataType {
	case MDTString:
		if limitLen == nil {
			return evt.Str(key, v.Value.(string))
		} else {
			return evt.Str(key, langext.StrLimit(v.Value.(string), *limitLen, "..."))
		}
	case MDTID:
		return evt.Str(key, v.Value.(IDWrap).Value)
	case MDTAny:
		if v.Value.(AnyWrap).IsError {
			return evt.Str(key, "(err)")
		} else {
			if limitLen == nil {
				return evt.Str(key, v.Value.(AnyWrap).Json)
			} else {
				return evt.Str(key, langext.StrLimit(v.Value.(AnyWrap).Json, *limitLen, "..."))
			}
		}
	case MDTStringPtr:
		if langext.IsNil(v.Value) {
			return evt.Str(key, "<<null>>")
		}
		if limitLen == nil {
			return evt.Str(key, langext.CoalesceString(v.Value.(*string), "<<null>>"))
		} else {
			return evt.Str(key, langext.StrLimit(langext.CoalesceString(v.Value.(*string), "<<null>>"), *limitLen, "..."))
		}
	case MDTInt:
		return evt.Int(key, v.Value.(int))
	case MDTInt8:
		return evt.Int8(key, v.Value.(int8))
	case MDTInt16:
		return evt.Int16(key, v.Value.(int16))
	case MDTInt32:
		return evt.Int32(key, v.Value.(int32))
	case MDTInt64:
		return evt.Int64(key, v.Value.(int64))
	case MDTFloat32:
		return evt.Float32(key, v.Value.(float32))
	case MDTFloat64:
		return evt.Float64(key, v.Value.(float64))
	case MDTBool:
		return evt.Bool(key, v.Value.(bool))
	case MDTBytes:
		return evt.Bytes(key, v.Value.([]byte))
	case MDTObjectID:
		return evt.Str(key, v.Value.(primitive.ObjectID).Hex())
	case MDTTime:
		return evt.Time(key, v.Value.(time.Time))
	case MDTDuration:
		return evt.Dur(key, v.Value.(time.Duration))
	case MDTStringArray:
		if langext.IsNil(v.Value) {
			return evt.Strs(key, nil)
		}
		return evt.Strs(key, v.Value.([]string))
	case MDTIntArray:
		if langext.IsNil(v.Value) {
			return evt.Ints(key, nil)
		}
		return evt.Ints(key, v.Value.([]int))
	case MDTInt32Array:
		if langext.IsNil(v.Value) {
			return evt.Ints32(key, nil)
		}
		return evt.Ints32(key, v.Value.([]int32))
	case MDTNil:
		return evt.Str(key, "<<null>>")
	case MDTEnum:
		if v.Value.(EnumWrap).IsNil {
			return evt.Any(key, nil)
		} else if v.Value.(EnumWrap).ValueRaw != nil {
			return evt.Any(key, v.Value.(EnumWrap).ValueRaw)
		} else {
			return evt.Str(key, v.Value.(EnumWrap).ValueString)
		}
	}
	return evt.Str(key, "(err)")
}

func (v MetaValue) MarshalJSON() ([]byte, error) {
	str, err := v.SerializeValue()
	if err != nil {
		return nil, err
	}
	return json.Marshal(string(v.DataType) + ":" + str)
}

func (v *MetaValue) UnmarshalJSON(data []byte) error {
	var str = ""
	err := json.Unmarshal(data, &str)
	if err != nil {
		return err
	}

	split := strings.SplitN(str, ":", 2)
	if len(split) != 2 {
		return errors.New("failed to decode MetaValue: '" + str + "'")
	}

	return v.Deserialize(split[1], metaDataType(split[0]))
}

func (v MetaValue) MarshalBSON() ([]byte, error) {
	serval, err := v.SerializeValue()
	if err != nil {
		return nil, Wrap(err, "failed to bson-marshal MetaValue (serialize)").Build()
	}

	// this is an kinda ugly hack - but serialization to mongodb and back can loose the correct type information....
	bin, err := bson.Marshal(metaValueSerialization{
		DataType: v.DataType,
		Value:    serval,
		Raw:      v.Value,
	})
	if err != nil {
		return nil, Wrap(err, "failed to bson-marshal MetaValue (marshal)").Build()
	}

	return bin, nil
}

func (v *MetaValue) UnmarshalBSON(bytes []byte) error {
	var serval metaValueSerialization
	err := bson.Unmarshal(bytes, &serval)
	if err != nil {
		return Wrap(err, "failed to bson-unmarshal MetaValue (unmarshal)").Build()
	}

	err = v.Deserialize(serval.Value, serval.DataType)
	if err != nil {
		return Wrap(err, "failed to deserialize MetaValue from bson").Str("raw", serval.Value).Build()
	}

	return nil
}

func (v *MetaValue) Deserialize(value string, datatype metaDataType) error {
	switch datatype {
	case MDTString:
		v.Value = value
		v.DataType = datatype
		return nil
	case MDTID:
		v.Value = deserializeIDWrap(value)
		v.DataType = datatype
		return nil
	case MDTAny:
		v.Value = deserializeAnyWrap(value)
		v.DataType = datatype
		return nil
	case MDTStringPtr:
		if len(value) <= 0 || (value[0] != '*' && value[0] != '#') {
			return errors.New("Invalid StringPtr: " + value)
		} else if value == "#" {
			v.Value = nil
			v.DataType = datatype
			return nil
		} else {
			v.Value = langext.Ptr(value[1:])
			v.DataType = datatype
			return nil
		}
	case MDTInt:
		pv, err := strconv.ParseInt(value, 10, 0)
		if err != nil {
			return err
		}
		v.Value = int(pv)
		v.DataType = datatype
		return nil
	case MDTInt8:
		pv, err := strconv.ParseInt(value, 10, 8)
		if err != nil {
			return err
		}
		v.Value = int8(pv)
		v.DataType = datatype
		return nil
	case MDTInt16:
		pv, err := strconv.ParseInt(value, 10, 16)
		if err != nil {
			return err
		}
		v.Value = int16(pv)
		v.DataType = datatype
		return nil
	case MDTInt32:
		pv, err := strconv.ParseInt(value, 10, 32)
		if err != nil {
			return err
		}
		v.Value = int32(pv)
		v.DataType = datatype
		return nil
	case MDTInt64:
		pv, err := strconv.ParseInt(value, 10, 64)
		if err != nil {
			return err
		}
		v.Value = pv
		v.DataType = datatype
		return nil
	case MDTFloat32:
		pv, err := strconv.ParseFloat(value, 64)
		if err != nil {
			return err
		}
		v.Value = float32(pv)
		v.DataType = datatype
		return nil
	case MDTFloat64:
		pv, err := strconv.ParseFloat(value, 64)
		if err != nil {
			return err
		}
		v.Value = pv
		v.DataType = datatype
		return nil
	case MDTBool:
		if value == "true" {
			v.Value = true
			v.DataType = datatype
			return nil
		}
		if value == "false" {
			v.Value = false
			v.DataType = datatype
			return nil
		}
		return errors.New("invalid bool value: " + value)
	case MDTBytes:
		r, err := hex.DecodeString(value)
		if err != nil {
			return err
		}
		v.Value = r
		v.DataType = datatype
		return nil
	case MDTObjectID:
		r, err := primitive.ObjectIDFromHex(value)
		if err != nil {
			return err
		}
		v.Value = r
		v.DataType = datatype
		return nil
	case MDTTime:
		ps := strings.Split(value, "|")
		if len(ps) != 2 {
			return errors.New("invalid time.time: " + value)
		}
		p1, err := strconv.ParseInt(ps[0], 10, 64)
		if err != nil {
			return err
		}
		p2, err := strconv.ParseInt(ps[1], 10, 32)
		if err != nil {
			return err
		}
		v.Value = time.Unix(p1, p2)
		v.DataType = datatype
		return nil
	case MDTDuration:
		r, err := time.ParseDuration(value)
		if err != nil {
			return err
		}
		v.Value = r
		v.DataType = datatype
		return nil
	case MDTStringArray:
		if value == "#" {
			v.Value = nil
			v.DataType = datatype
			return nil
		}
		pj := make([]string, 0)
		err := json.Unmarshal([]byte(value), &pj)
		if err != nil {
			return err
		}
		v.Value = pj
		v.DataType = datatype
		return nil
	case MDTIntArray:
		if value == "#" {
			v.Value = nil
			v.DataType = datatype
			return nil
		}
		pj := make([]int, 0)
		err := json.Unmarshal([]byte(value), &pj)
		if err != nil {
			return err
		}
		v.Value = pj
		v.DataType = datatype
		return nil
	case MDTInt32Array:
		if value == "#" {
			v.Value = nil
			v.DataType = datatype
			return nil
		}
		pj := make([]int32, 0)
		err := json.Unmarshal([]byte(value), &pj)
		if err != nil {
			return err
		}
		v.Value = pj
		v.DataType = datatype
		return nil
	case MDTNil:
		v.Value = nil
		v.DataType = datatype
		return nil
	case MDTEnum:
		v.Value = deserializeEnumWrap(value)
		v.DataType = datatype
		return nil
	}
	return errors.New("Unknown type: " + string(datatype))
}

func (v MetaValue) ValueString() string {
	switch v.DataType {
	case MDTString:
		return v.Value.(string)
	case MDTID:
		return v.Value.(IDWrap).String()
	case MDTAny:
		return v.Value.(AnyWrap).String()
	case MDTStringPtr:
		if langext.IsNil(v.Value) {
			return "<<null>>"
		}
		return langext.CoalesceString(v.Value.(*string), "<<null>>")
	case MDTInt:
		return strconv.Itoa(v.Value.(int))
	case MDTInt8:
		return strconv.FormatInt(int64(v.Value.(int8)), 10)
	case MDTInt16:
		return strconv.FormatInt(int64(v.Value.(int16)), 10)
	case MDTInt32:
		return strconv.FormatInt(int64(v.Value.(int32)), 10)
	case MDTInt64:
		return strconv.FormatInt(v.Value.(int64), 10)
	case MDTFloat32:
		return strconv.FormatFloat(float64(v.Value.(float32)), 'g', 4, 32)
	case MDTFloat64:
		return strconv.FormatFloat(v.Value.(float64), 'g', 4, 64)
	case MDTBool:
		return fmt.Sprintf("%v", v.Value.(bool))
	case MDTBytes:
		return hex.EncodeToString(v.Value.([]byte))
	case MDTObjectID:
		return v.Value.(primitive.ObjectID).Hex()
	case MDTTime:
		return v.Value.(time.Time).Format(time.RFC3339Nano)
	case MDTDuration:
		return v.Value.(time.Duration).String()
	case MDTStringArray:
		if langext.IsNil(v.Value) {
			return "<<null>>"
		}
		r, err := json.MarshalIndent(v.Value.([]string), "", "  ")
		if err != nil {
			return "(err)"
		}
		return string(r)
	case MDTIntArray:
		if langext.IsNil(v.Value) {
			return "<<null>>"
		}
		r, err := json.MarshalIndent(v.Value.([]int), "", "  ")
		if err != nil {
			return "(err)"
		}
		return string(r)
	case MDTInt32Array:
		if langext.IsNil(v.Value) {
			return "<<null>>"
		}
		r, err := json.MarshalIndent(v.Value.([]int32), "", "  ")
		if err != nil {
			return "(err)"
		}
		return string(r)
	case MDTNil:
		return "<<null>>"
	case MDTEnum:
		return v.Value.(EnumWrap).String()
	}
	return "(err)"
}

// rawValueForJson returns most-of-the-time the `Value` field
// but for some datatyes we do special processing
// all, so we can pluck the output value in json.Marshal without any suprises
func (v MetaValue) rawValueForJson() any {
	if v.DataType == MDTAny {
		if v.Value.(AnyWrap).IsNil {
			return nil
		}
		if v.Value.(AnyWrap).IsError {
			return bson.M{"@error": true}
		}
		jsonobj := primitive.M{}
		jsonarr := primitive.A{}
		if err := json.Unmarshal([]byte(v.Value.(AnyWrap).Json), &jsonobj); err == nil {
			return jsonobj
		} else if err := json.Unmarshal([]byte(v.Value.(AnyWrap).Json), &jsonarr); err == nil {
			return jsonarr
		} else {
			return bson.M{"type": v.Value.(AnyWrap).Type, "data": v.Value.(AnyWrap).Json}
		}
	}
	if v.DataType == MDTID {
		if v.Value.(IDWrap).IsNil {
			return nil
		}
		return v.Value.(IDWrap).Value
	}
	if v.DataType == MDTBytes {
		return hex.EncodeToString(v.Value.([]byte))
	}
	if v.DataType == MDTDuration {
		return v.Value.(time.Duration).String()
	}
	if v.DataType == MDTTime {
		return v.Value.(time.Time).Format(time.RFC3339Nano)
	}
	if v.DataType == MDTObjectID {
		return v.Value.(primitive.ObjectID).Hex()
	}
	if v.DataType == MDTNil {
		return nil
	}
	if v.DataType == MDTEnum {
		if v.Value.(EnumWrap).IsNil {
			return nil
		}
		if v.Value.(EnumWrap).ValueRaw != nil {
			return v.Value.(EnumWrap).ValueRaw
		}
		return v.Value.(EnumWrap).ValueString
	}
	if v.DataType == MDTFloat32 {
		if math.IsNaN(float64(v.Value.(float32))) {
			return "float64::NaN"
		} else if math.IsInf(float64(v.Value.(float32)), +1) {
			return "float64::+inf"
		} else if math.IsInf(float64(v.Value.(float32)), -1) {
			return "float64::-inf"
		} else {
			return v.Value
		}
	}
	if v.DataType == MDTFloat64 {
		if math.IsNaN(v.Value.(float64)) {
			return "float64::NaN"
		} else if math.IsInf(v.Value.(float64), +1) {
			return "float64::+inf"
		} else if math.IsInf(v.Value.(float64), -1) {
			return "float64::-inf"
		} else {
			return v.Value
		}
	}
	return v.Value
}

func (mm MetaMap) FormatOneLine(singleMaxLen int) string {
	r := ""

	i := 0
	for key, val := range mm {
		if i > 0 {
			r += ", "
		}

		r += "\"" + key + "\""
		r += ": "
		r += "\"" + val.ShortString(singleMaxLen) + "\""

		i++
	}

	return r
}

func (mm MetaMap) FormatMultiLine(indentFront string, indentKeys string, maxLenValue int) string {
	r := ""

	r += indentFront + "{" + "\n"
	for key, val := range mm {
		if key == "gin.body" {
			continue
		}

		r += indentFront
		r += indentKeys
		r += "\"" + key + "\""
		r += ": "
		r += "\"" + val.ShortString(maxLenValue) + "\""
		r += ",\n"
	}
	r += indentFront + "}"

	return r
}

func (mm MetaMap) Any() bool {
	return len(mm) > 0
}

func (mm MetaMap) Apply(evt *zerolog.Event, limitLen *int) *zerolog.Event {
	for key, val := range mm {
		evt = val.Apply(key, evt, limitLen)
	}
	return evt
}

func (mm MetaMap) add(key string, mdtype metaDataType, val interface{}) {
	if _, ok := mm[key]; !ok {
		mm[key] = MetaValue{DataType: mdtype, Value: val}
		return
	}
	for i := 2; ; i++ {
		realkey := key + "-" + strconv.Itoa(i)
		if _, ok := mm[realkey]; !ok {
			mm[realkey] = MetaValue{DataType: mdtype, Value: val}
			return
		}
	}
}