"""Connectivity check tool handler.

This module provides the handler for running local connectivity
checks on GDS files, including XML parsing and actionable violation summaries.
"""

from __future__ import annotations

import json
import logging
import re
import xml.etree.ElementTree as ET
from pathlib import PurePosixPath
from typing import TYPE_CHECKING, Any

from mcp.types import ImageContent, TextContent, Tool

from ..config import MCPConfig
from ..notify import notify_show_check_results
from .base import EndpointMapping, ToolHandler, add_project_param
from .rdb_parser import (
    extract_xml_string,
    parse_category_path,
    parse_polygon_location,
    parse_rdb_cells,
)

if TYPE_CHECKING:
    from ..client import FastAPIClient

logger = logging.getLogger(__name__)

__all__ = ["CheckConnectivityHandler"]

CONNECTIVITY_VIOLATION_INFO: dict[str, dict[str, str]] = {
    "InstanceshapeOverlap": {
        "description": "Instance shapes overlap where they should not",
        "guidance": (
            "Two component instances have overlapping geometry on the same layer. "
            "Check that instances are placed with sufficient spacing or that the "
            "overlap is intentional (e.g. for adiabatic transitions)."
            "Make sure that overlapping shapes are not due to routing paths "
            "crossing over each other without proper connections."
        ),
    },
    "ShapeOverlap": {
        "description": "Shapes overlap on the same net",
        "guidance": (
            "Overlapping shapes were found on the same layer. This may cause "
            "fabrication issues. Verify that shapes are merged or separated properly."
        ),
    },
    "DisconnectedPorts": {
        "description": "Ports that should be connected are not",
        "guidance": (
            "Ports expected to form a connection are disconnected. Check that "
            "waveguide routes fully connect to component ports and that port "
            "positions align."
        ),
    },
    "OpenNet": {
        "description": "Net has an open (unconnected) segment",
        "guidance": (
            "A net segment is not fully connected. This usually means a route "
            "does not reach its destination port. Check for gaps in waveguide paths."
        ),
    },
    "OrphanPort": {
        "description": "Port is not connected to any net",
        "guidance": (
            "A port is not connected to any net. Check that all component ports are "
            "connected to waveguides or other components as intended. Usually "
            "indicates your design is not fully connected and may not function as expected."
        ),
    },
}

_CONNECTIVITY_VIOLATION_INFO_LOWER = {
    k.lower(): v for k, v in CONNECTIVITY_VIOLATION_INFO.items()
}

_PORT_NAME_RE = re.compile(
    r"Port Name:\s*(\S+)"
    r"(?:\s*\(cell:\s*([^)]+)\))?"
)


def _parse_orphan_port_info(
    values_elem: ET.Element | None,
) -> dict[str, str] | None:
    """Extract port name and component from OrphanPort text values.

    Parses text values like:
        "Port Name: instance.o3 (cell: coupler_L10_G0p27)"

    The full reference is ``instance.port``; we strip the instance prefix
    and return only the port name (e.g. ``o3``).

    Args:
        values_elem: XML element containing <value> children

    Returns:
        Dict with 'port' and optionally 'component', or None
    """
    if values_elem is None:
        return None
    for value_elem in values_elem.findall("value"):
        if value_elem.text and "Port Name:" in value_elem.text:
            match = _PORT_NAME_RE.search(value_elem.text)
            if match:
                full_ref = match.group(1)
                # "component.port" → split into component and port name
                if "." in full_ref:
                    component, port = full_ref.rsplit(".", 1)
                    return {"port": port, "component": component}
                return {"port": full_ref}
    return None


def _lookup_violation_info(
    type_name: str,
    info_map: dict[str, dict[str, str]],
    lower_map: dict[str, dict[str, str]],
) -> dict[str, str] | None:
    """Case-insensitive lookup in a violation info dictionary."""
    return info_map.get(type_name) or lower_map.get(type_name.lower())


def _generate_connectivity_recommendations(
    total_violations: int,
    violations_by_type: list[dict[str, Any]],
) -> list[str]:
    """Generate actionable recommendations based on connectivity results.

    Args:
        total_violations: Total number of violations
        violations_by_type: List of violation types with counts

    Returns:
        List of recommendation strings with critical warnings
    """
    if total_violations == 0:
        return [
            "Design passes all connectivity checks",
            "All components are properly connected — device will function as intended",
        ]

    # Start with critical warning
    recommendations = [
        "⚠️  CRITICAL: This design has connectivity violations and will NOT function correctly",
        "⚠️  Connectivity errors indicate broken or incorrect connections that will cause device failure",
    ]

    for vtype in violations_by_type:
        type_name = vtype["type"]
        info = _lookup_violation_info(
            type_name, CONNECTIVITY_VIOLATION_INFO, _CONNECTIVITY_VIOLATION_INFO_LOWER
        )
        if info:
            recommendations.append(f"MUST FIX — {type_name}: {info['guidance']}")
        else:
            recommendations.append(
                f"MUST FIX — {type_name}: Review these violations — "
                "this is an unrecognized connectivity violation type"
            )

    if violations_by_type:
        top = violations_by_type[0]
        if top["count"] > 10:
            recommendations.append(
                f"PRIORITY: Address {top['type']} first ({top['count']:,} occurrences) — "
                "fixing systematic issues will resolve many violations at once"
            )

    recommendations.append(
        "After fixing violations, re-run connectivity check to verify all issues are resolved"
    )

    return recommendations


class CheckConnectivityHandler(ToolHandler):
    """Handler for running connectivity checks on GDS files.

    This is a fast, local check that verifies all layers are properly
    connected and identifies any connectivity violations. Parses KLayout
    RDB XML and extracts structured violation summaries with guidance.
    """

    @property
    def name(self) -> str:
        return "check_connectivity"

    @property
    def definition(self) -> Tool:
        return Tool(
            name="check_connectivity",
            description=(
                "Run a local connectivity check on a GDS file. This verifies that "
                "all layers are properly connected and identifies any connectivity "
                "violations. This is a fast, local check (does not require uploading "
                "to a remote server). Use this to quickly check for disconnected "
                "or overlapping components and port issues. IMPORTANT: Connectivity "
                "violations are CRITICAL errors that will cause the device to NOT "
                "function correctly. ALL violations must be fixed. Returns structured "
                "results showing connectivity issues with actionable recommendations."
            ),
            inputSchema=add_project_param(
                {
                    "type": "object",
                    "properties": {
                        "path": {
                            "type": "string",
                            "description": (
                                "Path to the GDS file to check. Can be absolute or "
                                "relative to the project directory."
                            ),
                        },
                    },
                    "required": ["path"],
                }
            ),
        )

    @property
    def mapping(self) -> EndpointMapping:
        return EndpointMapping(method="POST", path="/api/check-connectivity")

    def transform_request(self, args: dict[str, Any]) -> dict[str, Any]:
        """Transform check_connectivity MCP args to FastAPI params.

        Args:
            args: MCP tool arguments

        Returns:
            Dict with 'json_data' key for request body
        """
        return {"json_data": {"path": args["path"]}}

    def transform_response(self, response: Any) -> dict[str, Any]:
        """Transform connectivity response to LLM-friendly format.

        Parses KLayout RDB XML and extracts structured violation summaries
        with type-specific guidance. Connectivity results use nested
        categories (parent > child).

        Args:
            response: FastAPI response (XML string or dict with 'content' key)

        Returns:
            Structured summary with violations and recommendations,
            or error dict if parsing fails
        """
        xml_str, error = extract_xml_string(response, check_name="Connectivity")
        if error is not None:
            return error

        try:
            root = ET.fromstring(xml_str)
        except ET.ParseError as e:
            return {
                "error": f"Failed to parse connectivity XML: {e}",
                "raw_preview": xml_str[:500],
                "suggestion": "Check if the connectivity check returned valid XML",
            }

        cells = parse_rdb_cells(root)

        violations = []
        type_counts: dict[str, dict[str, Any]] = {}

        for item in root.findall(".//items/item"):
            category_elem = item.find("category")
            cell_elem = item.find("cell")
            values_elem = item.find("values")

            if category_elem is None or category_elem.text is None:
                continue

            parent, child = parse_category_path(category_elem.text)

            cell = (
                cell_elem.text
                if cell_elem is not None and cell_elem.text
                else "unknown"
            )

            violation_type = child if child else parent
            layer = parent if child else None

            location = parse_polygon_location(values_elem)

            violation: dict[str, Any] = {
                "category": violation_type,
                "cell": cell,
            }
            if layer:
                violation["layer"] = layer

            # Extract port/component info for OrphanPort violations
            if violation_type.lower() == "orphanport":
                port_info = _parse_orphan_port_info(values_elem)
                if port_info:
                    violation["port"] = port_info["port"]
                    if "component" in port_info:
                        violation["component"] = port_info["component"]

            if location is not None:
                violation["location"] = location
            elif values_elem is not None:
                violation["location_warning"] = "Could not parse coordinates"

            violations.append(violation)

            if violation_type not in type_counts:
                type_counts[violation_type] = {
                    "count": 0,
                    "affected_layers": set(),
                }
            type_counts[violation_type]["count"] += 1
            if layer:
                type_counts[violation_type]["affected_layers"].add(layer)

        total_violations = len(violations)
        status = "PASSED" if total_violations == 0 else "FAILED"
        functional = total_violations == 0

        summary: dict[str, Any] = {
            "total_violations": total_violations,
            "total_categories": len(type_counts),
            "cells_checked": cells,
            "status": status,
            "functional": functional,
            "severity": "NONE" if total_violations == 0 else "CRITICAL",
        }

        if total_violations == 0:
            summary["message"] = (
                "No connectivity violations found — device will function correctly"
            )
        else:
            summary["message"] = (
                f"CRITICAL: {total_violations:,} connectivity violation(s) found — "
                "device will NOT function correctly until ALL violations are fixed"
            )

        violations_by_type = [
            {
                "type": vtype,
                "description": (
                    _lookup_violation_info(
                        vtype,
                        CONNECTIVITY_VIOLATION_INFO,
                        _CONNECTIVITY_VIOLATION_INFO_LOWER,
                    )
                    or {}
                ).get("description", vtype),
                "count": data["count"],
                "affected_layers": sorted(data["affected_layers"]),
            }
            for vtype, data in sorted(
                type_counts.items(),
                key=lambda x: x[1]["count"],
                reverse=True,
            )
        ]

        recommendations = _generate_connectivity_recommendations(
            total_violations,
            violations_by_type,
        )

        return {
            "summary": summary,
            "violations_by_type": violations_by_type,
            "violations": violations,
            "recommendations": recommendations,
        }

    async def handle(
        self,
        arguments: dict[str, Any],
        client: FastAPIClient,
    ) -> list[TextContent | ImageContent]:
        """Run connectivity check and notify VS Code extension."""
        result = await super().handle(arguments, client)

        if MCPConfig.UI_NOTIFICATIONS:
            try:
                result_data = json.loads(result[0].text) if result else {}
                if "error" not in result_data:
                    cell_name = PurePosixPath(arguments["path"]).stem
                    await notify_show_check_results(
                        arguments.get("project"), "Connectivity", cell_name
                    )
            except Exception:
                logger.debug("Failed to send connectivity notification", exc_info=True)

        return result
