"""
login_limiter.py — Two-tier brute-force protection for the login endpoint.

Tier 1: 5 failures within 30 minutes  →  30-minute lockout.
Tier 2: Same IP gets locked out again within 24 hours  →  permanent lockout
         (requires admin action to unlock via Settings → Security).

All timestamps are unix wall-clock (time.time()) so they can be shown in the UI.
State is in-process memory; it resets on server restart.
"""
from __future__ import annotations

import logging
import time
from typing import Any

logger = logging.getLogger(__name__)

# ── Config ────────────────────────────────────────────────────────────────────

MAX_ATTEMPTS = 5          # failures before tier-1 lockout
ATTEMPT_WINDOW = 1800     # 30 min — window in which failures are counted
LOCKOUT_DURATION = 1800   # 30 min — tier-1 lockout duration
RECURRENCE_WINDOW = 86400 # 24 h  — if locked again within this period → tier-2

# ── State ─────────────────────────────────────────────────────────────────────

# Per-IP entry shape:
#   failures:     [unix_ts, ...]   recent failed attempts (pruned to ATTEMPT_WINDOW)
#   locked_until: float | None     unix_ts when tier-1 lockout expires
#   permanent:    bool             tier-2: admin must unlock
#   lockouts_24h: [unix_ts, ...]   when tier-1 lockouts were applied (pruned to 24 h)
#   locked_at:    float | None     when the current lockout started (for display)

_STATE: dict[str, dict[str, Any]] = {}


def _entry(ip: str) -> dict[str, Any]:
    if ip not in _STATE:
        _STATE[ip] = {
            "failures": [],
            "locked_until": None,
            "permanent": False,
            "lockouts_24h": [],
            "locked_at": None,
        }
    return _STATE[ip]


# ── Public API ────────────────────────────────────────────────────────────────

def is_locked(ip: str) -> tuple[bool, str]:
    """Return (locked, kind) where kind is 'permanent', 'temporary', or ''."""
    e = _entry(ip)
    if e["permanent"]:
        return True, "permanent"
    if e["locked_until"] and time.time() < e["locked_until"]:
        return True, "temporary"
    return False, ""


def record_failure(ip: str) -> None:
    """Record a failed login attempt; apply lockout if threshold is reached."""
    e = _entry(ip)
    now = time.time()

    e["failures"].append(now)
    # Prune to the counting window
    cutoff = now - ATTEMPT_WINDOW
    e["failures"] = [t for t in e["failures"] if t > cutoff]

    if len(e["failures"]) < MAX_ATTEMPTS:
        return  # threshold not reached yet

    # Threshold reached — determine tier
    cutoff_24h = now - RECURRENCE_WINDOW
    e["lockouts_24h"] = [t for t in e["lockouts_24h"] if t > cutoff_24h]

    if e["lockouts_24h"]:
        # Already locked before in the last 24 h → permanent
        e["permanent"] = True
        e["locked_until"] = None
        e["locked_at"] = now
        logger.warning("[login_limiter] %s permanently locked (repeat offender within 24 h)", ip)
    else:
        # First offence → 30-minute lockout
        e["locked_until"] = now + LOCKOUT_DURATION
        e["lockouts_24h"].append(now)
        e["locked_at"] = now
        logger.warning("[login_limiter] %s locked for 30 minutes", ip)

    e["failures"] = []  # reset after triggering lockout


def clear_failures(ip: str) -> None:
    """Called on successful login — clears the failure counter for this IP."""
    if ip in _STATE:
        _STATE[ip]["failures"] = []


def unlock(ip: str) -> bool:
    """Admin action: fully reset lockout state for an IP. Returns False if unknown."""
    if ip not in _STATE:
        return False
    _STATE[ip].update(permanent=False, locked_until=None, locked_at=None,
                      failures=[], lockouts_24h=[])
    logger.info("[login_limiter] %s unlocked by admin", ip)
    return True


def unlock_all() -> int:
    """Admin action: unlock every locked IP. Returns count unlocked."""
    count = 0
    for ip, e in _STATE.items():
        if e["permanent"] or (e["locked_until"] and time.time() < e["locked_until"]):
            e.update(permanent=False, locked_until=None, locked_at=None,
                     failures=[], lockouts_24h=[])
            count += 1
    return count


def list_locked() -> list[dict]:
    """Return info dicts for all currently locked IPs (for the admin UI)."""
    now = time.time()
    result = []
    for ip, e in _STATE.items():
        if e["permanent"]:
            result.append({
                "ip": ip,
                "type": "permanent",
                "locked_at": e["locked_at"],
                "locked_until": None,
            })
        elif e["locked_until"] and now < e["locked_until"]:
            result.append({
                "ip": ip,
                "type": "temporary",
                "locked_at": e["locked_at"],
                "locked_until": e["locked_until"],
            })
    return result
