package infra

import (
	"fmt"
	"os"
	"reflect"
	"strconv"
	"strings"
)

// ConfigFromEnv populates a configuration struct from environment variables
// using struct field tags. The target must be a non-nil pointer to a struct.
//
// Supported tags:
//   - env:"VAR_NAME"      — environment variable to read (required tag)
//   - default:"value"     — value used when env var is unset/empty
//   - required:"true"     — error if env var is unset/empty and no default
//   - secret:"true"       — documented only; no runtime effect on loading
//
// Supported field types: string, int, int64, bool, float64, []string.
// []string fields are parsed from comma-separated values.
//
// Example struct:
//
//	type Config struct {
//	    Port   int    `env:"PORT"    default:"8080" required:"true"`
//	    APIKey string `env:"API_KEY" required:"true" secret:"true"`
//	    Tags   []string `env:"TAGS"  default:"a,b"`
//	}
func ConfigFromEnv(target any) error {
	v := reflect.ValueOf(target)
	if v.Kind() != reflect.Ptr || v.IsNil() {
		return fmt.Errorf("config_from_env: target must be a non-nil pointer to a struct")
	}
	v = v.Elem()
	if v.Kind() != reflect.Struct {
		return fmt.Errorf("config_from_env: target must point to a struct, got %s", v.Kind())
	}

	t := v.Type()
	var errs []string

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		if !field.IsExported() {
			continue
		}

		envKey := field.Tag.Get("env")
		if envKey == "" {
			continue // no env tag → skip
		}

		defaultVal := field.Tag.Get("default")
		required := field.Tag.Get("required") == "true"

		raw := os.Getenv(envKey)
		if raw == "" {
			if defaultVal != "" {
				raw = defaultVal
			} else if required {
				errs = append(errs, fmt.Sprintf("field %s: env var %q is required but not set", field.Name, envKey))
				continue
			} else {
				continue // optional and unset — leave field at zero value
			}
		}

		fv := v.Field(i)
		if err := setField(fv, field.Name, raw); err != nil {
			errs = append(errs, err.Error())
		}
	}

	if len(errs) > 0 {
		return fmt.Errorf("config_from_env: %s", strings.Join(errs, "; "))
	}
	return nil
}

// setField converts the string raw to the appropriate type and sets fv.
func setField(fv reflect.Value, fieldName, raw string) error {
	switch fv.Kind() {
	case reflect.String:
		fv.SetString(raw)

	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		n, err := strconv.ParseInt(raw, 10, 64)
		if err != nil {
			return fmt.Errorf("field %s: cannot parse %q as int: %w", fieldName, raw, err)
		}
		fv.SetInt(n)

	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		n, err := strconv.ParseUint(raw, 10, 64)
		if err != nil {
			return fmt.Errorf("field %s: cannot parse %q as uint: %w", fieldName, raw, err)
		}
		fv.SetUint(n)

	case reflect.Float32, reflect.Float64:
		f, err := strconv.ParseFloat(raw, 64)
		if err != nil {
			return fmt.Errorf("field %s: cannot parse %q as float: %w", fieldName, raw, err)
		}
		fv.SetFloat(f)

	case reflect.Bool:
		b, err := strconv.ParseBool(raw)
		if err != nil {
			return fmt.Errorf("field %s: cannot parse %q as bool (use true/false/1/0): %w", fieldName, raw, err)
		}
		fv.SetBool(b)

	case reflect.Slice:
		if fv.Type().Elem().Kind() != reflect.String {
			return fmt.Errorf("field %s: slice of non-string is not supported, use []string", fieldName)
		}
		parts := strings.Split(raw, ",")
		trimmed := make([]string, 0, len(parts))
		for _, p := range parts {
			t := strings.TrimSpace(p)
			if t != "" {
				trimmed = append(trimmed, t)
			}
		}
		fv.Set(reflect.ValueOf(trimmed))

	default:
		return fmt.Errorf("field %s: unsupported type %s", fieldName, fv.Kind())
	}
	return nil
}
