# mypy: ignore-errors
"""Minimal LTspice ``.asc`` (schematic) reader/writer for CAT.

Limitations
-----------
* Supports a restricted symbol catalog (res/cap/ind/voltage/current) with
  horizontal orientation (``R0``). Other symbols/orientations raise
  ``ValueError``.
* Wiring is modelled as axis-aligned segments. Files generated by
  :func:`circuit_to_schematic` adhere to this convention and can be imported
  reliably. Arbitrary LTspice schematics may require adjustments before they can
  be parsed by this module.

Despite the simplifications, the implementation provides a full round-trip:
``Circuit`` → ``AscSchematic`` → ``Circuit``. This enables quick editing of
simple circuits in LTspice while keeping the topology synchronised with CAT.
"""

from __future__ import annotations

from collections.abc import Callable, Iterator, Mapping
from dataclasses import dataclass, field
from functools import partial
from pathlib import Path
from typing import cast

from ..core.circuit import Circuit
from ..core.components import (
    CCCS,
    CCVS,
    VCCS,
    VCVS,
    Capacitor,
    Component,
    Diode,
    Idc,
    Inductor,
    OpAmpIdeal,
    Resistor,
    Vdc,
)
from ..core.net import GND, Net, Port

_GRID_Y = 120
_COMPONENT_ANCHOR_X = 160
_PIN_SPACING = 180


# ---------------------------------------------------------------------------
# Basic ASC primitives
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class AscPoint:
    x: int
    y: int


@dataclass
class AscWire:
    p1: AscPoint
    p2: AscPoint


@dataclass
class AscLabel:
    name: str
    position: AscPoint


@dataclass
class AscText:
    position: AscPoint
    orientation: str
    font: int
    text: str


@dataclass
class AscComponent:
    symbol: str
    position: AscPoint
    orientation: str
    attributes: dict[str, str] = field(default_factory=dict)
    ref: str | None = None
    pins: list[AscPoint] = field(default_factory=list)

    def attr(self, key: str, default: str | None = None) -> str | None:
        return self.attributes.get(key, default)


@dataclass
class AscSchematic:
    version: str
    sheet: tuple[int, int, int]
    components: list[AscComponent]
    wires: list[AscWire]
    labels: list[AscLabel]
    texts: list[AscText]

    def iter_components(self) -> Iterator[AscComponent]:
        return iter(self.components)


# ---------------------------------------------------------------------------
# Symbol catalogue
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class SymbolSpec:
    symbol: str
    build_component: Callable[[AscComponent], Component]
    extract_attributes: Callable[[Component], dict[str, str]]
    pin_offsets: tuple[AscPoint, ...]
    allowed_orientations: tuple[str, ...] = ("R0",)
    choose_orientation: Callable[[list[str]], str] | None = None
    from_spice: Callable[[list[str], AscComponent], Component] | None = None

    def create_component(self, comp: AscComponent) -> Component:
        line = comp.attributes.get("SpiceLine")
        if self.from_spice and line:
            tokens = line.split()
            return self.from_spice(tokens, comp)
        if comp.orientation not in self.allowed_orientations:
            comp.orientation = self.allowed_orientations[0]
        return self.build_component(comp)


def _value_attr(component: Component) -> str:
    return str(component.value)


def _attrs_two_terminal(component: Component) -> dict[str, str]:
    return {"InstName": component.ref, "Value": _value_attr(component)}


def _attrs_diode(component: Component) -> dict[str, str]:
    diode = cast(Diode, component)
    return {"InstName": diode.ref, "Model": str(diode.value)}


def _attrs_cccs(component: Component) -> dict[str, str]:
    cccs = cast(CCCS, component)
    return {
        "InstName": cccs.ref,
        "Value": str(cccs.value),
        "Ctrl": cccs.ctrl_vsrc,
    }


def _attrs_ccvs(component: Component) -> dict[str, str]:
    ccvs = cast(CCVS, component)
    return {
        "InstName": ccvs.ref,
        "Value": str(ccvs.value),
        "Ctrl": ccvs.ctrl_vsrc,
    }


def _build_diode(comp: AscComponent) -> Diode:
    ref = comp.ref or comp.attributes.get("InstName") or "D?"
    model = comp.attributes.get("Model") or comp.attributes.get("Value") or "DDEFAULT"
    return Diode(ref, model)


def _build_vcvs(comp: AscComponent) -> VCVS:
    ref = comp.ref or comp.attributes.get("InstName") or "E?"
    gain = comp.attributes.get("Value", "1")
    return VCVS(ref, gain)


def _build_vccs(comp: AscComponent) -> VCCS:
    ref = comp.ref or comp.attributes.get("InstName") or "G?"
    gm = comp.attributes.get("Value", "1")
    return VCCS(ref, gm)


def _build_cccs(comp: AscComponent) -> CCCS:
    ref = comp.ref or comp.attributes.get("InstName") or "F?"
    ctrl = comp.attributes.get("Ctrl", "V1")
    gain = comp.attributes.get("Value", "1")
    return CCCS(ref, ctrl_vsrc=ctrl, gain=gain)


def _build_ccvs(comp: AscComponent) -> CCVS:
    ref = comp.ref or comp.attributes.get("InstName") or "H?"
    ctrl = comp.attributes.get("Ctrl", "V1")
    value = comp.attributes.get("Value", "1")
    return CCVS(ref, ctrl_vsrc=ctrl, r=value)


def _build_res(comp: AscComponent) -> Resistor:
    ref = comp.ref or comp.attributes.get("InstName") or "R?"
    value = comp.attributes.get("Value", "1k")
    return Resistor(ref, value)


def _build_cap(comp: AscComponent) -> Capacitor:
    ref = comp.ref or comp.attributes.get("InstName") or "C?"
    value = comp.attributes.get("Value", "1u")
    return Capacitor(ref, value)


def _build_ind(comp: AscComponent) -> Inductor:
    ref = comp.ref or comp.attributes.get("InstName") or "L?"
    value = comp.attributes.get("Value", "1m")
    return Inductor(ref, value)


def _build_vdc(comp: AscComponent) -> Vdc:
    ref = comp.ref or comp.attributes.get("InstName") or "V?"
    value = comp.attributes.get("Value", "0")
    return Vdc(ref, value)


def _build_idc(comp: AscComponent) -> Idc:
    ref = comp.ref or comp.attributes.get("InstName") or "I?"
    value = comp.attributes.get("Value", "0")
    return Idc(ref, value)


def _from_spice_res(tokens: list[str], comp: AscComponent) -> Resistor:
    ref = tokens[0][1:] if tokens else comp.ref or "?"
    value = tokens[3] if len(tokens) > 3 else comp.attributes.get("Value", "1k")
    return Resistor(ref, value)


def _from_spice_cap(tokens: list[str], comp: AscComponent) -> Capacitor:
    ref = tokens[0][1:] if tokens else comp.ref or "?"
    value = tokens[3] if len(tokens) > 3 else comp.attributes.get("Value", "1u")
    return Capacitor(ref, value)


def _from_spice_ind(tokens: list[str], comp: AscComponent) -> Inductor:
    ref = tokens[0][1:] if tokens else comp.ref or "?"
    value = tokens[3] if len(tokens) > 3 else comp.attributes.get("Value", "1m")
    return Inductor(ref, value)


def _from_spice_v(tokens: list[str], comp: AscComponent) -> Vdc:
    ref = tokens[0][1:] if tokens else comp.ref or "?"
    value = tokens[3] if len(tokens) > 3 else comp.attributes.get("Value", "0")
    return Vdc(ref, value)


def _from_spice_i(tokens: list[str], comp: AscComponent) -> Idc:
    ref = tokens[0][1:] if tokens else comp.ref or "?"
    value = tokens[3] if len(tokens) > 3 else comp.attributes.get("Value", "0")
    return Idc(ref, value)


def _from_spice_diode(tokens: list[str], comp: AscComponent) -> Diode:
    ref = tokens[0][1:] if tokens else comp.ref or "?"
    model = tokens[3] if len(tokens) > 3 else comp.attributes.get("Model", "DDEFAULT")
    return Diode(ref, model)


def _from_spice_vcvs(tokens: list[str], comp: AscComponent) -> VCVS:
    ref = tokens[0][1:] if tokens else comp.ref or "?"
    gain = tokens[5] if len(tokens) > 5 else comp.attributes.get("Value", "1")
    return VCVS(ref, gain)


def _from_spice_vccs(tokens: list[str], comp: AscComponent) -> VCCS:
    ref = tokens[0][1:] if tokens else comp.ref or "?"
    gm = tokens[5] if len(tokens) > 5 else comp.attributes.get("Value", "1")
    return VCCS(ref, gm)


def _from_spice_cccs(tokens: list[str], comp: AscComponent) -> CCCS:
    ref = tokens[0][1:] if tokens else comp.ref or "?"
    ctrl = tokens[3] if len(tokens) > 3 else comp.attributes.get("Ctrl", "V1")
    gain = tokens[4] if len(tokens) > 4 else comp.attributes.get("Value", "1")
    return CCCS(ref, ctrl_vsrc=ctrl, gain=gain)


def _from_spice_ccvs(tokens: list[str], comp: AscComponent) -> CCVS:
    ref = tokens[0][1:] if tokens else comp.ref or "?"
    ctrl = tokens[3] if len(tokens) > 3 else comp.attributes.get("Ctrl", "V1")
    value = tokens[4] if len(tokens) > 4 else comp.attributes.get("Value", "1")
    return CCVS(ref, ctrl_vsrc=ctrl, r=value)


def _orient_vertical_if_ground(pin_nets: list[str]) -> str:
    if any(name.lower() in {"0", "gnd"} for name in pin_nets):
        return "R90"
    return "R0"


SYMBOL_LIBRARY: dict[str, SymbolSpec] = {
    "res": SymbolSpec(
        symbol="res",
        build_component=_build_res,
        extract_attributes=_attrs_two_terminal,
        pin_offsets=(AscPoint(0, 0), AscPoint(_PIN_SPACING, 0)),
        allowed_orientations=("R0", "R90", "R180", "R270"),
        from_spice=_from_spice_res,
    ),
    "cap": SymbolSpec(
        symbol="cap",
        build_component=_build_cap,
        extract_attributes=_attrs_two_terminal,
        pin_offsets=(AscPoint(0, 0), AscPoint(_PIN_SPACING, 0)),
        allowed_orientations=("R0", "R90", "R180", "R270"),
        choose_orientation=_orient_vertical_if_ground,
        from_spice=_from_spice_cap,
    ),
    "ind": SymbolSpec(
        symbol="ind",
        build_component=_build_ind,
        extract_attributes=_attrs_two_terminal,
        pin_offsets=(AscPoint(0, 0), AscPoint(_PIN_SPACING, 0)),
        allowed_orientations=("R0", "R90", "R180", "R270"),
        from_spice=_from_spice_ind,
    ),
    "voltage": SymbolSpec(
        symbol="voltage",
        build_component=_build_vdc,
        extract_attributes=_attrs_two_terminal,
        pin_offsets=(AscPoint(0, 0), AscPoint(_PIN_SPACING, 0)),
        allowed_orientations=("R0", "R90", "R180", "R270"),
        choose_orientation=_orient_vertical_if_ground,
        from_spice=_from_spice_v,
    ),
    "current": SymbolSpec(
        symbol="current",
        build_component=_build_idc,
        extract_attributes=_attrs_two_terminal,
        pin_offsets=(AscPoint(0, 0), AscPoint(_PIN_SPACING, 0)),
        allowed_orientations=("R0", "R90", "R180", "R270"),
        choose_orientation=_orient_vertical_if_ground,
        from_spice=_from_spice_i,
    ),
    "dio": SymbolSpec(
        symbol="dio",
        build_component=_build_diode,
        extract_attributes=_attrs_diode,
        pin_offsets=(AscPoint(0, 0), AscPoint(_PIN_SPACING, 0)),
        allowed_orientations=("R0", "R90", "R180", "R270"),
        from_spice=_from_spice_diode,
    ),
    "vcvs": SymbolSpec(
        symbol="vcvs",
        build_component=_build_vcvs,
        extract_attributes=_attrs_two_terminal,
        pin_offsets=(
            AscPoint(0, 0),
            AscPoint(_PIN_SPACING, 0),
            AscPoint(0, _PIN_SPACING),
            AscPoint(_PIN_SPACING, _PIN_SPACING),
        ),
        allowed_orientations=("R0",),
        from_spice=_from_spice_vcvs,
    ),
    "vccs": SymbolSpec(
        symbol="vccs",
        build_component=_build_vccs,
        extract_attributes=_attrs_two_terminal,
        pin_offsets=(
            AscPoint(0, 0),
            AscPoint(_PIN_SPACING, 0),
            AscPoint(0, _PIN_SPACING),
            AscPoint(_PIN_SPACING, _PIN_SPACING),
        ),
        allowed_orientations=("R0",),
        from_spice=_from_spice_vccs,
    ),
    "cccs": SymbolSpec(
        symbol="cccs",
        build_component=_build_cccs,
        extract_attributes=_attrs_cccs,
        pin_offsets=(AscPoint(0, 0), AscPoint(_PIN_SPACING, 0)),
        allowed_orientations=("R0",),
        from_spice=_from_spice_cccs,
    ),
    "ccvs": SymbolSpec(
        symbol="ccvs",
        build_component=_build_ccvs,
        extract_attributes=_attrs_ccvs,
        pin_offsets=(AscPoint(0, 0), AscPoint(_PIN_SPACING, 0)),
        allowed_orientations=("R0",),
        from_spice=_from_spice_ccvs,
    ),
    # Minimal op-amp symbol support: 3 pins (inp, inn, out)
    "opamp": SymbolSpec(
        symbol="opamp2",
        build_component=lambda comp: OpAmpIdeal(
            ref=comp.attr("InstName", "OA1") or "OA1",
            gain=comp.attr("Value", "1e6") or "1e6",
        ),
        extract_attributes=lambda comp: {
            "InstName": getattr(comp, "ref", "OA1"),
            "Value": getattr(comp, "value", "1e6"),
        },
        pin_offsets=(
            AscPoint(0, 0),  # inp
            AscPoint(0, _PIN_SPACING),  # inn
            AscPoint(_PIN_SPACING, _PIN_SPACING // 2),  # out
        ),
        allowed_orientations=("R0",),
    ),
    # Alias symbol names used by LTspice for the generic op-amp to our spec
    "opamp2": None,  # placeholder, will be replaced below
    "OpAmps\\UniversalOpAmp": None,  # Windows path-like symbol name in some ASCs
}

# Fill in alias entries to point to the same SymbolSpec instance
_op_spec = SYMBOL_LIBRARY["opamp"]
SYMBOL_LIBRARY["opamp2"] = _op_spec
SYMBOL_LIBRARY["OpAmps\\UniversalOpAmp"] = _op_spec


def _resolve_symbol_spec(sym: str) -> SymbolSpec | None:
    spec = SYMBOL_LIBRARY.get(sym)
    if spec:
        return spec
    # Reverse lookup by declared symbol field
    for s in SYMBOL_LIBRARY.values():
        if s and s.symbol == sym:
            return s
    # Normalize path prefixes like 'OpAmps\\UniversalOpAmp'
    norm = sym.replace("\\", "/")
    base = norm.split("/")[-1]
    if base in {"UniversalOpAmp", "opamp2"}:
        return SYMBOL_LIBRARY["opamp"]
    return None


COMPONENT_TO_SYMBOL: dict[type[Component], SymbolSpec] = {
    Resistor: SYMBOL_LIBRARY["res"],
    Capacitor: SYMBOL_LIBRARY["cap"],
    Inductor: SYMBOL_LIBRARY["ind"],
    Vdc: SYMBOL_LIBRARY["voltage"],
    Idc: SYMBOL_LIBRARY["current"],
    Diode: SYMBOL_LIBRARY["dio"],
    VCVS: SYMBOL_LIBRARY["vcvs"],
    VCCS: SYMBOL_LIBRARY["vccs"],
    CCCS: SYMBOL_LIBRARY["cccs"],
    CCVS: SYMBOL_LIBRARY["ccvs"],
    OpAmpIdeal: SYMBOL_LIBRARY["opamp"],
}


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _rotate_point(point: AscPoint, orientation: str) -> AscPoint:
    if orientation == "R0":
        return point
    if orientation == "R180":
        return AscPoint(-point.x, -point.y)
    if orientation == "R90":
        return AscPoint(-point.y, point.x)
    if orientation == "R270":
        return AscPoint(point.y, -point.x)
    raise ValueError(f"Unsupported orientation '{orientation}'")


def _absolute_pins(comp: AscComponent, spec: SymbolSpec) -> list[AscPoint]:
    pins: list[AscPoint] = []
    for offset in spec.pin_offsets:
        rel = _rotate_point(offset, comp.orientation)
        pins.append(AscPoint(comp.position.x + rel.x, comp.position.y + rel.y))
    return pins


def _auto_net_name(counter: int) -> str:
    return f"N{counter:03d}"


# ---------------------------------------------------------------------------
# Net positioning helpers
# ---------------------------------------------------------------------------


def _assign_net_positions(net_names: list[str]) -> dict[str, int]:
    if not net_names:
        return {}
    ordered = sorted({name for name in net_names if name not in {"0", "gnd", "GND"}})
    if any(name in net_names for name in ("0", "gnd", "GND")):
        ordered = ["0"] + ordered
    positions: dict[str, int] = {}
    for idx, name in enumerate(ordered):
        positions[name] = (idx + 1) * _PIN_SPACING
    return positions


class _UnionFind:
    def __init__(self) -> None:
        self._parent: dict[tuple[int, int], tuple[int, int]] = {}

    def add(self, point: AscPoint) -> None:
        self._parent.setdefault((point.x, point.y), (point.x, point.y))

    def find(self, point: AscPoint) -> tuple[int, int]:
        coord = (point.x, point.y)
        if coord not in self._parent:
            self._parent[coord] = coord
            return coord
        parent = self._parent[coord]
        if parent != coord:
            parent = self.find(AscPoint(*parent))
            self._parent[coord] = parent
        return parent

    def union(self, a: AscPoint, b: AscPoint) -> None:
        ra = self.find(a)
        rb = self.find(b)
        if ra == rb:
            return
        self._parent[rb] = ra

    def groups(self) -> dict[tuple[int, int], list[tuple[int, int]]]:
        result: dict[tuple[int, int], list[tuple[int, int]]] = {}
        for coord in list(self._parent.keys()):
            parent = self.find(AscPoint(*coord))
            result.setdefault(parent, []).append(coord)
        return result


def _net_name_lookup(mapping: Mapping[Port, str], port: Port) -> str:
    return mapping.get(port, "0")


# ---------------------------------------------------------------------------
# Parsing
# ---------------------------------------------------------------------------


def parse_asc(text: str) -> AscSchematic:
    lines = [ln.rstrip("\n") for ln in text.splitlines() if ln.strip()]

    version = "4"
    sheet = (1, 880, 680)
    components: list[AscComponent] = []
    wires: list[AscWire] = []
    labels: list[AscLabel] = []
    texts: list[AscText] = []

    current: AscComponent | None = None

    for line in lines:
        tokens = line.split()
        kind = tokens[0]

        if kind == "Version" and len(tokens) >= 2:
            version = tokens[1]
            current = None
            continue

        if kind == "SHEET" and len(tokens) >= 4:
            sheet = (int(tokens[1]), int(tokens[2]), int(tokens[3]))
            current = None
            continue

        if kind == "WIRE" and len(tokens) >= 5:
            wires.append(
                AscWire(
                    AscPoint(int(tokens[1]), int(tokens[2])),
                    AscPoint(int(tokens[3]), int(tokens[4])),
                )
            )
            current = None
            continue

        if kind == "FLAG" and len(tokens) >= 4:
            labels.append(AscLabel(tokens[3], AscPoint(int(tokens[1]), int(tokens[2]))))
            current = None
            continue

        if kind == "TEXT" and len(tokens) >= 5:
            text_content = " ".join(tokens[5:]) if len(tokens) > 5 else ""
            texts.append(
                AscText(
                    position=AscPoint(int(tokens[1]), int(tokens[2])),
                    orientation=tokens[3],
                    font=int(tokens[4]),
                    text=text_content,
                )
            )
            current = None
            continue

        if kind == "SYMBOL" and len(tokens) >= 5:
            comp = AscComponent(
                symbol=tokens[1],
                position=AscPoint(int(tokens[2]), int(tokens[3])),
                orientation=tokens[4],
            )
            components.append(comp)
            current = comp
            continue

        if kind == "SYMATTR" and len(tokens) >= 3 and current is not None:
            key = tokens[1]
            value = " ".join(tokens[2:])
            current.attributes[key] = value
            if key.lower() == "instname":
                current.ref = value
            continue

        current = None

    # Assign pins using symbol catalogue (after parsing all components)
    for comp in components:
        spec = _resolve_symbol_spec(comp.symbol)
        if not spec:
            raise ValueError(f"Unsupported symbol '{comp.symbol}' in ASC file")
        comp.pins = _absolute_pins(comp, spec)

    return AscSchematic(
        version=version,
        sheet=sheet,
        components=components,
        wires=wires,
        labels=labels,
        texts=texts,
    )


def load_asc(path: str | Path) -> AscSchematic:
    p = Path(path)
    return parse_asc(p.read_text(encoding="utf-8", errors="ignore"))


# ---------------------------------------------------------------------------
# Circuit import
# ---------------------------------------------------------------------------


def schematic_to_circuit(schematic: AscSchematic) -> Circuit:
    if all("SpiceLine" in comp.attributes for comp in schematic.components):
        return _schematic_to_circuit_spice(schematic)
    return _schematic_to_circuit_geometry(schematic)


def _schematic_to_circuit_spice(schematic: AscSchematic) -> Circuit:
    circuit = Circuit("ltspice_import")
    nets: dict[str, Net] = {"0": GND}

    for comp in schematic.components:
        spec = _resolve_symbol_spec(comp.symbol)
        if not spec:
            raise ValueError(f"Unsupported symbol '{comp.symbol}' in ASC file")
        spice_line = comp.attributes.get("SpiceLine")
        if not spice_line:
            component_name = comp.ref or spec.symbol or "?"
            message = f"Component '{component_name}' missing SpiceLine attribute; unable to import"
            raise ValueError(message)

        tokens = spice_line.split()
        pin_count = len(spec.pin_offsets)
        if len(tokens) < pin_count + 1:
            component_name = comp.ref or spec.symbol or "?"
            message = f"Invalid SpiceLine for component '{component_name}': {spice_line}"
            raise ValueError(message)
        node_names = tokens[1 : 1 + pin_count]

        cat_component = spec.create_component(comp)
        circuit.add(cat_component)

        for port, node in zip(cat_component.ports, node_names, strict=False):
            name = node.strip()
            if name in {"0", "gnd", "GND"}:
                circuit.connect(port, GND)
            else:
                net = nets.setdefault(name, Net(name))
                circuit.connect(port, net)

    return circuit


def _schematic_to_circuit_geometry(schematic: AscSchematic) -> Circuit:
    uf = _UnionFind()

    for wire in schematic.wires:
        uf.add(wire.p1)
        uf.add(wire.p2)
        uf.union(wire.p1, wire.p2)

    for comp in schematic.components:
        for pin in comp.pins:
            uf.add(pin)

    for label in schematic.labels:
        uf.add(label.position)

    label_map: dict[tuple[int, int], str] = {}
    for label in schematic.labels:
        rep = uf.find(label.position)
        label_map[rep] = label.name

    nets: dict[tuple[int, int], Net] = {}
    auto_counter = 1

    def _net_for(pin: AscPoint) -> Net:
        nonlocal auto_counter
        rep = uf.find(pin)
        if rep in nets:
            return nets[rep]
        name = label_map.get(rep)
        if name is None:
            name = _auto_net_name(auto_counter)
            auto_counter += 1
        if name in {"0", "gnd", "GND"}:
            nets[rep] = GND
            return GND
        net = Net(name)
        nets[rep] = net
        return net

    circuit = Circuit("ltspice_import")

    for comp in schematic.components:
        spec = _resolve_symbol_spec(comp.symbol)
        if not spec:
            raise ValueError(f"Unsupported symbol '{comp.symbol}' in ASC file")
        cat_component = spec.create_component(comp)
        circuit.add(cat_component)

        if not comp.pins:
            comp.pins = _absolute_pins(comp, spec)

        for port, pin in zip(cat_component.ports, comp.pins, strict=False):
            circuit.connect(port, _net_for(pin))

    return circuit


def circuit_from_asc(path: str | Path) -> Circuit:
    return schematic_to_circuit(load_asc(path))


# ---------------------------------------------------------------------------
# Circuit export
# ---------------------------------------------------------------------------


def _net_name_for(circuit: Circuit, net: Net | None) -> str:
    if net is None or net is GND or getattr(net, "name", None) == "0":
        return "0"
    if getattr(net, "name", None):
        return str(net.name)
    node_id = circuit._net_ids.get(net)
    if node_id is None:
        circuit._assign_node_ids()
        node_id = circuit._net_ids.get(net)
        if node_id is None:
            node_id = 0
    return _auto_net_name(node_id)


def circuit_to_schematic(circuit: Circuit, *, include_wires: bool = True) -> AscSchematic:
    circuit.build_netlist()  # ensure node ids assigned

    components: list[AscComponent] = []
    wires: list[AscWire] = []
    labels: list[AscLabel] = []
    texts: list[AscText] = []

    net_names = [_net_name_for(circuit, net) for net in circuit._port_to_net.values()]
    net_positions = _assign_net_positions(net_names)
    if "0" not in net_positions:
        net_positions["0"] = _PIN_SPACING

    net_anchor_y: dict[str, int] = {name: 80 for name in net_positions}

    for name, x in net_positions.items():
        labels.append(AscLabel(name=name, position=AscPoint(x, 20)))
        if include_wires:
            wires.append(AscWire(AscPoint(x, 20), AscPoint(x, net_anchor_y[name])))

    for row, comp in enumerate(circuit._components):
        spec = COMPONENT_TO_SYMBOL.get(type(comp))
        if not spec:
            raise ValueError(f"Component type '{type(comp).__name__}' not supported for ASC export")

        attrs = spec.extract_attributes(comp)
        port_to_name = {
            port: _net_name_for(circuit, circuit._port_to_net.get(port)) for port in comp.ports
        }
        pin_nets = [port_to_name[port] for port in comp.ports]

        net_of = partial(_net_name_lookup, port_to_name)
        spice_line = comp.spice_card(net_of)
        attrs["SpiceLine"] = spice_line

        orientation = spec.allowed_orientations[0]
        if spec.choose_orientation:
            candidate = spec.choose_orientation(pin_nets)
            if candidate in spec.allowed_orientations:
                orientation = candidate

        base_x = min(net_positions.get(name, _PIN_SPACING) for name in pin_nets)
        position = AscPoint(base_x, 120 + row * _GRID_Y)
        asc_comp = AscComponent(
            symbol=spec.symbol,
            position=position,
            orientation=orientation,
            attributes=dict(attrs),
            ref=attrs.get("InstName"),
        )
        asc_comp.pins = _absolute_pins(asc_comp, spec) if include_wires else []
        components.append(asc_comp)

        if include_wires:
            for pin, net_name in zip(asc_comp.pins, pin_nets, strict=False):
                net_x = net_positions.get(net_name, base_x)
                anchor_y = net_anchor_y.get(net_name, 60)
                if anchor_y != pin.y:
                    wires.append(AscWire(AscPoint(net_x, anchor_y), AscPoint(net_x, pin.y)))
                if net_x != pin.x:
                    wires.append(AscWire(AscPoint(net_x, pin.y), AscPoint(pin.x, pin.y)))
                net_anchor_y[net_name] = pin.y + _GRID_Y // 2

    return AscSchematic(
        version="4",
        sheet=(1, 880, 680),
        components=components,
        wires=wires,
        labels=labels,
        texts=texts,
    )


def circuit_to_asc_text(circuit: Circuit, *, include_wires: bool = True) -> str:
    return dump_asc(circuit_to_schematic(circuit, include_wires=include_wires))


def save_circuit_as_asc(circuit: Circuit, path: str | Path, *, include_wires: bool = True) -> Path:
    text = circuit_to_asc_text(circuit, include_wires=include_wires)
    p = Path(path)
    p.write_text(text, encoding="utf-8")
    return p


# ---------------------------------------------------------------------------
# Dumping
# ---------------------------------------------------------------------------


def dump_asc(schematic: AscSchematic) -> str:
    lines: list[str] = []
    lines.append(f"Version {schematic.version}")
    lines.append(f"SHEET {schematic.sheet[0]} {schematic.sheet[1]} {schematic.sheet[2]}")

    for wire in schematic.wires:
        lines.append(f"WIRE {wire.p1.x} {wire.p1.y} {wire.p2.x} {wire.p2.y}")

    for label in schematic.labels:
        lines.append(f"FLAG {label.position.x} {label.position.y} {label.name}")

    for comp in schematic.components:
        lines.append(f"SYMBOL {comp.symbol} {comp.position.x} {comp.position.y} {comp.orientation}")
        for key, value in comp.attributes.items():
            lines.append(f"SYMATTR {key} {value}")

    for text in schematic.texts:
        text_line = (
            f"TEXT {text.position.x} {text.position.y} {text.orientation} {text.font} {text.text}"
        ).rstrip()
        lines.append(text_line)

    lines.append("")
    return "\n".join(lines)


__all__ = [
    "AscPoint",
    "AscWire",
    "AscLabel",
    "AscText",
    "AscComponent",
    "AscSchematic",
    "load_asc",
    "parse_asc",
    "dump_asc",
    "schematic_to_circuit",
    "circuit_from_asc",
    "circuit_to_schematic",
    "circuit_to_asc_text",
    "save_circuit_as_asc",
]
