"""TOML config loading, validation, and merging."""

from __future__ import annotations

import subprocess
import tomllib
from dataclasses import dataclass, field
from pathlib import Path


class ConfigError(Exception):
    """Raised for invalid configuration."""


RESERVED_FLAGS = frozenset({"--help", "-h", "--verbose", "-v", "--dry-run", "--version"})


@dataclass
class StaticVar:
    name: str
    value: str
    description: str = ""
    source: str = ""
    append: bool = False
    prepend: bool = False
    separator: str = ""


@dataclass
class FlagVar:
    name: str
    flag: str
    value: str | None = None
    description: str = ""
    source: str = ""
    append: bool = False
    prepend: bool = False
    separator: str = ""


@dataclass
class Config:
    static_vars: dict[str, StaticVar] = field(default_factory=dict)
    flag_vars: dict[str, FlagVar] = field(default_factory=dict)
    flag_lookup: dict[str, FlagVar] = field(default_factory=dict)
    loaded_files: list[str] = field(default_factory=list)
    file_configs: list[Config] = field(default_factory=list)


def _stringify(value: object) -> str:
    """Convert a TOML value to a string suitable for an env var."""
    if isinstance(value, bool):
        return "1" if value else "0"
    if isinstance(value, (int, float)):
        return str(value)
    if isinstance(value, str):
        return value
    raise ConfigError(f"unsupported value type: {type(value).__name__}")


def _parse_table_entry(name: str, table: dict, source: str) -> StaticVar | FlagVar:
    """Classify and parse a TOML table entry into a StaticVar or FlagVar."""
    has_flag = "flag" in table
    has_value = "value" in table

    if not has_flag and not has_value:
        raise ConfigError(
            f"table [{name}] in {source} must have at least 'flag' or 'value'"
        )

    description = table.get("description", "")
    append = table.get("append", False)
    prepend = table.get("prepend", False)
    separator = table.get("separator", "")

    if not isinstance(append, bool):
        raise ConfigError(f"[{name}] 'append' must be a boolean in {source}")
    if not isinstance(prepend, bool):
        raise ConfigError(f"[{name}] 'prepend' must be a boolean in {source}")
    if append and prepend:
        raise ConfigError(
            f"[{name}] cannot set both 'append' and 'prepend' in {source}"
        )
    if not isinstance(separator, str):
        raise ConfigError(f"[{name}] 'separator' must be a string in {source}")

    if has_flag:
        flag = table["flag"]
        if not isinstance(flag, str) or not flag.startswith("-"):
            raise ConfigError(
                f"[{name}] 'flag' must be a string starting with '-' in {source}"
            )
        value = _stringify(table["value"]) if has_value else None
        return FlagVar(
            name=name,
            flag=flag,
            value=value,
            description=str(description),
            source=source,
            append=append,
            prepend=prepend,
            separator=separator,
        )
    else:
        # Table with value but no flag → static var (verbose form)
        return StaticVar(
            name=name,
            value=_stringify(table["value"]),
            description=str(description),
            source=source,
            append=append,
            prepend=prepend,
            separator=separator,
        )


def parse_toml(path: str | Path) -> Config:
    """Parse a single TOML config file into a Config."""
    path = Path(path)
    source = str(path)
    text = path.read_text()
    data = tomllib.loads(text)

    config = Config(loaded_files=[source])

    for key, value in data.items():
        if isinstance(value, dict):
            entry = _parse_table_entry(key, value, source)
            if isinstance(entry, StaticVar):
                config.static_vars[key] = entry
            else:
                config.flag_vars[key] = entry
                config.flag_lookup[entry.flag] = entry
        else:
            # Top-level scalar → static var
            config.static_vars[key] = StaticVar(
                name=key,
                value=_stringify(value),
                source=source,
            )

    return config


def _find_git_root() -> Path | None:
    """Find the git repository root, or None if not in a repo."""
    try:
        result = subprocess.run(
            ["git", "rev-parse", "--show-toplevel"],
            capture_output=True,
            text=True,
            timeout=5,
        )
        if result.returncode == 0:
            return Path(result.stdout.strip())
    except (FileNotFoundError, subprocess.TimeoutExpired):
        pass
    return None


def _pick_file(directory: Path) -> Path | None:
    """Pick a config file from a directory: prefer non-dotfile over dotfile."""
    candidate = directory / "tacoshop.toml"
    if candidate.is_file():
        return candidate
    candidate = directory / ".tacoshop.toml"
    if candidate.is_file():
        return candidate
    return None


def discover_config_files() -> list[Path]:
    """Discover config files in priority order (lowest to highest)."""
    files: list[Path] = []
    seen: set[Path] = set()

    def _add(path: Path | None) -> None:
        if path is not None:
            resolved = path.resolve()
            if resolved not in seen:
                seen.add(resolved)
                files.append(resolved)

    # 1. System-wide (~/.tacoshop.toml)
    home = Path.home() / ".tacoshop.toml"
    if home.is_file():
        _add(home)

    # 2. Git repo root
    git_root = _find_git_root()
    cwd = Path.cwd().resolve()
    if git_root is not None and git_root.resolve() != cwd:
        _add(_pick_file(git_root))

    # 3. Current directory
    _add(_pick_file(cwd))

    return files


def merge_configs(configs: list[Config]) -> Config:
    """Merge multiple configs, later entries take priority."""
    merged = Config()
    for config in configs:
        merged.static_vars.update(config.static_vars)
        merged.flag_vars.update(config.flag_vars)
        merged.flag_lookup.update(config.flag_lookup)
        merged.loaded_files.extend(config.loaded_files)
    return merged


def validate_config(config: Config) -> None:
    """Validate a merged config. Raises ConfigError on problems."""
    # Check for flag collisions (two env vars mapped to the same flag)
    flag_to_vars: dict[str, list[str]] = {}
    for var in config.flag_vars.values():
        flag_to_vars.setdefault(var.flag, []).append(var.name)
    for flag, var_names in flag_to_vars.items():
        if len(var_names) > 1:
            raise ConfigError(
                f"flag '{flag}' is mapped to multiple env vars: {', '.join(var_names)}"
            )

    # Check for reserved flag collisions
    for var in config.flag_vars.values():
        if var.flag in RESERVED_FLAGS:
            raise ConfigError(
                f"[{var.name}] flag '{var.flag}' is reserved by tacoshop"
            )


def load_config() -> Config:
    """Discover, parse, merge, and validate config files."""
    files = discover_config_files()
    configs = [parse_toml(f) for f in files]
    config = merge_configs(configs) if configs else Config()
    config.file_configs = configs
    validate_config(config)
    return config
