package meta

import (
	"reflect"
	"strings"
)

// GetEnvPathMapByModel use reflection to read struct fields and generate a mapping of environment variable names.
//
// This is useful for generating environment variable names for all the config paths in the Config struct.
// The environment variable names are generated by converting the config path to uppercase and replacing
// all dots with underscores. For example, the config path `ip_region.db_path` will be converted to
// `ATK_IP_REGION_DB_PATH`.
//
// The $$ is symbol for number.
func GetEnvPathMapByModel(model any) map[string]string {
	// Use reflect to get every field tag of the struct
	var retrieve func(m map[string]string, rootPath string, s interface{}) map[string]string
	retrieve = func(m map[string]string, rootPath string, s interface{}) map[string]string {
		t := reflect.TypeOf(s)
		for i := 0; i < t.NumField(); i++ {
			field := t.Field(i)
			tag := field.Tag.Get("json")

			if tag == "" || tag == "-" {
				continue
			}

			var path string
			if rootPath != "" {
				path = rootPath + "." + tag
			} else {
				path = tag
			}

			if field.Type.Kind() == reflect.Struct {
				// recursively retrieve nested struct fields
				m = retrieve(m, path, reflect.New(field.Type).Elem().Interface())
			} else {
				envName := EnvPrefix + strings.ReplaceAll(strings.ToUpper(path), ".", "_")
				// For previous version support, eg. keep `ATK_IP__REGION_DB__PATH` for `ip_region.db_path`
				// @see https://github.com/knadh/koanf/issues/295 [Title: Difficulty Handling Multi-Word Environment Variables]
				envName2 := EnvPrefix + strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ToUpper(path), ".", "#"), "_", "__"), "#", "_")

				if field.Type.Kind() == reflect.Slice {
					// for slice type marks the environment variable as an array
					envName += "_$$"
					envName2 += "_$$"
					path += ".$$"

					if field.Type.Elem().Kind() == reflect.Struct {
						// if the el type is a struct, then we need to retrieve the struct fields
						m = retrieve(m, path, reflect.New(field.Type.Elem()).Elem().Interface())
						continue // not adding the slice field itself
					}
				}

				m[envName] = path
				m[envName2] = path
			}
		}
		return m
	}

	return retrieve(make(map[string]string), "", model)
}

// GetEnvPathMap read the model and YAML config template (with comments and default values)
// to get the mapping of environment variable names to their corresponding config paths.
func GetEnvPathMap(model any, yamlTemplate string) map[string]string {
	mp := map[string]string{}

	// Get the mapping from the model
	for k, v := range GetEnvPathMapByModel(model) {
		mp[k] = v
	}

	// Get the mapping from the config template
	metas, err := GetOptionsMetaData(yamlTemplate)
	if err != nil {
		return mp
	}
	for _, v := range metas {
		if v.AllowsSet {
			mp[v.Env] = v.Path
		}
	}

	return mp
}
