#!/usr/bin/env python3
"""Python runtime backend for benchmark runners."""

from __future__ import annotations

import csv
import importlib
import sys
import time
import zlib
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Sequence


@dataclass
class BackendRunResult:
    runtime_s: float
    steps: int
    mode: str
    telemetry: Dict[str, Optional[float]]


class BackendRunError(RuntimeError):
    """Structured backend execution error with optional diagnostic code."""

    def __init__(
        self,
        message: str,
        *,
        diagnostic: Optional[str] = None,
        mode: Optional[str] = None,
    ) -> None:
        super().__init__(message)
        self.diagnostic = diagnostic
        self.mode = mode


def _import_pulsim():
    ps, _ = _import_pulsim_with_diagnostics()
    return ps


def _import_pulsim_with_diagnostics() -> tuple[object | None, Optional[str]]:
    repo_root = Path(__file__).resolve().parent.parent
    build_python = repo_root / "build" / "python"
    last_error: Optional[str] = None

    def _clear_pulsim_modules() -> None:
        stale = [name for name in list(sys.modules) if name == "pulsim" or name.startswith("pulsim.")]
        for name in stale:
            sys.modules.pop(name, None)

    def _attempt_import() -> object | None:
        nonlocal last_error
        try:
            import pulsim as ps

            last_error = None
            return ps
        except Exception as exc:
            last_error = f"{exc.__class__.__name__}: {exc}"
            return None

    def _purge_shadow_paths() -> None:
        repo_build_python = str(build_python.resolve()) if build_python.exists() else None
        cleaned: list[str] = []
        for path in sys.path:
            try:
                resolved = str(Path(path).resolve())
            except Exception:
                resolved = path

            normalized = resolved.replace("\\", "/")
            is_shadow = normalized.endswith("/build/python")
            if repo_build_python is not None and resolved == repo_build_python:
                is_shadow = True

            if not is_shadow:
                cleaned.append(path)
        sys.path[:] = cleaned

    importlib.invalidate_caches()
    ps = _attempt_import()
    if ps is not None:
        return ps, None

    # If import failed due to a shadow package on PYTHONPATH (common in CI),
    # retry after removing build/python-like entries and clearing stale modules.
    _clear_pulsim_modules()
    _purge_shadow_paths()
    importlib.invalidate_caches()
    ps = _attempt_import()
    if ps is not None:
        return ps, None

    # Last fallback: explicitly prepend local build/python when available.
    if build_python.exists():
        build_python_str = str(build_python)
        if build_python_str not in sys.path:
            sys.path.insert(0, build_python_str)
        _clear_pulsim_modules()
        importlib.invalidate_caches()
        ps = _attempt_import()
        if ps is not None:
            return ps, None

    return None, last_error


def _has_runtime_api(ps: object) -> bool:
    required = [
        "YamlParser",
        "Simulator",
        "SimulationOptions",
        "SimulationResult",
        "PeriodicSteadyStateOptions",
        "HarmonicBalanceOptions",
        "FrequencyAnalysisOptions",
    ]
    return all(hasattr(ps, name) for name in required)


def _raise_if_parser_errors(parser: object) -> None:
    errors = list(getattr(parser, "errors", []))
    if errors:
        raise RuntimeError("; ".join(str(item) for item in errors))


def _write_state_csv(
    output_path: Path,
    times: Sequence[float],
    states: Sequence[Sequence[float]],
    signal_names: Sequence[str],
) -> int:
    if len(times) != len(states):
        raise RuntimeError("Simulation result time/state length mismatch")

    output_path.parent.mkdir(parents=True, exist_ok=True)

    with open(output_path, "w", newline="", encoding="utf-8") as handle:
        writer = csv.writer(handle)
        writer.writerow(["time", *signal_names])
        for t, state in zip(times, states):
            if len(state) != len(signal_names):
                raise RuntimeError("Simulation state size mismatch with circuit signals")
            row = [f"{float(t):.9e}"]
            row.extend(f"{float(value):.9e}" for value in state)
            writer.writerow(row)

    return max(0, len(times) - 1)


def _write_frequency_csv(
    output_path: Path,
    result: object,
) -> int:
    frequency = [float(value) for value in getattr(result, "frequency_hz", [])]
    response_real = [float(value) for value in getattr(result, "response_real", [])]
    response_imag = [float(value) for value in getattr(result, "response_imag", [])]
    magnitude = [float(value) for value in getattr(result, "magnitude", [])]
    magnitude_db = [float(value) for value in getattr(result, "magnitude_db", [])]
    phase_deg = [float(value) for value in getattr(result, "phase_deg", [])]

    n_points = len(frequency)
    vectors = [response_real, response_imag, magnitude, magnitude_db, phase_deg]
    if any(len(vector) != n_points for vector in vectors):
        raise RuntimeError("Frequency result vectors have inconsistent lengths")

    output_path.parent.mkdir(parents=True, exist_ok=True)
    with open(output_path, "w", newline="", encoding="utf-8") as handle:
        writer = csv.writer(handle)
        writer.writerow(
            [
                "frequency_hz",
                "response_real",
                "response_imag",
                "magnitude",
                "magnitude_db",
                "phase_deg",
            ]
        )
        for idx in range(n_points):
            writer.writerow(
                [
                    f"{frequency[idx]:.9e}",
                    f"{response_real[idx]:.9e}",
                    f"{response_imag[idx]:.9e}",
                    f"{magnitude[idx]:.9e}",
                    f"{magnitude_db[idx]:.9e}",
                    f"{phase_deg[idx]:.9e}",
                ]
            )

    return max(0, n_points - 1)


def _diagnostic_name_from_result(result: object) -> Optional[str]:
    diagnostic = getattr(result, "diagnostic", None)
    if diagnostic is None:
        return None
    name = getattr(diagnostic, "name", None)
    if isinstance(name, str) and name.strip():
        return name.strip()
    text = str(diagnostic).strip()
    return text or None


def _raise_result_failure(result: object, default_message: str, mode: str) -> None:
    message = str(getattr(result, "message", "") or default_message)
    diagnostic = _diagnostic_name_from_result(result)
    if diagnostic and diagnostic != "None":
        message = f"{diagnostic}: {message}"
    raise BackendRunError(message, diagnostic=diagnostic, mode=mode)


def _reshape_hb_solution(solution: Sequence[float], state_size: int) -> List[List[float]]:
    if state_size <= 0:
        raise RuntimeError("Invalid state size for harmonic balance output")
    if len(solution) % state_size != 0:
        raise RuntimeError("Harmonic balance solution size is not divisible by state size")

    samples = len(solution) // state_size
    out: List[List[float]] = []
    for idx in range(samples):
        start = idx * state_size
        end = start + state_size
        out.append([float(value) for value in solution[start:end]])
    return out


def _transient_telemetry(result: object, runtime_s: float) -> Dict[str, Optional[float]]:
    linear = getattr(result, "linear_solver_telemetry", None)
    backend = getattr(result, "backend_telemetry", None)

    state_space_primary_steps = (
        float(getattr(backend, "state_space_primary_steps", 0.0)) if backend is not None else None
    )
    dae_fallback_steps = (
        float(getattr(backend, "dae_fallback_steps", 0.0)) if backend is not None else None
    )
    segment_non_admissible_steps = (
        float(getattr(backend, "segment_non_admissible_steps", 0.0)) if backend is not None else None
    )
    segment_model_cache_hits = (
        float(getattr(backend, "segment_model_cache_hits", 0.0)) if backend is not None else None
    )
    segment_model_cache_misses = (
        float(getattr(backend, "segment_model_cache_misses", 0.0)) if backend is not None else None
    )
    linear_factor_cache_hits = (
        float(getattr(backend, "linear_factor_cache_hits", 0.0)) if backend is not None else None
    )
    linear_factor_cache_misses = (
        float(getattr(backend, "linear_factor_cache_misses", 0.0)) if backend is not None else None
    )
    runtime_module_count = (
        float(getattr(backend, "runtime_module_count", 0.0)) if backend is not None else None
    )
    runtime_module_order_crc32: Optional[float] = None
    runtime_module_count_match: Optional[float] = None
    if backend is not None:
        try:
            runtime_module_order = str(getattr(backend, "runtime_module_order", "")).strip()
            if runtime_module_order:
                runtime_module_order_crc32 = float(
                    zlib.crc32(runtime_module_order.encode("utf-8")) & 0xFFFFFFFF
                )
                normalized_order = runtime_module_order.replace(">", ",")
                order_tokens = [
                    token.strip()
                    for token in normalized_order.split(",")
                    if token.strip()
                ]
                if runtime_module_count is not None and runtime_module_count > 0.0:
                    runtime_module_count_match = (
                        1.0 if int(runtime_module_count) == len(order_tokens) else 0.0
                    )
        except Exception:
            runtime_module_order_crc32 = None
            runtime_module_count_match = None

    reserved_output_samples = (
        float(getattr(backend, "reserved_output_samples", 0.0)) if backend is not None else None
    )
    time_series_reallocations = (
        float(getattr(backend, "time_series_reallocations", 0.0)) if backend is not None else None
    )
    state_series_reallocations = (
        float(getattr(backend, "state_series_reallocations", 0.0)) if backend is not None else None
    )
    virtual_channel_reallocations = (
        float(getattr(backend, "virtual_channel_reallocations", 0.0))
        if backend is not None
        else None
    )
    output_reallocation_total: Optional[float] = None
    output_reallocation_free: Optional[float] = None
    if (
        time_series_reallocations is not None
        and state_series_reallocations is not None
        and virtual_channel_reallocations is not None
    ):
        output_reallocation_total = (
            time_series_reallocations
            + state_series_reallocations
            + virtual_channel_reallocations
        )
        output_reallocation_free = 1.0 if output_reallocation_total == 0.0 else 0.0

    state_space_primary_ratio: Optional[float]
    if state_space_primary_steps is None or dae_fallback_steps is None:
        state_space_primary_ratio = None
        dae_fallback_ratio = None
    else:
        total_path_steps = state_space_primary_steps + dae_fallback_steps
        state_space_primary_ratio = (
            state_space_primary_steps / total_path_steps if total_path_steps > 0.0 else None
        )
        dae_fallback_ratio = (
            dae_fallback_steps / total_path_steps if total_path_steps > 0.0 else None
        )

    loss_total_power_w: Optional[float] = None
    loss_total_energy_j: Optional[float] = None
    loss_duration_s: Optional[float] = None
    loss_energy_balance_error: Optional[float] = None
    loss_summary = getattr(result, "loss_summary", None)
    if loss_summary is not None:
        try:
            loss_total_power_w = float(getattr(loss_summary, "total_loss", 0.0))
            device_losses = list(getattr(loss_summary, "device_losses", []))
            loss_total_energy_j = float(
                sum(float(getattr(item, "total_energy", 0.0)) for item in device_losses)
            )
        except Exception:
            loss_total_power_w = None
            loss_total_energy_j = None

    times = list(getattr(result, "time", []))
    if len(times) >= 2:
        try:
            loss_duration_s = float(times[-1]) - float(times[0])
        except Exception:
            loss_duration_s = None
    if (
        loss_total_power_w is not None
        and loss_total_energy_j is not None
        and loss_duration_s is not None
        and loss_duration_s > 0.0
    ):
        expected_energy = loss_total_power_w * loss_duration_s
        denom = max(abs(loss_total_energy_j), abs(expected_energy), 1e-12)
        loss_energy_balance_error = abs(loss_total_energy_j - expected_energy) / denom

    component_declared_count: Optional[float] = None
    component_reported_count: Optional[float] = None
    component_coverage_rate: Optional[float] = None
    component_coverage_gap: Optional[float] = None
    component_thermal_enabled_count: Optional[float] = None
    component_loss_total_power_w: Optional[float] = None
    component_loss_total_energy_j: Optional[float] = None
    component_peak_temperature_c: Optional[float] = None
    component_loss_summary_consistency_error: Optional[float] = None
    component_thermal_summary_consistency_error: Optional[float] = None
    component_electrothermal = getattr(result, "component_electrothermal", None)
    if component_electrothermal is not None:
        try:
            component_rows = list(component_electrothermal)
            component_reported_count = float(len(component_rows))
            # Runtime result exposes only reported components; treat as declared for direct backend paths.
            component_declared_count = component_reported_count
            component_coverage_rate = 1.0
            component_coverage_gap = 0.0
            component_thermal_enabled_count = float(
                sum(1 for item in component_rows if bool(getattr(item, "thermal_enabled", False)))
            )
            component_loss_total_power_w = float(
                sum(float(getattr(item, "total_loss", 0.0)) for item in component_rows)
            )
            component_loss_total_energy_j = float(
                sum(float(getattr(item, "total_energy", 0.0)) for item in component_rows)
            )
            if component_rows:
                component_peak_temperature_c = float(
                    max(float(getattr(item, "peak_temperature", 25.0)) for item in component_rows)
                )
            else:
                component_peak_temperature_c = None
        except Exception:
            component_declared_count = None
            component_reported_count = None
            component_coverage_rate = None
            component_coverage_gap = None
            component_thermal_enabled_count = None
            component_loss_total_power_w = None
            component_loss_total_energy_j = None
            component_peak_temperature_c = None

    thermal_enabled: Optional[float] = None
    thermal_peak_temperature_c: Optional[float] = None
    thermal_final_temperature_c: Optional[float] = None
    thermal_peak_temperature_delta: Optional[float] = None
    thermal_summary = getattr(result, "thermal_summary", None)
    if thermal_summary is not None:
        try:
            ambient = float(getattr(thermal_summary, "ambient", 25.0))
            thermal_enabled = 1.0 if bool(getattr(thermal_summary, "enabled", False)) else 0.0
            thermal_peak_temperature_c = float(getattr(thermal_summary, "max_temperature", ambient))
            device_temperatures = list(getattr(thermal_summary, "device_temperatures", []))
            if device_temperatures:
                thermal_final_temperature_c = max(
                    float(getattr(item, "final_temperature", ambient))
                    for item in device_temperatures
                )
            else:
                thermal_final_temperature_c = thermal_peak_temperature_c
            thermal_peak_temperature_delta = thermal_peak_temperature_c - ambient
        except Exception:
            thermal_enabled = None
            thermal_peak_temperature_c = None
            thermal_final_temperature_c = None
            thermal_peak_temperature_delta = None

    if loss_total_power_w is not None and component_loss_total_power_w is not None:
        denom = max(abs(loss_total_power_w), abs(component_loss_total_power_w), 1e-12)
        component_loss_summary_consistency_error = (
            abs(loss_total_power_w - component_loss_total_power_w) / denom
        )

    if thermal_peak_temperature_c is not None and component_peak_temperature_c is not None:
        denom = max(abs(thermal_peak_temperature_c), abs(component_peak_temperature_c), 1e-12)
        component_thermal_summary_consistency_error = (
            abs(thermal_peak_temperature_c - component_peak_temperature_c) / denom
        )

    return {
        "newton_iterations": float(getattr(result, "newton_iterations_total", 0)),
        "linear_iterations": float(getattr(linear, "total_iterations", 0)) if linear is not None else None,
        "linear_solve_calls": float(getattr(linear, "total_solve_calls", 0)) if linear is not None else None,
        "linear_fallbacks": float(getattr(linear, "total_fallbacks", 0)) if linear is not None else None,
        "linear_analyze_calls": float(getattr(linear, "total_analyze_calls", 0)) if linear is not None else None,
        "linear_factorize_calls": float(getattr(linear, "total_factorize_calls", 0)) if linear is not None else None,
        "linear_analyze_time_s": float(getattr(linear, "total_analyze_time_seconds", 0.0))
        if linear is not None
        else None,
        "linear_factorize_time_s": float(getattr(linear, "total_factorize_time_seconds", 0.0))
        if linear is not None
        else None,
        "linear_solve_time_s": float(getattr(linear, "total_solve_time_seconds", 0.0))
        if linear is not None
        else None,
        "residual_norm": None,
        "timestep_rejections": float(getattr(result, "timestep_rejections", 0)),
        "runtime_kernel_s": float(getattr(result, "total_time_seconds", 0.0)),
        "runtime_s": float(runtime_s),
        "steps": float(getattr(result, "total_steps", 0)),
        "state_space_primary_steps": state_space_primary_steps,
        "dae_fallback_steps": dae_fallback_steps,
        "segment_non_admissible_steps": segment_non_admissible_steps,
        "segment_model_cache_hits": segment_model_cache_hits,
        "segment_model_cache_misses": segment_model_cache_misses,
        "linear_factor_cache_hits": linear_factor_cache_hits,
        "linear_factor_cache_misses": linear_factor_cache_misses,
        "runtime_module_count": runtime_module_count,
        "runtime_module_order_crc32": runtime_module_order_crc32,
        "runtime_module_count_match": runtime_module_count_match,
        "reserved_output_samples": reserved_output_samples,
        "time_series_reallocations": time_series_reallocations,
        "state_series_reallocations": state_series_reallocations,
        "virtual_channel_reallocations": virtual_channel_reallocations,
        "output_reallocation_total": output_reallocation_total,
        "output_reallocation_free": output_reallocation_free,
        "state_space_primary_ratio": state_space_primary_ratio,
        "dae_fallback_ratio": dae_fallback_ratio,
        "loss_total_power_w": loss_total_power_w,
        "loss_total_energy_j": loss_total_energy_j,
        "loss_duration_s": loss_duration_s,
        "loss_energy_balance_error": loss_energy_balance_error,
        "component_declared_count": component_declared_count,
        "component_reported_count": component_reported_count,
        "component_coverage_rate": component_coverage_rate,
        "component_coverage_gap": component_coverage_gap,
        "component_thermal_enabled_count": component_thermal_enabled_count,
        "component_loss_total_power_w": component_loss_total_power_w,
        "component_loss_total_energy_j": component_loss_total_energy_j,
        "component_peak_temperature_c": component_peak_temperature_c,
        "component_loss_summary_consistency_error": component_loss_summary_consistency_error,
        "component_thermal_summary_consistency_error": component_thermal_summary_consistency_error,
        "thermal_enabled": thermal_enabled,
        "thermal_peak_temperature_c": thermal_peak_temperature_c,
        "thermal_final_temperature_c": thermal_final_temperature_c,
        "thermal_peak_temperature_delta": thermal_peak_temperature_delta,
        "equation_assemble_system_calls": float(getattr(backend, "equation_assemble_system_calls", 0))
        if backend is not None
        else None,
        "equation_assemble_residual_calls": float(getattr(backend, "equation_assemble_residual_calls", 0))
        if backend is not None
        else None,
        "equation_assemble_system_time_s": float(
            getattr(backend, "equation_assemble_system_time_seconds", 0.0)
        )
        if backend is not None
        else None,
        "equation_assemble_residual_time_s": float(
            getattr(backend, "equation_assemble_residual_time_seconds", 0.0)
        )
        if backend is not None
        else None,
        "python_backend": 1.0,
    }


def _hb_telemetry(result: object, runtime_s: float, steps: int) -> Dict[str, Optional[float]]:
    return {
        "newton_iterations": float(getattr(result, "iterations", 0)),
        "linear_iterations": None,
        "linear_solve_calls": None,
        "linear_fallbacks": None,
        "residual_norm": float(getattr(result, "residual_norm", 0.0)),
        "timestep_rejections": None,
        "runtime_kernel_s": None,
        "runtime_s": float(runtime_s),
        "steps": float(steps),
        "python_backend": 1.0,
        "harmonic_balance_iterations": float(getattr(result, "iterations", 0)),
        "harmonic_balance_residual_norm": float(getattr(result, "residual_norm", 0.0)),
    }


def _frequency_telemetry(result: object, runtime_s: float, steps: int) -> Dict[str, Optional[float]]:
    frequency = list(getattr(result, "frequency_hz", []))
    return {
        "newton_iterations": None,
        "linear_iterations": None,
        "linear_solve_calls": None,
        "linear_fallbacks": None,
        "residual_norm": None,
        "timestep_rejections": None,
        "runtime_kernel_s": None,
        "runtime_s": float(runtime_s),
        "steps": float(steps),
        "python_backend": 1.0,
        "ac_sweep_case": 1.0,
        "ac_sweep_points": float(len(frequency)),
    }


def _run_transient_with_optional_x0(simulator: object, x0: Optional[object]) -> object:
    if x0 is not None:
        return simulator.run_transient(x0)
    return simulator.run_transient()


def _clone_shooting_options(ps: object, src: object) -> object:
    dst = ps.PeriodicSteadyStateOptions()
    dst.period = src.period
    dst.max_iterations = src.max_iterations
    dst.tolerance = src.tolerance
    dst.relaxation = src.relaxation
    dst.store_last_transient = src.store_last_transient
    return dst


def _select_mode(options: object, preferred_mode: Optional[str]) -> str:
    alias = {
        "frequency": "frequency_analysis",
        "ac": "frequency_analysis",
        "ac_sweep": "frequency_analysis",
    }
    if preferred_mode in alias:
        preferred_mode = alias[preferred_mode]

    valid_modes = {"transient", "shooting", "harmonic_balance", "frequency_analysis"}
    if preferred_mode is not None and preferred_mode not in valid_modes:
        raise RuntimeError(f"Invalid preferred mode: {preferred_mode}")

    enable_shooting = bool(getattr(options, "enable_periodic_shooting", False))
    enable_hb = bool(getattr(options, "enable_harmonic_balance", False))
    freq_options = getattr(options, "frequency_analysis", None)
    enable_frequency = bool(getattr(freq_options, "enabled", False))

    if preferred_mode == "transient":
        return "transient"
    if preferred_mode == "shooting":
        if not enable_shooting:
            raise RuntimeError("Scenario requested shooting mode but simulation.shooting is not configured")
        return "shooting"
    if preferred_mode == "harmonic_balance":
        if not enable_hb:
            raise RuntimeError("Scenario requested harmonic_balance mode but simulation.harmonic_balance is not configured")
        return "harmonic_balance"
    if preferred_mode == "frequency_analysis":
        if not enable_frequency:
            raise RuntimeError(
                "Scenario requested frequency_analysis mode but simulation.frequency_analysis.enabled=false"
            )
        return "frequency_analysis"

    if enable_shooting and not enable_hb:
        return "shooting"
    if enable_hb and not enable_shooting:
        return "harmonic_balance"
    if enable_frequency:
        return "frequency_analysis"
    if enable_shooting and enable_hb:
        return "shooting"
    return "transient"


def is_available() -> bool:
    ps = _import_pulsim()
    return bool(ps and _has_runtime_api(ps))


def availability_error() -> Optional[str]:
    ps, import_error = _import_pulsim_with_diagnostics()
    if ps is None:
        return import_error or "Python package 'pulsim' is not available"
    if not _has_runtime_api(ps):
        return "Installed pulsim package does not expose required runtime APIs"
    return None


def run_from_yaml(
    netlist_path: Path,
    output_path: Path,
    preferred_mode: Optional[str] = None,
    use_initial_conditions: bool = False,
) -> BackendRunResult:
    """Run benchmark netlist using Pulsim Python runtime and write output CSV."""

    ps = _import_pulsim()
    if ps is None:
        raise RuntimeError("Python package 'pulsim' is not available")
    if not _has_runtime_api(ps):
        raise RuntimeError("Installed pulsim package does not expose required runtime APIs")

    parser_options = ps.YamlParserOptions()
    parser_options.strict = False
    parser = ps.YamlParser(parser_options)
    circuit, options = parser.load(str(netlist_path))
    _raise_if_parser_errors(parser)

    # Some solver internals require dimensions to be set before Simulator construction.
    options.newton_options.num_nodes = int(circuit.num_nodes())
    options.newton_options.num_branches = int(circuit.num_branches())

    signal_names = list(circuit.signal_names())
    mode = _select_mode(options, preferred_mode)
    x0 = circuit.initial_state() if use_initial_conditions else None
    integrator_mapped_to_rosen = False

    if (
        mode == "transient"
        and hasattr(ps, "Integrator")
        and getattr(options, "integrator", None) == ps.Integrator.TRBDF2
    ):
        # Temporary migration mapping: TRBDF2 path is not yet parity-stable in benchmark runtime.
        options.integrator = ps.Integrator.RosenbrockW
        integrator_mapped_to_rosen = True

    simulator = ps.Simulator(circuit, options)

    if mode == "shooting":
        shooting_opts = options.periodic_options
        shooting_opts.store_last_transient = True
        used_warm_start = False

        start = time.perf_counter()
        if x0 is not None:
            shooting_result = simulator.run_periodic_shooting(x0, shooting_opts)
        else:
            shooting_result = simulator.run_periodic_shooting(shooting_opts)
        elapsed = time.perf_counter() - start

        if not shooting_result.success:
            # Warm-start shooting with a long transient trajectory to improve periodic convergence.
            warm_sim = ps.Simulator(circuit, options)
            warm_state = x0 if x0 is not None else circuit.initial_state()
            warm_ok = False
            for _ in range(5):
                warm = warm_sim.run_transient(warm_state)
                if not warm.success or not warm.states:
                    warm_ok = False
                    break
                warm_ok = True
                warm_state = warm.states[-1]

            if warm_ok:
                boosted = _clone_shooting_options(ps, shooting_opts)
                boosted.max_iterations = max(boosted.max_iterations * 6, 120)
                boosted.store_last_transient = True
                retry_sim = ps.Simulator(circuit, options)
                retry = retry_sim.run_periodic_shooting(warm_state, boosted)
                if retry.success:
                    shooting_result = retry
                    elapsed = time.perf_counter() - start
                    used_warm_start = True

        if not shooting_result.success:
            _raise_result_failure(shooting_result, "Periodic shooting failed", mode="shooting")

        cycle = shooting_result.last_cycle
        steps = _write_state_csv(output_path, cycle.time, cycle.states, signal_names)
        telemetry = _transient_telemetry(cycle, elapsed)
        telemetry["periodic_iterations"] = float(shooting_result.iterations)
        telemetry["periodic_residual_norm"] = float(shooting_result.residual_norm)
        telemetry["steps"] = float(steps)
        telemetry["shooting_warm_start_retry"] = 1.0 if used_warm_start else 0.0

        return BackendRunResult(
            runtime_s=elapsed,
            steps=steps,
            mode="shooting",
            telemetry=telemetry,
        )

    if mode == "harmonic_balance":
        start = time.perf_counter()
        if x0 is not None:
            hb_result = simulator.run_harmonic_balance(x0, options.harmonic_balance)
        else:
            hb_result = simulator.run_harmonic_balance(options.harmonic_balance)
        elapsed = time.perf_counter() - start

        if not hb_result.success:
            _raise_result_failure(hb_result, "Harmonic balance failed", mode="harmonic_balance")

        times = list(hb_result.sample_times)
        states = _reshape_hb_solution(hb_result.solution, len(signal_names))
        if len(times) != len(states):
            raise RuntimeError("Harmonic balance output time/state length mismatch")

        steps = _write_state_csv(output_path, times, states, signal_names)
        telemetry = _hb_telemetry(hb_result, elapsed, steps)

        return BackendRunResult(
            runtime_s=elapsed,
            steps=steps,
            mode="harmonic_balance",
            telemetry=telemetry,
        )

    if mode == "frequency_analysis":
        freq_options = getattr(options, "frequency_analysis", None)
        if freq_options is None:
            raise RuntimeError("Simulation options do not expose frequency_analysis")

        start = time.perf_counter()
        freq_result = simulator.run_frequency_analysis(freq_options)
        elapsed = time.perf_counter() - start

        if not bool(getattr(freq_result, "success", False)):
            _raise_result_failure(freq_result, "Frequency analysis failed", mode="frequency_analysis")

        steps = _write_frequency_csv(output_path, freq_result)
        telemetry = _frequency_telemetry(freq_result, elapsed, steps)
        return BackendRunResult(
            runtime_s=elapsed,
            steps=steps,
            mode="frequency_analysis",
            telemetry=telemetry,
        )

    start = time.perf_counter()
    transient_result = _run_transient_with_optional_x0(simulator, x0)
    used_integrator_fallback = False

    if (
        not transient_result.success
        and hasattr(ps, "Integrator")
        and getattr(options, "integrator", None) == ps.Integrator.TRBDF2
    ):
        # TRBDF2 can stall on some benchmark combinations; retry with RosenbrockW.
        retry_options = options
        retry_options.integrator = ps.Integrator.RosenbrockW
        retry_options.newton_options.num_nodes = int(circuit.num_nodes())
        retry_options.newton_options.num_branches = int(circuit.num_branches())
        retry_sim = ps.Simulator(circuit, retry_options)
        retry_result = _run_transient_with_optional_x0(retry_sim, x0)
        if retry_result.success:
            transient_result = retry_result
            used_integrator_fallback = True
    elapsed = time.perf_counter() - start

    if not transient_result.success:
        _raise_result_failure(transient_result, "Transient simulation failed", mode="transient")

    steps = _write_state_csv(output_path, transient_result.time, transient_result.states, signal_names)
    telemetry = _transient_telemetry(transient_result, elapsed)
    telemetry["steps"] = float(steps)
    telemetry["integrator_fallback_to_rosenbrockw"] = 1.0 if used_integrator_fallback else 0.0
    telemetry["integrator_mapped_to_rosenbrockw"] = 1.0 if integrator_mapped_to_rosen else 0.0

    return BackendRunResult(
        runtime_s=elapsed,
        steps=steps,
        mode="transient",
        telemetry=telemetry,
    )
