"""Persistent notification queue for deferred notifications."""

from __future__ import annotations

import asyncio
import copy
import logging
from datetime import datetime, timezone
from typing import Any, Final

from notify_bridge_core.storage import StorageBackend

_LOGGER = logging.getLogger(__name__)

# Bound on queue length. Without a cap, a misconfigured quiet-hour
# window plus high event throughput grows the persisted file unboundedly
# and every enqueue rewrites the whole file (O(n²) total writes). When
# the cap is hit we drop the oldest entry (FIFO) so the most recent
# events still reach the recipient when the window opens.
DEFAULT_MAX_QUEUE_SIZE: Final = 1000


class NotificationQueue:
    """Persistent queue for notifications deferred during quiet hours."""

    def __init__(
        self,
        backend: StorageBackend,
        *,
        max_size: int = DEFAULT_MAX_QUEUE_SIZE,
    ) -> None:
        self._backend = backend
        self._data: dict[str, Any] | None = None
        self._max_size = max_size
        # Coordinates load / enqueue / clear / remove so a write-while-load
        # race can't leave the in-memory copy out of sync with disk and so
        # bulk operations don't interleave their reads-then-writes.
        self._lock = asyncio.Lock()

    @staticmethod
    def _ensure_schema(data: Any) -> dict[str, Any]:
        if not isinstance(data, dict) or not isinstance(data.get("queue"), list):
            return {"queue": []}
        return data

    async def async_load(self) -> None:
        async with self._lock:
            raw = await self._backend.load()
            self._data = self._ensure_schema(raw)

    async def async_enqueue(self, notification_params: dict[str, Any]) -> None:
        async with self._lock:
            if self._data is None:
                self._data = {"queue": []}
            queue: list[dict[str, Any]] = self._data["queue"]
            queue.append({
                "params": notification_params,
                "queued_at": datetime.now(timezone.utc).isoformat(),
            })
            if self._max_size > 0 and len(queue) > self._max_size:
                # Drop oldest (FIFO) so a new event can still land.
                drop = len(queue) - self._max_size
                _LOGGER.warning(
                    "NotificationQueue: dropping %d oldest entries (cap=%d)",
                    drop, self._max_size,
                )
                del queue[:drop]
            await self._backend.save(self._data)

    def get_all(self) -> list[dict[str, Any]]:
        if not self._data:
            return []
        # Deep copy so callers can iterate / mutate without corrupting the
        # in-memory queue. The cost is bounded by ``max_size``.
        return copy.deepcopy(list(self._data.get("queue", [])))

    def has_pending(self) -> bool:
        return bool(self._data and self._data.get("queue"))

    async def async_clear(self) -> None:
        async with self._lock:
            if self._data:
                self._data["queue"] = []
                await self._backend.save(self._data)

    async def async_remove(self) -> None:
        async with self._lock:
            await self._backend.remove()
            self._data = None
