/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/

package utils

import (
	"errors"
	"fmt"
	"net"
	"reflect"
	"strconv"
	"strings"
	"time"

	"github.com/ericlagergren/decimal"
)

// StringToInterface will parse string into supported types
// if no other conversion possible, original string will be returned
func StringToInterface(s string) any {
	if s == EmptyString {
		return s
	}
	// int64
	if i, err := strconv.ParseInt(s, 10, 64); err == nil {
		return i
	}
	// bool
	if b, err := strconv.ParseBool(s); err == nil {
		return b
	}
	// float64
	if f, err := strconv.ParseFloat(s, 64); err == nil {
		return f
	}
	// time.Time
	if t, err := ParseTimeDetectLayout(s, "Local"); err == nil {
		return t
	}
	// time.Duration
	if d, err := time.ParseDuration(s); err == nil {
		return d
	}
	// string
	return s
}

// ReflectFieldInterface parses intf attepting to return the field value or error otherwise
// Supports "ExtraFields" where additional fields are dynamically inserted in map with field name: extraFieldsLabel
func ReflectFieldInterface(intf any, fldName, extraFieldsLabel string) (retIf any, err error) {
	v := reflect.ValueOf(intf)
	if v.Kind() == reflect.Ptr {
		v = v.Elem()
	}
	var field reflect.Value
	switch v.Kind() {
	case reflect.Struct:
		field = v.FieldByName(fldName)
	case reflect.Map:
		field = v.MapIndex(reflect.ValueOf(fldName))
		if !field.IsValid() { // Not looking in extra fields anymore
			return nil, ErrNotFound
		}
	default:
		return nil, fmt.Errorf("Unsupported field kind: %v", v.Kind())
	}

	if !field.IsValid() {
		if extraFieldsLabel == "" {
			return nil, ErrNotFound
		}
		mpVal := v.FieldByName(extraFieldsLabel)
		if !mpVal.IsValid() || mpVal.Kind() != reflect.Map {
			return nil, ErrNotFound
		}
		field = mpVal.MapIndex(reflect.ValueOf(fldName))
		if !field.IsValid() {
			return nil, ErrNotFound
		}
	}
	return field.Interface(), nil
}

// ReflectFieldAsString parses intf and attepting to return the field as string or error otherwise
// Supports "ExtraFields" where additional fields are dynamically inserted in map with field name: extraFieldsLabel
func ReflectFieldAsString(intf any, fldName, extraFieldsLabel string) (string, error) {
	field, err := ReflectFieldInterface(intf, fldName, extraFieldsLabel)
	if err != nil {
		return "", err
	}
	vOf := reflect.ValueOf(field)
	switch vOf.Kind() {
	case reflect.String:
		return vOf.String(), nil
	case reflect.Int, reflect.Int64:
		return strconv.FormatInt(vOf.Int(), 10), nil
	case reflect.Float64:
		return strconv.FormatFloat(vOf.Float(), 'f', -1, 64), nil
	default:
		return "", fmt.Errorf("Cannot convert to string field type: %s", vOf.Kind().String())
	}
}

func IfaceAsTime(itm any, timezone string) (t time.Time, err error) {
	switch v := itm.(type) {
	case time.Time:
		return v, nil
	case string:
		return ParseTimeDetectLayout(v, timezone)
	default:
		err = fmt.Errorf("cannot convert field: %+v to time.Time", itm)
	}
	return
}

func IfaceAsBig(itm any) (b *decimal.Big, err error) {
	switch it := itm.(type) {
	case time.Duration:
		return decimal.New(int64(it), 0), nil
	case int: // check every int type
		return decimal.New(int64(it), 0), nil
	case int8:
		return decimal.New(int64(it), 0), nil
	case int16:
		return decimal.New(int64(it), 0), nil
	case int32:
		return decimal.New(int64(it), 0), nil
	case int64:
		return decimal.New(it, 0), nil
	case uint:
		return new(decimal.Big).SetUint64(uint64(it)), nil
	case uint8:
		return new(decimal.Big).SetUint64(uint64(it)), nil
	case uint16:
		return new(decimal.Big).SetUint64(uint64(it)), nil
	case uint32:
		return new(decimal.Big).SetUint64(uint64(it)), nil
	case uint64:
		return new(decimal.Big).SetUint64(it), nil
	case float32: // automatically hitting here also ints
		return new(decimal.Big).SetFloat64(float64(it)), nil
	case float64: // automatically hitting here also ints
		return new(decimal.Big).SetFloat64(it), nil
	case string:
		if strings.HasSuffix(it, NsSuffix) ||
			strings.HasSuffix(it, UsSuffix) ||
			strings.HasSuffix(it, µSuffix) ||
			strings.HasSuffix(it, MsSuffix) ||
			strings.HasSuffix(it, SSuffix) ||
			strings.HasSuffix(it, MSuffix) ||
			strings.HasSuffix(it, HSuffix) {
			var tm time.Duration
			if tm, err = time.ParseDuration(it); err != nil {
				return
			}
			return decimal.New(int64(tm), 0), nil
		}
		z, ok := new(decimal.Big).SetString(it)
		// verify ok and check if the value was converted successfuly
		// and the big is a valid number
		if !ok || z.IsNaN(0) {
			return nil, fmt.Errorf("can't convert <%+v> to decimal", it)
		}
		return z, nil
	default:
		err = fmt.Errorf("cannot convert field: %+v to time.Duration", it)
	}
	return
}

func IfaceAsDuration(itm any) (d time.Duration, err error) {
	switch it := itm.(type) {
	case time.Duration:
		return it, nil
	case int: // check every int type
		return time.Duration(int64(it)), nil
	case int8:
		return time.Duration(int64(it)), nil
	case int16:
		return time.Duration(int64(it)), nil
	case int32:
		return time.Duration(int64(it)), nil
	case int64:
		return time.Duration(int64(it)), nil
	case uint:
		return time.Duration(int64(it)), nil
	case uint8:
		return time.Duration(int64(it)), nil
	case uint16:
		return time.Duration(int64(it)), nil
	case uint32:
		return time.Duration(int64(it)), nil
	case uint64:
		return time.Duration(int64(it)), nil
	case float32: // automatically hitting here also ints
		return time.Duration(int64(it)), nil
	case float64: // automatically hitting here also ints
		return time.Duration(int64(it)), nil
	case string:
		return ParseDurationWithNanosecs(it)
	default:
		err = fmt.Errorf("cannot convert field: %+v to time.Duration", it)
	}
	return
}

// IfaceAsTInt converts interface to type int
func IfaceAsTInt(itm any) (i int, err error) {
	switch it := itm.(type) {
	case int:
		return it, nil
	case time.Duration:
		return int(it.Nanoseconds()), nil
	case int32:
		return int(it), nil
	case int64:
		return int(it), nil
	case float32:
		return int(it), nil
	case float64:
		return int(it), nil
	case string:
		return strconv.Atoi(it)
	default:
		err = fmt.Errorf("cannot convert field<%T>: %+v to int", it, it)
	}
	return
}

func IfaceAsInt64(itm any) (i int64, err error) {
	switch it := itm.(type) {
	case int:
		return int64(it), nil
	case time.Duration:
		return it.Nanoseconds(), nil
	case int32:
		return int64(it), nil
	case int64:
		return it, nil
	case string:
		return strconv.ParseInt(it, 10, 64)
	default:
		err = fmt.Errorf("cannot convert field: %+v to int", it)
	}
	return
}

// same function as IfaceAsInt64 but if the value is float round it to int64 instead of returning error
func IfaceAsTInt64(itm any) (i int64, err error) {
	switch it := itm.(type) {
	case int:
		return int64(it), nil
	case time.Duration:
		return it.Nanoseconds(), nil
	case int32:
		return int64(it), nil
	case int64:
		return it, nil
	case float32:
		return int64(it), nil
	case float64:
		return int64(it), nil
	case string:
		return strconv.ParseInt(it, 10, 64)
	default:
		err = fmt.Errorf("cannot convert field<%T>: %+v to int", it, it)
	}
	return
}

func IfaceAsFloat64(itm any) (f float64, err error) {
	switch it := itm.(type) {
	case float64:
		return it, nil
	case time.Duration:
		return float64(it.Nanoseconds()), nil
	case int:
		return float64(it), nil
	case int64:
		return float64(it), nil
	case string:
		return strconv.ParseFloat(it, 64)
	default:
		err = fmt.Errorf("cannot convert field: %+v to float64", it)
	}
	return
}
func IfaceAsTFloat64(itm any) (f float64, err error) {
	switch it := itm.(type) {
	case float64:
		return it, nil
	case time.Duration:
		return float64(it.Nanoseconds()), nil
	case int:
		return float64(it), nil
	case int64:
		return float64(it), nil
	case string:
		if strings.HasSuffix(it, SSuffix) || strings.HasSuffix(it, MSuffix) || strings.HasSuffix(it, HSuffix) {
			var tm time.Duration
			if tm, err = time.ParseDuration(it); err != nil {
				return
			}
			return float64(tm), nil
		}
		return strconv.ParseFloat(it, 64)
	default:
		err = fmt.Errorf("cannot convert field: %+v to float64", it)
	}
	return
}

func IfaceAsBool(itm any) (b bool, err error) {
	switch v := itm.(type) {
	case bool:
		return v, nil
	case string:
		return strconv.ParseBool(v)
	case int:
		return v > 0, nil
	case int64:
		return v > 0, nil
	case float64:
		return v > 0, nil
	default:
		err = fmt.Errorf("cannot convert field: %+v to bool", itm)
	}
	return
}

// MapIfaceTimeAsString converts time.Time type fields in a map[string]any to RFC3339 time format string. Used before msgpack marshaling since time.Time variables put inside interfaces arent encoded/decoded into a readable string or time.Time type
func MapIfaceTimeAsString(me map[string]any) {
	for k, v := range me {
		if timeStr, ok := v.(time.Time); ok {
			v = timeStr.Format(time.RFC3339)
			me[k] = v
		}
	}

}

func IfaceAsString(fld any) (out string) {
	switch value := fld.(type) {
	case nil:
		return
	case int:
		return strconv.Itoa(value)
	case int32:
		return strconv.FormatInt(int64(value), 10)
	case int64:
		return strconv.FormatInt(value, 10)
	case uint32:
		return strconv.FormatUint(uint64(value), 10)
	case uint64:
		return strconv.FormatUint(value, 10)
	case bool:
		return strconv.FormatBool(value)
	case float32:
		return strconv.FormatFloat(float64(value), 'f', -1, 64)
	case float64:
		return strconv.FormatFloat(value, 'f', -1, 64)
	case []uint8:
		return string(value) // byte is an alias for uint8 conversions implicit
	case time.Duration:
		if value == -1 {
			return "-1"
		}
		return value.String()
	case time.Time:
		return value.Format(time.RFC3339)
	case net.IP:
		return value.String()
	case string:
		return value
	default: // Maybe we are lucky and the value converts to string
		return ToJSON(fld)
	}
}

// IfaceAsSliceString is trying to convert the interface to a slice of strings
func IfaceAsSliceString(fld any) (out []string, err error) {
	switch value := fld.(type) {
	case nil:
		return
	case []int:
		out = make([]string, len(value))
		for i, val := range value {
			out[i] = strconv.Itoa(val)
		}
	case []int32:
		out = make([]string, len(value))
		for i, val := range value {
			out[i] = strconv.FormatInt(int64(val), 10)
		}
	case []int64:
		out = make([]string, len(value))
		for i, val := range value {
			out[i] = strconv.FormatInt(val, 10)
		}
	case []uint:
		out = make([]string, len(value))
		for i, val := range value {
			out[i] = strconv.FormatUint(uint64(val), 10)
		}
	case []uint32:
		out = make([]string, len(value))
		for i, val := range value {
			out[i] = strconv.FormatUint(uint64(val), 10)
		}
	case []uint64:
		out = make([]string, len(value))
		for i, val := range value {
			out[i] = strconv.FormatUint(val, 10)
		}
	case []bool:
		out = make([]string, len(value))
		for i, val := range value {
			out[i] = strconv.FormatBool(val)
		}
	case []float32:
		out = make([]string, len(value))
		for i, val := range value {
			out[i] = strconv.FormatFloat(float64(val), 'f', -1, 64)
		}
	case []float64:
		out = make([]string, len(value))
		for i, val := range value {
			out[i] = strconv.FormatFloat(val, 'f', -1, 64)
		}
	case [][]uint8:
		out = make([]string, len(value))
		for i, val := range value {
			out[i] = string(val) // byte is an alias for uint8 conversions implicit
		}
	case []time.Duration:
		out = make([]string, len(value))
		for i, val := range value {
			out[i] = val.String()
		}
	case []time.Time:
		out = make([]string, len(value))
		for i, val := range value {
			out[i] = val.Format(time.RFC3339)
		}
	case []net.IP:
		out = make([]string, len(value))
		for i, val := range value {
			out[i] = val.String()
		}
	case []string:
		out = value
	case []any:
		out = make([]string, len(value))
		for i, val := range value {
			out[i] = IfaceAsString(val)
		}
	default:
		err = fmt.Errorf("cannot convert field: %T to []string", value)
	}
	return
}

func GetUniformType(item any) (any, error) {
	valItm := reflect.ValueOf(item)
	switch valItm.Kind() { // convert evreting to float64
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return float64(valItm.Int()), nil
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		return float64(valItm.Uint()), nil
	case reflect.Float32, reflect.Float64:
		return valItm.Float(), nil
	case reflect.Struct: // used only for time
		return valItm.Interface(), nil
	default:
		return nil, errors.New("incomparable")
	}
}

func GetBasicType(item any) any {
	valItm := reflect.ValueOf(item)
	switch valItm.Kind() { // convert evreting to float64
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return valItm.Int()
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		return valItm.Uint()
	case reflect.Float32, reflect.Float64:
		return valItm.Float()
	default:
		return item
	}
}

// GreaterThan attempts to compare two items
// returns the result or error if not comparable
func GreaterThan(item, oItem any, orEqual bool) (gte bool, err error) {
	item = GetBasicType(item)
	oItem = GetBasicType(oItem)
	typItem := reflect.TypeOf(item)
	typOItem := reflect.TypeOf(oItem)
	if typItem != typOItem {
		if item, err = GetUniformType(item); err == nil { // overwrite type only if possible
			typItem = reflect.TypeOf(item)
		}
		if oItem, err = GetUniformType(oItem); err == nil {
			typOItem = reflect.TypeOf(oItem)
		}
	}
	if !typItem.Comparable() ||
		!typOItem.Comparable() ||
		typItem != typOItem {
		return false, fmt.Errorf("incomparable: <%+v> with <%+v>", item, oItem)
	}
	switch tVal := item.(type) {
	case float64:
		tOVal := oItem.(float64)
		if orEqual {
			gte = tVal >= tOVal
		} else {
			gte = tVal > tOVal
		}
	case uint64:
		tOVal := oItem.(uint64)
		if orEqual {
			gte = tVal >= tOVal
		} else {
			gte = tVal > tOVal
		}
	case int64:
		tOVal := oItem.(int64)
		if orEqual {
			gte = tVal >= tOVal
		} else {
			gte = tVal > tOVal
		}
	case time.Time:
		tOVal := oItem.(time.Time)
		if orEqual {
			gte = tVal == tOVal
		}
		if !gte {
			gte = tVal.After(tOVal)
		}
	default: // unsupported comparison
		err = fmt.Errorf("unsupported comparison type: %v, kind: %v", typItem, typItem.Kind())
	}
	return
}

func EqualTo(item, oItem any) (eq bool, err error) {
	item = GetBasicType(item)
	oItem = GetBasicType(oItem)
	typItem := reflect.TypeOf(item)
	typOItem := reflect.TypeOf(oItem)
	if typItem != typOItem {
		if item, err = GetUniformType(item); err == nil { // overwrite type only if possible
			typItem = reflect.TypeOf(item)
		}
		if oItem, err = GetUniformType(oItem); err == nil {
			typOItem = reflect.TypeOf(oItem)
		}
	}
	if !typItem.Comparable() ||
		!typOItem.Comparable() ||
		typItem != typOItem {
		return false, fmt.Errorf("incomparable: <%+v> with <%+v>", item, oItem)
	}
	switch tVal := item.(type) {
	case float64:
		tOVal := oItem.(float64)
		eq = tVal == tOVal
	case uint64:
		tOVal := oItem.(uint64)
		eq = tVal == tOVal
	case int64:
		tOVal := oItem.(int64)
		eq = tVal == tOVal
	case time.Time:
		tOVal := oItem.(time.Time)
		eq = tVal == tOVal
	case string:
		tOVal := oItem.(string)
		eq = tVal == tOVal
	default: // unsupported comparison
		err = fmt.Errorf("unsupported comparison type: %v, kind: %v", typItem, typItem.Kind())
	}
	return
}

// Sum attempts to sum multiple items
// returns the result or error if not comparable
func Sum(items ...any) (sum any, err error) {
	//we need at least 2 items to sum them
	if len(items) < 2 {
		return nil, ErrNotEnoughParameters
	}

	switch dt := items[0].(type) {
	case time.Duration:
		s := dt
		for _, item := range items[1:] {
			if itmVal, err := IfaceAsDuration(item); err != nil {
				return nil, err
			} else {
				s += itmVal
			}
		}
		sum = s
	case time.Time:
		s := dt
		for _, item := range items[1:] {
			if itmVal, err := IfaceAsDuration(item); err != nil {
				return nil, err
			} else {
				s = s.Add(itmVal)
			}
		}
		sum = s
	case float64:
		s := dt
		for _, item := range items[1:] {
			if itmVal, err := IfaceAsFloat64(item); err != nil {
				return nil, err
			} else {
				s += itmVal
			}
		}
		sum = s
	case int64:
		s := dt
		for _, item := range items[1:] {
			if itmVal, err := IfaceAsInt64(item); err != nil {
				return nil, err
			} else {
				s += itmVal
			}
		}
		sum = s
	case int:
		s := int64(dt)
		for _, item := range items[1:] {
			if itmVal, err := IfaceAsInt64(item); err != nil {
				return nil, err
			} else {
				s += itmVal
			}
		}
		sum = s
	}
	return
}

// Difference attempts to sum multiple items
// returns the result or error if not comparable
func Difference(tm string, items ...any) (diff any, err error) {
	//we need at least 2 items to diff them
	if len(items) < 2 {
		return nil, ErrNotEnoughParameters
	}
	switch dt := items[0].(type) {
	case time.Duration:
		d := dt
		for _, item := range items[1:] {
			if itmVal, err := IfaceAsDuration(item); err != nil {
				return nil, err
			} else {
				d -= itmVal
			}
		}
		diff = d
	case time.Time:
		d := dt
		for i, item := range items[1:] {
			if itmVal, err := IfaceAsTime(item, tm); err == nil {
				diff = d.Sub(itmVal)
				if len(items) == i+1 {
					return diff, nil
				}
				items[i] = diff
				return Difference(tm, items[i:]...)
			}

			if itmVal, err := IfaceAsDuration(item); err != nil {
				return nil, err
			} else {
				d = d.Add(-itmVal)
			}
		}
		diff = d
	case float64:
		d := dt
		for _, item := range items[1:] {
			if itmVal, err := IfaceAsFloat64(item); err != nil {
				return nil, err
			} else {
				d -= itmVal
			}
		}
		diff = d
	case int64:
		d := dt
		for _, item := range items[1:] {
			if itmVal, err := IfaceAsInt64(item); err != nil {
				return nil, err
			} else {
				d -= itmVal
			}
		}
		diff = d
	case int:
		d := int64(dt)
		for _, item := range items[1:] {
			if itmVal, err := IfaceAsInt64(item); err != nil {
				return nil, err
			} else {
				d -= itmVal
			}
		}
		diff = d
	default: // unsupported comparison
		return nil, fmt.Errorf("unsupported type")
	}
	return
}

// Multiply attempts to multiply multiple items
// returns the result or error if not comparable
func Multiply(items ...any) (mlt any, err error) {
	//we need at least 2 items to diff them
	if len(items) < 2 {
		return nil, ErrNotEnoughParameters
	}
	switch dt := items[0].(type) {
	case float64:
		m := dt
		for _, item := range items[1:] {
			if itmVal, err := IfaceAsFloat64(item); err != nil {
				return nil, err
			} else {
				m *= itmVal
			}
		}
		mlt = m
	case int64:
		m := dt
		for _, item := range items[1:] {
			if itmVal, err := IfaceAsInt64(item); err != nil {
				return nil, err
			} else {
				m *= itmVal
			}
		}
		mlt = m
	case int:
		m := int64(dt)
		for _, item := range items[1:] {
			if itmVal, err := IfaceAsInt64(item); err != nil {
				return nil, err
			} else {
				m *= itmVal
			}
		}
		mlt = m
	default: // unsupported comparison
		return nil, fmt.Errorf("unsupported type")
	}
	return
}

// Divide attempts to divide multiple items
// returns the result or error if not comparable
func Divide(items ...any) (div any, err error) {
	//we need at least 2 items to diff them
	if len(items) < 2 {
		return nil, ErrNotEnoughParameters
	}
	switch dt := items[0].(type) {
	case float64:
		d := dt
		for _, item := range items[1:] {
			if itmVal, err := IfaceAsFloat64(item); err != nil {
				return nil, err
			} else {
				d /= itmVal
			}
		}
		div = d
	case int64:
		d := dt
		for _, item := range items[1:] {
			if itmVal, err := IfaceAsInt64(item); err != nil {
				return nil, err
			} else {
				d /= itmVal
			}
		}
		div = d
	case int:
		d := int64(dt)
		for _, item := range items[1:] {
			if itmVal, err := IfaceAsInt64(item); err != nil {
				return nil, err
			} else {
				d /= itmVal
			}
		}
		div = d
	default: // unsupported comparison
		return nil, fmt.Errorf("unsupported type")
	}
	return
}

// ReflectFieldMethodInterface parses intf attepting to return the field value or error otherwise
// Supports "ExtraFields" where additional fields are dynamically inserted in map with field name: extraFieldsLabel
func ReflectFieldMethodInterface(obj any, fldName string) (retIf any, err error) {
	v := reflect.ValueOf(obj)
	if v.Kind() == reflect.Ptr {
		v = v.Elem()
	}
	var field reflect.Value
	switch v.Kind() {
	case reflect.Struct:
		field = v.FieldByName(fldName)
	case reflect.Map:
		field = v.MapIndex(reflect.ValueOf(fldName))
	case reflect.Slice, reflect.Array:
		//convert fldName to int
		idx, err := strconv.Atoi(fldName)
		if err == nil {
			if idx >= v.Len() {
				return nil, fmt.Errorf("index out of range")
			}
			field = v.Index(idx)
		}
	default:
		return nil, fmt.Errorf("unsupported field kind: %v", v.Kind())
	}
	if !field.IsValid() {
		// handle function with pointer
		v = reflect.ValueOf(obj)
		field = v.MethodByName(fldName)
		if !field.IsValid() {
			return nil, ErrNotFound
		} else {
			if field.Type().NumIn() != 0 {
				return nil, fmt.Errorf("invalid function called")
			}
			if field.Type().NumOut() > 2 {
				return nil, fmt.Errorf("invalid function called")
			}
			// the function have two parameters in return and check if the second is of type error
			if field.Type().NumOut() == 2 {
				errorInterface := reflect.TypeOf((*error)(nil)).Elem()
				if !field.Type().Out(1).Implements(errorInterface) {
					return nil, fmt.Errorf("invalid function called")
				}
			}
			fields := field.Call([]reflect.Value{})
			if len(fields) == 2 && !fields[1].IsNil() {
				return fields[0].Interface(), fields[1].Interface().(error)
			}
			return fields[0].Interface(), nil
		}
	}
	return field.Interface(), nil
}
