package config

import (
	"fmt"
	"os"
	"strings"

	"github.com/hashicorp/hcl/v2/hclsimple"
)

type CapacityConfig struct {
	CPU      int `hcl:"cpu,optional"`
	MemoryMB int `hcl:"memory_mb,optional"`
}

type Config struct {
	DBPath         string          `hcl:"db_path,optional"`
	ListenAddr     string          `hcl:"listen_addr,optional"`
	CAPath         string          `hcl:"ca_path,optional"`
	ServerCertPath string          `hcl:"server_cert_path,optional"`
	ServerKeyPath  string          `hcl:"server_key_path,optional"`
	NodeCapacity   *CapacityConfig `hcl:"node_capacity,block"`
}

type Flags struct {
	DBPath         *string
	ListenAddr     *string
	CAPath         *string
	ServerCertPath *string
	ServerKeyPath  *string
	CPU            *int
	MemoryMB       *int
}

type Environ map[string]string

// Load is the config dispatcher (R-014). It tries each path in order,
// skipping missing files, and dispatches to the appropriate loader
// based on file extension: .hcl → LoadHCL (legacy, R-013),
// .md → LoadMarkdown (new Markdown-frontmatter loader), and
// .yaml/.yml → LoadMarkdown with an empty body. The first successfully
// decoded file wins. If no path exists or decodes, a zero Config is
// returned.
//
// The signature is preserved from the v0.8 single-loader API so
// internal/cli/root.go requires no changes yet.
func Load(paths ...string) (*Config, error) {
	for _, p := range paths {
		if _, err := os.Stat(p); err != nil {
			continue
		}
		cfg, err := loadByExtension(p)
		if err != nil {
			return nil, err
		}
		return cfg, nil
	}
	return &Config{}, nil
}

func loadByExtension(p string) (*Config, error) {
	ext := strings.ToLower(filepathExt(p))
	switch ext {
	case ".hcl":
		return LoadHCL(p)
	case ".md", ".markdown":
		return LoadMarkdown(p)
	case ".yaml", ".yml":
		return LoadMarkdownYAML(p)
	default:
		// Unknown extension: hclsimple.Decode rejects non-.hcl
		// suffixes, so for backward compat with the v0.8 single-loader
		// behavior (which assumed HCL), decode the file content as HCL
		// against a synthesized .hcl path.
		data, err := os.ReadFile(p)
		if err != nil {
			return nil, fmt.Errorf("read config %s: %w", p, err)
		}
		var cfg Config
		if err := hclsimple.Decode(p+".hcl", data, nil, &cfg); err != nil {
			return nil, fmt.Errorf("decode config %s: %w", p, err)
		}
		return &cfg, nil
	}
}

// filepathExt is a thin wrapper around filepath.Ext to keep the import
// localized to the dispatcher. Returns the extension including the dot,
// lowercased by the caller.
func filepathExt(p string) string {
	i := strings.LastIndex(p, ".")
	if i < 0 {
		return ""
	}
	return p[i:]
}

// LoadHCL decodes a legacy HCL config file (R-013).
//
// Deprecated: use LoadMarkdown or the dispatcher. HCL is legacy per
// R-013. Retained for the v0.9 dual-write window (REQ-090); new
// deployments should author config.md with YAML frontmatter.
func LoadHCL(path string) (*Config, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("read config %s: %w", path, err)
	}
	var cfg Config
	if err := hclsimple.Decode(path, data, nil, &cfg); err != nil {
		return nil, fmt.Errorf("decode config %s: %w", path, err)
	}
	return &cfg, nil
}

func (c *Config) MergeOverrides(flags Flags, env Environ) *Config {
	out := &Config{
		DBPath:         c.DBPath,
		ListenAddr:     c.ListenAddr,
		CAPath:         c.CAPath,
		ServerCertPath: c.ServerCertPath,
		ServerKeyPath:  c.ServerKeyPath,
		NodeCapacity:   c.NodeCapacity,
	}

	applyStr := func(flag *string, envKey, fileVal string) string {
		if flag != nil {
			return *flag
		}
		if v, ok := env[envKey]; ok && v != "" {
			return v
		}
		return fileVal
	}

	out.DBPath = applyStr(flags.DBPath, "ORCA_DB", out.DBPath)
	out.ListenAddr = applyStr(flags.ListenAddr, "ORCA_LISTEN_ADDR", out.ListenAddr)
	out.CAPath = applyStr(flags.CAPath, "ORCA_CA_PATH", out.CAPath)
	out.ServerCertPath = applyStr(flags.ServerCertPath, "ORCA_SERVER_CERT_PATH", out.ServerCertPath)
	out.ServerKeyPath = applyStr(flags.ServerKeyPath, "ORCA_SERVER_KEY_PATH", out.ServerKeyPath)

	if out.NodeCapacity == nil {
		out.NodeCapacity = &CapacityConfig{}
	} else {
		nc := *out.NodeCapacity
		out.NodeCapacity = &nc
	}

	if flags.CPU != nil {
		out.NodeCapacity.CPU = *flags.CPU
	} else if v, ok := env["ORCA_NODE_CPU"]; ok && v != "" {
		if n, err := atoi(v); err == nil {
			out.NodeCapacity.CPU = n
		}
	}

	if flags.MemoryMB != nil {
		out.NodeCapacity.MemoryMB = *flags.MemoryMB
	} else if v, ok := env["ORCA_NODE_MEMORY_MB"]; ok && v != "" {
		if n, err := atoi(v); err == nil {
			out.NodeCapacity.MemoryMB = n
		}
	}

	return out
}

func atoi(s string) (int, error) {
	n := 0
	if s == "" {
		return 0, fmt.Errorf("empty")
	}
	neg := false
	i := 0
	if s[0] == '-' {
		neg = true
		i = 1
	}
	for ; i < len(s); i++ {
		if s[i] < '0' || s[i] > '9' {
			return 0, fmt.Errorf("bad")
		}
		n = n*10 + int(s[i]-'0')
	}
	if neg {
		n = -n
	}
	return n, nil
}
