import io
import json
import os
import struct
import threading
import time
from pathlib import Path
from typing import BinaryIO

import serial

from vna_system.core import config as cfg
from vna_system.core.acquisition.port_manager import VNAPortLocator
from vna_system.core.acquisition.sweep_buffer import SweepBuffer
from vna_system.core.acquisition.sound_player import SoundPlayer
from vna_system.core.logging.logger import get_component_logger

# Import simulator if enabled
if cfg.USE_SIMULATOR:
    from vna_system.core.acquisition.simulator import VNASimulator
else:
    VNASimulator = None  # type: ignore

logger = get_component_logger(__file__)


class VNADataAcquisition:
    """Main data acquisition class with asynchronous sweep collection."""

    def __init__(self) -> None:
        # Configuration
        self.current_input_json = Path(cfg.BASE_DIR) / "binary_input" / "current_input.json"
        self.config_inputs_dir = Path(cfg.BASE_DIR) / "binary_input" / "config_inputs"
        self.baud: int = cfg.DEFAULT_BAUD_RATE

        # Dependencies
        self.vna_port_locator = VNAPortLocator()
        self._sweep_buffer = SweepBuffer()

        # Sound player for sweep notifications
        sound_file = Path(cfg.BASE_DIR) / "core" / "sound_sample.wav"
        self._sound_player = SoundPlayer(sound_file)

        # Simulator mode
        self._simulator = None
        if cfg.USE_SIMULATOR:
            self._simulator = VNASimulator()
            logger.info("VNA Simulator mode enabled")

        # Control flags
        self._running: bool = False
        self._thread: threading.Thread | None = None
        self._stop_event = threading.Event()
        self._paused: bool = False

        # Acquisition modes
        self._continuous_mode: bool = True
        self._single_sweep_requested: bool = False

        # Sweep collection state
        self._collecting: bool = False
        self._collected_rx_payloads: list[bytes] = []
        self._meas_cmds_in_sweep: int = 0

        logger.debug("VNADataAcquisition initialized", baud=self.baud, simulator_mode=cfg.USE_SIMULATOR)

    def _get_current_bin_path(self) -> Path | None:
        """Get the path to the current binary input file from JSON config."""
        if not self.current_input_json.exists():
            logger.warning("Current input JSON not found", path=str(self.current_input_json))
            return None

        try:
            with self.current_input_json.open("r", encoding="utf-8") as f:
                current_info = json.load(f)

            filename = current_info.get("filename")
            if not filename:
                logger.warning("Invalid current input JSON format")
                return None

            bin_path = self.config_inputs_dir / filename
            if not bin_path.exists():
                logger.warning("Current input binary file not found", path=str(bin_path))
                return None

            return bin_path
        except Exception as exc:  # noqa: BLE001
            logger.error("Failed to load current input config", error=repr(exc))
            return None

    # --------------------------------------------------------------------- #
    # Lifecycle
    # --------------------------------------------------------------------- #
    def start(self) -> None:
        """Start the data acquisition background thread."""
        if self._running:
            logger.debug("start() ignored; acquisition already running")
            return

        self._running = True
        self._stop_event.clear()
        self._thread = threading.Thread(target=self._acquisition_loop, daemon=True, name="VNA-Acq")
        self._thread.start()
        logger.info("Acquisition thread started")

    def stop(self) -> None:
        """Stop the data acquisition background thread."""
        if not self._running:
            logger.debug("stop() ignored; acquisition not running")
            return

        self._running = False
        self._stop_event.set()

        if self._thread and self._thread.is_alive():
            self._thread.join(timeout=5.0)
            logger.info("Acquisition thread joined")

    @property
    def is_running(self) -> bool:
        """Return True if the acquisition thread is alive."""
        return self._running and (self._thread is not None and self._thread.is_alive()) and not self._paused 

    @property
    def sweep_buffer(self) -> SweepBuffer:
        """Return a reference to the sweep buffer."""
        return self._sweep_buffer

    @property
    def is_paused(self) -> bool:
        """Return True if acquisition is paused."""
        return self._paused

    @property
    def is_continuous_mode(self) -> bool:
        """Return True if in continuous sweep mode."""
        return self._continuous_mode

    def pause(self) -> None:
        """Pause the data acquisition."""
        if not self._running:
            logger.warning("Cannot pause; acquisition not running")
            return

        self._paused = True
        logger.info("Data acquisition paused")

    def set_continuous_mode(self, continuous: bool = True) -> None:
        """Set continuous or single sweep mode. Also resumes if paused."""
        self._continuous_mode = continuous

        if continuous and self._paused:
            self._paused = False
            logger.info("Data acquisition resumed (continuous mode=True)")

        logger.info("Acquisition mode updated", continuous=continuous)

    def trigger_single_sweep(self) -> None:
        """Trigger a single sweep. Automatically switches to single sweep mode if needed."""
        if not self._running:
            logger.warning("Cannot trigger single sweep; acquisition not running")
            return

        if self._continuous_mode:
            self.set_continuous_mode(False)
            logger.info("Switched from continuous to single-sweep mode")

        self._single_sweep_requested = True
        if self._paused:
            self._paused = False
            logger.info("Data acquisition resumed for single sweep")
        logger.info("Single sweep triggered")

    # --------------------------------------------------------------------- #
    # Serial helpers
    # --------------------------------------------------------------------- #
    def _drain_serial_input(self, ser: serial.Serial) -> None:
        """Drain any pending bytes from the serial input buffer."""
        if not ser:
            return
        drained = 0
        while True:
            waiting = getattr(ser, "in_waiting", 0)
            if waiting <= 0:
                break
            drained += len(ser.read(waiting))
            time.sleep(cfg.SERIAL_DRAIN_CHECK_DELAY)
        if drained:
            logger.warning("Drained pending bytes from serial input", bytes=drained)

    # --------------------------------------------------------------------- #
    # Acquisition loop
    # --------------------------------------------------------------------- #
    def _simulator_acquisition_loop(self) -> None:
        """Simplified acquisition loop for simulator mode."""
        logger.info("Starting simulator acquisition loop")

        while self._running and not self._stop_event.is_set():
            try:
                # Honor pause
                if self._paused:
                    time.sleep(0.1)
                    continue

                # Get simulated sweep with noise
                points, timestamp = self._simulator.get_sweep()

                # Add sweep to buffer
                sweep_number = self._sweep_buffer.add_sweep(points, timestamp=timestamp)
                logger.info(
                    "Simulated sweep collected",
                    sweep_number=sweep_number,
                    points=len(points),
                    timestamp=timestamp
                )

                # Play sweep notification sound
                self._sound_player.play()

                # Handle single-sweep mode transitions
                if not self._continuous_mode:
                    if self._single_sweep_requested:
                        self._single_sweep_requested = False
                        logger.info("Single simulated sweep completed; pausing acquisition")
                        self.pause()
                    else:
                        self.pause()
                else:
                    # In continuous mode, add a small delay between sweeps
                    time.sleep(0.1)

            except Exception as exc:  # noqa: BLE001
                logger.error("Simulator acquisition loop error", error=repr(exc))
                time.sleep(1.0)

    def _acquisition_loop(self) -> None:
        """Main acquisition loop executed by the background thread."""
        # Use simulator loop if simulator is enabled
        if self._simulator is not None:
            self._simulator_acquisition_loop()
            return

        while self._running and not self._stop_event.is_set():
            try:
                # Honor pause
                if self._paused:
                    time.sleep(0.1)
                    continue

                # Auto-detect and validate port
                port = self.vna_port_locator.find_vna_port()
                if port is None:
                    logger.warning("VNA port not found; retrying shortly")
                    time.sleep(0.5)
                    continue

                logger.debug("Using port", device=port, baud=self.baud)

                # Get current binary input file path
                bin_path = self._get_current_bin_path()
                if not bin_path:
                    logger.error("No current binary input configured, waiting...")
                    time.sleep(1.0)
                    continue

                # Open serial + process one sweep from the binary log
                with serial.Serial(port, self.baud) as ser:
                    self._drain_serial_input(ser)

                    # Open the log file each iteration to read the next sweep from start
                    with open(bin_path, "rb") as raw:
                        buffered = io.BufferedReader(raw, buffer_size=cfg.SERIAL_BUFFER_SIZE)
                        self._process_sweep_data(buffered, ser)

                # Handle single-sweep mode transitions
                if not self._continuous_mode:
                    if self._single_sweep_requested:
                        self._single_sweep_requested = False
                        logger.info("Single sweep completed; pausing acquisition")
                        self.pause()
                    else:
                        self.pause()

            except Exception as exc:  # noqa: BLE001
                logger.error("Acquisition loop error", error=repr(exc))
                time.sleep(1.0)

    # --------------------------------------------------------------------- #
    # Log processing
    # --------------------------------------------------------------------- #
    def _process_sweep_data(self, f: BinaryIO, ser: serial.Serial) -> None:
        """Process the binary log file and collect sweep data for a single sweep."""
        try:
            # Validate header
            header = self._read_exact(f, len(cfg.MAGIC))
            if header != cfg.MAGIC:
                raise ValueError("Invalid log format: MAGIC header mismatch")

            self._reset_sweep_state()

            if self._stop_event.is_set():
                logger.debug("Stop requested before sweep start; aborting sweep replay")
                return

            # Read until exactly one sweep is completed. Honor stop requests only
            # between sweeps so we do not break out mid-collection.
            sweep_completed = False
            while not sweep_completed:
                if self._stop_event.is_set() and not self._collecting:
                    logger.debug("Stop requested with no active sweep; exiting loop")
                    break
                dir_b = f.read(1)
                if not dir_b:
                    # EOF reached; wait for more data to arrive on disk
                    if self._collecting:
                        logger.warning("EOF reached while sweep in progress; aborting partial sweep")
                    else:
                        logger.debug("EOF reached; waiting for more data")
                    time.sleep(0.1)
                    break

                direction = dir_b[0]
                (length,) = struct.unpack(">I", self._read_exact(f, 4))

                if direction == cfg.DIR_TO_DEV:
                    # TX path: forward to device and inspect for sweep start
                    first = self._serial_write_from_file(f, length, ser)

                    if not self._collecting and self._is_sweep_start_command(length, first):
                        self._collecting = True
                        self._collected_rx_payloads.clear()
                        self._meas_cmds_in_sweep = 0
                        logger.info("Sweep collection started")
                elif direction == cfg.DIR_FROM_DEV:
                    # RX path: capture bytes from device; keep file pointer in sync
                    rx_bytes = self._serial_read_exact(length, ser, capture=self._collecting)
                    self._skip_bytes(f, length)

                    if self._collecting:
                        self._collected_rx_payloads.append(rx_bytes)
                        self._meas_cmds_in_sweep += 1

                        if self._meas_cmds_in_sweep >= cfg.MEAS_CMDS_PER_SWEEP:
                            self._finalize_sweep()
                            sweep_completed = True
                else:
                    # Unknown record type: skip bytes to keep in sync
                    logger.warning("Unknown record direction; skipping", direction=direction, length=length)
                    self._skip_bytes(f, length)

        except Exception as exc:  # noqa: BLE001
            logger.error("Processing error", error=repr(exc))
            time.sleep(1.0)

    def _finalize_sweep(self) -> None:
        """Parse collected payloads into points and push to the buffer."""
        all_points: list[tuple[float, float]] = []
        for payload in self._collected_rx_payloads:
            if payload:
                all_points.extend(self._parse_measurement_data(payload))

        if all_points:
            sweep_number = self._sweep_buffer.add_sweep(all_points)
            logger.info("Sweep collected", sweep_number=sweep_number, points=len(all_points))
            if len(all_points) != cfg.EXPECTED_POINTS_PER_SWEEP:
                logger.warning(
                    "Unexpected number of points",
                    expected=cfg.EXPECTED_POINTS_PER_SWEEP,
                    actual=len(all_points),
                )

            # Play sweep notification sound
            self._sound_player.play()
        else:
            logger.warning("No points parsed for sweep")

        self._reset_sweep_state()

    def _reset_sweep_state(self) -> None:
        """Reset internal state for the next sweep collection."""
        self._collecting = False
        self._collected_rx_payloads.clear()
        self._meas_cmds_in_sweep = 0

    # --------------------------------------------------------------------- #
    # I/O helpers
    # --------------------------------------------------------------------- #
    def _read_exact(self, f: BinaryIO, n: int) -> bytes:
        """Read exactly *n* bytes from a file-like object or raise EOFError."""
        buf = bytearray()
        while len(buf) < n:
            chunk = f.read(n - len(buf))
            if not chunk:
                raise EOFError(f"Unexpected EOF while reading {n} bytes")
            buf += chunk
        return bytes(buf)

    def _skip_bytes(self, f: BinaryIO, n: int) -> None:
        """Skip *n* bytes in a file-like object efficiently."""
        if hasattr(f, "seek"):
            try:
                f.seek(n, os.SEEK_CUR)
                return
            except (OSError, io.UnsupportedOperation):
                pass

        remaining = n
        while remaining > 0:
            chunk = f.read(min(cfg.FILE_CHUNK_SIZE, remaining))
            if not chunk:
                raise EOFError(f"Unexpected EOF while skipping {n} bytes")
            remaining -= len(chunk)

    def _serial_write_from_file(self, f: BinaryIO, nbytes: int, ser: serial.Serial) -> bytes:
        """
        Stream *nbytes* from a file-like object to the serial port.

        Returns the first few bytes (up to cfg.SERIAL_PEEK_SIZE) for command inspection.
        """
        first = bytearray()
        remaining = nbytes

        while remaining > 0:
            to_read = min(cfg.TX_CHUNK_SIZE, remaining)
            chunk = f.read(to_read)
            if not chunk:
                raise EOFError("Log truncated while sending")

            # Capture a peek for command inspection
            need = max(0, cfg.SERIAL_PEEK_SIZE - len(first))
            if need:
                first.extend(chunk[:need])

            # Write to serial
            written = 0
            while written < len(chunk):
                n = ser.write(chunk[written:])
                if n is None:
                    n = 0
                written += n

            remaining -= len(chunk)

        return bytes(first)

    def _serial_read_exact(self, nbytes: int, ser: serial.Serial, capture: bool = False) -> bytes:
        """Read exactly *nbytes* from the serial port; optionally capture and return them."""
        deadline = time.monotonic() + cfg.RX_TIMEOUT
        total = 0
        out = bytearray() if capture else None

        old_timeout = ser.timeout
        ser.timeout = min(cfg.SERIAL_IDLE_TIMEOUT, cfg.RX_TIMEOUT)
        try:
            while total < nbytes:
                if time.monotonic() >= deadline:
                    raise TimeoutError(f"Timeout while waiting for {nbytes} bytes")
                chunk = ser.read(nbytes - total)
                if chunk:
                    total += len(chunk)
                    if capture and out is not None:
                        out.extend(chunk)
            return bytes(out) if (capture and out is not None) else b""
        finally:
            ser.timeout = old_timeout

    # --------------------------------------------------------------------- #
    # Parsing & detection
    # --------------------------------------------------------------------- #
    def _parse_measurement_data(self, payload: bytes) -> list[tuple[float, float]]:
        """Parse complex measurement samples (float32 pairs) from a payload."""
        if len(payload) <= cfg.MEAS_HEADER_LEN:
            return []

        data = memoryview(payload)[cfg.MEAS_HEADER_LEN:]
        # Use iter_unpack for speed and clarity
        points: list[tuple[float, float]] = []
        for real, imag in struct.iter_unpack("<ff", data[: (len(data) // 8) * 8]):
            points.append((real, imag))
        return points

    def _is_sweep_start_command(self, tx_len: int, first_bytes: bytes) -> bool:
        """Return True if a TX command indicates the start of a sweep."""
        return tx_len == cfg.SWEEP_CMD_LEN and first_bytes.startswith(cfg.SWEEP_CMD_PREFIX)
