"""Batch simulation runner for parameter sweeps and Monte Carlo analysis.

Runs multiple LTspice simulations sequentially (LTspice under Wine is
single-instance) and collects results into a single BatchResult.
"""

from __future__ import annotations

import random
import re
import tempfile
import time
from dataclasses import dataclass, field
from pathlib import Path

from .runner import SimulationResult, run_netlist


@dataclass
class BatchResult:
    """Aggregated results from a batch of simulations."""

    results: list[SimulationResult] = field(default_factory=list)
    parameter_values: list[dict] = field(default_factory=list)
    total_elapsed: float = 0.0

    @property
    def success_count(self) -> int:
        return sum(1 for r in self.results if r.success)

    @property
    def failure_count(self) -> int:
        return sum(1 for r in self.results if not r.success)


async def run_batch(
    netlists: list[tuple[str, str]],
    timeout: float = 300,
) -> BatchResult:
    """Run a list of netlists sequentially and collect results.

    Args:
        netlists: List of ``(name, netlist_text)`` pairs.  Each netlist
            is written to a temp ``.cir`` file and simulated.
        timeout: Per-simulation timeout in seconds.

    Returns:
        BatchResult with one SimulationResult per netlist.
    """
    batch = BatchResult()
    start = time.monotonic()

    for name, text in netlists:
        safe = _safe_filename(name)
        work_dir = Path(tempfile.mkdtemp(prefix=f"ltbatch_{safe}_"))
        cir_path = work_dir / f"{safe}.cir"
        cir_path.write_text(text, encoding="utf-8")

        result = await run_netlist(cir_path, timeout=timeout, work_dir=work_dir)
        batch.results.append(result)
        batch.parameter_values.append({"name": name})

    batch.total_elapsed = time.monotonic() - start
    return batch


async def run_parameter_sweep(
    netlist_template: str,
    param_name: str,
    values: list[float],
    timeout: float = 300,
) -> BatchResult:
    """Sweep a single parameter across a range of values.

    The *netlist_template* should contain a ``.param`` directive for
    *param_name* whose value will be replaced for each run.  If no
    ``.param`` line is found, one is inserted before the ``.end``
    directive.

    Args:
        netlist_template: Netlist text with a ``.param {param_name}=...`` line.
        param_name: Parameter to sweep (e.g., ``"Rval"``).
        values: List of numeric values to substitute.
        timeout: Per-simulation timeout in seconds.

    Returns:
        BatchResult indexed by parameter value.
    """
    batch = BatchResult()
    start = time.monotonic()

    param_re = re.compile(
        rf"(\.param\s+{re.escape(param_name)}\s*=\s*)(\S+)",
        re.IGNORECASE,
    )

    for val in values:
        val_str = _format_value(val)

        if param_re.search(netlist_template):
            text = param_re.sub(rf"\g<1>{val_str}", netlist_template)
        else:
            # Insert a .param line before .end
            text = _insert_before_end(netlist_template, f".param {param_name}={val_str}")

        safe = f"sweep_{param_name}_{val_str}"
        work_dir = Path(tempfile.mkdtemp(prefix=f"ltbatch_{_safe_filename(safe)}_"))
        cir_path = work_dir / f"{_safe_filename(safe)}.cir"
        cir_path.write_text(text, encoding="utf-8")

        result = await run_netlist(cir_path, timeout=timeout, work_dir=work_dir)
        batch.results.append(result)
        batch.parameter_values.append({param_name: val})

    batch.total_elapsed = time.monotonic() - start
    return batch


async def run_temperature_sweep(
    netlist_text: str,
    temperatures: list[float],
    timeout: float = 300,
) -> BatchResult:
    """Run the same netlist at different temperatures.

    A ``.temp`` directive is added (or replaced) for each run.

    Args:
        netlist_text: Base netlist text.
        temperatures: List of temperatures in degrees C.
        timeout: Per-simulation timeout in seconds.

    Returns:
        BatchResult indexed by temperature.
    """
    batch = BatchResult()
    start = time.monotonic()

    temp_re = re.compile(r"\.temp\s+\S+", re.IGNORECASE)

    for temp in temperatures:
        temp_str = _format_value(temp)

        if temp_re.search(netlist_text):
            text = temp_re.sub(f".temp {temp_str}", netlist_text)
        else:
            text = _insert_before_end(netlist_text, f".temp {temp_str}")

        safe = f"temp_{temp_str}"
        work_dir = Path(tempfile.mkdtemp(prefix=f"ltbatch_{_safe_filename(safe)}_"))
        cir_path = work_dir / f"{_safe_filename(safe)}.cir"
        cir_path.write_text(text, encoding="utf-8")

        result = await run_netlist(cir_path, timeout=timeout, work_dir=work_dir)
        batch.results.append(result)
        batch.parameter_values.append({"temperature": temp})

    batch.total_elapsed = time.monotonic() - start
    return batch


async def run_monte_carlo(
    netlist_text: str,
    n_runs: int,
    tolerances: dict[str, float],
    timeout: float = 300,
    seed: int | None = None,
) -> BatchResult:
    """Monte Carlo analysis with component tolerances.

    For each run, every component listed in *tolerances* has its value
    randomly varied using a normal distribution truncated to +/-3 sigma,
    where sigma equals the tolerance fraction.

    Args:
        netlist_text: Base netlist text.
        n_runs: Number of Monte Carlo iterations.
        tolerances: Mapping of component name to tolerance fraction
            (e.g., ``{"R1": 0.05}`` for 5%).
        timeout: Per-simulation timeout in seconds.
        seed: Optional RNG seed for reproducibility.

    Returns:
        BatchResult with per-run component values in ``parameter_values``.
    """
    rng = random.Random(seed)
    batch = BatchResult()
    start = time.monotonic()

    # Pre-extract nominal values from the netlist
    nominals = _extract_component_values(netlist_text, list(tolerances.keys()))

    for run_idx in range(n_runs):
        text = netlist_text
        params: dict[str, float] = {}

        for comp_name, tol_frac in tolerances.items():
            nominal = nominals.get(comp_name)
            if nominal is None:
                continue

            # Normal distribution, clamp to +/-3sigma
            sigma = nominal * tol_frac
            deviation = rng.gauss(0, sigma)
            deviation = max(-3 * sigma, min(3 * sigma, deviation))
            varied = nominal + deviation
            params[comp_name] = varied

            # Replace the component value in the netlist text
            text = _replace_component_value(text, comp_name, _format_value(varied))

        safe = f"mc_{run_idx:04d}"
        work_dir = Path(tempfile.mkdtemp(prefix=f"ltbatch_{safe}_"))
        cir_path = work_dir / f"{safe}.cir"
        cir_path.write_text(text, encoding="utf-8")

        result = await run_netlist(cir_path, timeout=timeout, work_dir=work_dir)
        batch.results.append(result)
        batch.parameter_values.append(params)

    batch.total_elapsed = time.monotonic() - start
    return batch


# ============================================================================
# Internal helpers
# ============================================================================

# SPICE engineering suffixes
_SUFFIX_MAP = {
    "T": 1e12,
    "G": 1e9,
    "MEG": 1e6,
    "K": 1e3,
    "M": 1e-3,
    "U": 1e-6,
    "N": 1e-9,
    "P": 1e-12,
    "F": 1e-15,
}


def _parse_spice_value(s: str) -> float | None:
    """Parse a SPICE value string like ``10k``, ``100n``, ``4.7meg`` to float."""
    s = s.strip().upper()
    if not s:
        return None

    # Try plain float first
    try:
        return float(s)
    except ValueError:
        pass

    # Try suffixed form
    for suffix, mult in sorted(_SUFFIX_MAP.items(), key=lambda x: -len(x[0])):
        if s.endswith(suffix):
            num_part = s[: -len(suffix)]
            try:
                return float(num_part) * mult
            except ValueError:
                continue

    return None


def _format_value(v: float) -> str:
    """Format a float for SPICE, using engineering suffixes where tidy."""
    if v == 0:
        return "0"

    abs_v = abs(v)
    # Walk the suffix table from large to small
    for suffix, mult in sorted(_SUFFIX_MAP.items(), key=lambda x: -x[1]):
        if abs_v >= mult * 0.999:
            scaled = v / mult
            formatted = f"{scaled:g}{suffix.lower()}"
            return formatted

    return f"{v:g}"


def _safe_filename(name: str) -> str:
    """Turn an arbitrary name into a safe filename fragment."""
    return re.sub(r"[^\w\-.]", "_", name)[:60]


def _insert_before_end(netlist: str, directive: str) -> str:
    """Insert a directive line just before ``.end``."""
    end_re = re.compile(r"^(\.end\b)", re.IGNORECASE | re.MULTILINE)
    if end_re.search(netlist):
        return end_re.sub(f"{directive}\n\\1", netlist)
    # No .end found -- append
    return netlist.rstrip() + f"\n{directive}\n.end\n"


def _extract_component_values(netlist: str, names: list[str]) -> dict[str, float]:
    """Extract numeric component values from a netlist by instance name.

    Looks for lines like ``R1 in out 10k`` and parses the value token.
    """
    result: dict[str, float] = {}

    for name in names:
        pattern = re.compile(
            rf"^\s*{re.escape(name)}\s+\S+\s+\S+\s+(\S+)",
            re.IGNORECASE | re.MULTILINE,
        )
        match = pattern.search(netlist)
        if match:
            parsed = _parse_spice_value(match.group(1))
            if parsed is not None:
                result[name] = parsed

    return result


def _replace_component_value(netlist: str, comp_name: str, new_value: str) -> str:
    """Replace a component's value token in the netlist text."""
    pattern = re.compile(
        rf"^(\s*{re.escape(comp_name)}\s+\S+\s+\S+\s+)\S+",
        re.IGNORECASE | re.MULTILINE,
    )
    return pattern.sub(rf"\g<1>{new_value}", netlist, count=1)
