"""Minimal `.dekk.toml` bootstrapping from detected project context."""

from __future__ import annotations

import re
from dataclasses import dataclass
from pathlib import Path
from typing import Final

from dekk._compat import load_json, load_toml
from dekk.detection.build import BuildSystem, BuildSystemDetector, BuildSystemInfo
from dekk.environment.spec import find_envspec
from dekk.environment.types import EnvironmentKind

PROJECT_SPEC_FILENAME: Final = ".dekk.toml"
DEFAULT_ENVIRONMENT_PATH: Final = "{project}/.dekk/env"
PROJECT_LAYOUT_SOURCE: Final = "project layout"
CONDA_FILE_CANDIDATES: Final = (
    "environment.yaml",
    "environment.yml",
    "conda.yaml",
    "conda.yml",
)
PROJECT_ROOT_MARKERS: Final = (
    PROJECT_SPEC_FILENAME,
    "pyproject.toml",
    "package.json",
    "Cargo.toml",
    "go.mod",
    "pom.xml",
    "build.gradle",
    "build.gradle.kts",
    "CMakeLists.txt",
    "Makefile",
    ".git",
    ".agents",
    *CONDA_FILE_CANDIDATES,
)
PYTEST_SIGNAL_FILES: Final = ("pytest.ini", "tox.ini")
NODE_SCRIPT_NAMES: Final = ("build", "test", "dev", "start", "lint", "typecheck", "check", "format")
PYTHON_BUILD_SYSTEMS: Final = {
    BuildSystem.POETRY,
    BuildSystem.PDM,
    BuildSystem.HATCH,
    BuildSystem.FLIT,
    BuildSystem.SETUPTOOLS,
    BuildSystem.MATURIN,
    BuildSystem.UV,
}
NODE_BUILD_SYSTEMS: Final = {
    BuildSystem.NPM,
    BuildSystem.PNPM,
    BuildSystem.YARN,
    BuildSystem.BUN,
}
DEFAULT_COMMAND_DESCRIPTIONS: Final = {
    "build": "Build the project",
    "test": "Run the test suite",
    "dev": "Run the development command",
    "start": "Start the application",
    "lint": "Run lint checks",
    "typecheck": "Run type checks",
    "check": "Run project checks",
    "format": "Format the codebase",
}
BUILD_COMMANDS: Final = {
    BuildSystem.CARGO: "cargo build",
    BuildSystem.POETRY: "poetry build",
    BuildSystem.PDM: "pdm build",
    BuildSystem.HATCH: "hatch build",
    BuildSystem.FLIT: "flit build",
    BuildSystem.SETUPTOOLS: "python -m build",
    BuildSystem.MATURIN: "maturin build",
    BuildSystem.UV: "uv build",
    BuildSystem.GO: "go build ./...",
    BuildSystem.MAVEN: "mvn package",
    BuildSystem.CMAKE: "cmake -B build && cmake --build build",
}
TEST_COMMANDS: Final = {
    BuildSystem.CARGO: "cargo test",
    BuildSystem.POETRY: "pytest -q",
    BuildSystem.PDM: "pytest -q",
    BuildSystem.HATCH: "pytest -q",
    BuildSystem.FLIT: "pytest -q",
    BuildSystem.SETUPTOOLS: "pytest -q",
    BuildSystem.MATURIN: "pytest -q",
    BuildSystem.UV: "pytest -q",
    BuildSystem.GO: "go test ./...",
    BuildSystem.MAVEN: "mvn test",
    BuildSystem.CMAKE: "ctest --test-dir build",
}


@dataclass(frozen=True)
class BootstrapResult:
    """Result of ensuring a project-local `.dekk.toml` exists."""

    path: Path
    created: bool
    source: str


def detect_project_root(start_dir: Path) -> Path:
    """Return the nearest ancestor that looks like a project root."""
    current = Path(start_dir)
    if current.is_file():
        current = current.parent
    current = current.resolve()

    for candidate in (current, *current.parents):
        if any((candidate / marker).exists() for marker in PROJECT_ROOT_MARKERS):
            return candidate

    return current


def ensure_envspec(start_dir: Path, *, target: Path | None = None) -> BootstrapResult:
    """Return an existing spec or write a minimal detected starter spec."""
    search_root = Path(target).resolve().parent if target is not None else Path(start_dir).resolve()
    if existing := find_envspec(search_root):
        return BootstrapResult(path=existing, created=False, source=existing.name)

    project_root = detect_project_root(search_root)
    spec_path = project_root / PROJECT_SPEC_FILENAME
    if spec_path.exists():
        return BootstrapResult(path=spec_path, created=False, source=spec_path.name)

    content, source = render_bootstrap_spec(project_root)
    spec_path.write_text(content, encoding="utf-8")
    return BootstrapResult(path=spec_path, created=True, source=source)


def render_bootstrap_spec(project_root: Path) -> tuple[str, str]:
    """Render a minimal `.dekk.toml` based on the detected project context."""
    build = BuildSystemDetector().detect_first(project_root)
    environment_file = _find_conda_file(project_root)
    source = (
        build.config_file.name
        if build is not None
        else environment_file.name if environment_file else PROJECT_LAYOUT_SOURCE
    )
    project_name = _infer_project_name(project_root, build)
    tools = _infer_tools(build)
    commands = _infer_commands(project_root, build)

    lines = [
        f"# Auto-generated by dekk from {source}. Edit as needed.",
        "",
        "[project]",
        f'name = "{_toml_string(project_name)}"',
    ]

    if environment_file is not None:
        lines.extend(
            [
                "",
                "[environment]",
                f'type = "{EnvironmentKind.CONDA.value}"',
                f'path = "{DEFAULT_ENVIRONMENT_PATH}"',
                f'file = "{_toml_string(environment_file.name)}"',
            ]
        )

    if tools:
        lines.extend(["", "[tools]"])
        for tool_name, command in tools.items():
            lines.append(
                f'{tool_name} = {{ command = "{_toml_string(command)}" }}'
            )

    if build is not None and build.system in PYTHON_BUILD_SYSTEMS:
        lines.extend(["", "[python]", 'pyproject = "pyproject.toml"'])

    if commands:
        lines.extend(["", "[commands]"])
        for command_name, run_command in commands.items():
            description = DEFAULT_COMMAND_DESCRIPTIONS.get(command_name, f"Run {command_name}")
            lines.append(
                f'{command_name} = {{ run = "{_toml_string(run_command)}", '
                f'description = "{_toml_string(description)}" }}'
            )

    return "\n".join(lines).rstrip() + "\n", source


def _find_conda_file(project_root: Path) -> Path | None:
    for filename in CONDA_FILE_CANDIDATES:
        candidate = project_root / filename
        if candidate.is_file():
            return candidate
    return None


def _infer_project_name(project_root: Path, build: BuildSystemInfo | None) -> str:
    raw_name = project_root.name
    if build is None:
        return _normalize_project_name(raw_name, fallback=project_root.name)

    config_file = build.config_file
    if config_file.name == "package.json":
        data = load_json(config_file) or {}
        raw_name = str(data.get("name") or raw_name)
    elif config_file.suffix == ".toml":
        data = load_toml(config_file) or {}
        raw_name = str(
            data.get("project", {}).get("name")
            or data.get("tool", {}).get("poetry", {}).get("name")
            or data.get("package", {}).get("name")
            or raw_name
        )
    elif config_file.name == "go.mod":
        raw_name = _module_name_from_go_mod(config_file) or raw_name

    return _normalize_project_name(raw_name, fallback=project_root.name)


def _module_name_from_go_mod(go_mod: Path) -> str | None:
    try:
        for line in go_mod.read_text(encoding="utf-8").splitlines():
            if line.startswith("module "):
                return line.removeprefix("module ").strip().rsplit("/", 1)[-1]
    except OSError:
        return None
    return None


def _normalize_project_name(value: str, *, fallback: str) -> str:
    trimmed = value.strip()
    if trimmed.startswith("@") and "/" in trimmed:
        trimmed = trimmed.rsplit("/", 1)[-1]
    normalized = re.sub(r"[^A-Za-z0-9._-]+", "-", trimmed).strip("-._")
    return normalized or fallback


def _infer_tools(build: BuildSystemInfo | None) -> dict[str, str]:
    if build is None:
        return {}

    system = build.system
    if system in PYTHON_BUILD_SYSTEMS:
        tools = {"python": "python"}
        if system is not BuildSystem.SETUPTOOLS:
            tools[system.value] = system.value
        return tools
    if system is BuildSystem.CARGO:
        return {"cargo": "cargo"}
    if system is BuildSystem.GO:
        return {"go": "go"}
    if system in NODE_BUILD_SYSTEMS:
        tools = {"node": "node"}
        tools[system.value] = system.value
        return tools
    if system is BuildSystem.MAKE:
        return {"make": "make"}
    if system is BuildSystem.CMAKE:
        return {"cmake": "cmake"}
    if system is BuildSystem.MAVEN:
        return {"mvn": "mvn"}
    if system is BuildSystem.GRADLE:
        return {"gradle": "gradle"}
    return {}


def _infer_commands(project_root: Path, build: BuildSystemInfo | None) -> dict[str, str]:
    if build is None:
        return {}

    if build.system in NODE_BUILD_SYSTEMS:
        return _node_commands(build)

    commands: dict[str, str] = {}

    if build.system is BuildSystem.MAKE:
        target_names = set(build.target_names)
        if "build" in target_names:
            commands["build"] = "make build"
        elif "all" in target_names or build.target_names:
            commands["build"] = "make"
        if "test" in target_names:
            commands["test"] = "make test"
        return commands

    if build.system is BuildSystem.GRADLE:
        gradle_cmd = "./gradlew" if (project_root / "gradlew").is_file() else "gradle"
        return {"build": f"{gradle_cmd} build", "test": f"{gradle_cmd} test"}

    if build.system in BUILD_COMMANDS:
        commands["build"] = BUILD_COMMANDS[build.system]

    if build.system in TEST_COMMANDS and _has_test_signal(project_root, build):
        commands["test"] = TEST_COMMANDS[build.system]

    return commands


def _node_commands(build: BuildSystemInfo) -> dict[str, str]:
    runner = {
        BuildSystem.NPM: "npm run",
        BuildSystem.PNPM: "pnpm run",
        BuildSystem.YARN: "yarn",
        BuildSystem.BUN: "bun run",
    }[build.system]

    script_names = {target.name for target in build.targets_of_kind("script")}
    commands: dict[str, str] = {}
    for name in NODE_SCRIPT_NAMES:
        if name in script_names:
            commands[name] = f"{runner} {name}"
    return commands


def _has_test_signal(project_root: Path, build: BuildSystemInfo) -> bool:
    if build.system is BuildSystem.CMAKE:
        return True
    if any((project_root / dirname).exists() for dirname in ("tests", "test")):
        return True
    return any((project_root / filename).exists() for filename in PYTEST_SIGNAL_FILES)


def _toml_string(value: str) -> str:
    return value.replace("\\", "\\\\").replace('"', '\\"')


__all__ = [
    "BootstrapResult",
    "PROJECT_SPEC_FILENAME",
    "detect_project_root",
    "ensure_envspec",
    "render_bootstrap_spec",
]
