"""Work out how a project is built, tested and put live, so nobody has to be asked.

Every answer here is a suggestion. Setup shows them and the person confirms — this file is
never the last word, because guessing wrong about how to check work is worse than asking.
"""
from __future__ import annotations

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

# npm writes this into every new project; running it fails on purpose.
NPM_PLACEHOLDER = "no test specified"

DOC_NAMES = ("CLAUDE.md", "AGENTS.md", "README.md", "PRD.md", "ARCHITECTURE.md", "CONTRIBUTING.md")


@dataclass
class Found:
    test_commands: list[str] = field(default_factory=list)   # best guess first
    extra_checks: list[str] = field(default_factory=list)
    install_hint: str = ""
    link_dirs: tuple[str, ...] = ()
    context_files: list[str] = field(default_factory=list)
    deploy_command: str | None = None
    language: str = ""

    @property
    def test_command(self) -> str:
        return self.test_commands[0] if self.test_commands else ""


def _read_json(path: Path) -> dict:
    try:
        data = json.loads(path.read_text())
        return data if isinstance(data, dict) else {}
    except (OSError, json.JSONDecodeError):
        return {}


def _read_toml(path: Path) -> dict:
    try:
        return tomllib.loads(path.read_text())
    except (OSError, tomllib.TOMLDecodeError):
        return {}


def _node(folder: Path, found: Found) -> None:
    pkg = _read_json(folder / "package.json")
    if not pkg:
        return
    found.language = "node"
    scripts = pkg.get("scripts") or {}
    test = str(scripts.get("test") or "")
    if test and NPM_PLACEHOLDER not in test:
        found.test_commands.append("npm test")
    for name in ("typecheck", "lint", "types", "check"):
        if scripts.get(name):
            found.extra_checks.append(f"npm run {name}")

    runner = "npm ci"
    for lock, cmd in (("pnpm-lock.yaml", "pnpm install --frozen-lockfile"),
                      ("yarn.lock", "yarn install --frozen-lockfile"),
                      ("bun.lockb", "bun install"),
                      ("package-lock.json", "npm ci")):
        if (folder / lock).exists():
            runner = cmd
            break
    found.install_hint = runner

    links = ["node_modules"]
    for spot in (pkg.get("workspaces") or []):
        if isinstance(spot, str) and "*" not in spot and (folder / spot / "package.json").exists():
            links.append(f"{spot}/node_modules")
    for extra in ("web", "app", "client", "server"):
        if (folder / extra / "package.json").exists() and f"{extra}/node_modules" not in links:
            links.append(f"{extra}/node_modules")
    found.link_dirs = tuple(links)


def _python(folder: Path, found: Found) -> None:
    pyproject = _read_toml(folder / "pyproject.toml")
    looks_python = bool(pyproject) or (folder / "setup.py").exists() or (folder / "tests").is_dir()
    if not looks_python:
        return
    found.language = found.language or "python"
    tool = pyproject.get("tool") or {}
    project = pyproject.get("project") or {}
    text = json.dumps(pyproject)
    uses_pytest = "pytest" in tool or "pytest" in text or (folder / "tests").is_dir()
    if not uses_pytest:
        return

    if (folder / "uv.lock").exists():
        found.test_commands.append("uv run pytest -q")
        found.install_hint = found.install_hint or "uv sync"
    elif (folder / "poetry.lock").exists():
        found.test_commands.append("poetry run pytest -q")
        found.install_hint = found.install_hint or "poetry install"
    elif (folder / ".venv" / "bin" / "python").exists():
        found.test_commands.append(".venv/bin/python -m pytest -q")
    elif (folder / ".venv" / "Scripts" / "python.exe").exists():
        found.test_commands.append(r".venv\Scripts\python -m pytest -q")
    else:
        found.test_commands.append("python -m pytest -q")
        if project.get("dependencies") or (folder / "requirements.txt").exists():
            found.install_hint = found.install_hint or "pip install -e ."


def _makefile(folder: Path, found: Found) -> None:
    path = folder / "Makefile"
    if not path.exists():
        return
    try:
        text = path.read_text()
    except OSError:
        return
    if re.search(r"^test\s*:", text, re.M):
        found.test_commands.append("make test")


def _others(folder: Path, found: Found) -> None:
    simple = [
        ("Cargo.toml", "cargo test", "rust", "cargo build"),
        ("go.mod", "go test ./...", "go", ""),
        ("mix.exs", "mix test", "elixir", "mix deps.get"),
        ("composer.json", "composer test", "php", "composer install"),
        ("pom.xml", "mvn -q test", "java", ""),
        ("build.gradle", "./gradlew test", "java", ""),
        ("build.gradle.kts", "./gradlew test", "java", ""),
    ]
    for name, command, language, install in simple:
        if (folder / name).exists():
            found.test_commands.append(command)
            found.language = found.language or language
            found.install_hint = found.install_hint or install
    if (folder / "Gemfile").exists() and (folder / "spec").is_dir():
        found.test_commands.append("bundle exec rspec")
        found.language = found.language or "ruby"
        found.install_hint = found.install_hint or "bundle install"
    if any(folder.glob("*.sln")) or any(folder.glob("*.csproj")):
        found.test_commands.append("dotnet test")
        found.language = found.language or "dotnet"
    if (folder / "Package.swift").exists():
        found.test_commands.append("swift test")
        found.language = found.language or "swift"


def _deploy(folder: Path, found: Found) -> None:
    if (folder / "fly.toml").exists():
        found.deploy_command = "fly deploy --remote-only"
    elif (folder / "vercel.json").exists():
        found.deploy_command = "vercel --prod"


def _docs(folder: Path, found: Found) -> None:
    files = [name for name in DOC_NAMES if (folder / name).exists()]
    docs = sorted((p for p in (folder / "docs").glob("*.md") if p.is_file()),
                  key=lambda p: p.stat().st_size, reverse=True)[:2] if (folder / "docs").is_dir() else []
    found.context_files = files + [f"docs/{p.name}" for p in docs]


def inspect(folder: Path) -> Found:
    """Everything worth guessing about a project, from what's on disk."""
    folder = Path(folder)
    found = Found()
    for step in (_node, _python, _makefile, _others, _deploy, _docs):
        step(folder, found)
    seen: set[str] = set()
    found.test_commands = [c for c in found.test_commands if not (c in seen or seen.add(c))]
    return found


def is_git_project(folder: Path) -> bool:
    return (Path(folder) / ".git").exists()
