"""Build-time paths, not a second runtime configuration or dependency resolver."""
from __future__ import annotations

from dataclasses import dataclass, field, fields
from pathlib import Path
import json
import re
import tomllib


RESOURCE_ENV = {
    "skills": "HERMES_BUNDLED_SKILLS",
    "optional-skills": "HERMES_OPTIONAL_SKILLS",
    "plugins": "HERMES_BUNDLED_PLUGINS",
    "locales": "HERMES_BUNDLED_LOCALES",
    "optional-mcps": "HERMES_OPTIONAL_MCPS",
}


def dependency_site(environment: Path, python_version: str, target: str) -> Path:
    """Providers know the target pin; the builder's interpreter is irrelevant."""
    minor = python_version.split("+")[0].rsplit(".", 1)[0]
    return environment / ("Lib/site-packages" if target.startswith("win32") else
                          f"lib/python{minor}/site-packages")


def project_entries(project: Path) -> dict[str, str]:
    entries = tomllib.loads(project.read_text(encoding="utf-8-sig"))["project"]["scripts"]
    for name, entry in entries.items():
        if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", name):
            raise ValueError(f"invalid command name: {name}")
        if not re.fullmatch(r"[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*:[A-Za-z_]\w*", entry):
            raise ValueError(f"invalid script entry: {entry}")
    return entries


@dataclass(frozen=True)
class AgentInputs:
    project: Path
    code: Path
    repo: str
    placement: str
    target: str
    python: Path
    site_packages: Path
    environment: Path
    pm_runtime: Path
    bin_dir: str = "bin"
    tools: Path | None = None
    command_dir: Path | None = None
    resources: dict[str, Path] = field(default_factory=dict)
    frontends: dict[str, Path] = field(default_factory=dict)
    ref: str | None = None
    stamp: Path | None = None
    features: Path | None = None
    env: dict[str, str] = field(default_factory=dict)

    @classmethod
    def from_dict(cls, data: dict) -> AgentInputs:
        unknown = data.keys() - {f.name for f in fields(cls)}
        if unknown:
            raise ValueError(f"unknown agent inputs: {', '.join(sorted(unknown))}")
        values = dict(data)
        for key in ("project", "code", "python", "site_packages", "environment", "tools",
                    "pm_runtime", "command_dir", "stamp", "features"):
            if values.get(key) is not None:
                values[key] = Path(values[key])
        for key in ("resources", "frontends"):
            values[key] = {name: Path(path) for name, path in values.get(key, {}).items()}
        return cls(**values)

    def validate(self, out: Path) -> None:
        if not re.fullmatch(r"(?:(?:linux|darwin|win32)-(?:x64|arm64)|linux-(?:x64|arm64)-musl|linux-arm64-bionic)", self.target):
            raise ValueError(f"unsupported target: {self.target}")
        if self.placement not in ("contained", "fixed", "references"):
            raise ValueError(f"unknown placement: {self.placement}")
        if Path(self.repo).is_absolute() or ".." in Path(self.repo).parts:
            raise ValueError("repo must be an output-relative path")
        if not re.fullmatch(r"[A-Za-z0-9_-]+", self.bin_dir):
            raise ValueError("bin_dir must be one output-relative directory")
        if self.resources.keys() - RESOURCE_ENV.keys():
            raise ValueError("unknown resource names")
        if self.frontends.keys() - {"tui", "web"}:
            raise ValueError("unknown frontend names")
        paths = [self.project, self.code, self.python, self.site_packages, self.environment,
                 self.pm_runtime, *self.resources.values(), *self.frontends.values()]
        paths += [p for p in (self.tools, self.command_dir, self.stamp, self.features) if p is not None]
        for path in paths:
            if not path.is_absolute():
                raise ValueError(f"input path must be absolute: {path}")
            if not path.exists():
                raise FileNotFoundError(f"required agent input missing: {path}")
        for path in (self.project, self.python):
            if not path.is_file():
                raise ValueError(f"expected a file: {path}")
        for path in (self.code, self.site_packages, self.environment, self.pm_runtime):
            if not path.is_dir():
                raise ValueError(f"expected a directory: {path}")
        marker = json.loads((self.pm_runtime / "pm-runtime.json").read_text(encoding="utf-8-sig"))
        pm_python = (self.pm_runtime / marker["python"]).resolve()
        pm_site = (self.pm_runtime / marker["sitePackages"]).resolve()
        if not pm_python.is_file() or not pm_site.is_dir():
            raise ValueError("prepared PM runtime is incomplete")
        if self.stamp is not None:
            stamp = json.loads(self.stamp.read_text(encoding="utf-8-sig"))
            if stamp.get("distribution") in ("nix", "docker") and stamp.get("pmRuntime") != str(self.pm_runtime):
                raise ValueError("install stamp must reference the supplied independent PM runtime")
        if self.placement == "contained":
            for path in (self.python, self.site_packages, self.environment, self.pm_runtime, self.tools):
                if path is not None and not path.resolve().is_relative_to(out):
                    raise ValueError(f"contained input must be prepared inside output: {path}")
        if self.placement == "references" and self.command_dir is None:
            raise ValueError("referenced placement requires command_dir")
        validate_frontends(self.frontends)


def validate_frontends(frontends: dict[str, Path]) -> None:
    for name, path in frontends.items():
        required = ("dist/entry.js", "package.json") if name == "tui" else ("index.html",)
        for entry in required:
            if not (path / entry).is_file():
                raise FileNotFoundError(path / entry)
