"""FastMCP server for LTspice circuit simulation automation.

This server provides tools for:
- Running SPICE simulations on schematics and netlists
- Extracting and analyzing waveform data
- Creating circuits programmatically
- Modifying schematic components
- Browsing component libraries, models, and examples
- Design rule checks and circuit comparison
"""

import csv
import io
import json
import math
import tempfile
from pathlib import Path

import numpy as np
from fastmcp import FastMCP
from fastmcp.prompts import Message

from .asc_generator import (
    generate_boost_converter as generate_boost_converter_asc,
)
from .asc_generator import (
    generate_buck_converter as generate_buck_converter_asc,
)
from .asc_generator import (
    generate_colpitts_oscillator as generate_colpitts_asc,
)
from .asc_generator import (
    generate_common_emitter_amp as generate_ce_amp_asc,
)
from .asc_generator import (
    generate_current_mirror as generate_current_mirror_asc,
)
from .asc_generator import (
    generate_differential_amp as generate_diff_amp_asc,
)
from .asc_generator import (
    generate_h_bridge as generate_h_bridge_asc,
)
from .asc_generator import (
    generate_instrumentation_amp as generate_inamp_asc,
)
from .asc_generator import (
    generate_inverting_amp,
)
from .asc_generator import (
    generate_ldo_regulator as generate_ldo_asc,
)
from .asc_generator import (
    generate_non_inverting_amp as generate_noninv_amp_asc,
)
from .asc_generator import (
    generate_rc_lowpass as generate_rc_lowpass_asc,
)
from .asc_generator import (
    generate_sallen_key_lowpass as generate_sallen_key_asc,
)
from .asc_generator import (
    generate_transimpedance_amp as generate_tia_asc,
)
from .asc_generator import (
    generate_voltage_divider as generate_voltage_divider_asc,
)
from .batch import (
    run_monte_carlo,
    run_parameter_sweep,
    run_temperature_sweep,
)
from .config import (
    LTSPICE_EXAMPLES,
    LTSPICE_LIB,
    validate_installation,
)
from .diff import diff_schematics as _diff_schematics
from .drc import run_drc as _run_drc
from .log_parser import parse_log
from .models import search_models as _search_models
from .models import search_subcircuits as _search_subcircuits
from .netlist import (
    Netlist,
    boost_converter,
    buck_converter,
    colpitts_oscillator,
    common_emitter_amplifier,
    current_mirror,
    differential_amplifier,
    h_bridge,
    instrumentation_amplifier,
    inverting_amplifier,
    ldo_regulator,
    non_inverting_amplifier,
    rc_lowpass,
    sallen_key_lowpass,
    transimpedance_amplifier,
    voltage_divider,
)
from .noise_analysis import (
    compute_noise_metrics,
    compute_spot_noise,
    compute_total_noise,
)
from .optimizer import (
    ComponentRange,
    OptimizationTarget,
    format_engineering,
    optimize_component_values,
)
from .power_analysis import compute_efficiency, compute_power_metrics
from .raw_parser import parse_raw_file
from .runner import run_netlist, run_simulation
from .schematic import modify_component_value, parse_schematic
from .stability import compute_stability_metrics
from .svg_plot import plot_bode, plot_spectrum, plot_timeseries
from .touchstone import parse_touchstone, s_param_to_db
from .waveform_expr import WaveformCalculator
from .waveform_math import (
    compute_bandwidth,
    compute_fft,
    compute_peak_to_peak,
    compute_rise_time,
    compute_rms,
    compute_settling_time,
    compute_thd,
)

mcp = FastMCP(
    name="mcltspice",
    instructions="""
    LTspice MCP Server - Circuit simulation automation.

    Use this server to:
    - Run SPICE simulations on .asc schematics or .cir netlists
    - Extract waveform data (voltages, currents) from simulation results
    - Analyze signals: FFT, THD, RMS, bandwidth, settling time
    - Create circuits from scratch using the netlist builder
    - Create circuits from 10 pre-built templates (list_templates)
    - Modify component values in schematics programmatically
    - Browse LTspice's component library (6500+ symbols)
    - Search 2800+ SPICE models and subcircuits
    - Access example circuits (4000+ examples)
    - Run design rule checks before simulation
    - Compare schematics to see what changed
    - Export waveform data to CSV
    - Extract DC operating point (.op) and transfer function (.tf) data
    - Analyze noise: spectral density, spot noise, RMS, noise figure, 1/f corner
    - Measure stability (gain/phase margins from AC loop gain)
    - Compute power and efficiency from voltage/current waveforms
    - Evaluate waveform math expressions (V*I, gain, dB, etc.)
    - Optimize component values to hit target specs automatically
    - Generate .asc schematic files (graphical format)
    - Run parameter sweeps, temperature sweeps, and Monte Carlo analysis
    - Handle stepped simulations: list runs, extract per-run data
    - Parse Touchstone (.s2p) S-parameter files

    LTspice runs via Wine on Linux. Simulations execute in batch mode
    and results are parsed from binary .raw files.
    """,
)


# ============================================================================
# SIMULATION TOOLS
# ============================================================================


@mcp.tool()
async def simulate(
    schematic_path: str,
    timeout_seconds: float = 300,
) -> dict:
    """Run an LTspice simulation on a schematic file.

    Executes any simulation directives (.tran, .ac, .dc, .op, etc.)
    found in the schematic. Returns available signal names and
    the path to the .raw file for waveform extraction.

    Args:
        schematic_path: Absolute path to .asc schematic file
        timeout_seconds: Maximum time to wait for simulation (default 5 min)
    """
    result = await run_simulation(
        schematic_path,
        timeout=timeout_seconds,
        parse_results=True,
    )

    response = {
        "success": result.success,
        "elapsed_seconds": result.elapsed_seconds,
        "error": result.error,
    }

    if result.raw_data:
        response["variables"] = [
            {"name": v.name, "type": v.type} for v in result.raw_data.variables
        ]
        response["points"] = result.raw_data.points
        response["plotname"] = result.raw_data.plotname
        response["raw_file"] = str(result.raw_file) if result.raw_file else None

    if result.log_file and result.log_file.exists():
        log = parse_log(result.log_file)
        if log.measurements:
            response["measurements"] = log.get_all_measurements()
        if log.errors:
            response["log_errors"] = log.errors

    return response


@mcp.tool()
async def simulate_netlist(
    netlist_path: str,
    timeout_seconds: float = 300,
) -> dict:
    """Run an LTspice simulation on a netlist file (.cir or .net).

    Args:
        netlist_path: Absolute path to .cir or .net netlist file
        timeout_seconds: Maximum time to wait for simulation
    """
    result = await run_netlist(
        netlist_path,
        timeout=timeout_seconds,
        parse_results=True,
    )

    response = {
        "success": result.success,
        "elapsed_seconds": result.elapsed_seconds,
        "error": result.error,
    }

    if result.raw_data:
        response["variables"] = [
            {"name": v.name, "type": v.type} for v in result.raw_data.variables
        ]
        response["points"] = result.raw_data.points
        response["raw_file"] = str(result.raw_file) if result.raw_file else None

    if result.log_file and result.log_file.exists():
        log = parse_log(result.log_file)
        if log.measurements:
            response["measurements"] = log.get_all_measurements()
        if log.errors:
            response["log_errors"] = log.errors

    return response


# ============================================================================
# WAVEFORM & ANALYSIS TOOLS
# ============================================================================


@mcp.tool()
def get_waveform(
    raw_file_path: str,
    signal_names: list[str],
    max_points: int = 1000,
    run: int | None = None,
) -> dict:
    """Extract waveform data from a .raw simulation results file.

    For transient analysis, returns time + voltage/current values.
    For AC analysis, returns frequency + magnitude(dB)/phase(degrees).

    For stepped simulations (.step, .mc, .temp), specify `run` (1-based)
    to extract a single run's data. Omit `run` to get all data combined.

    Args:
        raw_file_path: Path to .raw file from simulation
        signal_names: Signal names to extract, e.g. ["V(out)", "I(R1)"]
        max_points: Maximum data points (downsampled if needed)
        run: Run number (1-based) for stepped simulations (None = all data)
    """
    raw = parse_raw_file(raw_file_path)

    # Extract specific run if requested
    if run is not None:
        if not raw.is_stepped:
            return {"error": "Not a stepped simulation - no multiple runs available"}
        if run < 1 or run > raw.n_runs:
            return {"error": f"Run {run} out of range (1..{raw.n_runs})"}
        raw = raw.get_run_data(run)

    x_axis = raw.get_time()
    x_name = "time"
    if x_axis is None:
        x_axis = raw.get_frequency()
        x_name = "frequency"

    total_points = len(x_axis) if x_axis is not None else raw.points
    step = max(1, total_points // max_points)

    result = {
        "x_axis_name": x_name,
        "x_axis_data": [],
        "signals": {},
        "total_points": total_points,
        "returned_points": 0,
        "is_stepped": raw.is_stepped,
        "n_runs": raw.n_runs,
    }

    if x_axis is not None:
        sampled = x_axis[::step]
        if np.iscomplexobj(sampled):
            result["x_axis_data"] = sampled.real.tolist()
        else:
            result["x_axis_data"] = sampled.tolist()
        result["returned_points"] = len(result["x_axis_data"])

    for name in signal_names:
        data = raw.get_variable(name)
        if data is not None:
            sampled = data[::step]
            if np.iscomplexobj(sampled):
                result["signals"][name] = {
                    "magnitude_db": [
                        20 * math.log10(abs(x)) if abs(x) > 0 else -200 for x in sampled
                    ],
                    "phase_degrees": [math.degrees(math.atan2(x.imag, x.real)) for x in sampled],
                }
            else:
                result["signals"][name] = {"values": sampled.tolist()}

    return result


@mcp.tool()
def list_simulation_runs(raw_file_path: str) -> dict:
    """List runs in a stepped simulation (.step, .mc, .temp).

    Returns run count and boundary information for multi-run .raw files.

    Args:
        raw_file_path: Path to .raw file from simulation
    """
    raw = parse_raw_file(raw_file_path)

    result = {
        "is_stepped": raw.is_stepped,
        "n_runs": raw.n_runs,
        "total_points": raw.points,
        "plotname": raw.plotname,
        "variables": [{"name": v.name, "type": v.type} for v in raw.variables],
    }

    if raw.is_stepped and raw.run_boundaries:
        runs = []
        for i in range(raw.n_runs):
            start, end = raw._run_slice(i + 1)
            runs.append(
                {
                    "run": i + 1,
                    "start_index": start,
                    "end_index": end,
                    "points": end - start,
                }
            )
        result["runs"] = runs

    return result


@mcp.tool()
def analyze_waveform(
    raw_file_path: str,
    signal_name: str,
    analyses: list[str],
    settling_tolerance_pct: float = 2.0,
    settling_final_value: float | None = None,
    rise_low_pct: float = 10.0,
    rise_high_pct: float = 90.0,
    fft_max_harmonics: int = 50,
    thd_n_harmonics: int = 10,
) -> dict:
    """Analyze a signal from simulation results.

    Run one or more analyses on a waveform. Available analyses:
    - "rms": Root mean square value
    - "peak_to_peak": Min, max, peak-to-peak swing, mean
    - "settling_time": Time to settle within tolerance of final value
    - "rise_time": 10%-90% rise time (configurable)
    - "fft": Frequency spectrum via FFT
    - "thd": Total Harmonic Distortion

    Args:
        raw_file_path: Path to .raw file
        signal_name: Signal to analyze, e.g. "V(out)"
        analyses: List of analysis types to run
        settling_tolerance_pct: Tolerance for settling time (default 2%)
        settling_final_value: Target value (None = use last sample)
        rise_low_pct: Low threshold for rise time (default 10%)
        rise_high_pct: High threshold for rise time (default 90%)
        fft_max_harmonics: Max harmonics to return in FFT
        thd_n_harmonics: Number of harmonics for THD calculation
    """
    raw = parse_raw_file(raw_file_path)

    time = raw.get_time()
    signal = raw.get_variable(signal_name)

    if signal is None:
        return {
            "error": f"Signal '{signal_name}' not found. Available: "
            f"{[v.name for v in raw.variables]}"
        }

    # Use real parts for time-domain analysis
    if np.iscomplexobj(time):
        time = time.real
    if np.iscomplexobj(signal):
        signal = np.abs(signal)

    results = {"signal": signal_name}

    for analysis in analyses:
        if analysis == "rms":
            results["rms"] = compute_rms(signal)

        elif analysis == "peak_to_peak":
            results["peak_to_peak"] = compute_peak_to_peak(signal)

        elif analysis == "settling_time":
            if time is not None:
                results["settling_time"] = compute_settling_time(
                    time,
                    signal,
                    final_value=settling_final_value,
                    tolerance_percent=settling_tolerance_pct,
                )

        elif analysis == "rise_time":
            if time is not None:
                results["rise_time"] = compute_rise_time(
                    time,
                    signal,
                    low_pct=rise_low_pct,
                    high_pct=rise_high_pct,
                )

        elif analysis == "fft":
            if time is not None:
                results["fft"] = compute_fft(
                    time,
                    signal,
                    max_harmonics=fft_max_harmonics,
                )

        elif analysis == "thd":
            if time is not None:
                results["thd"] = compute_thd(
                    time,
                    signal,
                    n_harmonics=thd_n_harmonics,
                )

    return results


@mcp.tool()
def measure_bandwidth(
    raw_file_path: str,
    signal_name: str,
    ref_db: float | None = None,
) -> dict:
    """Measure -3dB bandwidth from an AC analysis result.

    Computes the frequency range where the signal is within 3dB
    of its peak (or a specified reference level).

    Args:
        raw_file_path: Path to .raw file from AC simulation
        signal_name: Signal to measure, e.g. "V(out)"
        ref_db: Reference level in dB (None = use peak)
    """
    raw = parse_raw_file(raw_file_path)

    freq = raw.get_frequency()
    signal = raw.get_variable(signal_name)

    if freq is None:
        return {"error": "Not an AC analysis - no frequency data found"}
    if signal is None:
        return {"error": f"Signal '{signal_name}' not found"}

    # Convert complex signal to magnitude in dB
    mag_db = np.array([20 * math.log10(abs(x)) if abs(x) > 0 else -200 for x in signal])

    return compute_bandwidth(freq.real, mag_db, ref_db=ref_db)


@mcp.tool()
def export_csv(
    raw_file_path: str,
    signal_names: list[str] | None = None,
    output_path: str | None = None,
    max_points: int = 10000,
) -> dict:
    """Export simulation waveform data to CSV format.

    Args:
        raw_file_path: Path to .raw file
        signal_names: Signals to export (None = all)
        output_path: Where to save CSV (None = auto-generate in /tmp)
        max_points: Maximum rows to export
    """
    raw = parse_raw_file(raw_file_path)

    # Determine x-axis
    x_axis = raw.get_time()
    x_name = "time"
    if x_axis is None:
        x_axis = raw.get_frequency()
        x_name = "frequency"

    # Select signals
    if signal_names is None:
        signal_names = [
            v.name for v in raw.variables if v.name not in (x_name, "time", "frequency")
        ]

    # Downsample
    total = raw.points
    step = max(1, total // max_points)

    # Build CSV
    buf = io.StringIO()
    writer = csv.writer(buf)

    # Header
    if x_axis is not None and np.iscomplexobj(x_axis):
        headers = [x_name]
    else:
        headers = [x_name]

    for name in signal_names:
        data = raw.get_variable(name)
        if data is not None:
            if np.iscomplexobj(data):
                headers.extend([f"{name}_magnitude_db", f"{name}_phase_deg"])
            else:
                headers.append(name)

    writer.writerow(headers)

    # Data rows
    indices = range(0, total, step)
    for i in indices:
        row = []
        if x_axis is not None:
            row.append(x_axis[i].real if np.iscomplexobj(x_axis) else x_axis[i])

        for name in signal_names:
            data = raw.get_variable(name)
            if data is not None:
                if np.iscomplexobj(data):
                    val = data[i]
                    row.append(20 * math.log10(abs(val)) if abs(val) > 0 else -200)
                    row.append(math.degrees(math.atan2(val.imag, val.real)))
                else:
                    row.append(data[i])

        writer.writerow(row)

    csv_content = buf.getvalue()

    # Save to file
    if output_path is None:
        raw_name = Path(raw_file_path).stem
        output_path = str(Path(tempfile.gettempdir()) / f"{raw_name}.csv")

    Path(output_path).write_text(csv_content)

    return {
        "output_path": output_path,
        "rows": len(indices),
        "columns": headers,
    }


# ============================================================================
# STABILITY ANALYSIS TOOLS
# ============================================================================


@mcp.tool()
def analyze_stability(
    raw_file_path: str,
    signal_name: str,
) -> dict:
    """Measure gain margin and phase margin from AC loop gain data.

    Computes Bode plot (magnitude + phase) and finds the crossover
    frequencies where gain = 0 dB and phase = -180 degrees.

    Args:
        raw_file_path: Path to .raw file from AC simulation
        signal_name: Loop gain signal, e.g. "V(out)" or "V(loop_gain)"
    """
    raw = parse_raw_file(raw_file_path)
    freq = raw.get_frequency()
    signal = raw.get_variable(signal_name)

    if freq is None:
        return {"error": "Not an AC analysis - no frequency data found"}
    if signal is None:
        return {
            "error": f"Signal '{signal_name}' not found. Available: "
            f"{[v.name for v in raw.variables]}"
        }

    return compute_stability_metrics(freq.real, signal)


# ============================================================================
# NOISE ANALYSIS TOOLS
# ============================================================================


@mcp.tool()
def analyze_noise(
    raw_file_path: str,
    noise_signal: str = "onoise",
    source_resistance: float = 50.0,
) -> dict:
    """Comprehensive noise analysis from a .noise simulation.

    Returns spectral density, spot noise at standard frequencies (10Hz,
    100Hz, 1kHz, 10kHz, 100kHz), total integrated RMS noise, noise figure,
    and 1/f corner frequency estimate.

    Run a simulation with .noise directive first, e.g.:
        .noise V(out) V1 dec 100 1 1meg

    Args:
        raw_file_path: Path to .raw file from .noise simulation
        noise_signal: Which noise variable to analyze ("onoise" for
            output-referred or "inoise" for input-referred)
        source_resistance: Source impedance in ohms for noise figure (default 50)
    """
    raw = parse_raw_file(raw_file_path)

    freq = raw.get_frequency()
    signal = raw.get_variable(noise_signal)

    if freq is None:
        return {"error": "No frequency data found. Is this a .noise simulation?"}
    if signal is None:
        available = [v.name for v in raw.variables]
        return {
            "error": f"Signal '{noise_signal}' not found. Available: {available}",
        }

    return compute_noise_metrics(freq.real, signal, source_resistance)


@mcp.tool()
def get_spot_noise(
    raw_file_path: str,
    target_freq: float,
    noise_signal: str = "onoise",
) -> dict:
    """Get noise spectral density at a specific frequency.

    Interpolates between data points to estimate the noise density
    at the requested frequency.

    Args:
        raw_file_path: Path to .raw file from .noise simulation
        target_freq: Frequency in Hz to measure noise at
        noise_signal: "onoise" (output-referred) or "inoise" (input-referred)
    """
    raw = parse_raw_file(raw_file_path)

    freq = raw.get_frequency()
    signal = raw.get_variable(noise_signal)

    if freq is None:
        return {"error": "No frequency data found"}
    if signal is None:
        return {"error": f"Signal '{noise_signal}' not found"}

    return compute_spot_noise(freq.real, signal, target_freq)


@mcp.tool()
def get_total_noise(
    raw_file_path: str,
    noise_signal: str = "onoise",
    f_low: float | None = None,
    f_high: float | None = None,
) -> dict:
    """Integrate noise over a frequency band to get total RMS noise.

    Computes total_rms = sqrt(integral(|noise|^2 * df)) over the
    specified frequency range.

    Args:
        raw_file_path: Path to .raw file from .noise simulation
        noise_signal: "onoise" or "inoise"
        f_low: Lower frequency bound in Hz (default: data minimum)
        f_high: Upper frequency bound in Hz (default: data maximum)
    """
    raw = parse_raw_file(raw_file_path)

    freq = raw.get_frequency()
    signal = raw.get_variable(noise_signal)

    if freq is None:
        return {"error": "No frequency data found"}
    if signal is None:
        return {"error": f"Signal '{noise_signal}' not found"}

    return compute_total_noise(freq.real, signal, f_low, f_high)


# ============================================================================
# DC OPERATING POINT & TRANSFER FUNCTION TOOLS
# ============================================================================


@mcp.tool()
def get_operating_point(raw_file_path: str) -> dict:
    """Extract DC operating point results from a .raw file.

    The .op analysis computes all node voltages and branch currents
    at the DC bias point, stored as a single data point in the .raw file.

    Run a simulation with .op directive first, then pass the .raw file.

    Args:
        raw_file_path: Path to .raw file from simulation
    """
    raw = parse_raw_file(raw_file_path)

    if raw.plotname and "operating point" not in raw.plotname.lower():
        return {
            "error": f"Not an operating point analysis (plotname: '{raw.plotname}'). "
            "Ensure the simulation uses a .op directive."
        }

    # Extract single-point values, separating voltages from currents
    voltages = {}
    currents = {}
    other = {}
    for v in raw.variables:
        data = raw.get_variable(v.name)
        if data is None or len(data) == 0:
            continue
        val = float(data[0].real) if hasattr(data[0], "real") else float(data[0])
        if v.name.lower().startswith("v("):
            voltages[v.name] = val
        elif v.name.lower().startswith("i(") or v.name.lower().startswith("ix("):
            currents[v.name] = val
        else:
            other[v.name] = val

    return {
        "voltages": voltages,
        "currents": currents,
        "device_params": other,
        "total_entries": len(voltages) + len(currents) + len(other),
    }


@mcp.tool()
def get_transfer_function(raw_file_path: str) -> dict:
    """Extract .tf (transfer function) results from a .raw file.

    The .tf analysis computes:
    - Transfer function (gain or transresistance)
    - Input impedance at the source
    - Output impedance at the output node

    Run a simulation with .tf directive first (e.g., ".tf V(out) V1"),
    then pass the .raw file.

    Args:
        raw_file_path: Path to .raw file from simulation
    """
    raw = parse_raw_file(raw_file_path)

    if raw.plotname and "transfer function" not in raw.plotname.lower():
        return {
            "error": f"Not a transfer function analysis (plotname: '{raw.plotname}'). "
            "Ensure the simulation uses a .tf directive, e.g., '.tf V(out) V1'."
        }

    result: dict = {}
    for v in raw.variables:
        data = raw.get_variable(v.name)
        if data is None or len(data) == 0:
            continue
        val = float(data[0].real) if hasattr(data[0], "real") else float(data[0])

        name_lower = v.name.lower()
        if "transfer_function" in name_lower:
            result["transfer_function"] = val
        elif "output_impedance" in name_lower:
            result["output_impedance_ohms"] = val
        elif "input_impedance" in name_lower:
            result["input_impedance_ohms"] = val

        # Always include raw data with original name
        result.setdefault("raw_data", {})[v.name] = val

    if not result:
        return {
            "error": "No transfer function data found in .raw file.",
            "plotname": raw.plotname,
            "variables": [v.name for v in raw.variables],
        }

    return result


# ============================================================================
# POWER ANALYSIS TOOLS
# ============================================================================


@mcp.tool()
def analyze_power(
    raw_file_path: str,
    voltage_signal: str,
    current_signal: str,
) -> dict:
    """Compute power metrics from voltage and current waveforms.

    Returns average power, RMS power, peak power, and power factor.

    Args:
        raw_file_path: Path to .raw file from transient simulation
        voltage_signal: Voltage signal name, e.g. "V(out)"
        current_signal: Current signal name, e.g. "I(R1)"
    """
    raw = parse_raw_file(raw_file_path)
    time = raw.get_time()
    voltage = raw.get_variable(voltage_signal)
    current = raw.get_variable(current_signal)

    if time is None:
        return {"error": "Not a transient analysis - no time data found"}
    if voltage is None:
        return {"error": f"Voltage signal '{voltage_signal}' not found"}
    if current is None:
        return {"error": f"Current signal '{current_signal}' not found"}

    return compute_power_metrics(time, voltage, current)


@mcp.tool()
def compute_efficiency_tool(
    raw_file_path: str,
    input_voltage_signal: str,
    input_current_signal: str,
    output_voltage_signal: str,
    output_current_signal: str,
) -> dict:
    """Compute power conversion efficiency.

    Compares input power to output power for regulators, converters, etc.

    Args:
        raw_file_path: Path to .raw file from transient simulation
        input_voltage_signal: Input voltage, e.g. "V(vin)"
        input_current_signal: Input current, e.g. "I(Vin)"
        output_voltage_signal: Output voltage, e.g. "V(out)"
        output_current_signal: Output current, e.g. "I(Rload)"
    """
    raw = parse_raw_file(raw_file_path)
    time = raw.get_time()
    if time is None:
        return {"error": "Not a transient analysis"}

    v_in = raw.get_variable(input_voltage_signal)
    i_in = raw.get_variable(input_current_signal)
    v_out = raw.get_variable(output_voltage_signal)
    i_out = raw.get_variable(output_current_signal)

    for name, sig in [
        (input_voltage_signal, v_in),
        (input_current_signal, i_in),
        (output_voltage_signal, v_out),
        (output_current_signal, i_out),
    ]:
        if sig is None:
            return {"error": f"Signal '{name}' not found"}

    return compute_efficiency(time, v_in, i_in, v_out, i_out)


# ============================================================================
# WAVEFORM EXPRESSION TOOLS
# ============================================================================


@mcp.tool()
def evaluate_waveform_expression(
    raw_file_path: str,
    expression: str,
    max_points: int = 1000,
) -> dict:
    """Evaluate a math expression on simulation waveforms.

    Supports: +, -, *, /, abs(), sqrt(), log10(), dB()
    Signal names reference variables from the .raw file.

    Examples:
        "V(out) * I(R1)"  - instantaneous power
        "V(out) / V(in)"  - voltage gain
        "dB(V(out))"      - magnitude in dB

    Args:
        raw_file_path: Path to .raw file
        expression: Math expression using signal names
        max_points: Maximum data points to return
    """
    raw = parse_raw_file(raw_file_path)
    calc = WaveformCalculator(raw)

    try:
        result = calc.calc(expression)
    except ValueError as e:
        return {"error": str(e), "available_signals": calc.available_signals()}

    # Get x-axis
    x_axis = raw.get_time()
    x_name = "time"
    if x_axis is None:
        x_axis = raw.get_frequency()
        x_name = "frequency"

    total = len(result)
    step = max(1, total // max_points)

    response = {
        "expression": expression,
        "total_points": total,
        "returned_points": len(result[::step]),
    }

    if x_axis is not None:
        sampled_x = x_axis[::step]
        response["x_axis_name"] = x_name
        response["x_axis_data"] = (
            sampled_x.real.tolist() if np.iscomplexobj(sampled_x) else sampled_x.tolist()
        )

    response["values"] = result[::step].tolist()
    response["available_signals"] = calc.available_signals()

    return response


# ============================================================================
# OPTIMIZER TOOLS
# ============================================================================


@mcp.tool()
async def optimize_circuit(
    netlist_template: str,
    targets: list[dict],
    component_ranges: list[dict],
    max_iterations: int = 20,
) -> dict:
    """Automatically optimize component values to hit target specifications.

    Runs real LTspice simulations in a loop, adjusting component values
    using binary search (single component) or coordinate descent (multiple).

    Args:
        netlist_template: Netlist text with {ComponentName} placeholders
            (e.g., {R1}, {C1}) that get substituted each iteration.
        targets: List of target specs, each with:
            - signal_name: Signal to measure (e.g., "V(out)")
            - metric: One of "bandwidth_hz", "rms", "peak_to_peak",
              "settling_time", "gain_db", "phase_margin_deg"
            - target_value: Desired value
            - weight: Importance weight (default 1.0)
        component_ranges: List of tunable components, each with:
            - component_name: Name matching {placeholder} (e.g., "R1")
            - min_value: Minimum value in base units
            - max_value: Maximum value in base units
            - preferred_series: Optional "E12", "E24", or "E96" for snapping
        max_iterations: Max simulation iterations (default 20)
    """
    opt_targets = [
        OptimizationTarget(
            signal_name=t["signal_name"],
            metric=t["metric"],
            target_value=t["target_value"],
            weight=t.get("weight", 1.0),
        )
        for t in targets
    ]

    opt_ranges = [
        ComponentRange(
            component_name=r["component_name"],
            min_value=r["min_value"],
            max_value=r["max_value"],
            preferred_series=r.get("preferred_series"),
        )
        for r in component_ranges
    ]

    result = await optimize_component_values(
        netlist_template,
        opt_targets,
        opt_ranges,
        max_iterations,
    )

    return {
        "best_values": {k: format_engineering(v) for k, v in result.best_values.items()},
        "best_values_raw": result.best_values,
        "best_cost": result.best_cost,
        "iterations": result.iterations,
        "targets_met": result.targets_met,
        "final_metrics": result.final_metrics,
        "history_length": len(result.history),
    }


# ============================================================================
# CIRCUIT TUNING TOOL
# ============================================================================


@mcp.tool()
async def tune_circuit(
    template: str,
    params: dict[str, str] | None = None,
    targets: dict[str, str] | None = None,
    signal: str = "V(out)",
) -> dict:
    """Measure circuit performance and suggest parameter adjustments.

    Single-shot workflow: generates a circuit from a template, simulates it,
    measures key metrics, compares against targets, and suggests what to change.
    Call this tool repeatedly with adjusted params until targets are met.

    Args:
        template: Template name (from list_templates). Works with both
            netlist templates (_TEMPLATES) and schematic templates (_ASC_TEMPLATES).
        params: Component value overrides (e.g., {"r": "2.2k", "c": "47n"}).
            Use list_templates to see available parameters.
        targets: Performance targets to check against. Each key is a metric name,
            value is a comparison string like ">5000" or "<0.1" or "~1000".
            Supported metrics: bandwidth_hz, gain_db, rms, peak_to_peak,
            settling_time_s, dc_value, fundamental_freq_hz.
            Example: {"bandwidth_hz": ">5000", "gain_db": ">20"}
        signal: Signal name to measure (default: "V(out)")

    Returns:
        Dict with metrics, target comparison, and tuning suggestions.
    """
    # --- 1. Look up template ---
    tmpl = _TEMPLATES.get(template)
    is_netlist = tmpl is not None
    if tmpl is None:
        tmpl = _ASC_TEMPLATES.get(template)
    if tmpl is None:
        names = sorted(set(list(_TEMPLATES.keys()) + list(_ASC_TEMPLATES.keys())))
        return {"error": f"Unknown template '{template}'. Available: {', '.join(names)}"}

    # --- 2. Build effective params ---
    effective_params: dict[str, str] = dict(tmpl["params"])
    if params:
        for k, v in params.items():
            if k not in tmpl["params"]:
                return {
                    "error": f"Unknown param '{k}' for {template}",
                    "valid_params": list(tmpl["params"].keys()),
                }
            effective_params[k] = v

    # --- 3. Generate and simulate ---
    import tempfile
    from pathlib import Path

    try:
        # Build kwargs (handle duty_cycle float conversion)
        call_kwargs: dict = {}
        for k, v in effective_params.items():
            if k == "duty_cycle":
                call_kwargs[k] = float(v)
            else:
                call_kwargs[k] = v

        if is_netlist:
            nl = tmpl["func"](**call_kwargs)
            out_path = Path(tempfile.gettempdir()) / f"tune_{template}.cir"
            nl.save(out_path)
            result = await run_netlist(out_path)
        else:
            sch = tmpl["func"](**call_kwargs)
            out_path = Path(tempfile.gettempdir()) / f"tune_{template}.asc"
            sch.save(out_path)
            result = await run_simulation(out_path)

        if not result.success:
            return {
                "error": "Simulation failed",
                "detail": result.error or result.stderr,
                "params_used": effective_params,
            }

    except Exception as e:
        return {"error": f"Generation/simulation failed: {e}", "params_used": effective_params}

    # --- 4. Extract waveform and compute metrics ---
    raw = result.raw_data
    if raw is None:
        return {"error": "No raw data from simulation", "params_used": effective_params}

    # Find the signal variable
    sig_idx = None
    time_idx = None
    freq_idx = None
    for var in raw.variables:
        if var.name.lower() == signal.lower():
            sig_idx = var.index
        if var.name.lower() == "time":
            time_idx = var.index
        if var.name.lower() == "frequency":
            freq_idx = var.index

    if sig_idx is None:
        available = [v.name for v in raw.variables]
        return {
            "error": f"Signal '{signal}' not found",
            "available_signals": available,
            "params_used": effective_params,
        }

    sig_data = raw.data[sig_idx]
    metrics: dict[str, float] = {}

    is_ac = freq_idx is not None

    if is_ac:
        # Frequency-domain metrics
        freq = np.abs(raw.data[freq_idx]).real
        mag_complex = sig_data
        mag_db = 20.0 * np.log10(np.maximum(np.abs(mag_complex), 1e-30))

        # Bandwidth
        try:
            bw_result = compute_bandwidth(freq, mag_db)
            metrics["bandwidth_hz"] = bw_result["bandwidth_hz"]
            if bw_result.get("f_low"):
                metrics["f_low_hz"] = bw_result["f_low"]
            if bw_result.get("f_high"):
                metrics["f_high_hz"] = bw_result["f_high"]
        except Exception:
            pass

        # DC gain (magnitude at lowest frequency)
        metrics["gain_db"] = float(mag_db[0])
        metrics["dc_value"] = float(np.abs(mag_complex[0]))

    else:
        # Time-domain metrics
        time_data = raw.data[time_idx] if time_idx is not None else None
        sig_real = np.real(sig_data)

        # RMS
        metrics["rms"] = float(compute_rms(sig_real))

        # Peak-to-peak
        pp = compute_peak_to_peak(sig_real)
        metrics["peak_to_peak"] = pp["peak_to_peak"]
        metrics["dc_value"] = pp["mean"]

        # Settling time (if signal looks like a step response)
        if time_data is not None:
            time_real = np.real(time_data)
            try:
                settle = compute_settling_time(time_real, sig_real)
                if settle["settled"]:
                    metrics["settling_time_s"] = settle["settling_time"]
            except Exception:
                pass

            # FFT for fundamental frequency
            try:
                fft = compute_fft(time_real, sig_real)
                if fft["fundamental_freq"] > 0:
                    metrics["fundamental_freq_hz"] = fft["fundamental_freq"]
            except Exception:
                pass

    # --- 5. Compare against targets ---
    targets_met = True
    target_results: dict[str, dict] = {}
    if targets:
        for metric_name, target_str in targets.items():
            if metric_name not in metrics:
                target_results[metric_name] = {
                    "status": "unmeasurable",
                    "reason": f"Metric '{metric_name}' not available from this simulation",
                }
                targets_met = False
                continue

            actual = metrics[metric_name]
            met = False
            target_val = 0.0

            if target_str.startswith(">"):
                target_val = float(target_str[1:])
                met = actual > target_val
            elif target_str.startswith("<"):
                target_val = float(target_str[1:])
                met = actual < target_val
            elif target_str.startswith("~"):
                target_val = float(target_str[1:])
                tolerance = target_val * 0.1  # 10% tolerance
                met = abs(actual - target_val) <= tolerance
            else:
                target_val = float(target_str)
                met = abs(actual - target_val) <= target_val * 0.1

            target_results[metric_name] = {
                "target": target_str,
                "actual": actual,
                "met": met,
            }
            if not met:
                targets_met = False

    # --- 6. Generate suggestions ---
    suggestions: list[str] = []
    if targets and not targets_met:
        for metric_name, result_info in target_results.items():
            if isinstance(result_info, dict) and not result_info.get("met", True):
                actual = result_info.get("actual")
                target_str_val = result_info.get("target", "")
                if actual is None:
                    continue

                if metric_name == "bandwidth_hz":
                    if str(target_str_val).startswith(">") and actual < float(
                        str(target_str_val)[1:]
                    ):
                        suggestions.append(
                            "To increase bandwidth: decrease R or C values "
                            "(f_c = 1/(2*pi*R*C) for RC filters)"
                        )
                    elif str(target_str_val).startswith("<"):
                        suggestions.append(
                            "To decrease bandwidth: increase R or C values"
                        )

                elif metric_name == "gain_db":
                    if str(target_str_val).startswith(">") and actual < float(
                        str(target_str_val)[1:]
                    ):
                        suggestions.append(
                            "To increase gain: increase Rf/Rin ratio for op-amp circuits, "
                            "or increase Rc/Re ratio for CE amplifiers"
                        )
                    elif str(target_str_val).startswith("<"):
                        suggestions.append("To decrease gain: decrease Rf/Rin ratio")

                elif metric_name == "peak_to_peak":
                    if str(target_str_val).startswith("<"):
                        suggestions.append(
                            "To reduce peak-to-peak (ripple): increase filter capacitance "
                            "or inductance, or increase switching frequency"
                        )

                elif metric_name == "settling_time_s":
                    if str(target_str_val).startswith("<"):
                        suggestions.append(
                            "To reduce settling time: increase bandwidth (decrease R*C), "
                            "or add damping to reduce ringing"
                        )

                elif metric_name == "rms":
                    suggestions.append(
                        f"RMS is {actual:.4g}, target was {target_str_val}. "
                        "Adjust source amplitude or gain."
                    )

    if not suggestions and not targets_met:
        suggestions.append(
            "Adjust component values toward the target. Use smaller steps for fine-tuning."
        )

    return {
        "template": template,
        "params_used": effective_params,
        "metrics": metrics,
        "targets": target_results if targets else {},
        "targets_met": targets_met,
        "suggestions": suggestions,
        "signal": signal,
        "analysis_type": "ac" if is_ac else "transient",
    }


# ============================================================================
# WAVEFORM PLOTTING TOOL
# ============================================================================


@mcp.tool()
async def plot_waveform(
    raw_file: str,
    signal: str = "V(out)",
    plot_type: str = "auto",
    output_path: str | None = None,
) -> dict:
    """Generate an SVG plot from simulation results.

    Parses a .raw file and creates a publication-quality SVG waveform plot.
    Supports time-domain, Bode (frequency response), and FFT spectrum plots.

    Args:
        raw_file: Path to the LTspice .raw binary file
        signal: Signal name to plot (e.g. "V(out)", "I(R1)")
        plot_type: "auto" (detect from data), "time", "bode", or "spectrum"
        output_path: Where to save SVG file (None = auto in /tmp)
    """
    raw_path = Path(raw_file)
    if not raw_path.exists():
        return {"error": f"Raw file not found: {raw_file}"}

    try:
        raw_data = parse_raw_file(str(raw_path))
    except Exception as e:
        return {"error": f"Failed to parse raw file: {e}"}

    # Find the requested signal
    sig_names = [v.name for v in raw_data.variables]
    sig_lower = {s.lower(): s for s in sig_names}
    actual_name = sig_lower.get(signal.lower())
    if actual_name is None:
        return {
            "error": f"Signal '{signal}' not found",
            "available_signals": sig_names,
        }

    sig_idx = next(i for i, v in enumerate(raw_data.variables) if v.name == actual_name)
    values = raw_data.data[:, sig_idx]

    # Get x-axis data (time or frequency)
    x_var = raw_data.variables[0]
    x_data = raw_data.data[:, 0]
    is_freq = x_var.name.lower() == "frequency"

    # Handle complex data (AC analysis produces complex values)
    if np.iscomplexobj(values):
        mag_db = 20.0 * np.log10(np.maximum(np.abs(values), 1e-30))
        phase_deg = np.degrees(np.angle(values))
        is_complex = True
    else:
        is_complex = False
        mag_db = None
        phase_deg = None

    # Determine plot type
    if plot_type == "auto":
        if is_freq and is_complex:
            plot_type = "bode"
        elif is_freq:
            plot_type = "spectrum"
        else:
            plot_type = "time"

    # Generate SVG
    if plot_type == "bode":
        if mag_db is None:
            mag_db = np.real(values)
            phase_deg = None
        freq = np.real(x_data)
        svg = plot_bode(
            freq=freq, mag_db=mag_db, phase_deg=phase_deg,
            title=f"Bode Plot — {actual_name}",
        )
    elif plot_type == "spectrum":
        freq = np.real(x_data)
        if mag_db is None:
            mag_db = 20.0 * np.log10(np.maximum(np.abs(np.real(values)), 1e-30))
        svg = plot_spectrum(
            freq=freq, mag_db=mag_db,
            title=f"Spectrum — {actual_name}",
        )
    else:  # time
        svg = plot_timeseries(
            time=np.real(x_data), values=np.real(values),
            title=f"Time Domain — {actual_name}",
            ylabel=actual_name,
        )

    # Save SVG
    if output_path is None:
        out = Path(tempfile.mktemp(suffix=".svg", prefix="ltspice_plot_"))
    else:
        out = Path(output_path)
    out.write_text(svg)

    return {
        "svg_path": str(out),
        "plot_type": plot_type,
        "signal": actual_name,
        "points": len(x_data),
    }


# ============================================================================
# BATCH SIMULATION TOOLS
# ============================================================================


@mcp.tool()
async def parameter_sweep(
    netlist_text: str,
    param_name: str,
    start: float,
    stop: float,
    num_points: int = 10,
    timeout_seconds: float = 300,
) -> dict:
    """Sweep a parameter across a range of values.

    Runs multiple simulations, substituting the parameter value each time.
    The netlist should contain a .param directive for the parameter.

    Args:
        netlist_text: Netlist with .param directive
        param_name: Parameter to sweep (e.g., "Rval")
        start: Start value
        stop: Stop value
        num_points: Number of sweep points
        timeout_seconds: Per-simulation timeout
    """
    values = np.linspace(start, stop, num_points).tolist()
    result = await run_parameter_sweep(
        netlist_text,
        param_name,
        values,
        timeout=timeout_seconds,
    )

    return {
        "success_count": result.success_count,
        "failure_count": result.failure_count,
        "total_elapsed": result.total_elapsed,
        "parameter_values": result.parameter_values,
        "raw_files": [str(r.raw_file) if r.raw_file else None for r in result.results],
    }


@mcp.tool()
async def temperature_sweep(
    netlist_text: str,
    temperatures: list[float],
    timeout_seconds: float = 300,
) -> dict:
    """Run simulations at different temperatures.

    Args:
        netlist_text: Netlist text
        temperatures: List of temperatures in degrees C
        timeout_seconds: Per-simulation timeout
    """
    result = await run_temperature_sweep(
        netlist_text,
        temperatures,
        timeout=timeout_seconds,
    )

    return {
        "success_count": result.success_count,
        "failure_count": result.failure_count,
        "total_elapsed": result.total_elapsed,
        "parameter_values": result.parameter_values,
        "raw_files": [str(r.raw_file) if r.raw_file else None for r in result.results],
    }


@mcp.tool()
async def monte_carlo(
    netlist_text: str,
    n_runs: int,
    tolerances: dict[str, float],
    timeout_seconds: float = 300,
    seed: int | None = None,
) -> dict:
    """Run Monte Carlo analysis with component tolerances.

    Randomly varies component values within tolerance using a normal
    distribution, then runs simulations for each variant.

    Args:
        netlist_text: Netlist text
        n_runs: Number of Monte Carlo iterations
        tolerances: Component tolerances, e.g. {"R1": 0.05} for 5%
        timeout_seconds: Per-simulation timeout
        seed: Optional RNG seed for reproducibility
    """
    result = await run_monte_carlo(
        netlist_text,
        n_runs,
        tolerances,
        timeout=timeout_seconds,
        seed=seed,
    )

    return {
        "success_count": result.success_count,
        "failure_count": result.failure_count,
        "total_elapsed": result.total_elapsed,
        "parameter_values": result.parameter_values,
        "raw_files": [str(r.raw_file) if r.raw_file else None for r in result.results],
    }


# ============================================================================
# SCHEMATIC GENERATION TOOLS
# ============================================================================


_ASC_TEMPLATES: dict[str, dict] = {
    "rc_lowpass": {
        "func": generate_rc_lowpass_asc,
        "description": "RC lowpass filter with AC analysis",
        "params": {"r": "1k", "c": "100n"},
    },
    "voltage_divider": {
        "func": generate_voltage_divider_asc,
        "description": "Resistive voltage divider with .op analysis",
        "params": {"r1": "10k", "r2": "10k", "vin": "5"},
    },
    "inverting_amp": {
        "func": generate_inverting_amp,
        "description": "Inverting op-amp amplifier (gain = -Rf/Rin)",
        "params": {"rin": "10k", "rf": "100k"},
    },
    "non_inverting_amp": {
        "func": generate_noninv_amp_asc,
        "description": "Non-inverting op-amp amplifier (gain = 1 + Rf/Rin)",
        "params": {"rin": "10k", "rf": "100k"},
    },
    "common_emitter_amp": {
        "func": generate_ce_amp_asc,
        "description": "Common-emitter BJT amplifier with voltage divider bias",
        "params": {
            "rc": "2.2k", "rb1": "56k", "rb2": "12k", "re": "1k",
            "cc_in": "10u", "cc_out": "10u", "ce": "47u",
            "vcc": "12", "bjt_model": "2N2222",
        },
    },
    "colpitts_oscillator": {
        "func": generate_colpitts_asc,
        "description": "Colpitts oscillator with LC tank and BJT",
        "params": {
            "ind": "1u", "c1": "100p", "c2": "100p",
            "rb": "47k", "rc": "1k", "re": "470",
            "vcc": "12", "bjt_model": "2N2222",
        },
    },
    "differential_amp": {
        "func": generate_diff_amp_asc,
        "description": "Differential amplifier with op-amp",
        "params": {"r1": "10k", "r2": "10k", "r3": "10k", "r4": "10k"},
    },
    "buck_converter": {
        "func": generate_buck_converter_asc,
        "description": "Buck (step-down) converter with NMOS switch",
        "params": {
            "ind": "10u", "c_out": "100u", "r_load": "10",
            "v_in": "12", "duty_cycle": "0.5", "freq": "100k",
            "mosfet_model": "IRF540N", "diode_model": "1N5819",
        },
    },
    "ldo_regulator": {
        "func": generate_ldo_asc,
        "description": "LDO voltage regulator with PMOS pass transistor",
        "params": {
            "r1": "10k", "r2": "10k", "pass_transistor": "IRF9540N",
            "v_in": "8", "v_ref": "2.5",
        },
    },
    "h_bridge": {
        "func": generate_h_bridge_asc,
        "description": "H-bridge motor driver with 4 NMOS transistors",
        "params": {"v_supply": "12", "r_load": "10", "mosfet_model": "IRF540N"},
    },
    "sallen_key_lowpass": {
        "func": generate_sallen_key_asc,
        "description": "Sallen-Key lowpass filter (unity gain, 2nd order)",
        "params": {"r1": "10k", "r2": "10k", "c1": "10n", "c2": "10n"},
    },
    "boost_converter": {
        "func": generate_boost_converter_asc,
        "description": "Boost (step-up) converter with NMOS switch",
        "params": {
            "ind": "10u", "c_out": "100u", "r_load": "50",
            "v_in": "5", "duty_cycle": "0.5", "freq": "100k",
        },
    },
    "instrumentation_amp": {
        "func": generate_inamp_asc,
        "description": "3-opamp instrumentation amplifier",
        "params": {"r1": "10k", "r2": "10k", "r3": "10k", "r_gain": "10k"},
    },
    "current_mirror": {
        "func": generate_current_mirror_asc,
        "description": "BJT current mirror with reference and load",
        "params": {"r_ref": "10k", "r_load": "1k", "vcc": "12"},
    },
    "transimpedance_amp": {
        "func": generate_tia_asc,
        "description": "Transimpedance amplifier (current to voltage)",
        "params": {"rf": "100k", "cf": "1p", "i_source": "1u"},
    },
}


@mcp.tool()
def generate_schematic(
    template: str,
    params: dict[str, str] | None = None,
    output_path: str | None = None,
) -> dict:
    """Generate an LTspice .asc graphical schematic file from a template.

    Creates a ready-to-simulate .asc file with proper component placement,
    wire routing, and simulation directives.

    Available templates (use list_templates for full details):
    - rc_lowpass, voltage_divider, inverting_amp, non_inverting_amp,
      common_emitter_amp, colpitts_oscillator, differential_amp,
      buck_converter, ldo_regulator, h_bridge,
      sallen_key_lowpass, boost_converter, instrumentation_amp,
      current_mirror, transimpedance_amp

    Args:
        template: Template name (see list above)
        params: Override default parameters as key-value pairs.
            Use list_templates to see available parameters for each template.
        output_path: Where to save the .asc file (None = auto in /tmp)
    """
    if template not in _ASC_TEMPLATES:
        names = ", ".join(sorted(_ASC_TEMPLATES))
        return {"error": f"Unknown template '{template}'. Available: {names}"}

    entry = _ASC_TEMPLATES[template]
    call_params: dict[str, str | float] = {}

    if params:
        for k, v in params.items():
            if k not in entry["params"]:
                valid = ", ".join(sorted(entry["params"]))
                return {
                    "error": f"Unknown param '{k}' for {template}. Valid: {valid}"
                }
            # duty_cycle needs float conversion for buck_converter
            if k == "duty_cycle":
                call_params[k] = float(v)
            else:
                call_params[k] = v

    sch = entry["func"](**call_params)

    if output_path is None:
        output_path = str(Path(tempfile.gettempdir()) / f"{template}.asc")

    saved = sch.save(output_path)
    return {
        "success": True,
        "output_path": str(saved),
        "template": template,
        "schematic_preview": sch.render()[:500],
    }


# ============================================================================
# TOUCHSTONE / S-PARAMETER TOOLS
# ============================================================================


@mcp.tool()
def read_touchstone(file_path: str) -> dict:
    """Parse a Touchstone (.s1p, .s2p, .snp) S-parameter file.

    Returns S-parameter data, frequency points, and port information.

    Args:
        file_path: Path to Touchstone file
    """
    try:
        data = parse_touchstone(file_path)
    except (ValueError, FileNotFoundError) as e:
        return {"error": str(e)}

    # Convert S-parameter data to a more digestible format
    s_params = {}
    for i in range(data.n_ports):
        for j in range(data.n_ports):
            key = f"S{i + 1}{j + 1}"
            s_data = data.data[:, i, j]
            s_params[key] = {
                "magnitude_db": s_param_to_db(s_data).tolist(),
            }

    return {
        "filename": data.filename,
        "n_ports": data.n_ports,
        "n_frequencies": len(data.frequencies),
        "freq_range_hz": [float(data.frequencies[0]), float(data.frequencies[-1])],
        "reference_impedance": data.reference_impedance,
        "s_parameters": s_params,
        "comments": data.comments[:5],  # First 5 comment lines
    }


# ============================================================================
# SCHEMATIC TOOLS
# ============================================================================


@mcp.tool()
def read_schematic(schematic_path: str) -> dict:
    """Read and parse an LTspice schematic file.

    Returns component list, net names, and SPICE directives.

    Args:
        schematic_path: Path to .asc schematic file
    """
    sch = parse_schematic(schematic_path)

    return {
        "version": sch.version,
        "components": [
            {
                "name": c.name,
                "symbol": c.symbol,
                "value": c.value,
                "x": c.x,
                "y": c.y,
                "attributes": c.attributes,
            }
            for c in sch.components
        ],
        "nets": [f.name for f in sch.flags],
        "directives": sch.get_spice_directives(),
        "wire_count": len(sch.wires),
    }


@mcp.tool()
def edit_component(
    schematic_path: str,
    component_name: str,
    new_value: str,
    output_path: str | None = None,
) -> dict:
    """Modify a component's value in a schematic.

    Args:
        schematic_path: Path to .asc schematic file
        component_name: Instance name like "R1", "C2", "M1"
        new_value: New value string, e.g., "10k", "100n", "2N7000"
        output_path: Where to save (None = overwrite original)
    """
    try:
        sch = modify_component_value(
            schematic_path,
            component_name,
            new_value,
            output_path,
        )
        comp = sch.get_component(component_name)
        return {
            "success": True,
            "component": component_name,
            "new_value": new_value,
            "output_path": output_path or schematic_path,
            "symbol": comp.symbol if comp else None,
        }
    except ValueError as e:
        return {"success": False, "error": str(e)}


@mcp.tool()
def diff_schematics(
    schematic_a: str,
    schematic_b: str,
) -> dict:
    """Compare two schematics and show what changed.

    Reports component additions, removals, value changes,
    directive changes, and wire/net topology differences.

    Args:
        schematic_a: Path to "before" .asc file
        schematic_b: Path to "after" .asc file
    """
    diff = _diff_schematics(schematic_a, schematic_b)
    return diff.to_dict()


@mcp.tool()
def run_drc(schematic_path: str) -> dict:
    """Run design rule checks on a schematic.

    Checks for common issues:
    - Missing ground connection
    - Floating nodes
    - Missing simulation directive
    - Voltage source loops
    - Missing component values
    - Duplicate component names
    - Unconnected components

    Args:
        schematic_path: Path to .asc schematic file
    """
    result = _run_drc(schematic_path)
    return result.to_dict()


# ============================================================================
# NETLIST BUILDER TOOLS
# ============================================================================


@mcp.tool()
def create_netlist(
    title: str,
    components: list[dict],
    directives: list[str],
    output_path: str | None = None,
) -> dict:
    """Create a SPICE netlist programmatically and save to a .cir file.

    Build circuits from scratch without needing a graphical schematic.
    The created .cir file can be simulated with simulate_netlist.

    Args:
        title: Circuit title/description
        components: List of component dicts, each with:
            - name: Component name (R1, C1, V1, M1, X1, etc.)
            - nodes: List of node names (use "0" for ground)
            - value: Value or model name
            - params: Optional extra parameters string
        directives: List of SPICE directives, e.g.:
            [".tran 10m", ".ac dec 100 1 1meg",
             ".meas tran vmax MAX V(out)"]
        output_path: Where to save .cir file (None = auto in /tmp)

    Example components:
        [
            {"name": "V1", "nodes": ["in", "0"], "value": "AC 1"},
            {"name": "R1", "nodes": ["in", "out"], "value": "10k"},
            {"name": "C1", "nodes": ["out", "0"], "value": "100n"}
        ]
    """
    nl = Netlist(title=title)

    for comp in components:
        nl.add_component(
            name=comp["name"],
            nodes=comp["nodes"],
            value=comp["value"],
            params=comp.get("params", ""),
        )

    for directive in directives:
        nl.add_directive(directive)

    # Determine output path
    if output_path is None:
        safe_title = "".join(c if c.isalnum() else "_" for c in title)[:30]
        output_path = str(Path(tempfile.gettempdir()) / f"{safe_title}.cir")

    saved = nl.save(output_path)

    return {
        "success": True,
        "output_path": str(saved),
        "netlist_preview": nl.render(),
        "component_count": len(nl.components),
    }


# ============================================================================
# CIRCUIT TEMPLATE TOOLS
# ============================================================================

# Registry of netlist templates with parameter metadata
_TEMPLATES: dict[str, dict] = {
    "voltage_divider": {
        "func": voltage_divider,
        "description": "Resistive voltage divider with .op or custom analysis",
        "params": {"v_in": "5", "r1": "10k", "r2": "10k", "sim_type": "op"},
    },
    "rc_lowpass": {
        "func": rc_lowpass,
        "description": "RC lowpass filter with AC sweep",
        "params": {"r": "1k", "c": "100n", "f_start": "1", "f_stop": "1meg"},
    },
    "inverting_amplifier": {
        "func": inverting_amplifier,
        "description": "Inverting op-amp (gain = -Rf/Rin), +/-15V supply",
        "params": {"r_in": "10k", "r_f": "100k", "opamp_model": "LT1001"},
    },
    "non_inverting_amplifier": {
        "func": non_inverting_amplifier,
        "description": "Non-inverting op-amp (gain = 1 + Rf/Rin), +/-15V supply",
        "params": {"r_in": "10k", "r_f": "100k", "opamp_model": "LT1001"},
    },
    "differential_amplifier": {
        "func": differential_amplifier,
        "description": "Diff amp: Vout = (R2/R1)*(V2-V1), +/-15V supply",
        "params": {
            "r1": "10k",
            "r2": "10k",
            "r3": "10k",
            "r4": "10k",
            "opamp_model": "LT1001",
        },
    },
    "common_emitter_amplifier": {
        "func": common_emitter_amplifier,
        "description": "BJT common-emitter with voltage divider bias",
        "params": {
            "rc": "2.2k",
            "rb1": "56k",
            "rb2": "12k",
            "re": "1k",
            "cc1": "10u",
            "cc2": "10u",
            "ce": "47u",
            "vcc": "12",
            "bjt_model": "2N2222",
        },
    },
    "buck_converter": {
        "func": buck_converter,
        "description": "Step-down DC-DC converter with MOSFET switch",
        "params": {
            "ind": "10u",
            "c_out": "100u",
            "r_load": "10",
            "v_in": "12",
            "duty_cycle": "0.5",
            "freq": "100k",
            "mosfet_model": "IRF540N",
            "diode_model": "1N5819",
        },
    },
    "ldo_regulator": {
        "func": ldo_regulator,
        "description": "LDO regulator: Vout = Vref * (1 + R1/R2)",
        "params": {
            "opamp_model": "LT1001",
            "r1": "10k",
            "r2": "10k",
            "pass_transistor": "IRF9540N",
            "v_in": "8",
            "v_ref": "2.5",
        },
    },
    "colpitts_oscillator": {
        "func": colpitts_oscillator,
        "description": "LC oscillator: f ~ 1/(2pi*sqrt(L*Cseries))",
        "params": {
            "ind": "1u",
            "c1": "100p",
            "c2": "100p",
            "rb": "47k",
            "rc": "1k",
            "re": "470",
            "vcc": "12",
            "bjt_model": "2N2222",
        },
    },
    "h_bridge": {
        "func": h_bridge,
        "description": "4-MOSFET H-bridge motor driver with dead time",
        "params": {
            "v_supply": "12",
            "r_load": "10",
            "mosfet_model": "IRF540N",
        },
    },
    "sallen_key_lowpass": {
        "func": sallen_key_lowpass,
        "description": "Sallen-Key lowpass filter (unity gain, 2nd order)",
        "params": {
            "r1": "10k",
            "r2": "10k",
            "c1": "10n",
            "c2": "10n",
            "opamp_model": "LT1001",
        },
    },
    "boost_converter": {
        "func": boost_converter,
        "description": "Step-up DC-DC converter with MOSFET switch",
        "params": {
            "ind": "10u",
            "c_out": "100u",
            "r_load": "50",
            "v_in": "5",
            "duty_cycle": "0.5",
            "freq": "100k",
            "mosfet_model": "IRF540N",
            "diode_model": "1N5819",
        },
    },
    "instrumentation_amplifier": {
        "func": instrumentation_amplifier,
        "description": "3-opamp instrumentation amp: gain = 1 + 2*R1/R_gain",
        "params": {
            "r1": "10k",
            "r2": "10k",
            "r3": "10k",
            "r_gain": "10k",
        },
    },
    "current_mirror": {
        "func": current_mirror,
        "description": "BJT current mirror with reference resistor",
        "params": {
            "r_ref": "10k",
            "r_load": "1k",
            "vcc": "12",
            "bjt_model": "2N2222",
        },
    },
    "transimpedance_amplifier": {
        "func": transimpedance_amplifier,
        "description": "TIA: converts input current to output voltage (Vout = -Iph * Rf)",
        "params": {
            "rf": "100k",
            "cf": "1p",
            "i_source": "1u",
        },
    },
}


@mcp.tool()
def create_from_template(
    template_name: str,
    params: dict[str, str] | None = None,
    output_path: str | None = None,
) -> dict:
    """Create a circuit netlist from a pre-built template.

    Available templates:
    - voltage_divider: params {v_in, r1, r2, sim_type}
    - rc_lowpass: params {r, c, f_start, f_stop}
    - inverting_amplifier: params {r_in, r_f, opamp_model}
    - non_inverting_amplifier: params {r_in, r_f, opamp_model}
    - differential_amplifier: params {r1, r2, r3, r4, opamp_model}
    - common_emitter_amplifier: params {rc, rb1, rb2, re, cc1, cc2, ce, vcc, bjt_model}
    - buck_converter: params {ind, c_out, r_load, v_in, duty_cycle, freq, mosfet_model, diode_model}
    - ldo_regulator: params {opamp_model, r1, r2, pass_transistor, v_in, v_ref}
    - colpitts_oscillator: params {ind, c1, c2, rb, rc, re, vcc, bjt_model}
    - h_bridge: params {v_supply, r_load, mosfet_model}
    - sallen_key_lowpass: params {r1, r2, c1, c2, opamp_model}
    - boost_converter: params {ind, c_out, r_load, v_in, duty_cycle, freq, mosfet_model, diode_model}
    - instrumentation_amplifier: params {r1, r2, r3, r_gain}
    - current_mirror: params {r_ref, r_load, vcc, bjt_model}
    - transimpedance_amplifier: params {rf, cf, i_source}

    All parameter values are optional -- defaults are used if omitted.

    Args:
        template_name: Template name from the list above
        params: Optional dict of parameter overrides (all values as strings)
        output_path: Where to save .cir file (None = auto in /tmp)
    """
    template = _TEMPLATES.get(template_name)
    if template is None:
        return {
            "error": f"Unknown template '{template_name}'",
            "available_templates": [
                {"name": k, "description": v["description"], "params": v["params"]}
                for k, v in _TEMPLATES.items()
            ],
        }

    # Build kwargs from params, converting duty_cycle to float for buck_converter
    kwargs: dict = {}
    if params:
        for k, v in params.items():
            if k not in template["params"]:
                return {
                    "error": f"Unknown parameter '{k}' for template '{template_name}'",
                    "valid_params": template["params"],
                }
            # duty_cycle needs to be float, not string
            if k == "duty_cycle":
                kwargs[k] = float(v)
            else:
                kwargs[k] = v

    nl = template["func"](**kwargs)

    if output_path is None:
        output_path = str(Path(tempfile.gettempdir()) / f"{template_name}.cir")

    saved = nl.save(output_path)

    return {
        "success": True,
        "template": template_name,
        "description": template["description"],
        "output_path": str(saved),
        "netlist_preview": nl.render(),
        "component_count": len(nl.components),
        "params_used": {**template["params"], **(params or {})},
    }


@mcp.tool()
def list_templates() -> dict:
    """List all available circuit templates with their parameters and defaults.

    Returns template names, descriptions, and the parameters each accepts
    with their default values.
    """
    return {
        "templates": [
            {
                "name": name,
                "description": info["description"],
                "params": info["params"],
            }
            for name, info in _TEMPLATES.items()
        ],
        "total_count": len(_TEMPLATES),
    }


# ============================================================================
# LIBRARY & MODEL TOOLS
# ============================================================================


@mcp.tool()
def list_symbols(
    category: str | None = None,
    search: str | None = None,
    limit: int = 50,
) -> dict:
    """List available component symbols from LTspice library.

    Args:
        category: Filter by category (e.g., "Opamps", "Comparators")
        search: Search term for symbol name (case-insensitive)
        limit: Maximum results to return
    """
    symbols = []
    sym_dir = LTSPICE_LIB / "sym"

    if not sym_dir.exists():
        return {"error": "Symbol library not found", "symbols": [], "total_count": 0}

    for asy_file in sym_dir.rglob("*.asy"):
        rel_path = asy_file.relative_to(sym_dir)
        cat = str(rel_path.parent) if rel_path.parent != Path(".") else "misc"
        name = asy_file.stem

        if category and cat.lower() != category.lower():
            continue
        if search and search.lower() not in name.lower():
            continue

        symbols.append({"name": name, "category": cat, "path": str(asy_file)})

    symbols.sort(key=lambda x: x["name"].lower())
    total = len(symbols)
    return {"symbols": symbols[:limit], "total_count": total, "returned_count": min(limit, total)}


@mcp.tool()
def list_examples(
    category: str | None = None,
    search: str | None = None,
    limit: int = 50,
) -> dict:
    """List example circuits from LTspice examples library.

    Args:
        category: Filter by category folder
        search: Search term for example name
        limit: Maximum results to return
    """
    examples = []

    if not LTSPICE_EXAMPLES.exists():
        return {"error": "Examples not found", "examples": [], "total_count": 0}

    for asc_file in LTSPICE_EXAMPLES.rglob("*.asc"):
        rel_path = asc_file.relative_to(LTSPICE_EXAMPLES)
        cat = str(rel_path.parent) if rel_path.parent != Path(".") else "misc"
        name = asc_file.stem

        if category and cat.lower() != category.lower():
            continue
        if search and search.lower() not in name.lower():
            continue

        examples.append({"name": name, "category": cat, "path": str(asc_file)})

    examples.sort(key=lambda x: x["name"].lower())
    total = len(examples)
    return {"examples": examples[:limit], "total_count": total, "returned_count": min(limit, total)}


@mcp.tool()
def get_symbol_info(symbol_path: str) -> dict:
    """Get detailed information about a component symbol.

    Reads the .asy file to extract pin names, attributes, and description.

    Args:
        symbol_path: Path to .asy symbol file
    """
    path = Path(symbol_path)
    if not path.exists():
        return {"error": f"Symbol not found: {symbol_path}"}

    content = path.read_text(errors="replace")
    lines = content.split("\n")

    info = {
        "name": path.stem,
        "pins": [],
        "attributes": {},
        "description": "",
        "prefix": "",
        "spice_prefix": "",
    }

    for line in lines:
        line = line.strip()

        if line.startswith("PIN"):
            parts = line.split()
            if len(parts) >= 5:
                info["pins"].append(
                    {
                        "x": int(parts[1]),
                        "y": int(parts[2]),
                        "justification": parts[3],
                        "rotation": parts[4] if len(parts) > 4 else "0",
                    }
                )

        elif line.startswith("PINATTR PinName"):
            pin_name = line.split(None, 2)[2] if len(line.split()) > 2 else ""
            if info["pins"]:
                info["pins"][-1]["name"] = pin_name

        elif line.startswith("SYMATTR"):
            parts = line.split(None, 2)
            if len(parts) >= 3:
                attr_name = parts[1]
                attr_value = parts[2]
                info["attributes"][attr_name] = attr_value
                if attr_name == "Description":
                    info["description"] = attr_value
                elif attr_name == "Prefix":
                    info["prefix"] = attr_value
                elif attr_name == "SpiceModel":
                    info["spice_prefix"] = attr_value

    return info


@mcp.tool()
def search_spice_models(
    search: str | None = None,
    model_type: str | None = None,
    limit: int = 50,
) -> dict:
    """Search for SPICE .model definitions in the library.

    Finds transistors, diodes, and other discrete devices.

    Args:
        search: Search term for model name (case-insensitive)
        model_type: Filter by type: NPN, PNP, NMOS, PMOS, D, NJF, PJF
        limit: Maximum results
    """
    models = _search_models(search=search, model_type=model_type, limit=limit)
    return {
        "models": [
            {
                "name": m.name,
                "type": m.type,
                "source_file": m.source_file,
                "parameters": m.parameters,
            }
            for m in models
        ],
        "total_count": len(models),
    }


@mcp.tool()
def search_spice_subcircuits(
    search: str | None = None,
    limit: int = 50,
) -> dict:
    """Search for SPICE .subckt definitions (op-amps, ICs, etc.).

    Args:
        search: Search term for subcircuit name
        limit: Maximum results
    """
    subs = _search_subcircuits(search=search, limit=limit)
    return {
        "subcircuits": [
            {
                "name": s.name,
                "pins": s.pins,
                "pin_names": s.pin_names,
                "description": s.description,
                "source_file": s.source_file,
                "n_components": s.n_components,
            }
            for s in subs
        ],
        "total_count": len(subs),
    }


@mcp.tool()
def check_installation() -> dict:
    """Verify LTspice and Wine are properly installed."""
    ok, msg = validate_installation()
    from .config import LTSPICE_DIR, LTSPICE_EXE, WINE_PREFIX

    return {
        "valid": ok,
        "message": msg,
        "paths": {
            "ltspice_dir": str(LTSPICE_DIR),
            "ltspice_exe": str(LTSPICE_EXE),
            "wine_prefix": str(WINE_PREFIX),
            "lib_dir": str(LTSPICE_LIB),
            "examples_dir": str(LTSPICE_EXAMPLES),
        },
        "lib_exists": LTSPICE_LIB.exists(),
        "examples_exist": LTSPICE_EXAMPLES.exists(),
    }


# ============================================================================
# RESOURCES
# ============================================================================


@mcp.resource("ltspice://symbols")
def resource_symbols() -> str:
    """All available LTspice symbols organized by category."""
    result = list_symbols(limit=10000)
    return json.dumps(result, indent=2)


@mcp.resource("ltspice://examples")
def resource_examples() -> str:
    """All LTspice example circuits."""
    result = list_examples(limit=10000)
    return json.dumps(result, indent=2)


@mcp.resource("ltspice://status")
def resource_status() -> str:
    """Current LTspice installation status."""
    return json.dumps(check_installation(), indent=2)


@mcp.resource("ltspice://templates")
def resource_templates() -> str:
    """All available circuit templates (netlist and schematic) with parameters."""
    templates = {
        "netlist_templates": [
            {"name": name, "description": info["description"], "params": info["params"]}
            for name, info in _TEMPLATES.items()
        ],
        "schematic_templates": [
            {"name": name, "description": info["description"], "params": info["params"]}
            for name, info in _ASC_TEMPLATES.items()
        ],
    }
    return json.dumps(templates, indent=2)


@mcp.resource("ltspice://template/{name}")
def resource_template_detail(name: str) -> str:
    """Detailed information about a specific circuit template."""
    # Check both registries
    netlist_info = _TEMPLATES.get(name)
    asc_info = _ASC_TEMPLATES.get(name)

    if not netlist_info and not asc_info:
        return json.dumps({"error": f"Template '{name}' not found"})

    result = {"name": name}
    if netlist_info:
        result["netlist"] = {
            "description": netlist_info["description"],
            "params": netlist_info["params"],
            "type": "netlist (.cir)",
        }
    if asc_info:
        result["schematic"] = {
            "description": asc_info["description"],
            "params": asc_info["params"],
            "type": "schematic (.asc)",
        }

    return json.dumps(result, indent=2)


# ============================================================================
# PROMPTS
# ============================================================================


@mcp.prompt()
def design_filter(
    filter_type: str = "lowpass",
    topology: str = "rc",
    cutoff_freq: str = "1kHz",
) -> list:
    """Guide through designing and simulating a filter circuit.

    Args:
        filter_type: lowpass, highpass, bandpass, or notch
        topology: rc (1st order), rlc (2nd order), or sallen-key (active)
        cutoff_freq: Target cutoff frequency with units
    """
    return [Message(role="user", content=f"""Design a {filter_type} filter with these requirements:
- Topology: {topology}
- Cutoff frequency: {cutoff_freq}

Workflow:
1. Use create_netlist to build the circuit
2. Add .ac analysis directive for frequency sweep
3. Add .meas directive for -3dB bandwidth
4. Simulate with simulate_netlist
5. Use measure_bandwidth to verify cutoff frequency
6. Use get_waveform to inspect the frequency response
7. Adjust component values with create_netlist if needed

Tips:
- For RC lowpass: f_c = 1/(2*pi*R*C)
- For 2nd order: Q controls peaking, Butterworth Q=0.707
- Use search_spice_models to find op-amp models for active filters
""")]


@mcp.prompt()
def analyze_power_supply(schematic_path: str = "") -> list:
    """Guide through analyzing a power supply circuit.

    Args:
        schematic_path: Path to the power supply schematic
    """
    path_instruction = (
        f"The schematic is at: {schematic_path}"
        if schematic_path
        else "First, identify or create the power supply schematic."
    )

    return [Message(role="user", content=f"""Analyze a power supply circuit for key performance metrics.

{path_instruction}

Workflow:
1. Use read_schematic to understand the circuit topology
2. Use run_drc to check for design issues
3. Simulate with .tran analysis (include load step if applicable)
4. Use analyze_waveform with these analyses:
   - "peak_to_peak" on output for ripple measurement
   - "settling_time" for transient response
   - "fft" on output to identify noise frequencies
5. If AC analysis available, use measure_bandwidth for loop gain

Key metrics to extract:
- Output voltage regulation (DC accuracy)
- Ripple voltage (peak-to-peak on output)
- Load transient response (settling time after step)
- Efficiency (input power vs output power)
""")]


@mcp.prompt()
def debug_circuit(schematic_path: str = "") -> list:
    """Guide through debugging a circuit that isn't working.

    Args:
        schematic_path: Path to the problematic schematic
    """
    path_instruction = (
        f"The schematic is at: {schematic_path}"
        if schematic_path
        else "First, identify the schematic file."
    )

    return [Message(role="user", content=f"""Systematic approach to debugging a circuit.

{path_instruction}

Step 1 - Validate the schematic:
- Use run_drc to catch obvious issues (missing ground, floating nodes)
- Use read_schematic to review component values and connections

Step 2 - Check simulation setup:
- Verify simulation directives are correct
- Check that models/subcircuits are available (search_spice_models)

Step 3 - Run and analyze:
- Simulate the circuit
- Use get_waveform to inspect key node voltages
- Compare expected vs actual values at each stage

Step 4 - Isolate the problem:
- Use edit_component to simplify (replace active devices with ideal)
- Use diff_schematics to track what changes fixed the issue
- Re-simulate after each change

Common issues:
- Wrong node connections (check wire endpoints)
- Missing bias voltages or ground
- Component values off by orders of magnitude
- Wrong model (check with search_spice_models)
""")]


@mcp.prompt()
def optimize_design(
    circuit_type: str = "filter",
    target_spec: str = "1kHz bandwidth",
) -> list:
    """Guide through optimizing a circuit to meet target specifications.

    Args:
        circuit_type: Type of circuit (filter, amplifier, regulator, oscillator)
        target_spec: Target specification to achieve
    """
    return [Message(role="user", content=f"""Optimize a {circuit_type} circuit to achieve: {target_spec}

Workflow:
1. Start with a template: use list_templates to see available circuits
2. Create the initial circuit with create_from_template
3. Simulate and measure the current performance
4. Use optimize_circuit to automatically tune component values:
   - Define target metrics (bandwidth, gain, settling time, etc.)
   - Specify component ranges with preferred E-series values
   - Let the optimizer iterate (typically 10-20 simulations)
5. Verify the optimized design with a full simulation
6. Run Monte Carlo (monte_carlo tool) to check yield with tolerances

Tips:
- Start with reasonable initial values from the template
- Use E24 or E96 series for resistors/capacitors
- For filters: target bandwidth_hz metric
- For amplifiers: target gain_db and phase_margin_deg
- For regulators: target settling_time and peak_to_peak (ripple)
""")]


@mcp.prompt()
def monte_carlo_analysis(
    circuit_description: str = "RC filter",
    n_runs: str = "100",
) -> list:
    """Guide through Monte Carlo tolerance analysis.

    Args:
        circuit_description: What circuit to analyze
        n_runs: Number of Monte Carlo iterations
    """
    return [Message(role="user", content=f"""Run Monte Carlo tolerance analysis on: {circuit_description}
Number of runs: {n_runs}

Workflow:
1. Create or identify the netlist for your circuit
2. Use monte_carlo tool with component tolerances:
   - Resistors: typically 1% (0.01) or 5% (0.05)
   - Capacitors: typically 10% (0.1) or 20% (0.2)
   - Inductors: typically 10% (0.1)
3. For each completed run, extract key metrics:
   - Use get_waveform on each raw file
   - Use analyze_waveform for RMS, peak-to-peak, etc.
   - Use measure_bandwidth for filter circuits
4. Compute statistics across all runs:
   - Mean and standard deviation of each metric
   - Min/max (worst case)
   - Yield: what percentage meet spec?

Tips:
- Use list_simulation_runs to understand stepped data
- For stepped simulations, use get_waveform with run parameter
- Start with fewer runs (10-20) to verify setup, then scale up
- Set seed for reproducible results during development
- Typical component tolerances:
  - Metal film resistors: 1%
  - Ceramic capacitors: 10-20%
  - Electrolytic capacitors: 20%
  - Inductors: 10-20%
""")]


@mcp.prompt()
def circuit_from_scratch(
    description: str = "audio amplifier",
) -> list:
    """Guide through creating a complete circuit from scratch.

    Args:
        description: What circuit to build
    """
    return [Message(role="user", content=f"""Build a complete circuit from scratch: {description}

Approach 1 - Use a template (recommended for common circuits):
1. Use list_templates to see available circuit templates
2. Use create_from_template with custom parameters
3. Simulate with simulate_netlist
4. Analyze results with get_waveform and analyze_waveform

Approach 2 - Build from components:
1. Use create_netlist to define components and connections
2. Use search_spice_models to find transistor/diode models
3. Use search_spice_subcircuits to find op-amp/IC models
4. Add simulation directives (.tran, .ac, .dc, .op, .tf)
5. Simulate and analyze

Approach 3 - Graphical schematic (.asc file):
1. Use generate_schematic for any of 10 topologies: rc_lowpass,
   voltage_divider, inverting_amp, non_inverting_amp, common_emitter_amp,
   colpitts_oscillator, differential_amp, buck_converter, ldo_regulator,
   h_bridge
2. The .asc file can be opened in LTspice GUI for editing
3. Simulate with the simulate tool

Verification workflow:
1. Run run_drc to check for design issues before simulating
2. Start with .op analysis to verify DC bias point
3. Run .tf analysis for gain and impedance
4. Run .ac analysis for frequency response
5. Run .tran analysis for time-domain behavior
6. Use diff_schematics to compare design iterations
""")]


@mcp.prompt()
def troubleshoot_simulation(
    error_description: str = "",
    schematic_path: str = "",
) -> list:
    """Systematic checklist for diagnosing simulation failures.

    Args:
        error_description: What went wrong (error message, unexpected results, etc.)
        schematic_path: Path to the problematic schematic or netlist
    """
    path_note = (
        f"Schematic/netlist: {schematic_path}"
        if schematic_path
        else "First, identify the schematic or netlist file."
    )
    error_note = (
        f"Reported issue: {error_description}"
        if error_description
        else "No specific error described -- run full diagnostic."
    )

    return [Message(role="user", content=f"""Troubleshoot a simulation that isn't working correctly.

{path_note}
{error_note}

Diagnostic checklist (work through in order):

1. **Design Rule Check**
   - Run run_drc on the schematic
   - Fix any: missing ground, floating nodes, duplicate names, missing sim directive

2. **Installation & Setup**
   - Run check_installation to verify Wine + LTspice
   - Check that required .lib files exist

3. **Model Availability**
   - Use search_spice_models to verify all transistor/diode models exist
   - Use search_spice_subcircuits to verify op-amp models
   - Common issue: model name in schematic doesn't match library

4. **Simulation Directive**
   - Use read_schematic to check the directive
   - Verify analysis type matches what you want (.tran, .ac, .dc, .op, .tf)
   - For .tran: is the stop time long enough?
   - For .ac: are start/stop frequencies reasonable?

5. **Node Connections**
   - Use read_schematic to list all components and nets
   - Check for disconnected nodes (components not wired)
   - Verify ground connections on all return paths

6. **Run & Inspect**
   - Simulate the circuit
   - Check the log file for convergence warnings
   - Use get_waveform to inspect node voltages
   - Compare expected vs actual at each circuit stage

7. **Simplify & Isolate**
   - Use edit_component to replace active devices with ideal ones
   - Remove non-essential subcircuits
   - Test each stage independently
   - Add .ic directives if oscillators won't start

Common failure modes:
- Convergence failure: reduce timestep, add initial conditions
- All zeros: check ground connections and source polarity
- Unexpected clipping: check supply voltages and headroom
- Oscillation in DC circuit: add small capacitors on feedback
- Model not found: verify .lib/.include paths
""")]


# ============================================================================
# ENTRY POINT
# ============================================================================


def main():
    """Run the MCP server."""
    mcp.run()


if __name__ == "__main__":
    main()
