"""Registry reporter — produces toolcalls.yaml.

Generates a YAML file documenting all tool calls with side effects,
grouped by severity: NO CHECKS → PARTIAL CHECKS → CONFIRMED.
"""

from __future__ import annotations

import datetime
from io import StringIO
from pathlib import Path

from agent_canary import __version__
from agent_canary.models import ScanResult, Tool


# Effect-type priority groups for NO CHECKS ordering
_EFFECT_PRIORITY: list[tuple[int, frozenset[str]]] = [
    (0, frozenset({"payment", "database_delete", "dynamic_code"})),
    (1, frozenset({"llm_call", "agent_invocation"})),
    (2, frozenset({"database_write", "http_write"})),
    (3, frozenset({"email", "messaging", "publish", "file_delete"})),
]

# Human-readable labels for each priority group
_GROUP_LABELS: dict[int, list[str]] = {
    0: ["payment", "database_delete", "dynamic_code"],
    1: ["llm_call", "dynamic_code"],
    2: ["database_write", "http_write"],
    3: ["email", "messaging", "publish", "file_delete"],
}


def _effect_priority(tool: Tool) -> int:
    """Return the priority of the most critical effect in a tool."""
    categories = {se.category for se in tool.side_effects}
    for priority, group in _EFFECT_PRIORITY:
        if categories & group:
            return priority
    return 4  # unknown category, sort last


def _group_label(tool: Tool) -> str:
    """Return the comment label for a tool's effect group."""
    categories = {se.category for se in tool.side_effects}
    priority = _effect_priority(tool)
    for p, group in _EFFECT_PRIORITY:
        if p == priority:
            present = sorted(categories & group)
            if present:
                return " / ".join(present)
    # Fallback: list all categories
    return " / ".join(sorted(categories))


def _yaml_escape(s: str) -> str:
    """Escape a string for YAML output."""
    if not s:
        return '""'
    # Quote if it contains special chars
    needs_quote = any(c in s for c in (':', '#', '{', '}', '[', ']', ',', '&', '*', '?', '|', '-', '<', '>', '=', '!', '%', '@', '`', '"', "'", '\n'))
    if needs_quote or s.startswith(' ') or s.endswith(' '):
        escaped = s.replace('\\', '\\\\').replace('"', '\\"')
        return f'"{escaped}"'
    return s


def _indent(level: int) -> str:
    return "  " * level


def _render_tool_entry(tool: Tool, buf: StringIO, include_confirmed: bool = False) -> None:
    """Render a single tool entry as YAML."""
    buf.write(f"  - function: {tool.name}\n")
    buf.write(f"    file: {tool.file}\n")
    buf.write(f"    line: {tool.line}\n")

    # actions
    if tool.side_effects:
        buf.write("    actions:\n")
        for se in tool.side_effects:
            buf.write(f"      - {_yaml_escape(se.evidence)}\n")
    else:
        buf.write("    actions: []\n")

    # checks
    if tool.guards:
        buf.write("    checks:\n")
        for g in tool.guards:
            buf.write(f"      - type: {g.type}\n")
            buf.write(f"        code: {_yaml_escape(g.evidence)}\n")
    else:
        buf.write("    checks: []\n")

    # missing
    if tool.missing_hints:
        buf.write("    missing:\n")
        for hint in tool.missing_hints:
            buf.write(f"      - {_yaml_escape(hint)}\n")
    else:
        buf.write("    missing: []\n")

    # confirmed (only for CONFIRMED section)
    if include_confirmed and tool.ignored:
        buf.write(f"    confirmed: {_yaml_escape(tool.ignore_reason)}\n")


def generate(result: ScanResult, output_path: str | Path, scanned_path: str = ".") -> str:
    """Generate toolcalls.yaml content and write to output_path.

    Returns the generated YAML content.
    """
    buf = StringIO()

    # Header
    buf.write("# toolcalls.yaml\n")
    buf.write("# What your agent can do to the real world — and what checks exist.\n")
    buf.write("# Generated by agent-canary · https://github.com/Diplomat-agents/agent-canary\n")
    buf.write("#\n")
    buf.write("# Commit this file. Review changes in PRs.\n")
    buf.write("# In source code:\n")
    buf.write("#   Fix          → add validation, the next scan picks it up\n")
    buf.write("#   Acknowledge  → add  # checked:ok  to confirm you've reviewed it\n")
    buf.write("#   Protected elsewhere → add  # checked:ok — protected by [middleware/gateway/etc]\n")
    buf.write("#   CI enforcement → --fail-on-unchecked blocks PRs with new unreviewed tool calls\n")
    buf.write("\n")

    # Metadata
    now = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
    buf.write(f"generated: \"{now}\"\n")
    buf.write(f"version: \"{__version__}\"\n")
    buf.write(f"path: \"{scanned_path}\"\n")
    buf.write("\n")

    # Classify tools
    no_checks: list[Tool] = []
    partial_checks: list[Tool] = []
    confirmed: list[Tool] = []

    for tool in result.tools:
        if tool.verdict == "LOW_RISK":
            continue
        if tool.ignored:
            confirmed.append(tool)
        elif not tool.guards:
            no_checks.append(tool)
        elif tool.missing_hints:
            partial_checks.append(tool)
        else:
            confirmed.append(tool)

    # Summary
    total = len(no_checks) + len(partial_checks) + len(confirmed)
    buf.write("summary:\n")
    buf.write(f"  total: {total}\n")
    buf.write(f"  no_checks: {len(no_checks)}\n")
    buf.write(f"  partial_checks: {len(partial_checks)}\n")
    buf.write(f"  confirmed: {len(confirmed)}\n")
    buf.write("\n")

    buf.write("tool_calls:\n")

    # Section 1: NO CHECKS — grouped by effect priority
    if no_checks:
        # Group by priority
        groups: dict[int, list[Tool]] = {}
        for tool in no_checks:
            p = _effect_priority(tool)
            groups.setdefault(p, []).append(tool)

        for priority in sorted(groups.keys()):
            tools_in_group = groups[priority]
            # Build group label from actual categories present
            all_cats: set[str] = set()
            for t in tools_in_group:
                for se in t.side_effects:
                    for p, group_cats in _EFFECT_PRIORITY:
                        if p == priority and se.category in group_cats:
                            all_cats.add(se.category)
            if not all_cats:
                all_cats = {se.category for t in tools_in_group for se in t.side_effects}
            label = " / ".join(sorted(all_cats))
            buf.write(f"\n  # ⚠ NO CHECKS — {label}\n")

            # Sort by function name within group
            for tool in sorted(tools_in_group, key=lambda t: t.name):
                _render_tool_entry(tool, buf)
                buf.write("\n")

    # Section 2: PARTIAL CHECKS
    if partial_checks:
        buf.write("\n  # ~ PARTIAL CHECKS\n")
        for tool in sorted(partial_checks, key=lambda t: t.name):
            _render_tool_entry(tool, buf)
            buf.write("\n")

    # Section 3: CONFIRMED
    if confirmed:
        buf.write("\n  # ✓ CONFIRMED\n")
        for tool in sorted(confirmed, key=lambda t: t.name):
            _render_tool_entry(tool, buf, include_confirmed=True)
            buf.write("\n")

    content = buf.getvalue()

    # Write to file
    Path(output_path).write_text(content, encoding="utf-8")

    return content


def load_baseline(path: Path) -> list[dict]:
    """Load existing toolcalls.yaml and extract tool_call entries."""
    try:
        import yaml  # type: ignore[import]
    except ImportError:
        # Fallback: simple line-based parsing for function/file pairs
        return _load_baseline_simple(path)

    with open(path) as f:
        data = yaml.safe_load(f)
    if not data or not isinstance(data, dict):
        return []
    return [tc for tc in data.get("tool_calls", []) if isinstance(tc, dict)]


def _load_baseline_simple(path: Path) -> list[dict]:
    """Fallback baseline loader without PyYAML — extracts function/file pairs."""
    entries: list[dict] = []
    current: dict[str, str] = {}
    for line in path.read_text(encoding="utf-8").splitlines():
        stripped = line.strip()
        if stripped.startswith("- function:"):
            if current:
                entries.append(current)
            current = {"function": stripped.split(":", 1)[1].strip()}
        elif stripped.startswith("file:") and current:
            current["file"] = stripped.split(":", 1)[1].strip()
    if current:
        entries.append(current)
    return entries
