package flatted

import (
	"encoding/json"
	"reflect"
	"sort"
	"strconv"
	"strings"
)

// flattedIndex is a internal type used to distinguish between
// actual strings and flatted indices during the reconstruction phase.
type flattedIndex string

// Stringify converts a Go value into a specialized flatted JSON string.
func Stringify(value, replacer, space any) (string, error) {
	knownKeys := []any{}
	knownValues := []string{}
	input := []any{}

	index := func(v any) string {
		input = append(input, v)
		idx := strconv.Itoa(len(input) - 1)
		knownKeys = append(knownKeys, v)
		knownValues = append(knownValues, idx)
		return idx
	}

	relate := func(v any) any {
		if v == nil {
			return nil
		}
		rv := reflect.ValueOf(v)
		kind := rv.Kind()
		if kind == reflect.String || kind == reflect.Slice || kind == reflect.Map || kind == reflect.Pointer {
			for i, k := range knownKeys {
				if kind == reflect.String {
					if k == v {
						return knownValues[i]
					}
				} else {
					rk := reflect.ValueOf(k)
					if rk.Kind() == kind && rk.Pointer() == rv.Pointer() {
						return knownValues[i]
					}
				}
			}
			return index(v)
		}
		return v
	}

	transform := func(v any) any {
		rv := reflect.ValueOf(v)
		if !rv.IsValid() {
			return nil
		}
		if _, ok := v.(json.Marshaler); ok {
			return v
		}
		// Dereference pointers to process the underlying Slice, Map, or Array
		for rv.Kind() == reflect.Pointer && !rv.IsNil() {
			rv = rv.Elem()
		}
		switch rv.Kind() {
		case reflect.Slice, reflect.Array:
			res := make([]any, rv.Len())
			for i := 0; i < rv.Len(); i++ {
				res[i] = relate(rv.Index(i).Interface())
			}
			return res
		case reflect.Map:
			res := make(map[string]any)
			keys := rv.MapKeys()
			sort.Slice(keys, func(i, j int) bool {
				return keys[i].String() < keys[j].String()
			})

			whitelist, isWhitelist := replacer.([]string)
			for _, key := range keys {
				kStr := key.String()
				if isWhitelist {
					found := false
					for _, w := range whitelist {
						if w == kStr {
							found = true
							break
						}
					}
					if !found {
						continue
					}
				}
				res[kStr] = relate(rv.MapIndex(key).Interface())
			}
			return res
		case reflect.Struct:
			res := make(map[string]any)
			t := rv.Type()
			for i := 0; i < rv.NumField(); i++ {
				field := t.Field(i)
				if field.PkgPath != "" {
					continue
				}
				name := field.Name
				if tag := field.Tag.Get("json"); tag != "" {
					name = strings.Split(tag, ",")[0]
				}
				res[name] = relate(rv.Field(i).Interface())
			}
			return res
		default:
			return v
		}
	}

	index(value)
	output := []any{}
	for i := 0; i < len(input); i++ {
		output = append(output, transform(input[i]))
	}

	var b []byte
	var err error
	indent := ""
	switch v := space.(type) {
	case string:
		indent = v
	case int:
		indent = strings.Repeat(" ", v)
	}

	if indent != "" {
		b, err = json.MarshalIndent(output, "", indent)
	} else {
		b, err = json.Marshal(output)
	}

	if err != nil {
		return "", err
	}
	return string(b), nil
}

// Parse converts a specialized flatted string into a Go value.
func Parse(text string, reviver func(key string, value any) any) (any, error) {
	var jsonInput []any
	if err := json.Unmarshal([]byte(text), &jsonInput); err != nil {
		return nil, err
	}

	var wrap func(any) any
	wrap = func(v any) any {
		if s, ok := v.(string); ok {
			return flattedIndex(s)
		}
		if arr, ok := v.([]any); ok {
			for i, item := range arr {
				arr[i] = wrap(item)
			}
			return arr
		}
		if m, ok := v.(map[string]any); ok {
			for k, item := range m {
				m[k] = wrap(item)
			}
			return m
		}
		return v
	}

	wrapped := make([]any, len(jsonInput))
	for i, v := range jsonInput {
		wrapped[i] = wrap(v)
	}

	input := make([]any, len(wrapped))
	for i, v := range wrapped {
		if fi, ok := v.(flattedIndex); ok {
			input[i] = string(fi)
		} else {
			input[i] = v
		}
	}

	if len(input) == 0 {
		return nil, nil
	}

	value := input[0]
	rv := reflect.ValueOf(value)
	if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Map) {
		set := make(map[uintptr]bool)
		set[rv.Pointer()] = true
		res := loop(value, input, set)
		if reviver != nil {
			return revive("", res, reviver), nil
		}
		return res, nil
	}

	if reviver != nil {
		return reviver("", value), nil
	}
	return value, nil
}

func revive(key string, value any, reviver func(k string, v any) any) any {
	switch v := value.(type) {
	case []any:
		for i, el := range v {
			v[i] = revive(strconv.Itoa(i), el, reviver)
		}
	case map[string]any:
		keys := make([]string, 0, len(v))
		for k := range v {
			keys = append(keys, k)
		}
		sort.Strings(keys)
		for _, k := range keys {
			v[k] = revive(k, v[k], reviver)
		}
	}
	return reviver(key, value)
}

func loop(value any, input []any, set map[uintptr]bool) any {
	if arr, ok := value.([]any); ok {
		for i, v := range arr {
			if fi, ok := v.(flattedIndex); ok {
				idx, _ := strconv.Atoi(string(fi))
				arr[i] = ref(input[idx], input, set)
			}
		}
		return arr
	}
	if m, ok := value.(map[string]any); ok {
		for k, v := range m {
			if fi, ok := v.(flattedIndex); ok {
				idx, _ := strconv.Atoi(string(fi))
				m[k] = ref(input[idx], input, set)
			}
		}
		return m
	}
	return value
}

func ref(value any, input []any, set map[uintptr]bool) any {
	rv := reflect.ValueOf(value)
	if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Map) {
		ptr := rv.Pointer()
		if !set[ptr] {
			set[ptr] = true
			return loop(value, input, set)
		}
	}
	return value
}

// ToJSON converts a generic value into a JSON serializable object without losing recursion.
func ToJSON(value any) (any, error) {
	s, err := Stringify(value, nil, nil)
	if err != nil {
		return nil, err
	}
	var res any
	err = json.Unmarshal([]byte(s), &res)
	return res, err
}

// FromJSON converts a previously serialized object with recursion into a recursive one.
func FromJSON(value any) (any, error) {
	b, err := json.Marshal(value)
	if err != nil {
		return nil, err
	}
	return Parse(string(b), nil)
}
