"""Parser for KiCad schematic files (.kicad_sch).

Extracts components, nets, wires, labels, and hierarchy information
from KiCad schematics using the S-expression parser.
"""

from __future__ import annotations

from pathlib import Path

from .sexpr import parse_file, find_node, find_nodes, find_nodes_recursive, node_value


def parse_schematic(filepath: str | Path) -> dict:
    """Parse a .kicad_sch file and return structured data.

    Returns a dict with keys: version, generator, uuid, paper, title_block,
    lib_symbols, components, wires, junctions, labels, no_connects,
    sheets, sheet_instances.
    """
    tree = parse_file(filepath)
    return _extract_schematic(tree, filepath)


def _extract_schematic(tree: list, filepath: str | Path) -> dict:
    """Extract all relevant data from a parsed schematic tree."""
    return {
        "file": str(filepath),
        "version": node_value(find_node(tree, "version")),
        "generator": node_value(find_node(tree, "generator")),
        "uuid": node_value(find_node(tree, "uuid")),
        "paper": node_value(find_node(tree, "paper")),
        "title_block": _extract_title_block(find_node(tree, "title_block")),
        "components": _extract_components(tree),
        "wires": _extract_wires(tree),
        "labels": _extract_labels(tree),
        "global_labels": _extract_global_labels(tree),
        "hierarchical_labels": _extract_hierarchical_labels(tree),
        "junctions": _extract_junctions(tree),
        "no_connects": _extract_no_connects(tree),
        "sheets": _extract_sheets(tree),
        "power_symbols": _extract_power_symbols(tree),
    }


def _extract_title_block(node: list | None) -> dict:
    """Extract title block metadata."""
    if not node:
        return {}
    result = {}
    for field in ("title", "date", "rev", "company"):
        val = node_value(find_node(node, field))
        if val:
            result[field] = val
    # Comments are (comment N "text")
    for child in node:
        if isinstance(child, list) and child and child[0] == "comment" and len(child) >= 3:
            result[f"comment_{child[1]}"] = child[2]
    return result


def _extract_property(node: list) -> dict | None:
    """Extract a property node: (property "Name" "Value" (at x y rot) ...)."""
    if not node or node[0] != "property" or len(node) < 3:
        return None
    name = node[1]
    value = node[2]
    hidden = False
    # Check for (effects ... (hide yes))
    effects = find_node(node, "effects")
    if effects:
        hide = find_node(effects, "hide")
        if hide and node_value(hide) == "yes":
            hidden = True
    return {"name": name, "value": value, "hidden": hidden}


def _extract_position(node: list) -> dict | None:
    """Extract position from an (at x y [rot]) node."""
    at_node = find_node(node, "at")
    if not at_node or len(at_node) < 3:
        return None
    try:
        result = {"x": float(at_node[1]), "y": float(at_node[2])}
        if len(at_node) >= 4:
            result["rotation"] = float(at_node[3])
        return result
    except (ValueError, TypeError):
        return None


def _extract_components(tree: list) -> list[dict]:
    """Extract component instances from the schematic.

    Components are (symbol ...) nodes at the sheet level that have a
    (lib_id "...") child — distinct from symbol definitions in lib_symbols
    which don't have lib_id.
    """
    components = []
    for node in tree:
        if not isinstance(node, list) or not node or node[0] != "symbol":
            continue
        lib_id_node = find_node(node, "lib_id")
        if not lib_id_node:
            continue  # This is a lib_symbols definition, not an instance

        comp = {
            "lib_id": node_value(lib_id_node),
            "uuid": node_value(find_node(node, "uuid")),
            "position": _extract_position(node),
            "unit": node_value(find_node(node, "unit")),
            "dnp": node_value(find_node(node, "dnp")) == "yes",
            "exclude_from_sim": node_value(find_node(node, "exclude_from_sim")) == "yes",
            "in_bom": node_value(find_node(node, "in_bom")) != "no",
            "on_board": node_value(find_node(node, "on_board")) != "no",
        }

        # Extract properties (Reference, Value, Footprint, etc.)
        props = {}
        for child in node:
            p = _extract_property(child) if isinstance(child, list) else None
            if p:
                props[p["name"]] = p["value"]
        comp["properties"] = props
        comp["reference"] = props.get("Reference", "")
        comp["value"] = props.get("Value", "")
        comp["footprint"] = props.get("Footprint", "")

        # Extract instance references (for hierarchical designs)
        instances_node = find_node(node, "instances")
        if instances_node:
            instances = []
            for proj in find_nodes(instances_node, "project"):
                proj_name = proj[1] if len(proj) > 1 and isinstance(proj[1], str) else ""
                for path_node in find_nodes(proj, "path"):
                    path_str = path_node[1] if len(path_node) > 1 and isinstance(path_node[1], str) else ""
                    ref_node = find_node(path_node, "reference")
                    unit_node = find_node(path_node, "unit")
                    instances.append({
                        "project": proj_name,
                        "path": path_str,
                        "reference": node_value(ref_node) or "",
                        "unit": node_value(unit_node) or "",
                    })
            comp["instances"] = instances

        components.append(comp)
    return components


def _extract_wires(tree: list) -> list[dict]:
    """Extract wire segments."""
    wires = []
    for node in find_nodes(tree, "wire"):
        pts_node = find_node(node, "pts")
        if not pts_node:
            continue
        points = []
        for xy in find_nodes(pts_node, "xy"):
            if len(xy) >= 3:
                try:
                    points.append({"x": float(xy[1]), "y": float(xy[2])})
                except (ValueError, TypeError):
                    pass
        wires.append({
            "points": points,
            "uuid": node_value(find_node(node, "uuid")),
        })
    return wires


def _extract_labels(tree: list) -> list[dict]:
    """Extract net labels."""
    labels = []
    for node in find_nodes(tree, "label"):
        if len(node) < 2:
            continue
        labels.append({
            "name": node[1] if isinstance(node[1], str) else "",
            "position": _extract_position(node),
            "uuid": node_value(find_node(node, "uuid")),
        })
    return labels


def _extract_global_labels(tree: list) -> list[dict]:
    """Extract global labels."""
    labels = []
    for node in find_nodes(tree, "global_label"):
        if len(node) < 2:
            continue
        labels.append({
            "name": node[1] if isinstance(node[1], str) else "",
            "shape": node_value(find_node(node, "shape")),
            "position": _extract_position(node),
            "uuid": node_value(find_node(node, "uuid")),
        })
    return labels


def _extract_hierarchical_labels(tree: list) -> list[dict]:
    """Extract hierarchical labels."""
    labels = []
    for node in find_nodes(tree, "hierarchical_label"):
        if len(node) < 2:
            continue
        labels.append({
            "name": node[1] if isinstance(node[1], str) else "",
            "shape": node_value(find_node(node, "shape")),
            "position": _extract_position(node),
            "uuid": node_value(find_node(node, "uuid")),
        })
    return labels


def _extract_junctions(tree: list) -> list[dict]:
    """Extract junction points."""
    junctions = []
    for node in find_nodes(tree, "junction"):
        junctions.append({
            "position": _extract_position(node),
            "uuid": node_value(find_node(node, "uuid")),
        })
    return junctions


def _extract_no_connects(tree: list) -> list[dict]:
    """Extract no-connect markers."""
    ncs = []
    for node in find_nodes(tree, "no_connect"):
        ncs.append({
            "position": _extract_position(node),
            "uuid": node_value(find_node(node, "uuid")),
        })
    return ncs


def _extract_sheets(tree: list) -> list[dict]:
    """Extract sub-sheet references."""
    sheets = []
    for node in find_nodes(tree, "sheet"):
        sheet = {
            "uuid": node_value(find_node(node, "uuid")),
            "position": _extract_position(node),
        }
        # Size
        size_node = find_node(node, "size")
        if size_node and len(size_node) >= 3:
            try:
                sheet["width"] = float(size_node[1])
                sheet["height"] = float(size_node[2])
            except (ValueError, TypeError):
                pass

        # Properties (sheet name, sheet file)
        props = {}
        for child in node:
            p = _extract_property(child) if isinstance(child, list) else None
            if p:
                props[p["name"]] = p["value"]
        sheet["properties"] = props
        sheet["name"] = props.get("Sheetname", props.get("Sheet name", ""))
        sheet["file"] = props.get("Sheetfile", props.get("Sheet file", ""))

        # Pins on the sheet symbol
        pins = []
        for pin_node in find_nodes(node, "pin"):
            if len(pin_node) >= 2:
                pin = {"name": pin_node[1] if isinstance(pin_node[1], str) else ""}
                direction = node_value(find_node(pin_node, "direction"))
                if direction:
                    pin["direction"] = direction
                pins.append(pin)
        sheet["pins"] = pins

        sheets.append(sheet)
    return sheets


def _extract_power_symbols(tree: list) -> list[dict]:
    """Identify power symbols among the components.

    Power symbols have lib_id containing a power flag or their lib_symbols
    entry has the (power) tag.
    """
    # First collect all lib_symbols that are marked as power
    power_lib_ids = set()
    lib_symbols = find_node(tree, "lib_symbols")
    if lib_symbols:
        for sym in find_nodes(lib_symbols, "symbol"):
            if len(sym) < 2:
                continue
            # Check if the symbol has a bare (power) child
            for child in sym:
                if isinstance(child, list) and child == ["power"]:
                    power_lib_ids.add(sym[1] if isinstance(sym[1], str) else "")
                    break
                if child == "power":
                    power_lib_ids.add(sym[1] if isinstance(sym[1], str) else "")
                    break

    # Filter components to only power symbols
    power_syms = []
    for node in tree:
        if not isinstance(node, list) or not node or node[0] != "symbol":
            continue
        lib_id_node = find_node(node, "lib_id")
        if not lib_id_node:
            continue
        lib_id = node_value(lib_id_node)
        if lib_id in power_lib_ids:
            props = {}
            for child in node:
                p = _extract_property(child) if isinstance(child, list) else None
                if p:
                    props[p["name"]] = p["value"]
            power_syms.append({
                "lib_id": lib_id,
                "value": props.get("Value", ""),
                "reference": props.get("Reference", ""),
                "position": _extract_position(node),
                "uuid": node_value(find_node(node, "uuid")),
            })
    return power_syms


# ── High-level query functions ──────────────────────────────────────


def list_schematic_components(filepath: str | Path) -> list[dict]:
    """List all component instances in a schematic.

    Returns a list of dicts with: reference, value, footprint, lib_id,
    position, dnp, in_bom, on_board.
    """
    data = parse_schematic(filepath)
    result = []
    for comp in data["components"]:
        result.append({
            "reference": comp["reference"],
            "value": comp["value"],
            "footprint": comp["footprint"],
            "lib_id": comp["lib_id"],
            "position": comp["position"],
            "dnp": comp["dnp"],
            "in_bom": comp["in_bom"],
            "on_board": comp["on_board"],
        })
    return result


def get_component_details(filepath: str | Path, reference: str) -> dict | None:
    """Get detailed info for a specific component by reference designator."""
    data = parse_schematic(filepath)
    for comp in data["components"]:
        if comp["reference"] == reference:
            return comp
        # Also check instance references for hierarchical designs
        for inst in comp.get("instances", []):
            if inst.get("reference") == reference:
                result = dict(comp)
                result["reference"] = reference
                result["instance_path"] = inst.get("path", "")
                return result
    return None


def list_schematic_nets(filepath: str | Path) -> list[dict]:
    """List all net names found in the schematic via labels.

    Returns nets from labels, global_labels, and power symbols.
    """
    data = parse_schematic(filepath)
    nets = {}

    for label in data["labels"]:
        name = label["name"]
        if name not in nets:
            nets[name] = {"name": name, "type": "local", "count": 0}
        nets[name]["count"] += 1

    for label in data["global_labels"]:
        name = label["name"]
        if name not in nets:
            nets[name] = {"name": name, "type": "global", "count": 0}
        nets[name]["count"] += 1

    for ps in data["power_symbols"]:
        name = ps["value"]
        if name not in nets:
            nets[name] = {"name": name, "type": "power", "count": 0}
        nets[name]["count"] += 1

    return list(nets.values())


def get_schematic_stats(filepath: str | Path) -> dict:
    """Get summary statistics for a schematic."""
    data = parse_schematic(filepath)

    # Count non-power components
    components = [c for c in data["components"]
                  if not any(ps["uuid"] == c["uuid"] for ps in data["power_symbols"])]

    # Unique footprints
    footprints = set(c["footprint"] for c in components if c["footprint"])

    return {
        "file": str(filepath),
        "title": data["title_block"].get("title", ""),
        "component_count": len(components),
        "power_symbol_count": len(data["power_symbols"]),
        "wire_count": len(data["wires"]),
        "junction_count": len(data["junctions"]),
        "label_count": len(data["labels"]),
        "global_label_count": len(data["global_labels"]),
        "hierarchical_label_count": len(data["hierarchical_labels"]),
        "no_connect_count": len(data["no_connects"]),
        "sheet_count": len(data["sheets"]),
        "unique_footprints": len(footprints),
        "dnp_count": sum(1 for c in components if c["dnp"]),
    }


def get_schematic_hierarchy(filepath: str | Path) -> dict:
    """Get the sheet hierarchy starting from this schematic.

    Returns a tree structure of sheets with their sub-sheets.
    """
    data = parse_schematic(filepath)
    base_dir = Path(filepath).parent

    result = {
        "file": str(filepath),
        "title": data["title_block"].get("title", ""),
        "sheets": [],
    }

    for sheet in data["sheets"]:
        sheet_file = sheet.get("file", "")
        sub = {
            "name": sheet.get("name", ""),
            "file": sheet_file,
        }
        # Recursively parse sub-sheets if the file exists
        sub_path = base_dir / sheet_file
        if sub_path.exists():
            try:
                sub_data = parse_schematic(sub_path)
                sub["component_count"] = len(sub_data["components"])
                sub["sheets"] = [
                    {"name": s.get("name", ""), "file": s.get("file", "")}
                    for s in sub_data["sheets"]
                ]
            except Exception:
                sub["error"] = "Failed to parse sub-sheet"
        result["sheets"].append(sub)

    return result
