"""
AnnotationLoader — resolves KiCad IPC proto annotations by MCP tool name.

The annotations JSON (generated by scripts/generate_tool_annotations.py) maps
proto message names in PascalCase to structured descriptions.  MCP tool names
are snake_case and don't always match 1:1, so resolution uses three layers:

  1. An explicit TOOL_TO_PROTO mapping for cases where auto-conversion fails.
  2. Automatic snake_case → PascalCase conversion (get_nets → GetNets).
  3. Suffix-stripped variants (get_nets_list → GetNets).

Usage::

    loader = AnnotationLoader()          # loads bundled annotations
    ann = loader.get("refill_zones")     # -> {"description": ..., "blocking": True, ...}
    desc = loader.description("refill_zones")  # -> str | None
"""

from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Optional

# ---------------------------------------------------------------------------
# Explicit overrides where snake→Pascal conversion would produce the wrong name
# ---------------------------------------------------------------------------

TOOL_TO_PROTO: dict[str, str] = {
    # board
    "get_nets_list": "GetNets",
    "add_copper_pour": "AddZone",
    "get_board_info": "GetBoardEnabledLayers",
    "get_layer_list": "GetBoardEnabledLayers",
    "set_active_layer": "SetActiveLayer",
    "set_design_rules": "SetBoardDesignRules",
    "get_design_rules": "GetBoardDesignRules",
    "refill_zones": "RefillZones",
    # editor lifecycle
    "begin_commit": "BeginCommit",
    "end_commit": "EndCommit",
    # schematic
    "run_erc": "GetSchematicHierarchy",  # closest available proto
    # common
    "check_kicad_ui": "Ping",
}

# ---------------------------------------------------------------------------
# Annotation loader
# ---------------------------------------------------------------------------

_DEFAULT_ANNOTATIONS_PATH = Path(__file__).parent / "kicad_ipc_tool_annotations.json"


def _snake_to_pascal(name: str) -> str:
    """Convert snake_case to PascalCase: get_nets_list → GetNetsList."""
    return "".join(part.capitalize() for part in name.split("_"))


def _candidate_names(tool_name: str) -> list[str]:
    """
    Return a prioritised list of proto message names to try for a given tool name.

    Tries:
    1. Explicit override from TOOL_TO_PROTO
    2. Direct PascalCase conversion
    3. PascalCase with common suffixes stripped (_list, _info, _view, _region)
    """
    candidates: list[str] = []

    if tool_name in TOOL_TO_PROTO:
        candidates.append(TOOL_TO_PROTO[tool_name])

    pascal = _snake_to_pascal(tool_name)
    candidates.append(pascal)

    # Try stripping trailing noise words that appear in tool names but not proto names
    for suffix in ("List", "Info", "View", "Region", "Violations", "Url"):
        if pascal.endswith(suffix):
            candidates.append(pascal[: -len(suffix)])

    return candidates


class AnnotationLoader:
    """
    Loads and queries KiCad IPC API tool annotations.

    Parameters
    ----------
    path:
        Path to the annotations JSON file.  Defaults to the bundled file at
        ``python/annotations/kicad_ipc_tool_annotations.json``.
    """

    def __init__(self, path: Optional[Path] = None) -> None:
        self._path = path or _DEFAULT_ANNOTATIONS_PATH
        self._annotations: dict[str, dict] = {}
        self._load()

    def _load(self) -> None:
        if not self._path.exists():
            return
        try:
            data = json.loads(self._path.read_text(encoding="utf-8"))
            self._annotations = data.get("annotations", {})
        except (json.JSONDecodeError, OSError):
            pass

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def get(self, tool_name: str) -> Optional[dict]:
        """Return the full annotation dict for a tool, or None if not found."""
        for candidate in _candidate_names(tool_name):
            ann = self._annotations.get(candidate)
            if ann:
                return ann
        return None

    def description(self, tool_name: str) -> Optional[str]:
        """Return the description string for a tool, or None if not found."""
        ann = self.get(tool_name)
        return ann.get("description") if ann else None

    def enrich_schema(self, tool_name: str, schema: dict) -> dict:
        """
        Return a copy of ``schema`` enriched with annotation data.

        Fills in ``description`` and ``annotations`` (MCP ToolAnnotations) if
        the schema doesn't already have them.  Existing values are preserved.
        """
        ann = self.get(tool_name)
        if not ann:
            return schema

        schema = schema.copy()

        if not schema.get("description") and ann.get("description"):
            schema["description"] = ann["description"]

        # Build MCP ToolAnnotations block from blocking/interactive flags
        mcp_hints: dict[str, bool] = {}
        if ann.get("blocking"):
            # Blocking tools are read-only from the caller's perspective
            # (they mutate KiCad state but don't modify files via the API)
            mcp_hints["readOnlyHint"] = False
        if ann.get("interactive"):
            mcp_hints["readOnlyHint"] = False

        if ann.get("warnings"):
            schema.setdefault("_warnings", ann["warnings"])

        if mcp_hints:
            schema.setdefault("annotations", {}).update(mcp_hints)

        return schema

    def summary(self) -> dict[str, str]:
        """Return a dict of proto_message_name → description for all loaded annotations."""
        return {k: v.get("description", "") for k, v in self._annotations.items()}

    def __len__(self) -> int:
        return len(self._annotations)

    def __repr__(self) -> str:
        return f"AnnotationLoader({self._path}, {len(self)} annotations)"
