"""Programmatic SPICE netlist generation for LTspice."""

from dataclasses import dataclass, field
from pathlib import Path


@dataclass
class NetlistComponent:
    """A component in the netlist."""

    name: str  # R1, C1, V1, M1, X1, etc.
    nodes: list[str]  # Connected node names
    value: str  # Value or model name
    params: str = ""  # Additional parameters


@dataclass
class Netlist:
    """A SPICE netlist that can be saved as a .cir file.

    Supports a builder pattern -- all add_* methods return self for chaining:

        netlist = (Netlist("My Circuit")
            .add_resistor("R1", "in", "out", "10k")
            .add_capacitor("C1", "out", "0", "100n")
            .add_voltage_source("V1", "in", "0", ac="1")
            .add_directive(".ac dec 100 1 1meg"))
    """

    title: str = "LTspice Simulation"
    components: list[NetlistComponent] = field(default_factory=list)
    directives: list[str] = field(default_factory=list)
    comments: list[str] = field(default_factory=list)
    includes: list[str] = field(default_factory=list)

    # -- Passive components ---------------------------------------------------

    def add_resistor(self, name: str, node_p: str, node_n: str, value: str) -> "Netlist":
        """Add a resistor. Example: add_resistor('R1', 'in', 'out', '10k')"""
        self.components.append(NetlistComponent(name=name, nodes=[node_p, node_n], value=value))
        return self

    def add_capacitor(self, name: str, node_p: str, node_n: str, value: str) -> "Netlist":
        """Add a capacitor."""
        self.components.append(NetlistComponent(name=name, nodes=[node_p, node_n], value=value))
        return self

    def add_inductor(
        self,
        name: str,
        node_p: str,
        node_n: str,
        value: str,
        series_resistance: str | None = None,
    ) -> "Netlist":
        """Add an inductor with optional series resistance (Rser)."""
        params = f"Rser={series_resistance}" if series_resistance else ""
        self.components.append(
            NetlistComponent(name=name, nodes=[node_p, node_n], value=value, params=params)
        )
        return self

    # -- Sources --------------------------------------------------------------

    def add_voltage_source(
        self,
        name: str,
        node_p: str,
        node_n: str,
        dc: str | None = None,
        ac: str | None = None,
        pulse: tuple | None = None,
        sin: tuple | None = None,
    ) -> "Netlist":
        """Add a voltage source.

        Args:
            name: Source name (V1, V2, etc.)
            node_p: Positive node
            node_n: Negative node
            dc: DC value (e.g., "5")
            ac: AC magnitude (e.g., "1")
            pulse: (Vinitial, Von, Tdelay, Trise, Tfall, Ton, Tperiod)
            sin: (Voffset, Vamp, Freq, Td, Theta, Phi)
        """
        value = self._build_source_value(dc=dc, ac=ac, pulse=pulse, sin=sin)
        self.components.append(NetlistComponent(name=name, nodes=[node_p, node_n], value=value))
        return self

    def add_current_source(
        self,
        name: str,
        node_p: str,
        node_n: str,
        dc: str | None = None,
        ac: str | None = None,
    ) -> "Netlist":
        """Add a current source."""
        value = self._build_source_value(dc=dc, ac=ac)
        self.components.append(NetlistComponent(name=name, nodes=[node_p, node_n], value=value))
        return self

    # -- Semiconductors -------------------------------------------------------

    def add_diode(self, name: str, anode: str, cathode: str, model: str) -> "Netlist":
        """Add a diode. Example: add_diode('D1', 'a', 'k', '1N4148')"""
        self.components.append(NetlistComponent(name=name, nodes=[anode, cathode], value=model))
        return self

    def add_mosfet(
        self,
        name: str,
        drain: str,
        gate: str,
        source: str,
        body: str,
        model: str,
        w: str | None = None,
        length: str | None = None,
    ) -> "Netlist":
        """Add a MOSFET."""
        params_parts: list[str] = []
        if w:
            params_parts.append(f"W={w}")
        if length:
            params_parts.append(f"L={length}")
        params = " ".join(params_parts)
        self.components.append(
            NetlistComponent(
                name=name,
                nodes=[drain, gate, source, body],
                value=model,
                params=params,
            )
        )
        return self

    def add_bjt(self, name: str, collector: str, base: str, emitter: str, model: str) -> "Netlist":
        """Add a BJT transistor."""
        self.components.append(
            NetlistComponent(name=name, nodes=[collector, base, emitter], value=model)
        )
        return self

    # -- Subcircuits ----------------------------------------------------------

    def add_opamp(
        self,
        name: str,
        inp: str,
        inn: str,
        out: str,
        vpos: str,
        vneg: str,
        model: str,
    ) -> "Netlist":
        """Add an op-amp subcircuit instance.

        Pin order follows the LTspice convention:
            X<name> <inp> <inn> <vpos> <vneg> <out> <model>
        """
        self.components.append(
            NetlistComponent(name=name, nodes=[inp, inn, vpos, vneg, out], value=model)
        )
        return self

    def add_subcircuit(self, name: str, nodes: list[str], model: str) -> "Netlist":
        """Add a generic subcircuit instance."""
        self.components.append(NetlistComponent(name=name, nodes=list(nodes), value=model))
        return self

    # -- Generic component ----------------------------------------------------

    def add_component(self, name: str, nodes: list[str], value: str, params: str = "") -> "Netlist":
        """Add any component with explicit nodes."""
        self.components.append(
            NetlistComponent(name=name, nodes=list(nodes), value=value, params=params)
        )
        return self

    # -- Directives -----------------------------------------------------------

    def add_directive(self, directive: str) -> "Netlist":
        """Add a SPICE directive (e.g., '.tran 10m', '.ac dec 100 1 1meg')."""
        self.directives.append(directive)
        return self

    def add_meas(self, analysis: str, name: str, expression: str) -> "Netlist":
        """Add a .meas directive.

        Example:
            add_meas('tran', 'rise_time',
                     'TRIG V(out) VAL=0.1 RISE=1 TARG V(out) VAL=0.9 RISE=1')
        """
        self.directives.append(f".meas {analysis} {name} {expression}")
        return self

    def add_param(self, name: str, value: str) -> "Netlist":
        """Add a .param directive."""
        self.directives.append(f".param {name}={value}")
        return self

    def add_include(self, path: str) -> "Netlist":
        """Add a .include directive for library files."""
        self.includes.append(f".include {path}")
        return self

    def add_lib(self, path: str) -> "Netlist":
        """Add a .lib directive."""
        self.includes.append(f".lib {path}")
        return self

    def add_comment(self, text: str) -> "Netlist":
        """Add a comment line."""
        self.comments.append(text)
        return self

    # -- Rendering / I/O ------------------------------------------------------

    def render(self) -> str:
        """Render the netlist to a SPICE string."""
        lines = [f"* {self.title}"]

        for comment in self.comments:
            lines.append(f"* {comment}")

        for inc in self.includes:
            lines.append(inc)  # Already formatted as .include or .lib

        lines.append("")  # blank separator

        for comp in self.components:
            line = f"{comp.name} {' '.join(comp.nodes)} {comp.value}"
            if comp.params:
                line += f" {comp.params}"
            lines.append(line)

        lines.append("")

        for directive in self.directives:
            lines.append(directive)

        lines.append(".backanno")
        lines.append(".end")

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

    def save(self, path: Path | str) -> Path:
        """Save netlist to a .cir file."""
        path = Path(path)
        path.write_text(self.render())
        return path

    # -- Internal helpers -----------------------------------------------------

    @staticmethod
    def _build_source_value(
        dc: str | None = None,
        ac: str | None = None,
        pulse: tuple | None = None,
        sin: tuple | None = None,
    ) -> str:
        """Build the value string for a voltage/current source."""
        parts: list[str] = []
        if dc is not None:
            parts.append(dc)
        if ac is not None:
            parts.append(f"AC {ac}")
        if pulse is not None:
            params_str = " ".join(str(p) for p in pulse)
            parts.append(f"PULSE({params_str})")
        if sin is not None:
            params_str = " ".join(str(p) for p in sin)
            parts.append(f"SIN({params_str})")
        return " ".join(parts) if parts else "0"


# ---------------------------------------------------------------------------
# Convenience functions for common circuit topologies
# ---------------------------------------------------------------------------


def voltage_divider(
    v_in: str = "5",
    r1: str = "10k",
    r2: str = "10k",
    sim_type: str = "op",
) -> Netlist:
    """Create a voltage divider circuit.

    Args:
        v_in: Input voltage (DC).
        r1: Top resistor value.
        r2: Bottom resistor value.
        sim_type: Simulation directive -- "op" for operating point,
                  or a full directive string like ".tran 10m".
    """
    netlist = (
        Netlist("Voltage Divider")
        .add_voltage_source("V1", "in", "0", dc=v_in)
        .add_resistor("R1", "in", "out", r1)
        .add_resistor("R2", "out", "0", r2)
    )
    directive = sim_type if sim_type.startswith(".") else f".{sim_type}"
    netlist.add_directive(directive)
    return netlist


def rc_lowpass(
    r: str = "1k",
    c: str = "100n",
    f_start: str = "1",
    f_stop: str = "1meg",
) -> Netlist:
    """Create an RC lowpass filter with AC analysis.

    The circuit is driven by a 1V AC source and outputs at node 'out'.
    """
    return (
        Netlist("RC Lowpass Filter")
        .add_voltage_source("V1", "in", "0", ac="1")
        .add_resistor("R1", "in", "out", r)
        .add_capacitor("C1", "out", "0", c)
        .add_directive(f".ac dec 100 {f_start} {f_stop}")
    )


def inverting_amplifier(
    r_in: str = "10k",
    r_f: str = "100k",
    opamp_model: str = "LT1001",
) -> Netlist:
    """Create an inverting op-amp amplifier.

    Topology:
        V1 --[R_in]--> inv(-) --[R_f]--> out
                        non-inv(+) --> GND
        Supply: +/-15V
    """
    return (
        Netlist("Inverting Amplifier")
        .add_comment(f"Gain = -{r_f}/{r_in}")
        .add_lib(opamp_model)
        .add_voltage_source("V1", "in", "0", ac="1")
        .add_voltage_source("Vpos", "vdd", "0", dc="15")
        .add_voltage_source("Vneg", "0", "vss", dc="15")
        .add_resistor("Rin", "in", "inv", r_in)
        .add_resistor("Rf", "inv", "out", r_f)
        .add_opamp("X1", "0", "inv", "out", "vdd", "vss", opamp_model)
        .add_directive(".ac dec 100 1 1meg")
    )


def non_inverting_amplifier(
    r_in: str = "10k",
    r_f: str = "100k",
    opamp_model: str = "LT1001",
) -> Netlist:
    """Create a non-inverting op-amp amplifier.

    Topology:
        V1 --> non-inv(+)
               inv(-) --[R_in]--> GND
               inv(-) --[R_f]--> out
        Supply: +/-15V
        Gain = 1 + R_f / R_in
    """
    return (
        Netlist("Non-Inverting Amplifier")
        .add_comment(f"Gain = 1 + {r_f}/{r_in}")
        .add_lib(opamp_model)
        .add_voltage_source("V1", "in", "0", ac="1")
        .add_voltage_source("Vpos", "vdd", "0", dc="15")
        .add_voltage_source("Vneg", "0", "vss", dc="15")
        .add_resistor("Rin", "inv", "0", r_in)
        .add_resistor("Rf", "inv", "out", r_f)
        .add_opamp("X1", "in", "inv", "out", "vdd", "vss", opamp_model)
        .add_directive(".ac dec 100 1 1meg")
    )


def differential_amplifier(
    r1: str = "10k",
    r2: str = "10k",
    r3: str = "10k",
    r4: str = "10k",
    opamp_model: str = "LT1001",
) -> Netlist:
    """Create a classic differential amplifier.

    Topology:
        V1 --[R1]--> inv(-)  --[R2]--> out
        V2 --[R3]--> non-inv(+)
                     non-inv(+) --[R4]--> GND
        Supply: +/-15V
        Vout = (R2/R1) * (V2 - V1)  when R2/R1 = R4/R3
    """
    return (
        Netlist("Differential Amplifier")
        .add_comment(f"Vout = ({r2}/{r1}) * (V2 - V1) when R2/R1 = R4/R3")
        .add_lib(opamp_model)
        .add_voltage_source("V1", "in1", "0", ac="1")
        .add_voltage_source("V2", "in2", "0", ac="1")
        .add_voltage_source("Vpos", "vdd", "0", dc="15")
        .add_voltage_source("Vneg", "0", "vss", dc="15")
        .add_resistor("R1", "in1", "inv", r1)
        .add_resistor("R2", "inv", "out", r2)
        .add_resistor("R3", "in2", "noninv", r3)
        .add_resistor("R4", "noninv", "0", r4)
        .add_opamp("X1", "noninv", "inv", "out", "vdd", "vss", opamp_model)
        .add_directive(".ac dec 100 1 1meg")
    )


def common_emitter_amplifier(
    rc: str = "2.2k",
    rb1: str = "56k",
    rb2: str = "12k",
    re: str = "1k",
    cc1: str = "10u",
    cc2: str = "10u",
    ce: str = "47u",
    vcc: str = "12",
    bjt_model: str = "2N2222",
) -> Netlist:
    """Create a common-emitter amplifier with voltage divider bias.

    Topology:
        VCC --[RC]--> collector --> [CC2] --> out
        VCC --[RB1]--> base
        base --[RB2]--> GND
        emitter --[RE]--> GND
        emitter --[CE]--> GND  (bypass cap)
        in --[CC1]--> base     (input coupling cap)
    """
    return (
        Netlist("Common Emitter Amplifier")
        .add_comment("Voltage divider bias with emitter bypass cap")
        .add_lib(bjt_model)
        .add_voltage_source("Vcc", "vcc", "0", dc=vcc)
        .add_voltage_source("Vin", "in", "0", ac="1", sin=("0", "10m", "1k"))
        .add_resistor("RC", "vcc", "collector", rc)
        .add_resistor("RB1", "vcc", "base", rb1)
        .add_resistor("RB2", "base", "0", rb2)
        .add_resistor("RE", "emitter", "0", re)
        .add_capacitor("CC1", "in", "base", cc1)
        .add_capacitor("CC2", "collector", "out", cc2)
        .add_capacitor("CE", "emitter", "0", ce)
        .add_bjt("Q1", "collector", "base", "emitter", bjt_model)
        .add_directive(".tran 5m")
        .add_directive(".ac dec 100 10 10meg")
    )


def buck_converter(
    ind: str = "10u",
    c_out: str = "100u",
    r_load: str = "10",
    v_in: str = "12",
    duty_cycle: float = 0.5,
    freq: str = "100k",
    mosfet_model: str = "IRF540N",
    diode_model: str = "1N5819",
) -> Netlist:
    """Create a buck (step-down) converter.

    Topology:
        V_in --> MOSFET (high-side switch) --> sw node
        sw --> [L] --> out
        sw --> Diode (cathode) --> GND  (freewheeling)
        out --> [C_out] --> GND
        out --> [R_load] --> GND
        Gate driven by PULSE source at specified frequency and duty cycle.
    """
    # Compute timing from duty cycle and frequency
    # freq string may use SPICE suffixes; keep it as-is for the netlist.
    # For the PULSE source we need numeric period and on-time.
    freq_hz = _parse_spice_value(freq)
    period = 1.0 / freq_hz
    t_on = period * duty_cycle
    t_rise = period * 0.01  # 1% of period
    t_fall = t_rise

    return (
        Netlist("Buck Converter")
        .add_comment(f"Duty cycle = {duty_cycle:.0%}, Fsw = {freq}")
        .add_lib(mosfet_model)
        .add_lib(diode_model)
        .add_voltage_source("Vin", "vin", "0", dc=v_in)
        .add_voltage_source(
            "Vgate",
            "gate",
            "0",
            pulse=(
                "0",
                v_in,
                "0",
                f"{t_rise:.4g}",
                f"{t_fall:.4g}",
                f"{t_on:.4g}",
                f"{period:.4g}",
            ),
        )
        .add_mosfet("M1", "vin", "gate", "sw", "sw", mosfet_model)
        .add_diode("D1", "0", "sw", diode_model)
        .add_inductor("L1", "sw", "out", ind)
        .add_capacitor("Cout", "out", "0", c_out)
        .add_resistor("Rload", "out", "0", r_load)
        .add_directive(f".tran {period * 200:.4g}")
    )


def ldo_regulator(
    opamp_model: str = "LT1001",
    r1: str = "10k",
    r2: str = "10k",
    pass_transistor: str = "IRF9540N",
    v_in: str = "8",
    v_ref: str = "2.5",
) -> Netlist:
    """Create a simple LDO voltage regulator.

    Topology:
        V_in --> PMOS pass transistor (source) --> out (drain)
        Error amp: non-inv(+) = V_ref, inv(-) = feedback
        Feedback divider: out --[R1]--> fb --[R2]--> GND
        Error amp output drives PMOS gate
        Vout = V_ref * (1 + R1/R2)
    """
    return (
        Netlist("LDO Regulator")
        .add_comment(f"Vout = {v_ref} * (1 + {r1}/{r2})")
        .add_lib(opamp_model)
        .add_lib(pass_transistor)
        .add_voltage_source("Vin", "vin", "0", dc=v_in)
        .add_voltage_source("Vref", "vref", "0", dc=v_ref)
        .add_voltage_source("Vpos", "vdd", "0", dc=v_in)
        .add_voltage_source("Vneg", "0", "vss", dc=v_in)
        .add_mosfet("M1", "out", "gate", "vin", "vin", pass_transistor)
        .add_opamp("X1", "vref", "fb", "gate", "vdd", "vss", opamp_model)
        .add_resistor("R1", "out", "fb", r1)
        .add_resistor("R2", "fb", "0", r2)
        .add_resistor("Rload", "out", "0", "100")
        .add_capacitor("Cout", "out", "0", "10u")
        .add_directive(".tran 10m")
    )


def colpitts_oscillator(
    ind: str = "1u",
    c1: str = "100p",
    c2: str = "100p",
    rb: str = "47k",
    rc: str = "1k",
    re: str = "470",
    vcc: str = "12",
    bjt_model: str = "2N2222",
) -> Netlist:
    """Create a Colpitts oscillator.

    Topology:
        VCC --[RC]--> collector
        collector --[L]--> tank
        tank --[C1]--> base
        tank --[C2]--> GND
        base --[RB]--> VCC  (bias)
        emitter --[RE]--> GND
        Output taken at the collector.

    The oscillation frequency is approximately:
        f = 1 / (2*pi*sqrt(L * C1*C2/(C1+C2)))
    """
    return (
        Netlist("Colpitts Oscillator")
        .add_comment("f ~ 1 / (2*pi*sqrt(L * Cseries))")
        .add_lib(bjt_model)
        .add_voltage_source("Vcc", "vcc", "0", dc=vcc)
        .add_resistor("RC", "vcc", "collector", rc)
        .add_resistor("RB", "vcc", "base", rb)
        .add_resistor("RE", "emitter", "0", re)
        .add_inductor("L1", "collector", "tank", ind)
        .add_capacitor("C1", "tank", "base", c1)
        .add_capacitor("C2", "tank", "0", c2)
        .add_bjt("Q1", "collector", "base", "emitter", bjt_model)
        .add_directive(".tran 100u")
        .add_directive(".ic V(collector)=6")
    )


def h_bridge(
    v_supply: str = "12",
    r_load: str = "10",
    mosfet_model: str = "IRF540N",
) -> Netlist:
    """Create an H-bridge motor driver.

    Topology:
        V+ --[M1 (high-side A)]--> outA --[M3 (low-side A)]--> GND
        V+ --[M2 (high-side B)]--> outB --[M4 (low-side B)]--> GND
        R_load between outA and outB.
        M1 & M4 driven together (forward), M2 & M3 driven together (reverse).
        Gate signals are complementary PULSE sources with dead time.
    """
    # Drive at 1 kHz with 50% duty, small dead time to prevent shoot-through
    period = "1m"
    t_on = "450u"
    t_dead = "25u"

    return (
        Netlist("H-Bridge Motor Driver")
        .add_comment("Complementary gate drives with dead time")
        .add_lib(mosfet_model)
        .add_voltage_source("Vsupply", "vcc", "0", dc=v_supply)
        # Forward drive: M1 (high-A) and M4 (low-B) on first half
        .add_voltage_source(
            "Vg_fwd",
            "gate_fwd",
            "0",
            pulse=("0", v_supply, t_dead, "10n", "10n", t_on, period),
        )
        # Reverse drive: M2 (high-B) and M3 (low-A) on second half
        .add_voltage_source(
            "Vg_rev",
            "gate_rev",
            "0",
            pulse=("0", v_supply, "525u", "10n", "10n", t_on, period),
        )
        # High-side A
        .add_mosfet("M1", "vcc", "gate_fwd", "outA", "outA", mosfet_model)
        # High-side B
        .add_mosfet("M2", "vcc", "gate_rev", "outB", "outB", mosfet_model)
        # Low-side A
        .add_mosfet("M3", "outA", "gate_rev", "0", "0", mosfet_model)
        # Low-side B
        .add_mosfet("M4", "outB", "gate_fwd", "0", "0", mosfet_model)
        .add_resistor("Rload", "outA", "outB", r_load)
        .add_directive(".tran 5m")
    )


def sallen_key_lowpass(
    r1: str = "10k",
    r2: str = "10k",
    c1: str = "10n",
    c2: str = "10n",
    opamp_model: str = "LT1001",
) -> Netlist:
    """Create a Sallen-Key lowpass filter (unity gain).

    Topology:
        in --[R1]--> n1 --[R2]--> n2
        n2 --> opamp In+ (non-inverting)
        opamp out --> In- (unity gain feedback)
        C1 from n1 to out (feedback element)
        C2 from n2 to GND

        f_c = 1 / (2*pi*sqrt(R1*R2*C1*C2))
        Supply: +/-15V
    """
    return (
        Netlist("Sallen-Key Lowpass Filter")
        .add_comment(f"f_c = 1/(2*pi*sqrt({r1}*{r2}*{c1}*{c2}))")
        .add_lib(opamp_model)
        .add_voltage_source("V1", "in", "0", ac="1")
        .add_voltage_source("Vpos", "vdd", "0", dc="15")
        .add_voltage_source("Vneg", "0", "vss", dc="15")
        .add_resistor("R1", "in", "n1", r1)
        .add_resistor("R2", "n1", "n2", r2)
        .add_capacitor("C1", "n1", "out", c1)
        .add_capacitor("C2", "n2", "0", c2)
        .add_opamp("X1", "n2", "out", "out", "vdd", "vss", opamp_model)
        .add_directive(".ac dec 100 1 1meg")
    )


def boost_converter(
    ind: str = "10u",
    c_out: str = "100u",
    r_load: str = "50",
    v_in: str = "5",
    duty_cycle: float = 0.5,
    freq: str = "100k",
    mosfet_model: str = "IRF540N",
    diode_model: str = "1N5819",
) -> Netlist:
    """Create a boost (step-up) converter.

    Topology:
        Vin --[L1]--> sw_node --[D1 (anode->cathode)]--> out
                        |                                  |
                      [MOSFET]                     [Cout]  [Rload]
                      drain=sw                       |       |
                      gate=gate                     GND     GND
                      source=GND

        Vout_ideal = Vin / (1 - duty_cycle)
        Gate driven by PULSE source at switching frequency and duty cycle.
    """
    freq_hz = _parse_spice_value(freq)
    period = 1.0 / freq_hz
    t_on = period * duty_cycle
    t_rise = period * 0.01
    t_fall = t_rise

    return (
        Netlist("Boost Converter")
        .add_comment(f"Duty cycle = {duty_cycle:.0%}, Fsw = {freq}")
        .add_lib(mosfet_model)
        .add_lib(diode_model)
        .add_voltage_source("Vin", "vin", "0", dc=v_in)
        .add_voltage_source(
            "Vgate",
            "gate",
            "0",
            pulse=(
                "0",
                v_in,
                "0",
                f"{t_rise:.4g}",
                f"{t_fall:.4g}",
                f"{t_on:.4g}",
                f"{period:.4g}",
            ),
        )
        .add_inductor("L1", "vin", "sw", ind)
        .add_mosfet("M1", "sw", "gate", "0", "0", mosfet_model)
        .add_diode("D1", "sw", "out", diode_model)
        .add_capacitor("Cout", "out", "0", c_out)
        .add_resistor("Rload", "out", "0", r_load)
        .add_directive(f".tran {period * 200:.4g}")
    )


def instrumentation_amplifier(
    r1: str = "10k",
    r2: str = "10k",
    r3: str = "10k",
    r_gain: str = "10k",
) -> Netlist:
    """Create a classic 3-opamp instrumentation amplifier.

    Stage 1 (input buffers with gain):
        X1: In+ = Vin+, In- = node_a, Out = out1
        X2: In+ = Vin-, In- = node_b, Out = out2
        R1 from out1 to node_a (feedback)
        R1_match from out2 to node_b (feedback)
        Rgain between node_a and node_b

    Stage 2 (difference amplifier):
        X3: In- receives out1 via R2, feedback via R3 to Vout
            In+ receives out2 via R2_match, R3_match to GND

    Gain = (1 + 2*R1/Rgain) * (R3/R2)
    All opamps use LT1001, supply +/-15V.
    """
    opamp_model = "LT1001"
    return (
        Netlist("Instrumentation Amplifier")
        .add_comment(f"Gain = (1 + 2*{r1}/{r_gain}) * ({r3}/{r2})")
        .add_lib(opamp_model)
        .add_voltage_source("V1", "vinp", "0", ac="1")
        .add_voltage_source("V2", "vinn", "0", dc="0")
        .add_voltage_source("Vpos", "vdd", "0", dc="15")
        .add_voltage_source("Vneg", "0", "vss", dc="15")
        # Stage 1: input buffers
        .add_opamp("X1", "vinp", "node_a", "out1", "vdd", "vss", opamp_model)
        .add_opamp("X2", "vinn", "node_b", "out2", "vdd", "vss", opamp_model)
        .add_resistor("R1", "out1", "node_a", r1)
        .add_resistor("R1b", "out2", "node_b", r1)
        .add_resistor("Rgain", "node_a", "node_b", r_gain)
        # Stage 2: difference amplifier
        .add_opamp("X3", "noninv3", "inv3", "vout", "vdd", "vss", opamp_model)
        .add_resistor("R2", "out1", "inv3", r2)
        .add_resistor("R3", "inv3", "vout", r3)
        .add_resistor("R2b", "out2", "noninv3", r2)
        .add_resistor("R3b", "noninv3", "0", r3)
        .add_directive(".ac dec 100 1 1meg")
    )


def current_mirror(
    r_ref: str = "10k",
    r_load: str = "1k",
    vcc: str = "12",
    bjt_model: str = "2N2222",
) -> Netlist:
    """Create a basic BJT current mirror.

    Topology:
        Vcc --[Rref]--> collector_Q1 = base_Q1 = base_Q2
                         emitter_Q1 = GND
        Vcc --[Rload]--> collector_Q2
                          emitter_Q2 = GND

    Q1 is diode-connected (collector tied to base).
    I_ref = (Vcc - Vbe) / Rref, I_load ~ I_ref.
    """
    return (
        Netlist("Current Mirror")
        .add_comment(f"I_ref ~ ({vcc} - 0.7) / {r_ref}")
        .add_lib(bjt_model)
        .add_voltage_source("Vcc", "vcc", "0", dc=vcc)
        .add_resistor("Rref", "vcc", "mirror", r_ref)
        .add_resistor("Rload", "vcc", "out", r_load)
        .add_bjt("Q1", "mirror", "mirror", "0", bjt_model)
        .add_bjt("Q2", "out", "mirror", "0", bjt_model)
        .add_directive(".op")
        .add_directive(".tran 1m")
    )


def transimpedance_amplifier(
    rf: str = "100k",
    cf: str = "1p",
    i_source: str = "1u",
) -> Netlist:
    """Create a transimpedance amplifier (TIA).

    Topology:
        I1 (current source, AC) --> In- (inverting)
        In+ (non-inverting) --> GND
        Rf from In- to out (feedback resistor)
        Cf from In- to out (feedback cap, parallel with Rf for stability)

    Vout = -I_in * Rf (at low frequencies)
    Bandwidth limited by Cf.
    Supply: +/-15V, opamp_model = LT1001
    """
    opamp_model = "LT1001"
    return (
        Netlist("Transimpedance Amplifier")
        .add_comment(f"Vout = -I_in * {rf}")
        .add_lib(opamp_model)
        .add_current_source("I1", "inv", "0", ac=i_source)
        .add_voltage_source("Vpos", "vdd", "0", dc="15")
        .add_voltage_source("Vneg", "0", "vss", dc="15")
        .add_resistor("Rf", "inv", "out", rf)
        .add_capacitor("Cf", "inv", "out", cf)
        .add_opamp("X1", "0", "inv", "out", "vdd", "vss", opamp_model)
        .add_directive(".ac dec 100 1 1meg")
    )


def _parse_spice_value(value: str) -> float:
    """Convert a SPICE-style value string to a float.

    Handles common suffixes: T, G, meg, k, m, u, n, p, f.
    """
    suffixes = {
        "T": 1e12,
        "G": 1e9,
        "MEG": 1e6,
        "K": 1e3,
        "M": 1e-3,
        "U": 1e-6,
        "N": 1e-9,
        "P": 1e-12,
        "F": 1e-15,
    }
    value = value.strip()
    # Try plain float first
    try:
        return float(value)
    except ValueError:
        pass

    # Try suffixes (longest match first to catch "meg" before "m")
    upper = value.upper()
    for suffix, mult in sorted(suffixes.items(), key=lambda x: -len(x[0])):
        if upper.endswith(suffix):
            num_part = value[: len(value) - len(suffix)]
            try:
                return float(num_part) * mult
            except ValueError:
                continue

    raise ValueError(f"Cannot parse SPICE value: {value!r}")
