"""Mappings from MCP tools to FastAPI endpoints.

This module defines how MCP tool arguments are transformed into HTTP
requests to the FastAPI backend, and how responses are transformed back.
"""

from __future__ import annotations

import xml.etree.ElementTree as ET
from collections.abc import Callable
from typing import Any

__all__ = [
    "TOOL_MAPPINGS",
    "get_mapping",
    "transform_request",
    "transform_response",
]


class EndpointMapping:
    """Configuration for mapping an MCP tool to a FastAPI endpoint."""

    def __init__(
        self,
        method: str,
        path: str,
        request_transformer: Callable[[dict[str, Any]], dict[str, Any]] | None = None,
        response_transformer: Callable[[Any], Any] | None = None,
    ) -> None:
        """Initialize endpoint mapping.

        Args:
            method: HTTP method (GET, POST, etc.)
            path: API endpoint path
            request_transformer: Optional function to transform MCP args to
                HTTP params
            response_transformer: Optional function to transform HTTP response
                to MCP format
        """
        self.method = method
        self.path = path
        self.request_transformer = request_transformer or (lambda x: x)
        self.response_transformer = response_transformer or (lambda x: x)


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

    Args:
        args: MCP tool arguments

    Returns:
        Dict with 'params' key for query parameters and 'json_data' for body
    """
    return {
        "params": {
            "with_metadata": args.get("with_metadata", True),
            "register": args.get("register", True),
        },
        "json_data": args["names"],
    }


def _list_cells_response(response: Any) -> dict[str, Any]:
    """Transform list_cells response to MCP format.

    Args:
        response: FastAPI response (list of cell names)

    Returns:
        Formatted response with cell names
    """
    if isinstance(response, list):
        return {"cells": response, "count": len(response)}
    return response


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

    Args:
        args: MCP tool arguments

    Returns:
        Dict with 'params' key for query parameters
    """
    return {"params": {"name": args["name"]}}


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

    Args:
        args: MCP tool arguments

    Returns:
        Dict with 'json_data' key for request body
    """
    json_data: dict[str, Any] = {"path": args["path"]}

    if "pdk" in args and args["pdk"]:
        json_data["pdk"] = args["pdk"]
    if "process" in args and args["process"]:
        json_data["process"] = args["process"]
    if "timeout" in args and args["timeout"]:
        json_data["timeout"] = args["timeout"]
    if "host" in args and args["host"]:
        json_data["host"] = args["host"]

    return {"json_data": json_data}


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

    Parses KLayout RDB XML and extracts actionable violation summary
    without polygon coordinates or verbose metadata. Computes simplified
    location data (bounding box, centroid, size) from polygon coordinates
    to reduce token usage by ~82% while preserving all violation details.

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

    Returns:
        Structured summary with all violations including location data,
        or error dict if parsing fails
    """
    try:
        if isinstance(response, dict) and "content" in response:
            xml_str = response["content"]
        elif isinstance(response, str):
            xml_str = response
        else:
            return {
                "error": f"Unexpected response type: {type(response).__name__}",
                "suggestion": "Expected XML string or dict with 'content' key",
            }
    except Exception as e:
        return {
            "error": f"Failed to extract XML from response: {e}",
            "response_preview": str(response)[:500],
        }

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

    categories_map = {}
    for category in root.findall(".//categories/category"):
        name_elem = category.find("name")
        desc_elem = category.find("description")
        if name_elem is not None and name_elem.text:
            categories_map[name_elem.text] = (
                desc_elem.text
                if desc_elem is not None and desc_elem.text
                else name_elem.text
            )

    cells = []
    for cell in root.findall(".//cells/cell"):
        name_elem = cell.find("name")
        if name_elem is not None and name_elem.text:
            cells.append(name_elem.text)

    violations = []
    violation_counts: dict[str, int] = {}

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

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

        category = category_elem.text
        cell = cell_elem.text if cell_elem is not None and cell_elem.text else "unknown"
        description = (
            comment_elem.text
            if comment_elem is not None and comment_elem.text
            else categories_map.get(category, category)
        )

        location = _parse_polygon_location(values_elem)

        violation = {
            "category": category,
            "cell": cell,
            "description": description,
        }

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

        violations.append(violation)

        violation_counts[category] = violation_counts.get(category, 0) + 1

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

    summary = {
        "total_violations": total_violations,
        "total_categories": len(violation_counts),
        "cells_checked": cells,
        "status": status,
    }

    if total_violations == 0:
        summary["message"] = "No DRC violations found"

    violations_by_category = [
        {
            "category": cat,
            "description": categories_map.get(cat, cat),
            "count": count,
        }
        for cat, count in sorted(
            violation_counts.items(),
            key=lambda x: x[1],
            reverse=True,
        )
    ]

    recommendations = _generate_drc_recommendations(
        total_violations,
        violations_by_category,
        cells,
    )

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


def _parse_polygon_location(values_elem: ET.Element | None) -> dict[str, Any] | None:
    """Parse polygon coordinates and compute location data.

    Args:
        values_elem: XML element containing polygon coordinates

    Returns:
        Dict with bbox, centroid, and size, or None if parsing fails
    """
    if values_elem is None:
        return None

    value_elem = values_elem.find("value")
    if value_elem is None or value_elem.text is None:
        return None

    value_text = value_elem.text
    if "polygon:" not in value_text:
        return None

    try:
        coords_str = value_text.replace("polygon:", "").strip().strip("()")
        if not coords_str:
            return None

        pairs = coords_str.split(";")
        coords = []
        for pair in pairs:
            if "," not in pair:
                continue
            x_str, y_str = pair.split(",", 1)
            coords.append((float(x_str), float(y_str)))

        if not coords:
            return None

        xs = [x for x, y in coords]
        ys = [y for x, y in coords]

        bbox = {
            "min_x": round(min(xs), 3),
            "min_y": round(min(ys), 3),
            "max_x": round(max(xs), 3),
            "max_y": round(max(ys), 3),
        }

        centroid = {
            "x": round(sum(xs) / len(xs), 3),
            "y": round(sum(ys) / len(ys), 3),
        }

        size = {
            "width": round(bbox["max_x"] - bbox["min_x"], 3),
            "height": round(bbox["max_y"] - bbox["min_y"], 3),
        }

        return {
            "bbox": bbox,
            "centroid": centroid,
            "size": size,
        }

    except (ValueError, IndexError):
        return None


def _generate_drc_recommendations(
    total_violations: int,
    violations_by_category: list[dict[str, Any]],
    cells: list[str],
) -> list[str]:
    """Generate actionable recommendations based on DRC results.

    Args:
        total_violations: Total number of violations
        violations_by_category: List of violation categories with counts
        cells: List of cells checked

    Returns:
        List of recommendation strings
    """
    if total_violations == 0:
        return ["Design passes all DRC checks"]

    recommendations = []

    if violations_by_category:
        top_category = violations_by_category[0]
        if top_category["count"] > 10:
            recommendations.append(
                f"Focus on {top_category['category']} violations "
                f"({top_category['count']:,} occurrences)"
            )
        elif top_category["count"] > 1:
            recommendations.append(
                f"Check {top_category['category']} rules in {', '.join(cells)}"
            )

    if len(violations_by_category) > 1:
        recommendations.append(
            f"Review {len(violations_by_category)} different DRC rule categories"
        )

    if total_violations > 100:
        recommendations.append(
            "Consider fixing systematic issues first to reduce violation count"
        )

    return recommendations


def _check_connectivity_request(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 _check_lvs_request(args: dict[str, Any]) -> dict[str, Any]:
    """Transform check_lvs MCP args to FastAPI params.

    Args:
        args: MCP tool arguments

    Returns:
        Dict with 'json_data' key for request body
    """
    return {
        "json_data": {
            "cell": args["cell"],
            "netpath": args["netpath"],
            "cellargs": args.get("cellargs", ""),
        }
    }


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

    Args:
        args: MCP tool arguments

    Returns:
        Dict with 'params' key for query parameters
    """
    return {"params": {"name": args["name"]}}


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

    Args:
        args: MCP tool arguments

    Returns:
        Dict with 'params' key for query parameters
    """
    return {
        "params": {
            "netlist": args["netlist"],
            "instance": args["instance"],
            "port": args["port"],
        }
    }


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

    Args:
        args: MCP tool arguments

    Returns:
        Dict with 'json_data' key for request body
    """
    json_data: dict[str, Any] = {"path": args["path"]}

    if "outpath" in args and args["outpath"]:
        json_data["outpath"] = args["outpath"]
    if "layers_to_keep" in args and args["layers_to_keep"]:
        json_data["layers_to_keep"] = args["layers_to_keep"]
    if "bbox_layer" in args and args["bbox_layer"]:
        json_data["bbox_layer"] = args["bbox_layer"]
    if "ignore_ports" in args:
        json_data["ignore_ports"] = args["ignore_ports"]

    return {"json_data": json_data}


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

    Args:
        args: MCP tool arguments

    Returns:
        Dict with 'path' and 'json_data' for the request
    """
    cell_name = args["cell_name"]
    kwargs = args.get("kwargs", {})

    return {
        "path": f"/freeze/{cell_name}",
        "json_data": kwargs,  # httpx will JSON-encode this to a string
    }


TOOL_MAPPINGS: dict[str, EndpointMapping] = {
    "build_cells": EndpointMapping(
        method="POST",
        path="/api/build-cells",
        request_transformer=_build_cells_request,
    ),
    "list_cells": EndpointMapping(
        method="GET",
        path="/api/cells",
        response_transformer=_list_cells_response,
    ),
    "get_cell_info": EndpointMapping(
        method="GET",
        path="/api/cell-info",
        request_transformer=_get_cell_info_request,
    ),
    "check_drc": EndpointMapping(
        method="POST",
        path="/api/check-drc",
        request_transformer=_check_drc_request,
        response_transformer=_check_drc_response,
    ),
    "check_connectivity": EndpointMapping(
        method="POST",
        path="/api/check-connectivity",
        request_transformer=_check_connectivity_request,
    ),
    "check_lvs": EndpointMapping(
        method="POST",
        path="/api/check-lvs",
        request_transformer=_check_lvs_request,
    ),
    "simulate_component": EndpointMapping(
        method="GET",
        path="/api/simulate",
        request_transformer=_simulate_component_request,
    ),
    "get_port_center": EndpointMapping(
        method="GET",
        path="/api/port-center",
        request_transformer=_get_port_center_request,
    ),
    "generate_bbox": EndpointMapping(
        method="POST",
        path="/api/bbox",
        request_transformer=_generate_bbox_request,
    ),
    "freeze_cell": EndpointMapping(
        method="POST",
        path="/freeze/{cell_name}",
        request_transformer=_freeze_cell_request,
    ),
    "get_pdk_info": EndpointMapping(
        method="GET",
        path="/info",
    ),
}


def get_mapping(tool_name: str) -> EndpointMapping | None:
    """Get the endpoint mapping for a tool.

    Args:
        tool_name: Name of the MCP tool

    Returns:
        EndpointMapping or None if not found
    """
    return TOOL_MAPPINGS.get(tool_name)


def transform_request(
    tool_name: str,
    args: dict[str, Any],
) -> dict[str, Any]:
    """Transform MCP tool arguments to HTTP request parameters.

    Args:
        tool_name: Name of the MCP tool
        args: Tool arguments from MCP

    Returns:
        Dict containing HTTP request parameters (params, json_data, path, etc.)
    """
    mapping = get_mapping(tool_name)
    if mapping is None:
        return {}

    return mapping.request_transformer(args)


def transform_response(tool_name: str, response: Any) -> Any:
    """Transform HTTP response to MCP format.

    Args:
        tool_name: Name of the MCP tool
        response: HTTP response from FastAPI

    Returns:
        Transformed response for MCP
    """
    mapping = get_mapping(tool_name)
    if mapping is None:
        return response

    return mapping.response_transformer(response)
