"""
Schematic field-placement commands.

Tools:
  - set_schematic_property_position:          move one Reference/Value field
  - batch_set_schematic_property_positions:   move many fields in one file read/write
  - autoplace_schematic_fields:               auto-position Ref/Value fields outside the
                                              component body and any attached net labels

This module is self-contained: it gathers the enriched component/label data it needs
(body bounding boxes, field positions, pin tips) directly from the schematic + PinLocator,
rather than depending on the output shape of other handlers.
"""

import logging
from pathlib import Path
from typing import Any, Dict, List

from commands.pin_locator import PinLocator
from commands.schematic import SchematicManager
from commands.schematic_text_utils import (
    _extract_property_position,
    _extract_property_visible,
    _find_placed_symbol_block,
    _move_property_in_block,
)

logger = logging.getLogger("kicad_interface")

_GRID = 1.27  # 50-mil KiCad schematic grid (mm)
_BODY_PAD_MM = 1.27

# ── Enriched data gathering (replicates list_schematic_components' fork enrichment) ──


def _gather_components(
    schematic, sch_path: Path, raw_content: str, locator: PinLocator
) -> List[Dict[str, Any]]:
    """Build enriched component dicts: position, value, libId, pins, body_bbox, ref/value_field."""
    components: List[Dict[str, Any]] = []
    for symbol in schematic.symbol:
        if not hasattr(symbol.property, "Reference"):
            continue
        ref = symbol.property.Reference.value
        if ref.startswith("_TEMPLATE"):
            continue
        lib_id = symbol.lib_id.value if hasattr(symbol, "lib_id") else ""
        value = symbol.property.Value.value if hasattr(symbol.property, "Value") else ""
        position = symbol.at.value if hasattr(symbol, "at") else [0, 0, 0]

        comp: Dict[str, Any] = {
            "reference": ref,
            "libId": lib_id,
            "value": value,
            "position": {"x": float(position[0]), "y": float(position[1])},
            "rotation": float(position[2]) if len(position) > 2 else 0,
        }

        block_text, _, _ = _find_placed_symbol_block(raw_content, ref)
        if block_text:
            ref_pos = _extract_property_position(block_text, "Reference")
            if ref_pos:
                ref_pos["visible"] = _extract_property_visible(block_text, "Reference")
                comp["ref_field"] = ref_pos
            val_pos = _extract_property_position(block_text, "Value")
            if val_pos:
                val_pos["visible"] = _extract_property_visible(block_text, "Value")
                comp["value_field"] = val_pos

        try:
            all_pins = locator.get_all_symbol_pins(sch_path, ref)
            if all_pins:
                pins_def = locator.get_symbol_pins(sch_path, lib_id) or {}
                pin_list = []
                for pin_num, coords in all_pins.items():
                    pin_info = {"number": pin_num, "position": {"x": coords[0], "y": coords[1]}}
                    if pin_num in pins_def:
                        pin_info["name"] = pins_def[pin_num].get("name", pin_num)
                    pin_list.append(pin_info)
                comp["pins"] = pin_list
                xs = [p["position"]["x"] for p in pin_list]
                ys = [p["position"]["y"] for p in pin_list]
                comp["body_bbox"] = {
                    "x_min": min(xs) - _BODY_PAD_MM,
                    "y_min": min(ys) - _BODY_PAD_MM,
                    "x_max": max(xs) + _BODY_PAD_MM,
                    "y_max": max(ys) + _BODY_PAD_MM,
                }
        except Exception:
            pass  # pin lookup is best-effort

        components.append(comp)
    return components


def _gather_labels(schematic) -> List[Dict[str, Any]]:
    """Net + global labels as {type, name, position, angle}."""
    labels = []
    for attr, kind in (("label", "net"), ("global_label", "global")):
        for lbl in getattr(schematic, attr, []):
            if not hasattr(lbl, "value"):
                continue
            pos = lbl.at.value if hasattr(lbl, "at") and hasattr(lbl.at, "value") else [0, 0, 0]
            labels.append(
                {
                    "type": kind,
                    "name": lbl.value,
                    "position": {"x": float(pos[0]), "y": float(pos[1])},
                    "angle": float(pos[2]) if len(pos) > 2 else 0,
                }
            )
    return labels


def _bbox_overlaps(a, b, margin=0.0):
    return (
        a["x_min"] - margin < b["x_max"]
        and a["x_max"] + margin > b["x_min"]
        and a["y_min"] - margin < b["y_max"]
        and a["y_max"] + margin > b["y_min"]
    )


class SchematicFieldLayoutCommands:
    """Handlers for schematic field placement."""

    def set_schematic_property_position(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Move a symbol's Reference or Value property field to a new coordinate."""
        logger.info("Setting schematic property position")
        try:
            schematic_path = params.get("schematicPath")
            reference = params.get("reference")
            property_name = params.get("property")
            x = params.get("x")
            y = params.get("y")
            angle = params.get("angle", 0)
            visible = params.get("visible", True)

            if not all([schematic_path, reference, property_name, x is not None, y is not None]):
                return {
                    "success": False,
                    "message": "Missing required parameters: schematicPath, reference, property, x, y",
                }
            if property_name not in ("Reference", "Value"):
                return {"success": False, "message": "property must be 'Reference' or 'Value'"}

            sch_path = Path(schematic_path)
            if not sch_path.exists():
                return {"success": False, "message": f"Schematic not found: {schematic_path}"}

            content = sch_path.read_text(encoding="utf-8")
            block_text, block_start, block_end = _find_placed_symbol_block(content, reference)
            if block_text is None:
                return {
                    "success": False,
                    "message": f"Component '{reference}' not found in schematic",
                }

            old_pos = _extract_property_position(block_text, property_name)
            new_block, n_subs = _move_property_in_block(
                block_text, property_name, x, y, angle, visible
            )
            if n_subs == 0:
                return {
                    "success": False,
                    "message": f"Property '{property_name}' not found in {reference}",
                }

            new_content = content[:block_start] + new_block + content[block_end + 1 :]
            sch_path.write_text(new_content, encoding="utf-8")

            old_str = (
                f"({old_pos['x']}, {old_pos['y']}, {old_pos['angle']}°)" if old_pos else "unknown"
            )
            return {
                "success": True,
                "message": f"Moved {reference}.{property_name} from {old_str} to ({x}, {y}, {angle}°)",
            }
        except Exception as e:
            logger.error(f"Error setting property position: {e}")
            import traceback

            logger.error(traceback.format_exc())
            return {"success": False, "message": str(e)}

    def batch_set_schematic_property_positions(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Batch-move Reference/Value property fields for many components in one read/write."""
        logger.info("Batch setting schematic property positions")
        try:
            schematic_path = params.get("schematicPath")
            updates = params.get("updates", [])

            if not schematic_path:
                return {"success": False, "message": "schematicPath is required"}
            if not updates:
                return {"success": False, "message": "updates list is required"}

            sch_path = Path(schematic_path)
            if not sch_path.exists():
                return {"success": False, "message": f"Schematic not found: {schematic_path}"}

            content = sch_path.read_text(encoding="utf-8")
            applied: List[Dict[str, Any]] = []
            failed: List[Dict[str, Any]] = []

            for upd in updates:
                reference = upd.get("reference")
                property_name = upd.get("property")
                x = upd.get("x")
                y = upd.get("y")
                angle = upd.get("angle", 0)
                visible = upd.get("visible", True)

                if not reference or not property_name or x is None or y is None:
                    failed.append(
                        {
                            "reference": reference,
                            "property": property_name,
                            "reason": "Missing required fields: reference, property, x, y",
                        }
                    )
                    continue
                if property_name not in ("Reference", "Value"):
                    failed.append(
                        {
                            "reference": reference,
                            "property": property_name,
                            "reason": "property must be 'Reference' or 'Value'",
                        }
                    )
                    continue

                block_text, block_start, block_end = _find_placed_symbol_block(content, reference)
                if block_text is None:
                    failed.append(
                        {
                            "reference": reference,
                            "property": property_name,
                            "reason": f"Component '{reference}' not found in schematic",
                        }
                    )
                    continue

                new_block, n_subs = _move_property_in_block(
                    block_text, property_name, x, y, angle, visible
                )
                if n_subs == 0:
                    failed.append(
                        {
                            "reference": reference,
                            "property": property_name,
                            "reason": f"Property '{property_name}' not found in {reference} block",
                        }
                    )
                    continue

                content = content[:block_start] + new_block + content[block_end + 1 :]
                applied.append(
                    {
                        "reference": reference,
                        "property": property_name,
                        "x": x,
                        "y": y,
                        "angle": angle,
                        "visible": visible,
                    }
                )

            if applied:
                sch_path.write_text(content, encoding="utf-8")

            return {
                "success": len(failed) == 0,
                "applied": applied,
                "failed": failed,
                "applied_count": len(applied),
                "failed_count": len(failed),
            }
        except Exception as e:
            logger.error(f"Error in batch_set_schematic_property_positions: {e}")
            import traceback

            logger.error(traceback.format_exc())
            return {"success": False, "message": str(e)}

    def autoplace_schematic_fields(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Re-position Reference and Value fields outside the body and any attached net labels."""
        logger.info("Auto-placing schematic fields")
        try:
            schematic_path = params.get("schematicPath")
            references_filter = params.get("references")
            clearance = float(params.get("clearance", _GRID))

            if not schematic_path:
                return {"success": False, "message": "schematicPath is required"}
            sch_path = Path(schematic_path)
            if not sch_path.exists():
                return {"success": False, "message": f"Schematic not found: {schematic_path}"}

            chars_per_mm, text_height = 1.5, 1.27

            def snap(val):
                return round(round(val / _GRID) * _GRID, 4)

            def label_bbox(lx, ly, angle, name):
                length = max(len(name), 1) * chars_per_mm + 1.0
                half_h = text_height / 2.0
                a = round(angle / 90) * 90 % 360
                if a == 0:
                    return {
                        "x_min": lx,
                        "y_min": ly - half_h,
                        "x_max": lx + length,
                        "y_max": ly + half_h,
                    }
                if a == 90:
                    return {
                        "x_min": lx - half_h,
                        "y_min": ly - length,
                        "x_max": lx + half_h,
                        "y_max": ly,
                    }
                if a == 180:
                    return {
                        "x_min": lx - length,
                        "y_min": ly - half_h,
                        "x_max": lx,
                        "y_max": ly + half_h,
                    }
                return {
                    "x_min": lx - half_h,
                    "y_min": ly,
                    "x_max": lx + half_h,
                    "y_max": ly + length,
                }

            def union(bb, other):
                return {
                    "x_min": min(bb["x_min"], other["x_min"]),
                    "y_min": min(bb["y_min"], other["y_min"]),
                    "x_max": max(bb["x_max"], other["x_max"]),
                    "y_max": max(bb["y_max"], other["y_max"]),
                }

            def field_bbox(fx, fy, text):
                half_w = max(len(str(text)), 1) * 0.75 / 2.0
                half_h = text_height / 2.0
                return {
                    "x_min": fx - half_w,
                    "y_min": fy - half_h,
                    "x_max": fx + half_w,
                    "y_max": fy + half_h,
                }

            schematic = SchematicManager.load_schematic(schematic_path)
            if not schematic:
                return {"success": False, "message": "Failed to load schematic"}
            raw_content = sch_path.read_text(encoding="utf-8")
            locator = PinLocator()

            components = _gather_components(schematic, sch_path, raw_content, locator)
            if references_filter:
                components = [c for c in components if c["reference"] in references_filter]
            net_labels = [
                lb for lb in _gather_labels(schematic) if lb.get("type") in ("net", "global")
            ]

            comp_ext_bboxes: Dict[str, Dict[str, float]] = {}
            comp_pin_map: Dict[str, Dict[str, Any]] = {}

            for comp in components:
                ref = comp["reference"]
                cx, cy = comp["position"]["x"], comp["position"]["y"]
                body_bb = comp.get("body_bbox") or {
                    "x_min": cx - 2.54,
                    "y_min": cy - 2.54,
                    "x_max": cx + 2.54,
                    "y_max": cy + 2.54,
                }
                ext_bb = dict(body_bb)

                all_pins: Dict[str, Any] = {}
                for p in comp.get("pins", []):
                    pnum = str(p.get("number", p.get("name", "")))
                    px = p.get("x", p.get("position", {}).get("x", cx))
                    py = p.get("y", p.get("position", {}).get("y", cy))
                    all_pins[pnum] = [float(px), float(py)]
                if not all_pins:
                    all_pins = locator.get_all_symbol_pins(sch_path, ref) or {}
                comp_pin_map[ref] = all_pins

                for lbl in net_labels:
                    lx, ly = lbl["position"]["x"], lbl["position"]["y"]
                    for pin_coords in all_pins.values():
                        px = (
                            pin_coords[0]
                            if isinstance(pin_coords, (list, tuple))
                            else pin_coords["x"]
                        )
                        py = (
                            pin_coords[1]
                            if isinstance(pin_coords, (list, tuple))
                            else pin_coords["y"]
                        )
                        if abs(lx - px) < 0.6 and abs(ly - py) < 0.6:
                            ext_bb = union(
                                ext_bb, label_bbox(lx, ly, lbl.get("angle", 0), lbl.get("name", ""))
                            )
                            break
                comp_ext_bboxes[ref] = ext_bb

            updates: List[Dict[str, Any]] = []
            placed_field_bboxes: List[Dict[str, float]] = []

            def has_collision(ref_bb, val_bb, exclude_ref):
                for other_ref, other_ext in comp_ext_bboxes.items():
                    if other_ref == exclude_ref:
                        continue
                    if _bbox_overlaps(ref_bb, other_ext, 0.3) or _bbox_overlaps(
                        val_bb, other_ext, 0.3
                    ):
                        return True
                for fb in placed_field_bboxes:
                    if _bbox_overlaps(ref_bb, fb, 0.2) or _bbox_overlaps(val_bb, fb, 0.2):
                        return True
                return False

            for comp in components:
                ref = comp["reference"]
                lib_id = comp.get("libId", "")
                if ref.startswith("#") or ref.startswith("_TEMPLATE"):
                    continue
                cx, cy = comp["position"]["x"], comp["position"]["y"]
                val_text = comp.get("value", ref)

                is_power = lib_id.startswith("kicad_power:") or lib_id.startswith("power:")
                if is_power:
                    ext_bb = dict(
                        comp.get("body_bbox")
                        or {
                            "x_min": cx - 1.27,
                            "y_min": cy - 1.27,
                            "x_max": cx + 1.27,
                            "y_max": cy + 1.27,
                        }
                    )
                    num_pins = 0
                else:
                    ext_bb = comp_ext_bboxes[ref]
                    num_pins = len(comp_pin_map[ref])

                if num_pins == 2:
                    coords = list(comp_pin_map[ref].values())
                    p1x = (
                        float(coords[0][0])
                        if isinstance(coords[0], (list, tuple))
                        else float(coords[0]["x"])
                    )
                    p1y = (
                        float(coords[0][1])
                        if isinstance(coords[0], (list, tuple))
                        else float(coords[0]["y"])
                    )
                    p2x = (
                        float(coords[1][0])
                        if isinstance(coords[1], (list, tuple))
                        else float(coords[1]["x"])
                    )
                    p2y = (
                        float(coords[1][1])
                        if isinstance(coords[1], (list, tuple))
                        else float(coords[1]["y"])
                    )
                    sides = (
                        ["right", "left", "above", "below"]
                        if abs(p1y - p2y) > abs(p1x - p2x)
                        else ["above", "below", "right", "left"]
                    )
                else:
                    sides = ["above", "below", "right", "left"]

                def try_side(side):
                    half_ref_h = text_height / 2.0
                    half_ref_w = max(len(ref), 1) * 0.75 / 2.0
                    half_val_w = max(len(str(val_text)), 1) * 0.75 / 2.0
                    stack = text_height
                    if side == "above":
                        ref_y = snap(ext_bb["y_min"] - half_ref_h - clearance)
                        return cx, ref_y, cx, ref_y - stack
                    if side == "below":
                        ref_y = snap(ext_bb["y_max"] + half_ref_h + clearance)
                        return cx, ref_y, cx, ref_y + stack
                    if side == "right":
                        x0 = ext_bb["x_max"] + clearance
                        ref_y = snap(cy)
                        return snap(x0 + half_ref_w), ref_y, snap(x0 + half_val_w), ref_y + stack
                    x0 = ext_bb["x_min"] - clearance
                    ref_y = snap(cy)
                    return snap(x0 - half_ref_w), ref_y, snap(x0 - half_val_w), ref_y + stack

                ref_x = ref_y = val_x = val_y = ref_bb = val_bb = None
                for side in sides:
                    rx, ry, vx, vy = try_side(side)
                    rb, vb = field_bbox(rx, ry, ref), field_bbox(vx, vy, val_text)
                    if not has_collision(rb, vb, ref):
                        ref_x, ref_y, val_x, val_y, ref_bb, val_bb = rx, ry, vx, vy, rb, vb
                        break
                if ref_x is None:
                    ref_x, ref_y, val_x, val_y = try_side(sides[0])
                    ref_bb, val_bb = field_bbox(ref_x, ref_y, ref), field_bbox(
                        val_x, val_y, val_text
                    )

                placed_field_bboxes.append(ref_bb)
                placed_field_bboxes.append(val_bb)
                updates.append(
                    {"reference": ref, "property": "Reference", "x": ref_x, "y": ref_y, "angle": 0}
                )
                updates.append(
                    {"reference": ref, "property": "Value", "x": val_x, "y": val_y, "angle": 0}
                )

            if not updates:
                return {"success": True, "message": "No components to update.", "updated_count": 0}

            batch_result = self.batch_set_schematic_property_positions(
                {"schematicPath": schematic_path, "updates": updates}
            )
            applied = batch_result.get("applied_count", 0)
            failed = batch_result.get("failed_count", 0)
            return {
                "success": batch_result.get("success", False),
                "message": f"Auto-placed fields for {applied // 2} component(s) "
                f"({applied} fields updated{', ' + str(failed) + ' failed' if failed else ''}).",
                "updated_count": applied,
                "failed_count": failed,
                "failed": batch_result.get("failed", []),
            }

        except Exception as e:
            logger.error(f"Error in autoplace_schematic_fields: {e}")
            import traceback

            logger.error(traceback.format_exc())
            return {"success": False, "message": str(e)}
