package rest

import (
	"fmt"
	"reflect"
	"strings"

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

// Field реализует ...
type Field struct {
	Name  string
	Names []any
}

type FieldList []*Field

func (s FieldList) Field(name string) (*Field, bool) {
	for _, f := range s {
		if f.Name == name {
			return f, true
		}
	}
	return nil, false
}

// IFielder реадизует интерфейс завершения формирования полей объекта в "ручном" режиме.
// RestFields будет вызван после завершения автматического формирования полей объекта
// result - массив с полями, сформированными автоматически, в него можно вносить правки
// files - глобальный массив файловых дескрипторов, который будет передан в ответе клиенту
type IFielder interface {
	RestFields(result json.Map, files map[string]IReadCloserLen, names FieldList)
}

func fieldsDefault(t reflect.Type) (res []any) {
	for i := 0; i < t.NumField(); i++ {
		fType := t.Field(i)
		// проверяем тег, если он есть
		tag := fType.Tag.Get("rest")
		if len(tag) > 0 && strings.Contains(tag, "default") {
			res = append(res, camelToSnake(fType.Name))
		}
	}
	return
}

func parseName(val reflect.Value) (res *Field, err error) {
	switch val.Kind() {
	case reflect.String:
		return &Field{
			Name: camelToSnake(val.String()),
		}, nil
	case reflect.Map:
		jm := make(json.Map)
		jmVal := reflect.ValueOf(jm)
		if !val.CanConvert(jmVal.Type()) {
			err = fmt.Errorf("expected map[string]any")
		}
		jm = val.Convert(jmVal.Type()).Interface().(json.Map)
		name := jm.String("name", "")
		if len(name) == 0 {
			err = fmt.Errorf("name is empty")
			return
		}
		name = camelToSnake(name)
		fields := jm.Slice("fields", nil)
		res = &Field{
			Name:  name,
			Names: fields,
		}
		return
	default:
		err = fmt.Errorf("invalid request %s", val.Kind())
	}
	return
}

func fieldVal(val reflect.Value, fieldName string, files map[string]IReadCloserLen, names ...any) (res reflect.Value, err IErrorArgs) {
	switch val.Kind() {
	case reflect.Ptr, reflect.Interface:
		return fieldVal(val.Elem(), fieldName, files, names...)
	case reflect.Struct:
		if len(names) == 0 {
			names = fieldsDefault(val.Type())
		}
		jm := json.Map{}
		var fields FieldList
	loop:
		for _, name := range names {
			rField, rErr := parseName(reflect.ValueOf(name))
			if rErr != nil {
				message := fmt.Sprintf("%v: %v", name, err)
				err = ErrorFiled(fieldName, message)
				return
			}
			fields = append(fields, rField)
			var field reflect.Value
			for i := 0; i < val.NumField(); i++ {
				f := val.Type().Field(i)
				if camelToSnake(f.Name) == rField.Name {
					tag := f.Tag.Get("rest")
					if len(tag) > 0 && strings.Contains(tag, "ignore") {
						continue loop
					}
					field = val.Field(i)
					break
				}
			}
			if !field.IsValid() {
				err = ErrorFiled(rField.Name, "field is not found")
				return
			}
			var fVal reflect.Value
			if fVal, err = fieldVal(
				field,
				fmt.Sprintf("%v.%v", fieldName, rField.Name),
				files,
				rField.Names...,
			); err != nil {
				return
			}
			jm[rField.Name] = fVal.Interface()
		}
		if fielder, check := val.Interface().(IFielder); check {
			fielder.RestFields(jm, files, fields)
		} else if val.CanAddr() {
			if fielder, check := val.Addr().Interface().(IFielder); check {
				fielder.RestFields(jm, files, fields)
			}
		}
		res = reflect.ValueOf(jm)

		return
	case reflect.Slice:
		sl := make([]any, val.Len())
		for i := 0; i < val.Len(); i++ {
			fName := fmt.Sprintf("%v[%v]", fieldName, i)
			var rVal reflect.Value
			rVal, err = fieldVal(
				val.Index(i),
				fName,
				files,
				names...,
			)
			if err != nil {
				return
			}
			sl[i] = rVal.Interface()
		}
		res = reflect.ValueOf(sl)
		return
	case reflect.Map:
		res = reflect.MakeMap(val.Type())
		for _, key := range val.MapKeys() {
			fName := fmt.Sprintf("%v[%v]", fieldName, key.String())
			var rVal reflect.Value
			iVal := val.MapIndex(key)
			rVal, err = fieldVal(iVal, fName, files)
			if err != nil {
				return
			}
			res.SetMapIndex(key, rVal)
		}
		return
	default:
		res = val
		return
	}
}

// Fields позволяет получить значения объекта в json
func Fields(obj any, files map[string]IReadCloserLen, names ...any) (json.Map, IErrorArgs) {
	sVal := reflect.ValueOf(obj)
	rVal, err := fieldVal(sVal.Elem(), "", files, names...)
	if err != nil {
		return nil, err
	}
	res, check := rVal.Interface().(json.Map)
	if !check {
		return nil, ErrorMessage("ErrFields", "Empty object")
	}
	return res, nil
}

/////////////////////////////////////////////
