"""
Serial port manager for ESPal daemon.

Manages serial port connection, continuous reading, and port
release/reclaim for upload operations.
"""

import threading
import time
import logging
from typing import Optional, Callable
from enum import Enum
import serial

from .device import DeviceDetector
from .buffer import RingBuffer


logger = logging.getLogger(__name__)


class PortState(Enum):
    """State of the serial port."""
    DISCONNECTED = "disconnected"
    CONNECTED = "connected"
    RELEASED = "released"  # Temporarily released for upload


class SerialManager:
    """
    Manages serial port reading and writes to ring buffer.

    Handles:
    - Device detection and connection
    - Continuous reading in background thread
    - Port release/reclaim for upload operations
    - Automatic reconnection after device reset
    """

    def __init__(
        self,
        buffer: RingBuffer,
        baudrate: int = 115200,
        vid: Optional[str] = None,
        pid: Optional[str] = None,
        device: Optional[str] = None,
        on_state_change: Optional[Callable[[PortState, Optional[str]], None]] = None
    ):
        """
        Initialise the serial manager.

        Args:
            buffer: Ring buffer to write data to
            baudrate: Serial baudrate
            vid: USB Vendor ID for device detection
            pid: USB Product ID for device detection
            device: Specific device path (overrides VID/PID)
            on_state_change: Callback when port state changes
        """
        self._buffer = buffer
        self._baudrate = baudrate
        self._vid = vid
        self._pid = pid
        self._device_path = device
        self._on_state_change = on_state_change

        # State
        self._state = PortState.DISCONNECTED
        self._state_lock = threading.Lock()
        self._current_device: Optional[str] = None
        self._serial: Optional[serial.Serial] = None

        # Device detector
        self._detector = DeviceDetector(
            callback=self._on_device_change,
            specific_device=device
        )

        # Threading
        self._running = False
        self._read_thread: Optional[threading.Thread] = None
        self._stop_event = threading.Event()

        # Release/reclaim coordination
        self._release_event = threading.Event()
        self._release_complete = threading.Event()
        self._reclaim_event = threading.Event()

    @property
    def state(self) -> PortState:
        """Current port state."""
        with self._state_lock:
            return self._state

    @property
    def device_path(self) -> Optional[str]:
        """Current device path."""
        return self._current_device

    @property
    def baudrate(self) -> int:
        """Serial baudrate."""
        return self._baudrate

    @baudrate.setter
    def baudrate(self, value: int) -> None:
        """Set baudrate (takes effect on next connection)."""
        self._baudrate = value

    def _set_state(self, state: PortState, device: Optional[str] = None) -> None:
        """Update state and notify callback."""
        with self._state_lock:
            old_state = self._state
            self._state = state

        if state != old_state or device != self._current_device:
            self._current_device = device
            logger.info(f"Port state: {state.value} (device: {device})")

            if self._on_state_change:
                try:
                    self._on_state_change(state, device)
                except Exception as e:
                    logger.error(f"State change callback error: {e}")

    def _on_device_change(self, connected: bool, device: str) -> None:
        """Callback from device detector."""
        if connected:
            logger.info(f"Device connected: {device}")
            # Will be picked up by read loop
        else:
            logger.info(f"Device disconnected: {device}")
            # Close serial if it's the current device
            if device == self._current_device:
                self._close_serial()
                self._set_state(PortState.DISCONNECTED, None)

    def _open_serial(self, device: str) -> bool:
        """Open serial port."""
        if self._serial and self._serial.is_open:
            if self._serial.port == device:
                return True
            self._close_serial()

        try:
            self._serial = serial.Serial(
                port=device,
                baudrate=self._baudrate,
                timeout=0.1
            )
            self._current_device = device
            self._set_state(PortState.CONNECTED, device)
            logger.info(f"Opened serial port: {device} at {self._baudrate} baud")
            return True

        except serial.SerialException as e:
            logger.warning(f"Failed to open {device}: {e}")
            return False

    def _close_serial(self) -> None:
        """Close serial port."""
        if self._serial:
            try:
                self._serial.close()
            except Exception:
                pass
            self._serial = None
            logger.debug("Closed serial port")

    def _find_and_connect(self) -> bool:
        """Find device and connect."""
        device = self._detector.find_device(self._vid, self._pid)
        if device:
            return self._open_serial(device)
        return False

    def _read_loop(self) -> None:
        """Main read loop thread."""
        reconnect_delay = 1.0
        max_reconnect_delay = 5.0

        while self._running:
            # Check for release request
            if self._release_event.is_set():
                self._handle_release()
                continue

            # Try to connect if not connected
            if self.state != PortState.CONNECTED:
                if self._find_and_connect():
                    reconnect_delay = 1.0  # Reset on successful connect
                else:
                    # Wait before retrying
                    self._stop_event.wait(reconnect_delay)
                    reconnect_delay = min(reconnect_delay * 1.5, max_reconnect_delay)
                    continue

            # Read data
            try:
                data = self._serial.read(1024)
                if data:
                    self._buffer.append("serial", data)
                    reconnect_delay = 1.0

            except serial.SerialException as e:
                logger.warning(f"Serial read error: {e}")
                self._close_serial()
                self._set_state(PortState.DISCONNECTED, None)

            except Exception as e:
                logger.error(f"Unexpected read error: {e}")
                self._close_serial()
                self._set_state(PortState.DISCONNECTED, None)

    def _handle_release(self) -> None:
        """Handle port release request."""
        logger.info("Releasing serial port for upload")

        # Close the port
        self._close_serial()
        self._set_state(PortState.RELEASED, self._current_device)

        # Signal release is complete
        self._release_complete.set()
        self._release_event.clear()

        # Wait for reclaim signal
        while self._running and not self._reclaim_event.is_set():
            self._stop_event.wait(0.5)

        if self._running:
            self._reclaim_event.clear()
            logger.info("Reclaiming serial port")

            # Wait for device to reconnect (it may have reset)
            reconnect_attempts = 0
            max_attempts = 20  # 10 seconds

            while self._running and reconnect_attempts < max_attempts:
                if self._find_and_connect():
                    break
                self._stop_event.wait(0.5)
                reconnect_attempts += 1

            if self.state != PortState.CONNECTED:
                logger.warning("Failed to reconnect after upload")

    def start(self) -> None:
        """Start the serial manager."""
        if self._running:
            return

        self._running = True
        self._stop_event.clear()

        # Start device monitoring
        self._detector.start_monitoring()

        # Start read thread
        self._read_thread = threading.Thread(target=self._read_loop, daemon=True)
        self._read_thread.start()

        logger.info("Serial manager started")

    def stop(self) -> None:
        """Stop the serial manager."""
        if not self._running:
            return

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

        # Stop device monitoring
        self._detector.stop_monitoring()

        # Wait for read thread
        if self._read_thread and self._read_thread.is_alive():
            # Unblock any waiting
            self._release_event.set()
            self._reclaim_event.set()
            self._read_thread.join(timeout=2.0)

        # Close serial
        self._close_serial()
        self._set_state(PortState.DISCONNECTED, None)

        logger.info("Serial manager stopped")

    def release_port(self, timeout: float = 5.0) -> bool:
        """
        Release the serial port for upload operations.

        Args:
            timeout: Maximum seconds to wait for release

        Returns:
            True if port was released, False on timeout
        """
        if self.state == PortState.RELEASED:
            return True

        if self.state == PortState.DISCONNECTED:
            # Already available
            self._set_state(PortState.RELEASED, None)
            return True

        # Request release
        self._release_complete.clear()
        self._release_event.set()

        # Wait for release to complete
        if self._release_complete.wait(timeout):
            return True

        logger.warning("Port release timed out")
        return False

    def reclaim_port(self) -> None:
        """
        Reclaim the serial port after upload.

        Call this after upload operations complete.
        """
        if self.state != PortState.RELEASED:
            return

        self._reclaim_event.set()

    def wait_for_device(self, timeout: float = 30.0) -> bool:
        """
        Wait for device to connect.

        Args:
            timeout: Maximum seconds to wait

        Returns:
            True if device connected, False on timeout
        """
        start = time.time()
        while time.time() - start < timeout:
            if self.state == PortState.CONNECTED:
                return True
            time.sleep(0.5)
        return False


def create_virtual_serial_pair() -> tuple[str, str]:
    """
    Create a virtual serial port pair for testing.

    Requires socat to be installed.

    Returns:
        Tuple of (master_path, slave_path)

    Raises:
        RuntimeError: If socat is not available
    """
    import subprocess
    import os
    import tempfile

    # Check for socat
    try:
        subprocess.run(["which", "socat"], check=True, capture_output=True)
    except subprocess.CalledProcessError:
        raise RuntimeError("socat is not installed")

    # Create temp directory for PTYs
    temp_dir = tempfile.mkdtemp(prefix="esppal_test_")
    master_link = os.path.join(temp_dir, "master")
    slave_link = os.path.join(temp_dir, "slave")

    # Start socat
    proc = subprocess.Popen(
        [
            "socat", "-d", "-d",
            f"pty,raw,echo=0,link={master_link}",
            f"pty,raw,echo=0,link={slave_link}"
        ],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )

    # Wait for links to be created
    for _ in range(50):  # 5 seconds
        if os.path.exists(master_link) and os.path.exists(slave_link):
            break
        time.sleep(0.1)
    else:
        proc.terminate()
        raise RuntimeError("Failed to create virtual serial pair")

    return master_link, slave_link
