"""Per-key connection-slot cap over a Redis counter (the R-28 pattern).

INCR on acquire, DECR on release, with a TTL so a crashed process can't
leak a slot forever. Best-effort + **fail-OPEN**: a Redis outage lets
connections through rather than locking everyone out (the push path is
already degraded in an outage; denying authenticated users on top is the
wrong failure mode). Optional ``on_acquire`` / ``on_release`` callbacks
let a consumer wire its own observability without this module importing
it.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from collections.abc import Callable

__all__ = ["SlotCap"]

LOG = logging.getLogger(__name__)


class SlotCap:
    def __init__(
        self,
        redis: Any,
        *,
        cap: int = 20,
        key_prefix: str = "livepush:slot:",
        ttl_seconds: int = 300,
        on_acquire: Callable[[str], None] | None = None,
        on_release: Callable[[str], None] | None = None,
    ) -> None:
        self._redis = redis
        self.cap = cap
        self._prefix = key_prefix
        self._ttl = ttl_seconds
        self._on_acquire = on_acquire
        self._on_release = on_release

    def _key(self, slot_key: str) -> str:
        return f"{self._prefix}{slot_key}"

    async def acquire(self, slot_key: str) -> bool:
        """Reserve one slot for ``slot_key``. Returns ``False`` if the cap
        is reached (caller refuses); ``True`` otherwise (caller MUST
        eventually :meth:`release`). Fails OPEN (returns ``True``) on any
        Redis error."""
        key = self._key(slot_key)
        try:
            count = await self._redis.incr(key)
            if count == 1:
                await self._redis.expire(key, self._ttl)
            if count > self.cap:
                await self._redis.decr(key)
                return False
        except Exception:
            LOG.warning(
                "livepush: slot acquire failed; failing open",
                exc_info=True,
                extra={
                    "event": "mgf.livepush.slot_acquire_failed",  # LG-15
                    "slot_key": slot_key,
                    "cap": self.cap,
                    "fingerprint": f"livepush.slot_acquire_failed:{slot_key}",
                },
            )
            return True
        if self._on_acquire is not None:
            self._on_acquire(slot_key)
        return True

    async def release(self, slot_key: str) -> None:
        """Release one slot for ``slot_key``. No-op on Redis failure;
        floors the counter at 0."""
        key = self._key(slot_key)
        try:
            count = await self._redis.decr(key)
            if count is not None and count < 0:
                await self._redis.set(key, 0, ex=self._ttl)
        except Exception:
            LOG.warning(
                "livepush: slot release failed (best-effort)",
                exc_info=True,
                extra={
                    "event": "mgf.livepush.slot_release_failed",  # LG-15
                    "slot_key": slot_key,
                    "fingerprint": f"livepush.slot_release_failed:{slot_key}",
                },
            )
        if self._on_release is not None:
            self._on_release(slot_key)
