package rest

import (
	"fmt"
	"reflect"
	"strings"

	"git.ali33.ru/fcg-xvii/go-tools/json"
)

func parseVal(from, to reflect.Value, fieldName string) IErrorArgs {
	if from.Kind() == reflect.Interface {
		from = from.Elem()
	}
	if !from.CanConvert(to.Type()) {
		return NewError(
			"FieldType",
			json.Map{
				"field":    fieldName,
				"expected": to.Type().String(),
				"received": from.Type().String(),
			},
		)
	}
	to.Set(from.Convert(to.Type()))
	return nil
}

func parseType(from, to reflect.Value, fieldName string) IErrorArgs {
	from = realValue(from)
	to = realValue(to)
	switch to.Kind() {
	case reflect.Interface:
		to.Set(from)
		return nil
	case reflect.Struct:
		return parseStruct(from, to, fieldName)
	case reflect.Slice:
		return parseSlice(from, to, fieldName)
	case reflect.Map:
		return parseMap(from, to, fieldName)
	default:
		return parseVal(from, to, fieldName)
	}
}

func realValue(val reflect.Value) reflect.Value {
	//log.Println(val.IsNil())
	if val.Kind() == reflect.Ptr {
		if val.IsNil() {
			val.Set(reflect.New(val.Type().Elem()))
		}
		val = val.Elem()
		if val.Kind() == reflect.Ptr {
			return realValue(val)
		}
	}
	return val
}

func parseSlice(from, to reflect.Value, fieldName string) IErrorArgs {
	if to.IsNil() {
		to.Set(reflect.MakeSlice(to.Type(), 0, from.Cap()))
	}
	eType := to.Type().Elem()
	// Перебираем элементы среза, откуда копируем.
	for i := 0; i < from.Len(); i++ {
		iFieldName := fmt.Sprintf("%v[%v]", fieldName, i)
		fromVal := from.Index(i)
		toVal := reflect.New(eType).Elem()
		if err := parseType(fromVal, toVal.Addr(), iFieldName); err != nil {
			return err
		}
		to.Set(reflect.Append(to, toVal))
	}
	return nil
}

func parseStruct(from, to reflect.Value, fieldName string) IErrorArgs {
	for i := 0; i < to.NumField(); i++ {
		required := false
		// устанавливаем тип и указатель поля объекта, куда копируем
		fType := to.Type().Field(i)
		fVal := to.Field(i)
		// проверяем тег, если он есть
		tag := fType.Tag.Get("rest")
		if len(tag) > 0 {
			required = strings.Contains(tag, "required")
		}
		var rVal reflect.Value
		switch from.Kind() {
		case reflect.Struct:
			if rVal = from.FieldByName(fType.Name); !rVal.IsValid() {
				rVal = from.FieldByName(camelToSnake(fType.Name))
			}
		case reflect.Map:
			if rVal = from.MapIndex(reflect.ValueOf(fType.Name)); !rVal.IsValid() {
				rVal = from.MapIndex(reflect.ValueOf(camelToSnake(fType.Name)))
			}
		default:
			return NewError(
				"FieldType",
				json.Map{
					"field":    fieldName,
					"expected": to.Type().String(),
					"received": from.Type().String(),
				},
			)
		}
		if !rVal.IsValid() {
			if required {
				return ErrorMessage("RequiredField", fType.Name)
			}
			continue
		}
		if rVal.Kind() == reflect.Interface {
			rVal = rVal.Elem()
		}
		iFieldName := fmt.Sprintf("%v.%v", fieldName, fType.Name)
		if err := parseType(rVal, fVal.Addr(), iFieldName); err != nil {
			return err
		}
	}
	return nil
}

func parseMap(from, to reflect.Value, fieldName string) IErrorArgs {
	if to.IsNil() {
		to.Set(reflect.MakeMap(to.Type()))
	}
	switch from.Kind() {
	case reflect.Map:
		for _, key := range from.MapKeys() {
			iFieldName := fmt.Sprintf("%v[%v]", fieldName, key)
			fromVal := from.MapIndex(key)
			toVal := reflect.New(to.Type().Elem()).Elem()
			if err := parseType(fromVal, toVal.Addr(), iFieldName); err != nil {
				return err
			}
			to.SetMapIndex(key, toVal)
		}
	case reflect.Struct:
		for i := 0; i < from.NumField(); i++ {
			iFieldName := fmt.Sprintf("%v.%v", fieldName, from.Type().Field(i).Name)
			fromVal := from.Field(i)
			toVal := reflect.New(to.Type().Elem()).Elem()
			if err := parseType(fromVal, toVal.Addr(), iFieldName); err != nil {
				return err
			}
			fName := camelToSnake(from.Type().Field(i).Name)
			to.SetMapIndex(reflect.ValueOf(fName), toVal)
		}
	default:
		return NewError(
			"FieldType",
			json.Map{
				"field":    fieldName,
				"expected": to.Type().String(),
				"received": from.Type().String(),
			},
		)
	}
	return nil
}

func Serialize(from json.Map, to any) IErrorArgs {
	elemTo := reflect.ValueOf(to).Elem()
	return parseType(
		reflect.ValueOf(&from),
		elemTo,
		"",
	)
}
