#!/usr/bin/env python3
"""Fail when kanban worker prompts reference tools the assignee cannot invoke.

Guards WCR-01–03 and WCR-02: phase nudges and council gather counters must stay
aligned with SUBAGENT_PROFILES tool allowlists.

Usage:
    python scripts/check_prompt_tool_alignment.py

Exit codes:
    0 — OK
    1 — alignment violation
    2 — script error
"""
from __future__ import annotations

import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(REPO_ROOT))

# Tool names that phase nudges may mention (subset we validate).
_PHASE_NUDGE_TOOLS = frozenset({
    "analyze_multiple_sources",
    "append_research_scratch",
    "deep_web_search",
    "hyperframes_lint",
    "hyperframes_render",
    "read_and_analyze_page",
    "read_file",
    "read_webpage",
    "run_web_script",
    "search_web",
    "write_text_file",
    "write_web_act_file",
})

_RESEARCH_ONLY_NUDGE_TOOLS = frozenset({
    "deep_web_search",
    "read_and_analyze_page",
    "append_research_scratch",
})

_HYPERFRAMES_NUDGE_TOOLS = frozenset({
    "hyperframes_lint",
    "hyperframes_render",
    "write_text_file",
})

_KANBAN_PHASE_ASSIGNEES = (
    "research",
    "general",
    "web_research_light",
    "code_readonly",
    "ops_cautious",
    "council_researcher",
    "council_verifier",
    "council_contrarian",
)

# Ingested-corpus tools. Profiles whose job is research/knowledge MUST expose at least one
# so a worker can consult studied corpora instead of always falling back to the web. This
# guards the capability-drift class that hid the council_researcher and web_research_light
# bugs (tool present in one path, silently missing in a parallel one).
_CORPUS_TOOLS = frozenset({"query_knowledge", "ask_studied_corpus"})

# Profiles that gather/verify knowledge and must therefore reach ingested corpora.
_RESEARCH_CORPUS_PROFILES = frozenset({
    "research",
    "web_research_light",
    "council_researcher",
    "council_verifier",
    "heartbeat_standard",
    "heartbeat_maintenance",
    "heartbeat_drive_action",
    "active_memory",
})


def _check_research_corpus_access(errors: list[str]) -> None:
    from subagent_runner import SUBAGENT_PROFILES

    for name in sorted(_RESEARCH_CORPUS_PROFILES):
        profile = SUBAGENT_PROFILES.get(name)
        if not profile:
            errors.append(f"_RESEARCH_CORPUS_PROFILES names unknown profile {name!r}")
            continue
        tools = profile.get("tools")
        # ``tools is None`` means the profile inherits the full default tool set (which
        # includes the corpus tools), so it is implicitly covered.
        if tools is None:
            continue
        if not (_CORPUS_TOOLS & set(tools)):
            errors.append(
                f"research/knowledge profile {name!r} cannot reach ingested corpora "
                f"(needs one of {sorted(_CORPUS_TOOLS)})"
            )


def _tools_mentioned_in_nudge(text: str) -> set[str]:
    hay = str(text or "").lower()
    return {name for name in _PHASE_NUDGE_TOOLS if name in hay}


def _check_council_gather_alignment(errors: list[str]) -> None:
    from agent_loop_guardrails import COUNCIL_RESEARCHER_GATHER_TOOLS
    from subagent_runner import SUBAGENT_PROFILES

    profile_tools = frozenset(SUBAGENT_PROFILES["council_researcher"]["tools"])
    extra = COUNCIL_RESEARCHER_GATHER_TOOLS - profile_tools
    missing = profile_tools - COUNCIL_RESEARCHER_GATHER_TOOLS - frozenset({"read_file_chunk"})
    if extra:
        errors.append(
            "COUNCIL_RESEARCHER_GATHER_TOOLS has tools not on council_researcher profile: "
            + ", ".join(sorted(extra))
        )
    if missing:
        errors.append(
            "council_researcher profile tools missing from COUNCIL_RESEARCHER_GATHER_TOOLS: "
            + ", ".join(sorted(missing))
        )


def _check_phase_nudges(errors: list[str]) -> None:
    from utils.kanban_context import (
        format_kanban_phase_nudge,
        profile_tool_names,
        KANBAN_PHASE_GATHER,
        KANBAN_PHASE_VERIFY,
        KANBAN_PHASE_MUTATE,
        KANBAN_PHASE_CLOSE,
    )

    for assignee in _KANBAN_PHASE_ASSIGNEES:
        profile = profile_tool_names(assignee)
        tm_base = {"_assignee": assignee}

        gather_research = format_kanban_phase_nudge(
            KANBAN_PHASE_GATHER,
            task_kind="research",
            task_metadata=tm_base,
            assignee=assignee,
        )
        for tool in _tools_mentioned_in_nudge(gather_research):
            if tool in _RESEARCH_ONLY_NUDGE_TOOLS and assignee != "research":
                errors.append(
                    f"GATHER/research nudge for {assignee!r} mentions {tool!r}: {gather_research!r}"
                )
            elif profile is not None and tool not in profile:
                errors.append(
                    f"GATHER/research nudge for {assignee!r} mentions {tool!r} not in profile"
                )

        if assignee != "general":
            video_tm = {**tm_base, "task_kind": "deliver", "deliverable_type": "video_render"}
            for phase in (KANBAN_PHASE_VERIFY, KANBAN_PHASE_MUTATE):
                nudge = format_kanban_phase_nudge(
                    phase,
                    task_kind="deliver",
                    task_metadata=video_tm,
                    assignee=assignee,
                )
                for tool in _tools_mentioned_in_nudge(nudge):
                    if tool in _HYPERFRAMES_NUDGE_TOOLS and not (
                        assignee == "ops_cautious"
                    ):
                        errors.append(
                            f"{phase} video_render nudge for {assignee!r} mentions {tool!r}"
                        )

        close_research = format_kanban_phase_nudge(
            KANBAN_PHASE_CLOSE,
            task_kind="research",
            task_metadata=tm_base,
            assignee=assignee,
        )
        if "append_research_scratch" in close_research and assignee != "research":
            errors.append(
                f"CLOSE/research nudge for {assignee!r} mentions append_research_scratch"
            )


def _tools_positively_mentioned(text: str) -> set[str]:
    """Tool names the prompt instructs the worker to use (not prohibition lines)."""
    hay = str(text or "")
    out: set[str] = set()
    for line in hay.splitlines():
        lower = line.lower()
        if "do not use" in lower or "don't use" in lower or "never use" in lower:
            continue
        for name in _PHASE_NUDGE_TOOLS:
            if name in lower:
                out.add(name)
    return out


def _check_council_verifier_frame_tools(errors: list[str]) -> None:
    from utils.council_orchestrator import (
        COUNCIL_FRAME_META,
        council_verifier_tool_names_for_frame,
    )
    from utils.council_prompts import council_verifier_hint_for_frame

    general_frame = "Best local LLMs for M5 Macs with 128GB RAM?"
    product_frame = "Should the Kanban dispatcher keep cooperative deadline release?"

    general_tools = council_verifier_tool_names_for_frame(general_frame)
    product_tools = council_verifier_tool_names_for_frame(product_frame)
    if "read_webpage" not in general_tools:
        errors.append("general council_verifier frame missing read_webpage")
    if "read_webpage" in product_tools:
        errors.append("product council_verifier frame must not expose read_webpage")
    if "search_web" in general_tools or "search_web" in product_tools:
        errors.append("council_verifier must not expose search_web")

    general_hint = council_verifier_hint_for_frame(general_frame)
    if "read_webpage" not in general_hint:
        errors.append("general council_verifier hint must mention read_webpage")
    for tool in _tools_positively_mentioned(general_hint):
        if tool not in general_tools:
            errors.append(
                f"general council_verifier hint mentions {tool!r} not in frame tools"
            )

    product_hint = council_verifier_hint_for_frame(product_frame)
    for tool in _tools_positively_mentioned(product_hint):
        if tool not in product_tools:
            errors.append(
                f"product council_verifier hint mentions {tool!r} not in frame tools"
            )

    # profile_tool_names + resolve_effective_worker_tools must agree with frame resolver.
    from utils.kanban_context import profile_tool_names, resolve_effective_worker_tools

    md_general = {COUNCIL_FRAME_META: general_frame}
    profile_general = profile_tool_names("council_verifier", metadata=md_general)
    resolved_general = resolve_effective_worker_tools("council_verifier", md_general)
    if profile_general != general_tools:
        errors.append("profile_tool_names disagrees with council_verifier_tool_names_for_frame")
    if resolved_general != general_tools:
        errors.append("resolve_effective_worker_tools disagrees with frame resolver")


def main() -> int:
    errors: list[str] = []
    try:
        _check_council_gather_alignment(errors)
        _check_research_corpus_access(errors)
        _check_council_verifier_frame_tools(errors)
        _check_phase_nudges(errors)
    except Exception as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 2

    if errors:
        print("prompt/tool alignment violations:", file=sys.stderr)
        for err in errors:
            print(f"  - {err}", file=sys.stderr)
        return 1

    print("OK: prompt/tool alignment checks passed")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
