"""
Ring buffer for ESPal daemon.

Provides a thread-safe circular buffer for storing timestamped serial data,
build output, and markers. Supports multiple concurrent readers with
independent read positions.
"""

import threading
import time
from dataclasses import dataclass, field
from typing import Iterator, Optional
from collections import deque


@dataclass
class BufferEntry:
    """A single entry in the ring buffer."""
    timestamp: float
    source: str  # "serial", "stdout", "stderr", "marker"
    data: bytes = field(default_factory=bytes)
    marker: Optional[str] = None  # Marker name if source == "marker"

    @property
    def size(self) -> int:
        """Size of this entry in bytes (approximate)."""
        # Rough estimate: timestamp(8) + source string + data + marker
        base = 8 + len(self.source) + len(self.data)
        if self.marker:
            base += len(self.marker)
        return base


class RingBuffer:
    """
    Thread-safe ring buffer with marker support.

    Stores BufferEntry objects up to a maximum size in bytes.
    Supports named markers for position tracking and replay.
    """

    def __init__(self, max_size_bytes: int = 50 * 1024 * 1024):
        """
        Initialise the ring buffer.

        Args:
            max_size_bytes: Maximum buffer size in bytes (default 50MB)
        """
        self._max_size = max_size_bytes
        self._entries: deque[BufferEntry] = deque()
        self._current_size = 0
        self._lock = threading.RLock()

        # Markers map name -> global sequence number
        self._markers: dict[str, int] = {}

        # Global sequence number (monotonically increasing)
        self._seq_start = 0  # Sequence number of first entry in buffer
        self._seq_end = 0    # Sequence number of next entry to be added

        # Condition for notifying waiting readers
        self._new_data = threading.Condition(self._lock)

    @property
    def size(self) -> int:
        """Current buffer size in bytes."""
        with self._lock:
            return self._current_size

    @property
    def max_size(self) -> int:
        """Maximum buffer size in bytes."""
        return self._max_size

    @property
    def entry_count(self) -> int:
        """Number of entries in the buffer."""
        with self._lock:
            return len(self._entries)

    @property
    def sequence_range(self) -> tuple[int, int]:
        """Return (start_seq, end_seq) of entries currently in buffer."""
        with self._lock:
            return (self._seq_start, self._seq_end)

    def append(self, source: str, data: bytes, timestamp: Optional[float] = None) -> int:
        """
        Append data to the buffer.

        Args:
            source: Data source ("serial", "stdout", "stderr")
            data: Raw bytes to store
            timestamp: Unix timestamp (default: current time)

        Returns:
            Sequence number of the appended entry
        """
        if timestamp is None:
            timestamp = time.time()

        entry = BufferEntry(timestamp=timestamp, source=source, data=data)
        return self._append_entry(entry)

    def mark(self, name: str, timestamp: Optional[float] = None) -> int:
        """
        Insert a named marker at the current position.

        Args:
            name: Marker name (e.g., "upload_start", "upload_complete")
            timestamp: Unix timestamp (default: current time)

        Returns:
            Sequence number of the marker entry
        """
        if timestamp is None:
            timestamp = time.time()

        entry = BufferEntry(
            timestamp=timestamp,
            source="marker",
            data=b"",
            marker=name
        )

        with self._lock:
            seq = self._append_entry(entry)
            self._markers[name] = seq
            return seq

    def _append_entry(self, entry: BufferEntry) -> int:
        """Internal method to append an entry and manage buffer size."""
        entry_size = entry.size

        with self._lock:
            # Evict old entries if necessary
            while self._current_size + entry_size > self._max_size and self._entries:
                old_entry = self._entries.popleft()
                self._current_size -= old_entry.size
                self._seq_start += 1

            # Append new entry
            self._entries.append(entry)
            self._current_size += entry_size
            seq = self._seq_end
            self._seq_end += 1

            # Notify waiting readers
            self._new_data.notify_all()

            return seq

    def get_marker_seq(self, name: str) -> Optional[int]:
        """
        Get the sequence number of a named marker.

        Args:
            name: Marker name

        Returns:
            Sequence number, or None if marker doesn't exist or was evicted
        """
        with self._lock:
            seq = self._markers.get(name)
            if seq is None:
                return None
            # Check if marker has been evicted
            if seq < self._seq_start:
                return None
            return seq

    def get_markers(self) -> dict[str, int]:
        """Get all valid markers (not evicted)."""
        with self._lock:
            return {
                name: seq
                for name, seq in self._markers.items()
                if seq >= self._seq_start
            }

    def iterate_from_seq(self, start_seq: int) -> Iterator[tuple[int, BufferEntry]]:
        """
        Iterate entries starting from a sequence number.

        Args:
            start_seq: Starting sequence number

        Yields:
            Tuples of (sequence_number, entry)
        """
        with self._lock:
            # Clamp to valid range
            if start_seq < self._seq_start:
                start_seq = self._seq_start

            # Calculate index in deque
            index = start_seq - self._seq_start

            # Yield entries
            for i, entry in enumerate(self._entries):
                if i >= index:
                    yield (self._seq_start + i, entry)

    def iterate_from_marker(self, name: str) -> Iterator[tuple[int, BufferEntry]]:
        """
        Iterate entries starting from a named marker.

        Args:
            name: Marker name

        Yields:
            Tuples of (sequence_number, entry)

        Raises:
            KeyError: If marker doesn't exist or was evicted
        """
        seq = self.get_marker_seq(name)
        if seq is None:
            raise KeyError(f"Marker '{name}' not found or evicted")

        yield from self.iterate_from_seq(seq)

    def iterate_tail(self, count: int) -> Iterator[tuple[int, BufferEntry]]:
        """
        Iterate the last N entries.

        Args:
            count: Number of entries to return

        Yields:
            Tuples of (sequence_number, entry)
        """
        with self._lock:
            start_index = max(0, len(self._entries) - count)
            start_seq = self._seq_start + start_index

        yield from self.iterate_from_seq(start_seq)

    def wait_for_data(self, after_seq: int, timeout: Optional[float] = None) -> bool:
        """
        Wait for new data after a sequence number.

        Args:
            after_seq: Wait for data after this sequence number
            timeout: Maximum seconds to wait (None = forever)

        Returns:
            True if new data is available, False if timed out
        """
        with self._new_data:
            # Check if data already available
            if self._seq_end > after_seq:
                return True

            # Wait for notification
            return self._new_data.wait(timeout)

    def get_current_seq(self) -> int:
        """Get the sequence number that would be assigned to the next entry."""
        with self._lock:
            return self._seq_end

    def clear(self) -> None:
        """Clear all entries and markers."""
        with self._lock:
            self._entries.clear()
            self._markers.clear()
            self._current_size = 0
            self._seq_start = self._seq_end


class BufferReader:
    """
    A reader that tracks its position in the buffer.

    Provides a convenient interface for clients to read from a specific
    position and follow new data as it arrives.
    """

    def __init__(self, buffer: RingBuffer, start_seq: Optional[int] = None):
        """
        Create a buffer reader.

        Args:
            buffer: The RingBuffer to read from
            start_seq: Starting sequence number (None = current position)
        """
        self._buffer = buffer
        if start_seq is None:
            self._position = buffer.get_current_seq()
        else:
            self._position = start_seq

    @classmethod
    def from_marker(cls, buffer: RingBuffer, marker: str) -> "BufferReader":
        """
        Create a reader starting from a marker.

        Args:
            buffer: The RingBuffer to read from
            marker: Marker name to start from

        Raises:
            KeyError: If marker doesn't exist or was evicted
        """
        seq = buffer.get_marker_seq(marker)
        if seq is None:
            raise KeyError(f"Marker '{marker}' not found or evicted")
        return cls(buffer, seq)

    @classmethod
    def from_tail(cls, buffer: RingBuffer, count: int) -> "BufferReader":
        """
        Create a reader starting from the last N entries.

        Args:
            buffer: The RingBuffer to read from
            count: Number of entries to start back from
        """
        start_seq, end_seq = buffer.sequence_range
        target_seq = max(start_seq, end_seq - count)
        return cls(buffer, target_seq)

    @property
    def position(self) -> int:
        """Current read position (sequence number)."""
        return self._position

    def read_available(self) -> list[tuple[int, BufferEntry]]:
        """
        Read all available entries from current position.

        Returns:
            List of (sequence_number, entry) tuples
        """
        entries = list(self._buffer.iterate_from_seq(self._position))
        if entries:
            self._position = entries[-1][0] + 1
        return entries

    def read_one(self, timeout: Optional[float] = None) -> Optional[tuple[int, BufferEntry]]:
        """
        Read the next entry, waiting if necessary.

        Args:
            timeout: Maximum seconds to wait (None = forever)

        Returns:
            (sequence_number, entry) tuple, or None if timed out
        """
        # First check if data is available
        entries = list(self._buffer.iterate_from_seq(self._position))
        if entries:
            self._position = entries[0][0] + 1
            return entries[0]

        # Wait for new data
        if self._buffer.wait_for_data(self._position, timeout):
            entries = list(self._buffer.iterate_from_seq(self._position))
            if entries:
                self._position = entries[0][0] + 1
                return entries[0]

        return None

    def iter_blocking(self, timeout: float = 1.0) -> Iterator[tuple[int, BufferEntry]]:
        """
        Iterate entries, blocking for new data.

        Args:
            timeout: Timeout between checks (for clean shutdown)

        Yields:
            (sequence_number, entry) tuples
        """
        while True:
            entry = self.read_one(timeout)
            if entry is not None:
                yield entry
