"""Configuration loading and validation."""

from __future__ import annotations

from dataclasses import dataclass
import os
from typing import Any

import tomllib


@dataclass
class MainConfig:
    base_dir: str
    templates_prefix: str
    containers_prefix: str
    jail_conf_dir: str
    log_dir: str


@dataclass
class Mount:
    host: str
    jail: str
    readonly: bool = False


@dataclass
class JailConfig:
    name: str
    config_path: str
    template: str
    overlay: str | None
    mounts: list[Mount]
    env: dict[str, str]
    jailconf: dict[str, object]
    cmds: dict[str, str]


class ConfigError(RuntimeError):
    pass


def load_main_config() -> MainConfig:
    """Load main config from /usr/local/etc/jprov.conf or /etc/jprov.conf."""
    candidates = ["/usr/local/etc/jprov.conf", "/etc/jprov.conf"]
    path = next((p for p in candidates if os.path.exists(p)), None)
    if not path:
        raise ConfigError("main config not found (expected /usr/local/etc/jprov.conf or /etc/jprov.conf)")

    with open(path, "rb") as fh:
        data = tomllib.load(fh)

    try:
        base_dir = data["base_dir"]
        datasets = data["datasets"]
        templates_prefix = datasets["templates_prefix"]
        containers_prefix = datasets["containers_prefix"]
        jail_conf_dir = data["jail_conf_dir"]
        log_dir = data["log_dir"]
    except KeyError as exc:
        raise ConfigError(f"missing main config key: {exc}") from exc

    for key, val in [
        ("base_dir", base_dir),
        ("templates_prefix", templates_prefix),
        ("containers_prefix", containers_prefix),
        ("jail_conf_dir", jail_conf_dir),
        ("log_dir", log_dir),
    ]:
        if not isinstance(val, str) or not val:
            raise ConfigError(f"invalid main config value for {key}")

    if not templates_prefix.endswith("/"):
        raise ConfigError("datasets.templates_prefix must end with '/'")
    if not containers_prefix.endswith("/"):
        raise ConfigError("datasets.containers_prefix must end with '/'")

    return MainConfig(
        base_dir=base_dir,
        templates_prefix=templates_prefix,
        containers_prefix=containers_prefix,
        jail_conf_dir=jail_conf_dir,
        log_dir=log_dir,
    )


def load_jail_config(main: MainConfig, jailname: str) -> JailConfig:
    """Load per-jail TOML config for the given jail name."""
    jail_dir = os.path.join(main.base_dir, "defs", jailname)
    path = os.path.join(jail_dir, "jprov.conf")
    if not os.path.exists(path):
        raise ConfigError(f"jail config not found: {path}")

    with open(path, "rb") as fh:
        data = tomllib.load(fh)

    try:
        template = data["template"]
    except KeyError as exc:
        raise ConfigError(f"missing jail config key: {exc}") from exc

    overlay = data.get("overlay")
    env = data.get("env", {})
    jailconf = data.get("jailconf", {})
    cmds = data.get("cmds", {})
    mounts_raw = data.get("mounts", [])

    if not isinstance(template, str) or not template:
        raise ConfigError("invalid jail config template")
    if cmds and not isinstance(cmds, dict):
        raise ConfigError("invalid jail config cmds")
    if overlay is not None and not isinstance(overlay, str):
        raise ConfigError("invalid jail config overlay")
    if env and not isinstance(env, dict):
        raise ConfigError("invalid jail config env")
    if jailconf and not isinstance(jailconf, dict):
        raise ConfigError("invalid jail config jailconf")
    if mounts_raw and not isinstance(mounts_raw, list):
        raise ConfigError("invalid jail config mounts")

    if not isinstance(cmds, dict) or "provision" not in cmds or not isinstance(cmds["provision"], str):
        raise ConfigError("missing or invalid cmds.provision")
    for key, val in cmds.items():
        if not isinstance(val, str) or not val:
            raise ConfigError(f"invalid cmds.{key}")

    mounts: list[Mount] = []
    for item in mounts_raw:
        if not isinstance(item, dict):
            raise ConfigError("invalid mount entry")
        try:
            host = item["host"]
            jail = item["jail"]
        except KeyError as exc:
            raise ConfigError(f"missing mount key: {exc}") from exc
        readonly = bool(item.get("readonly", False))
        if not isinstance(host, str) or not isinstance(jail, str):
            raise ConfigError("invalid mount host/jail")
        mounts.append(Mount(host=host, jail=jail, readonly=readonly))

    return JailConfig(
        name=jailname,
        config_path=path,
        template=template,
        overlay=overlay,
        mounts=mounts,
        env={str(k): str(v) for k, v in env.items()} if env else {},
        jailconf=jailconf,
        cmds={str(k): str(v) for k, v in cmds.items()} if cmds else {},
    )


def validate_configs(main: MainConfig, jail: JailConfig) -> None:
    """Validate referenced paths and dataset names exist."""
    if not os.path.isdir(main.base_dir):
        raise ConfigError(f"base_dir does not exist: {main.base_dir}")

    jail_dir = os.path.join(main.base_dir, "defs", jail.name)
    if not os.path.isdir(jail_dir):
        raise ConfigError(f"jail defs dir does not exist: {jail_dir}")

    overlay_path = resolve_overlay_path(main, jail)
    if overlay_path and not os.path.isdir(overlay_path):
        raise ConfigError(f"overlay dir does not exist: {overlay_path}")

    for mnt in jail.mounts:
        if not os.path.exists(mnt.host):
            raise ConfigError(f"mount host path does not exist: {mnt.host}")



def resolve_overlay_path(main: MainConfig, jail: JailConfig) -> str:
    """Return resolved overlay path for the jail."""
    if jail.overlay is None or jail.overlay == "":
        return os.path.join(main.base_dir, "defs", jail.name, "overlay")
    if os.path.isabs(jail.overlay):
        return jail.overlay
    return os.path.join(main.base_dir, "defs", jail.name, jail.overlay)


def jail_conf_path(main: MainConfig, jailname: str) -> str:
    """Return path to the generated jail.conf.d file for this jail."""
    return os.path.join(main.jail_conf_dir, f"{jailname}.conf")
