"""Ring buffer for resilient metric storage."""

from __future__ import annotations

import asyncio
import pickle
from collections import deque
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, TypeVar

import structlog

from curadise_agent.errors import StateError

if TYPE_CHECKING:
    from collections.abc import Iterator
    from pathlib import Path

log = structlog.get_logger(__name__)

T = TypeVar("T")


@dataclass
class BufferStats:
    """Statistics about buffer state."""

    current_size: int
    max_size: int
    total_added: int
    total_removed: int
    total_dropped: int
    persistence_enabled: bool

    @property
    def utilization(self) -> float:
        """Buffer utilization as a percentage."""
        if self.max_size == 0:
            return 0.0
        return (self.current_size / self.max_size) * 100


@dataclass
class RingBuffer[T]:
    """
    Thread-safe ring buffer with optional persistence.

    Provides FIFO storage for metrics with configurable max size.
    When full, oldest items are dropped to make room for new ones.
    """

    max_size: int
    persistence_path: Path | None = None
    _buffer: deque[T] = field(default_factory=deque, init=False)
    _lock: asyncio.Lock = field(default_factory=asyncio.Lock, init=False)
    _total_added: int = field(default=0, init=False)
    _total_removed: int = field(default=0, init=False)
    _total_dropped: int = field(default=0, init=False)

    def __post_init__(self) -> None:
        """Initialize buffer with maxlen."""
        self._buffer = deque(maxlen=self.max_size)

    @property
    def size(self) -> int:
        """Current number of items in buffer."""
        return len(self._buffer)

    @property
    def is_empty(self) -> bool:
        """Check if buffer is empty."""
        return len(self._buffer) == 0

    @property
    def is_full(self) -> bool:
        """Check if buffer is at capacity."""
        return len(self._buffer) >= self.max_size

    def stats(self) -> BufferStats:
        """Get buffer statistics."""
        return BufferStats(
            current_size=len(self._buffer),
            max_size=self.max_size,
            total_added=self._total_added,
            total_removed=self._total_removed,
            total_dropped=self._total_dropped,
            persistence_enabled=self.persistence_path is not None,
        )

    async def add(self, item: T) -> bool:
        """
        Add an item to the buffer.

        If buffer is full, oldest item is dropped.

        Returns:
            True if item was added without dropping, False if an item was dropped
        """
        async with self._lock:
            dropped = len(self._buffer) >= self.max_size
            if dropped:
                self._total_dropped += 1
                log.warning("buffer_item_dropped", buffer_size=len(self._buffer))

            self._buffer.append(item)
            self._total_added += 1
            return not dropped

    async def add_many(self, items: list[T]) -> int:
        """
        Add multiple items to the buffer.

        Returns:
            Number of items that were dropped to make room
        """
        async with self._lock:
            dropped = 0
            for item in items:
                if len(self._buffer) >= self.max_size:
                    dropped += 1
                    self._total_dropped += 1
                self._buffer.append(item)
                self._total_added += 1

            if dropped > 0:
                log.warning("buffer_items_dropped", dropped_count=dropped, added_count=len(items))

            return dropped

    async def get(self) -> T | None:
        """
        Remove and return the oldest item.

        Returns:
            The oldest item, or None if buffer is empty
        """
        async with self._lock:
            if not self._buffer:
                return None
            item = self._buffer.popleft()
            self._total_removed += 1
            return item

    async def get_batch(self, max_count: int) -> list[T]:
        """
        Remove and return up to max_count oldest items.

        Args:
            max_count: Maximum number of items to return

        Returns:
            List of items (may be empty)
        """
        async with self._lock:
            items: list[T] = []
            count = min(max_count, len(self._buffer))
            for _ in range(count):
                items.append(self._buffer.popleft())
            self._total_removed += len(items)
            return items

    async def peek(self) -> T | None:
        """Return the oldest item without removing it."""
        async with self._lock:
            if not self._buffer:
                return None
            return self._buffer[0]

    async def peek_batch(self, max_count: int) -> list[T]:
        """Return up to max_count oldest items without removing them."""
        async with self._lock:
            count = min(max_count, len(self._buffer))
            return list(self._buffer)[:count]

    async def clear(self) -> int:
        """
        Clear all items from the buffer.

        Returns:
            Number of items that were cleared
        """
        async with self._lock:
            count = len(self._buffer)
            self._buffer.clear()
            self._total_dropped += count
            log.info("buffer_cleared", cleared_count=count)
            return count

    def __iter__(self) -> Iterator[T]:
        """Iterate over buffer items (not thread-safe, use for debugging)."""
        return iter(self._buffer)

    async def persist(self) -> None:
        """Persist buffer contents to disk."""
        if self.persistence_path is None:
            return

        async with self._lock:
            try:
                # Ensure directory exists
                self.persistence_path.parent.mkdir(parents=True, exist_ok=True)

                # Write to temp file then rename for atomicity
                temp_path = self.persistence_path.with_suffix(".tmp")
                data = {
                    "items": list(self._buffer),
                    "total_added": self._total_added,
                    "total_removed": self._total_removed,
                    "total_dropped": self._total_dropped,
                }

                # Use asyncio to write in thread pool
                loop = asyncio.get_running_loop()
                await loop.run_in_executor(None, self._write_pickle, temp_path, data)
                await loop.run_in_executor(None, temp_path.rename, self.persistence_path)

                log.debug(
                    "buffer_persisted",
                    item_count=len(self._buffer),
                    path=str(self.persistence_path),
                )
            except Exception as e:
                log.error("buffer_persist_failed", error=str(e))
                raise StateError(f"Failed to persist buffer: {e}", cause=e) from e

    def _write_pickle(self, path: Path, data: dict[str, Any]) -> None:
        """Write pickle data to file."""
        with open(path, "wb") as f:
            pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL)

    async def restore(self) -> int:
        """
        Restore buffer contents from disk.

        Returns:
            Number of items restored
        """
        if self.persistence_path is None or not self.persistence_path.exists():
            return 0

        async with self._lock:
            try:
                loop = asyncio.get_running_loop()
                data = await loop.run_in_executor(None, self._read_pickle, self.persistence_path)

                items = data.get("items", [])
                self._buffer = deque(items, maxlen=self.max_size)
                self._total_added = data.get("total_added", len(items))
                self._total_removed = data.get("total_removed", 0)
                self._total_dropped = data.get("total_dropped", 0)

                log.info("buffer_restored", item_count=len(items), path=str(self.persistence_path))
                return len(items)
            except Exception as e:
                log.error("buffer_restore_failed", error=str(e))
                # Don't fail startup, just return 0
                return 0

    def _read_pickle(self, path: Path) -> dict[str, Any]:
        """Read pickle data from file."""
        with path.open("rb") as f:
            result: dict[str, Any] = pickle.load(f)  # noqa: S301 - trusted internal data
            return result


def create_buffer(
    max_size: int,
    persistence_path: Path | None = None,
) -> RingBuffer[Any]:
    """
    Factory function to create a configured ring buffer.

    Args:
        max_size: Maximum number of items
        persistence_path: Optional path for disk persistence

    Returns:
        Configured RingBuffer instance
    """
    return RingBuffer(max_size=max_size, persistence_path=persistence_path)
