"""Per-user account lockout after repeated failed login attempts.

Complements the per-IP rate limiting in ``security/rate_limiter.py`` by
tracking failures at the *username* level. A user who exceeds the
configured threshold is temporarily locked out regardless of the source IP.

Note: lockout state is in-memory and per-process. In multi-worker
deployments (e.g. gunicorn), each worker maintains separate state.
"""

import threading
from datetime import datetime, timedelta, timezone

from loguru import logger

from .security_settings import get_security_default


class AccountLockoutManager:
    """Track failed login attempts and lock accounts after a threshold."""

    _MAX_STATE_ENTRIES = 10_000

    def __init__(
        self,
        threshold: int | None = None,
        lockout_minutes: int | None = None,
    ) -> None:
        if threshold is None:
            threshold = get_security_default(
                "security.account_lockout_threshold", 10
            )
        if lockout_minutes is None:
            lockout_minutes = get_security_default(
                "security.account_lockout_duration_minutes", 15
            )

        self.threshold: int = threshold
        self.lockout_minutes: int = lockout_minutes

        # {username: {"count": int, "locked_until": datetime | None}}
        self._state: dict[str, dict] = {}
        self._lock = threading.Lock()

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def is_locked(self, username: str) -> bool:
        """Return ``True`` if *username* is currently locked out."""
        with self._lock:
            entry = self._state.get(username)
            if entry is None:
                return False
            locked_until = entry.get("locked_until")
            if locked_until is None:
                return False
            if datetime.now(timezone.utc) >= locked_until:
                # Lockout expired — reset automatically
                self._state.pop(username, None)
                logger.info("Account lockout expired")
                return False
            return True

    def _evict(self) -> None:
        """Remove expired/unlocked entries to reclaim memory.

        Must be called while ``self._lock`` is held.
        """
        now = datetime.now(timezone.utc)
        expired_keys = [
            key
            for key, entry in self._state.items()
            if entry.get("locked_until") is None or entry["locked_until"] <= now
        ]
        for key in expired_keys:
            del self._state[key]

        logger.info(
            "Evicted {} expired/unlocked lockout entries", len(expired_keys)
        )

        # Last resort: if still over limit, blanket clear
        if len(self._state) > self._MAX_STATE_ENTRIES:
            self._state.clear()
            logger.info("Blanket-cleared lockout state (still over limit)")

    def record_failure(self, username: str) -> None:
        """Record a failed login attempt for *username*."""
        with self._lock:
            if len(self._state) > self._MAX_STATE_ENTRIES:
                self._evict()
            entry = self._state.setdefault(
                username, {"count": 0, "locked_until": None}
            )
            entry["count"] += 1
            if entry["count"] >= self.threshold:
                entry["locked_until"] = datetime.now(timezone.utc) + timedelta(
                    minutes=self.lockout_minutes
                )
                logger.warning(
                    "Account locked after {} failed attempts",
                    self.threshold,
                )

    def record_success(self, username: str) -> None:
        """Clear the failure counter for *username* after a successful login."""
        with self._lock:
            removed = self._state.pop(username, None)
            if removed is not None:
                logger.info("Account lockout cleared after successful login")


# ------------------------------------------------------------------
# Module-level singleton
# ------------------------------------------------------------------

_manager: AccountLockoutManager | None = None
_singleton_lock = threading.Lock()


def get_account_lockout_manager() -> AccountLockoutManager:
    """Return the module-level singleton ``AccountLockoutManager``."""
    global _manager
    if _manager is None:
        with _singleton_lock:
            if _manager is None:
                _manager = AccountLockoutManager()
    return _manager
