"""Decide whether a plan's command cell invokes the project's build toolchain.

The planning worktree has no dependencies installed, so a stage that calls the
toolchain needs its precondition declared once and referenced (see the
plan-body contract's "Planning-time environment gap"). This module owns the
set of commands that counts as such a call.

An allowlist, not a denylist: a tool nobody listed goes undetected, which
weakens one advisory check, whereas treating every unknown leading token as a
build call would fire on `grep` and `sed` in every plan and train readers to
ignore the warning.
"""
from __future__ import annotations

import json
import shlex
from pathlib import Path

from .json_boundary import JsonBoundaryError, load_owned_object

DEFAULT_BUILD_TOOL_TOKENS: tuple[str, ...] = (
    "npm",
    "yarn",
    "pnpm",
    "npx",
    "bun",
    "pytest",
    "tox",
    "poetry",
    "uv",
    "cargo",
    "go",
    "gradle",
    "mvn",
    "make",
    "bundle",
    "composer",
    "dotnet",
)

# Prefixes that stand in front of the real command without being it.
_TRANSPARENT_LEADERS = frozenset({"sudo", "env", "time", "nice", "exec", "command"})
_CLAUSE_SEPARATORS = ("&&", "||", ";", "|", "\n")


def resolve_build_tool_tokens(project_root: Path) -> tuple[str, ...]:
    """Project override, else the built-in list.

    `buildToolTokens` in `.okstra/project.json` REPLACES the defaults and an
    empty array disables detection — the same precedence `worktreeSyncDirs`
    uses, so one rule covers both.
    """
    try:
        payload = load_owned_object(
            Path(project_root) / ".okstra" / "project.json",
            artifact="project configuration",
        )
    except JsonBoundaryError:
        return DEFAULT_BUILD_TOOL_TOKENS
    configured = payload.get("buildToolTokens") if isinstance(payload, dict) else None
    if not isinstance(configured, list):
        return DEFAULT_BUILD_TOOL_TOKENS
    return tuple(str(token).strip() for token in configured if str(token).strip())


def _clauses(command: str) -> list[str]:
    parts = [command]
    for separator in _CLAUSE_SEPARATORS:
        parts = [piece for part in parts for piece in part.split(separator)]
    return [part.strip() for part in parts if part.strip()]


def _leading_token(clause: str) -> str:
    try:
        words = shlex.split(clause)
    except ValueError:
        words = clause.split()
    for word in words:
        # `cd <dir>` and `VAR=value` lead a clause without being the command;
        # step past them rather than reading the directory as the tool.
        if word == "cd" or "=" in word.split("/")[0] and not word.startswith("-"):
            continue
        if word in _TRANSPARENT_LEADERS:
            continue
        if word.startswith("-"):
            continue
        return Path(word).name
    return ""


def command_invokes_build_tool(
    command: str, *, tokens: tuple[str, ...] | None = None
) -> bool:
    """True when any clause of *command* starts with an allowlisted tool."""
    allowed = DEFAULT_BUILD_TOOL_TOKENS if tokens is None else tokens
    if not allowed:
        return False
    return any(_leading_token(clause) in allowed for clause in _clauses(command))
