"""VISA HiSLIP driver for Compact-M K209 / S2VNA analyzers."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

import numpy as np
import pyvisa

from python_app.hardware_full.librevna_driver.models import SweepResult
from python_app.models.run_config_model import RadarSweepModel


@dataclass(slots=True)
class CompactMK209InterleavedSweep:
    """Raw corrected K209 traces as interleaved REAL32 arrays."""

    frequency_hz: np.ndarray
    s11_values: np.ndarray
    s21_values: np.ndarray


@dataclass(slots=True)
class CompactMK209Service:
    """Acquire corrected S11/S21 sweeps from a K209 analyzer through VISA HiSLIP."""

    resource: str
    timeout_ms: int = 20_000
    preset_on_open: bool = True
    visa_library: str = "@ivi"
    _resource_manager: pyvisa.ResourceManager | None = field(init=False, default=None, repr=False)
    _instrument: Any | None = field(init=False, default=None, repr=False)
    _settings: RadarSweepModel | None = field(init=False, default=None, repr=False)
    _frequency_hz: np.ndarray | None = field(init=False, default=None, repr=False)

    def __post_init__(self) -> None:
        """Normalize constructor values."""
        self.resource = str(self.resource).strip()
        if not self.resource:
            raise ValueError("K209 VISA resource must not be empty")
        self.timeout_ms = int(self.timeout_ms)
        if self.timeout_ms <= 0:
            raise ValueError("K209 timeout_ms must be > 0")
        self.visa_library = str(self.visa_library).strip() or "@ivi"
        if self.visa_library == "@py" or self.visa_library.endswith("@py"):
            raise ValueError("K209 requires an IVI/Vendor VISA backend, not pyvisa-py")

    @property
    def is_open(self) -> bool:
        """Return whether the VISA session is open."""
        return self._instrument is not None

    def open(self) -> None:
        """Open VISA resource and apply stored sweep settings when available."""
        if self._instrument is not None:
            return

        try:
            self._resource_manager = pyvisa.ResourceManager(self.visa_library)
            self._instrument = self._resource_manager.open_resource(self.resource)
            self._instrument.timeout = self.timeout_ms
            self._instrument.write_termination = "\n"
            self._instrument.read_termination = None
            self._instrument.chunk_size = max(int(getattr(self._instrument, "chunk_size", 20_480)), 8 * 1024 * 1024)
            self._instrument.write("*CLS")

            if self._settings is not None:
                self._apply_configuration(self._settings)
        except Exception:
            self.close()
            raise

    def close(self) -> None:
        """Close VISA sessions."""
        if self._instrument is not None:
            self._instrument.close()
            self._instrument = None
        if self._resource_manager is not None:
            self._resource_manager.close()
            self._resource_manager = None

    def __enter__(self) -> CompactMK209Service:
        """Open and return this service."""
        self.open()
        return self

    def __exit__(self, exc_type: object, exc: object, traceback: object) -> None:
        """Close VISA resources."""
        self.close()

    def query_identity(self) -> str:
        """Read analyzer identity string."""
        instrument = self._require_instrument()
        return str(instrument.query("*IDN?")).strip()

    def query_system_error(self) -> str:
        """Read one analyzer SCPI error queue entry."""
        instrument = self._require_instrument()
        return str(instrument.query("SYST:ERR?")).strip()

    def configure(self, sweep: RadarSweepModel) -> None:
        """Store and apply sweep settings."""
        self._validate_sweep(sweep)
        self._settings = sweep
        self._frequency_hz = None
        if self._instrument is None:
            return
        self._apply_configuration(sweep)

    def read_device_limits(self) -> dict[str, float | int]:
        """Read analyzer limits through SCPI capability/service queries."""
        opened_here = self._instrument is None
        try:
            if opened_here:
                self.open()
            instrument = self._require_instrument()
            return {
                "min_frequency_hz": float(instrument.query("SERV:SWE:FREQ:MIN?")),
                "max_frequency_hz": float(instrument.query("SERV:SWE:FREQ:MAX?")),
                "min_ifbw_hz": float(instrument.query("SYST:CAP:IFBW:MIN?")),
                "max_ifbw_hz": float(instrument.query("SYST:CAP:IFBW:MAX?")),
                "max_points": int(float(instrument.query("SERV:SWE:POIN?"))),
                "min_power_dbm": float(instrument.query("SERV:SWE:POW:MIN?")),
                "max_power_dbm": float(instrument.query("SERV:SWE:POW:MAX?")),
            }
        finally:
            if opened_here:
                self.close()

    def frequency_axis(self) -> np.ndarray:
        """Return the configured frequency axis."""
        if self._frequency_hz is None:
            raise RuntimeError("K209 frequency axis is not configured")
        return self._frequency_hz

    def acquire_interleaved(self) -> CompactMK209InterleavedSweep:
        """Acquire one corrected sweep without converting interleaved arrays."""
        if self._settings is None:
            raise RuntimeError("K209 service is not configured")
        if self._frequency_hz is None:
            raise RuntimeError("K209 frequency axis is not configured")
        points = int(self._settings.points)

        s11_values, s21_values = self._query_sweep_trace_pair(points)
        return CompactMK209InterleavedSweep(
            frequency_hz=self._frequency_hz,
            s11_values=s11_values,
            s21_values=s21_values,
        )

    def acquire(self) -> SweepResult:
        """Acquire one corrected S11/S21 sweep."""
        raw = self.acquire_interleaved()

        return SweepResult(
            x=raw.frequency_hz.copy(),
            traces={
                "s11": self._complex_from_interleaved(raw.s11_values),
                "s21": self._complex_from_interleaved(raw.s21_values),
            },
        )

    def _apply_configuration(self, sweep: RadarSweepModel) -> None:
        instrument = self._require_instrument()
        if self.preset_on_open:
            instrument.write("SYST:PRES")
            self._expect_opc("*OPC?", context="K209 preset")

        instrument.write(f"SENS:FREQ:STAR {float(sweep.start_hz):.9f}")
        instrument.write(f"SENS:FREQ:STOP {float(sweep.stop_hz):.9f}")
        instrument.write(f"SENS:SWE:POIN {int(sweep.points)}")
        instrument.write("SENS:SWE:POIN:TIME 0")
        instrument.write(f"SENS:BAND {float(sweep.if_bandwidth_hz):.9f}")
        instrument.write(f"SOUR:POW {float(sweep.power_dbm):.3f}")
        instrument.write("SENS:AVER OFF")
        instrument.write("CALC:PAR1:DEF S21")
        instrument.write("CALC:PAR1:SEL")
        instrument.write("FORM:DATA REAL32")
        instrument.write("FORM:BORD SWAP")
        instrument.write("INIT:CONT ON")
        instrument.write("TRIG:SOUR BUS")
        self._expect_opc("*OPC?", context="K209 setup")
        self._frequency_hz = self._query_float32_array("SENS:FREQ:DATA?", int(sweep.points))

    def _expect_opc(self, command: str, *, context: str) -> None:
        instrument = self._require_instrument()
        response = str(instrument.query(command)).strip()
        if response != "1":
            raise RuntimeError(f"{context} returned unexpected *OPC? response: {response!r}")

    def _query_float32_array(self, command: str, expected_values: int) -> np.ndarray:
        instrument = self._require_instrument()
        instrument.write(command)
        return self._read_float32_block(command, expected_values)

    def _query_sweep_trace_pair(self, points: int) -> tuple[np.ndarray, np.ndarray]:
        instrument = self._require_instrument()
        instrument.write("TRIG:SING;*OPC?;:SENS:DATA:CORR? S11;:SENS:DATA:CORR? S21")
        response = self._read_ascii_token()
        if response != "1":
            raise RuntimeError(f"K209 sweep returned unexpected *OPC? response: {response!r}")
        return (
            self._read_float32_block("SENS:DATA:CORR? S11", points * 2),
            self._read_float32_block("SENS:DATA:CORR? S21", points * 2),
        )

    def _read_ascii_token(self) -> str:
        token = bytearray()
        while True:
            byte = self._read_response_bytes(1)
            if byte in (b";", b"\n", b"\r"):
                if token:
                    return token.decode("ascii").strip()
                continue
            token.extend(byte)
            if len(token) > 64 * 1024:
                raise RuntimeError("K209 ASCII response token is too long")

    def _read_float32_block(self, context: str, expected_values: int) -> np.ndarray:
        marker = self._read_response_bytes(1)
        while marker in (b";", b"\n", b"\r"):
            marker = self._read_response_bytes(1)
        if marker != b"#":
            raise RuntimeError(f"K209 response for {context!r} does not start with IEEE block marker")

        width = self._read_response_bytes(1)
        if width != b"8":
            raise RuntimeError(f"K209 response for {context!r} uses unsupported IEEE block header width")

        payload_size = int(self._read_response_bytes(8).decode("ascii"))
        expected_size = expected_values * np.dtype(np.float32).itemsize
        if payload_size != expected_size:
            raise RuntimeError(
                f"K209 response for {context!r} returned {payload_size} payload bytes, "
                f"expected {expected_size}"
            )

        payload = self._read_response_bytes(payload_size)
        array = np.frombuffer(payload, dtype="<f4")
        if array.size != expected_values:
            raise RuntimeError(
                f"K209 response for {context!r} returned {array.size} float32 values, "
                f"expected {expected_values}"
            )
        return array

    def _read_response_bytes(self, count: int) -> bytes:
        instrument = self._require_instrument()
        data = instrument.read_bytes(count, break_on_termchar=False)
        if len(data) != count:
            raise RuntimeError(f"K209 response ended after {len(data)} bytes, expected {count}")
        return data

    def _require_instrument(self) -> Any:
        if self._instrument is None:
            raise RuntimeError("K209 VISA instrument is not open")
        return self._instrument

    @staticmethod
    def _validate_sweep(sweep: RadarSweepModel) -> None:
        if int(sweep.points) < 2:
            raise ValueError("K209 sweep points must be >= 2")
        if float(sweep.stop_hz) < float(sweep.start_hz):
            raise ValueError("K209 sweep stop_hz must be >= start_hz")
        if float(sweep.if_bandwidth_hz) <= 0.0:
            raise ValueError("K209 IF bandwidth must be > 0")

    @staticmethod
    def _complex_from_interleaved(values: np.ndarray) -> np.ndarray:
        if values.size % 2 != 0:
            raise RuntimeError("K209 complex trace payload has odd scalar count")
        reshaped = np.asarray(values, dtype=np.float32).reshape((-1, 2))
        return (reshaped[:, 0] + 1j * reshaped[:, 1]).astype(np.complex64)
