// Package config provides TOML configuration loading with environment
// variable overrides for Metacircular services.
//
// Services define their own config struct embedding [Base], which provides
// the standard sections (Server, Database, MCIAS, Log). Use [Load] to
// parse a TOML file, apply environment overrides, set defaults, and
// validate required fields.
//
// # Duration fields
//
// Timeout fields in [ServerConfig] use the [Duration] type rather than
// [time.Duration] because go-toml v2 does not natively decode strings
// (e.g., "30s") into time.Duration. Access the underlying value via
// the embedded field:
//
//	cfg.Server.ReadTimeout.Duration  // time.Duration
//
// In TOML files, durations are written as Go duration strings:
//
//	read_timeout = "30s"
//	idle_timeout = "2m"
//
// Environment variable overrides also use this format:
//
//	MCR_SERVER_READ_TIMEOUT=30s
package config

import (
	"fmt"
	"os"
	"reflect"
	"strings"
	"time"

	"github.com/pelletier/go-toml/v2"

	"git.wntrmute.dev/mc/mcdsl/auth"
)

// Base contains the configuration sections common to all Metacircular
// services. Services embed this in their own config struct and add
// service-specific sections.
//
// Example:
//
//	type MyConfig struct {
//	    config.Base
//	    MyService MyServiceSection `toml:"my_service"`
//	}
type Base struct {
	Server   ServerConfig   `toml:"server"`
	Database DatabaseConfig `toml:"database"`
	MCIAS    auth.Config    `toml:"mcias"`
	Log      LogConfig      `toml:"log"`
}

// ServerConfig holds TLS server settings.
type ServerConfig struct {
	// ListenAddr is the HTTPS listen address (e.g., ":8443"). Required.
	ListenAddr string `toml:"listen_addr"`

	// GRPCAddr is the gRPC listen address (e.g., ":9443"). Optional;
	// gRPC is disabled if empty.
	GRPCAddr string `toml:"grpc_addr"`

	// TLSCert is the path to the TLS certificate file (PEM). Required.
	TLSCert string `toml:"tls_cert"`

	// TLSKey is the path to the TLS private key file (PEM). Required.
	TLSKey string `toml:"tls_key"`

	// ReadTimeout is the maximum duration for reading the entire request.
	// Defaults to 30s.
	ReadTimeout Duration `toml:"read_timeout"`

	// WriteTimeout is the maximum duration before timing out writes.
	// Defaults to 30s.
	WriteTimeout Duration `toml:"write_timeout"`

	// IdleTimeout is the maximum time to wait for the next request on
	// a keep-alive connection. Defaults to 120s.
	IdleTimeout Duration `toml:"idle_timeout"`

	// ShutdownTimeout is the maximum time to wait for in-flight requests
	// to drain during graceful shutdown. Defaults to 60s.
	ShutdownTimeout Duration `toml:"shutdown_timeout"`
}

// DatabaseConfig holds SQLite database settings.
type DatabaseConfig struct {
	// Path is the path to the SQLite database file. Required.
	Path string `toml:"path"`
}

// LogConfig holds logging settings.
type LogConfig struct {
	// Level is the log level (debug, info, warn, error). Defaults to "info".
	Level string `toml:"level"`
}

// WebConfig holds web UI server settings. This is not part of Base because
// not all services have a web UI — services that do can add it to their
// own config struct.
type WebConfig struct {
	// ListenAddr is the web UI listen address (e.g., "127.0.0.1:8080").
	ListenAddr string `toml:"listen_addr"`

	// GRPCAddr is the gRPC address of the API server that the web UI
	// connects to.
	GRPCAddr string `toml:"grpc_addr"`

	// CACert is an optional CA certificate for verifying the API server's
	// TLS certificate.
	CACert string `toml:"ca_cert"`
}

// Validator is an optional interface that config structs can implement
// to add service-specific validation. If the config type implements
// Validator, its Validate method is called after defaults and env
// overrides are applied.
type Validator interface {
	Validate() error
}

// Load reads a TOML config file at path, applies environment variable
// overrides using envPrefix (e.g., "MCR" maps MCR_SERVER_LISTEN_ADDR to
// Server.ListenAddr), sets defaults for unset optional fields, and
// validates required fields.
//
// If T implements [Validator], its Validate method is called after all
// other processing.
func Load[T any](path string, envPrefix string) (*T, error) {
	data, err := os.ReadFile(path) //nolint:gosec // config path is operator-supplied
	if err != nil {
		return nil, fmt.Errorf("config: read %s: %w", path, err)
	}

	var cfg T
	if err := toml.Unmarshal(data, &cfg); err != nil {
		return nil, fmt.Errorf("config: parse %s: %w", path, err)
	}

	if envPrefix != "" {
		applyEnvToStruct(reflect.ValueOf(&cfg).Elem(), envPrefix)
	}

	applyPortEnv(&cfg)

	applyBaseDefaults(&cfg)

	if err := validateBase(&cfg); err != nil {
		return nil, err
	}

	if v, ok := any(&cfg).(Validator); ok {
		if err := v.Validate(); err != nil {
			return nil, fmt.Errorf("config: %w", err)
		}
	}

	return &cfg, nil
}

// applyBaseDefaults sets defaults on the embedded Base struct if present.
func applyBaseDefaults(cfg any) {
	base := findBase(cfg)
	if base == nil {
		return
	}

	if base.Server.ReadTimeout.Duration == 0 {
		base.Server.ReadTimeout.Duration = 30 * time.Second
	}
	if base.Server.WriteTimeout.Duration == 0 {
		base.Server.WriteTimeout.Duration = 30 * time.Second
	}
	if base.Server.IdleTimeout.Duration == 0 {
		base.Server.IdleTimeout.Duration = 120 * time.Second
	}
	if base.Server.ShutdownTimeout.Duration == 0 {
		base.Server.ShutdownTimeout.Duration = 60 * time.Second
	}
	if base.Log.Level == "" {
		base.Log.Level = "info"
	}
}

// validateBase checks required fields on the embedded Base struct if present.
func validateBase(cfg any) error {
	base := findBase(cfg)
	if base == nil {
		return nil
	}

	required := []struct {
		name  string
		value string
	}{
		{"server.listen_addr", base.Server.ListenAddr},
		{"server.tls_cert", base.Server.TLSCert},
		{"server.tls_key", base.Server.TLSKey},
	}

	for _, r := range required {
		if r.value == "" {
			return fmt.Errorf("config: required field %q is missing", r.name)
		}
	}

	return nil
}

// findBase returns a pointer to the embedded Base struct, or nil if the
// config type does not embed Base.
func findBase(cfg any) *Base {
	v := reflect.ValueOf(cfg)
	if v.Kind() == reflect.Ptr {
		v = v.Elem()
	}
	if v.Kind() != reflect.Struct {
		return nil
	}

	// Check if cfg *is* a Base.
	if b, ok := v.Addr().Interface().(*Base); ok {
		return b
	}

	// Check embedded fields.
	t := v.Type()
	for i := range t.NumField() {
		field := t.Field(i)
		if field.Anonymous && field.Type == reflect.TypeOf(Base{}) {
			b, ok := v.Field(i).Addr().Interface().(*Base)
			if ok {
				return b
			}
		}
	}

	return nil
}

// applyPortEnv overrides ServerConfig.ListenAddr and ServerConfig.GRPCAddr
// from $PORT and $PORT_GRPC respectively. These environment variables are
// set by the MCP agent to assign authoritative port bindings, so they take
// precedence over both TOML values and generic env overrides.
func applyPortEnv(cfg any) {
	sc := findServerConfig(cfg)
	if sc == nil {
		return
	}

	if port, ok := os.LookupEnv("PORT"); ok {
		sc.ListenAddr = ":" + port
	}
	if port, ok := os.LookupEnv("PORT_GRPC"); ok {
		sc.GRPCAddr = ":" + port
	}
}

// findServerConfig returns a pointer to the ServerConfig in the config
// struct. It first checks for an embedded Base (which contains Server),
// then walks the struct tree via reflection to find any ServerConfig field
// directly (e.g., the Metacrypt pattern where ServerConfig is embedded
// without Base).
func findServerConfig(cfg any) *ServerConfig {
	if base := findBase(cfg); base != nil {
		return &base.Server
	}

	return findServerConfigReflect(reflect.ValueOf(cfg))
}

// findServerConfigReflect walks the struct tree to find a ServerConfig field.
func findServerConfigReflect(v reflect.Value) *ServerConfig {
	if v.Kind() == reflect.Ptr {
		v = v.Elem()
	}
	if v.Kind() != reflect.Struct {
		return nil
	}

	scType := reflect.TypeOf(ServerConfig{})
	t := v.Type()
	for i := range t.NumField() {
		field := t.Field(i)
		fv := v.Field(i)

		if field.Type == scType {
			sc, ok := fv.Addr().Interface().(*ServerConfig)
			if ok {
				return sc
			}
		}

		// Recurse into embedded or nested structs.
		if fv.Kind() == reflect.Struct && field.Type != scType {
			if sc := findServerConfigReflect(fv); sc != nil {
				return sc
			}
		}
	}

	return nil
}

// applyEnvToStruct recursively walks a struct and overrides field values
// from environment variables. The env variable name is built from the
// prefix and the toml tag: PREFIX_SECTION_FIELD (uppercased).
//
// Supported field types: string, time.Duration (as int64), []string
// (comma-separated), bool, int.
func applyEnvToStruct(v reflect.Value, prefix string) {
	if v.Kind() == reflect.Ptr {
		v = v.Elem()
	}
	t := v.Type()

	for i := range t.NumField() {
		field := t.Field(i)
		fv := v.Field(i)

		// For anonymous (embedded) fields, recurse with the same prefix.
		if field.Anonymous {
			applyEnvToStruct(fv, prefix)
			continue
		}

		tag := field.Tag.Get("toml")
		if tag == "" || tag == "-" {
			continue
		}
		envKey := prefix + "_" + strings.ToUpper(tag)

		// Handle Duration wrapper before generic struct recursion.
		if field.Type == reflect.TypeOf(Duration{}) {
			envVal, ok := os.LookupEnv(envKey)
			if ok {
				d, parseErr := time.ParseDuration(envVal)
				if parseErr == nil {
					fv.Set(reflect.ValueOf(Duration{d}))
				}
			}
			continue
		}

		if field.Type.Kind() == reflect.Struct {
			applyEnvToStruct(fv, envKey)
			continue
		}

		envVal, ok := os.LookupEnv(envKey)
		if !ok {
			continue
		}

		switch fv.Kind() {
		case reflect.String:
			fv.SetString(envVal)
		case reflect.Bool:
			fv.SetBool(envVal == "true" || envVal == "1")
		case reflect.Slice:
			if field.Type.Elem().Kind() == reflect.String {
				parts := strings.Split(envVal, ",")
				for j := range parts {
					parts[j] = strings.TrimSpace(parts[j])
				}
				fv.Set(reflect.ValueOf(parts))
			}
		}
	}
}
