package infra

import (
	"fmt"
	"reflect"
	"regexp"
	"strings"
)

// emailRE is a basic email format check.
var emailRE = regexp.MustCompile(`^[^@\s]+@[^@\s]+\.[^@\s]+$`)

// urlRE checks for http:// or https:// prefix.
var urlRE = regexp.MustCompile(`^https?://`)

// ConfigValidate validates a configuration struct populated with values.
// It reads struct field tags to determine validation rules:
//   - required:"true" — field must be non-zero / non-empty
//   - format:"email"  — field value must match email pattern
//   - format:"url"    — field value must start with http:// or https://
//
// Only exported fields are inspected. Validation errors are accumulated; all
// fields are checked even if earlier ones fail. Returns a ConfigValidation
// with IsValid=true when no errors are found.
func ConfigValidate(cfg any) ConfigValidation {
	var errs []ConfigError

	v := reflect.ValueOf(cfg)
	if v.Kind() == reflect.Ptr {
		if v.IsNil() {
			return ConfigValidation{Errors: []ConfigError{{Field: "", Message: "cfg is nil", Tag: "required"}}, IsValid: false}
		}
		v = v.Elem()
	}
	if v.Kind() != reflect.Struct {
		return ConfigValidation{Errors: []ConfigError{{Field: "", Message: "cfg must be a struct", Tag: "required"}}, IsValid: false}
	}

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

		// required check
		required := field.Tag.Get("required")
		if required == "true" {
			if isZero(fv) {
				errs = append(errs, ConfigError{
					Field:   fieldName,
					Message: fmt.Sprintf("field %s is required", fieldName),
					Tag:     "required",
				})
				continue // skip format check when value is missing
			}
		}

		// format check (only for string fields)
		format := field.Tag.Get("format")
		if format != "" && fv.Kind() == reflect.String {
			val := fv.String()
			switch format {
			case "email":
				if !emailRE.MatchString(val) {
					errs = append(errs, ConfigError{
						Field:   fieldName,
						Message: fmt.Sprintf("field %s has invalid email format: %q", fieldName, val),
						Tag:     "format",
					})
				}
			case "url":
				if !urlRE.MatchString(val) {
					errs = append(errs, ConfigError{
						Field:   fieldName,
						Message: fmt.Sprintf("field %s has invalid URL format (must start with http:// or https://): %q", fieldName, val),
						Tag:     "format",
					})
				}
			default:
				errs = append(errs, ConfigError{
					Field:   fieldName,
					Message: fmt.Sprintf("field %s has unknown format tag: %q", fieldName, format),
					Tag:     "format",
				})
			}
		}

		// min/max for numeric fields
		minTag := field.Tag.Get("min")
		maxTag := field.Tag.Get("max")
		if (minTag != "" || maxTag != "") && isNumeric(fv) {
			val := toFloat(fv)
			if minTag != "" {
				var minVal float64
				fmt.Sscanf(minTag, "%f", &minVal)
				if val < minVal {
					errs = append(errs, ConfigError{
						Field:   fieldName,
						Message: fmt.Sprintf("field %s value %v is below minimum %v", fieldName, val, minVal),
						Tag:     "min",
					})
				}
			}
			if maxTag != "" {
				var maxVal float64
				fmt.Sscanf(maxTag, "%f", &maxVal)
				if val > maxVal {
					errs = append(errs, ConfigError{
						Field:   fieldName,
						Message: fmt.Sprintf("field %s value %v is above maximum %v", fieldName, val, maxVal),
						Tag:     "max",
					})
				}
			}
		}

		// oneof check for string fields
		oneof := field.Tag.Get("oneof")
		if oneof != "" && fv.Kind() == reflect.String {
			val := fv.String()
			allowed := strings.Split(oneof, " ")
			found := false
			for _, a := range allowed {
				if a == val {
					found = true
					break
				}
			}
			if !found {
				errs = append(errs, ConfigError{
					Field:   fieldName,
					Message: fmt.Sprintf("field %s value %q is not one of: %s", fieldName, val, oneof),
					Tag:     "oneof",
				})
			}
		}
	}

	return ConfigValidation{
		Errors:  errs,
		IsValid: len(errs) == 0,
	}
}

func isZero(v reflect.Value) bool {
	switch v.Kind() {
	case reflect.String:
		return v.Len() == 0
	case reflect.Slice, reflect.Map:
		return v.IsNil() || v.Len() == 0
	case reflect.Ptr, reflect.Interface:
		return v.IsNil()
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return v.Int() == 0
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		return v.Uint() == 0
	case reflect.Float32, reflect.Float64:
		return v.Float() == 0
	case reflect.Bool:
		return !v.Bool()
	default:
		return false
	}
}

func isNumeric(v reflect.Value) bool {
	switch v.Kind() {
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
		reflect.Float32, reflect.Float64:
		return true
	default:
		return false
	}
}

func toFloat(v reflect.Value) float64 {
	switch v.Kind() {
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return float64(v.Int())
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		return float64(v.Uint())
	case reflect.Float32, reflect.Float64:
		return v.Float()
	default:
		return 0
	}
}
