from __future__ import annotations

from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from threading import Lock
import time

from fastapi import HTTPException, status

from app.services.redis_client import get_sync_redis_client


@dataclass
class _Counter:
    value: int
    expires_at: float


class SecurityStateService:
    _counters: dict[str, _Counter] = {}
    _lock = Lock()

    @classmethod
    def enforce_limit(cls, key: str, *, limit: int, window_seconds: int, message: str) -> None:
        count = cls._increment(key, window_seconds)
        if count > limit:
            raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail=message)

    @classmethod
    def record_failure(cls, key: str, *, limit: int, lock_seconds: int, message: str) -> None:
        count = cls._increment(key, lock_seconds)
        if count >= limit:
            raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail=message)

    @classmethod
    def ensure_not_locked(cls, key: str, *, limit: int, message: str) -> None:
        if cls._get(key) >= limit:
            raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail=message)

    @classmethod
    def clear(cls, key: str) -> None:
        client = get_sync_redis_client()
        if client is not None:
            try:
                client.delete(key)
            except Exception:
                pass
        with cls._lock:
            cls._counters.pop(key, None)

    @classmethod
    def mark_flag(cls, key: str, ttl_seconds: int) -> None:
        client = get_sync_redis_client()
        if client is not None:
            try:
                client.set(key, "1", ex=ttl_seconds)
                return
            except Exception:
                pass
        with cls._lock:
            cls._counters[key] = _Counter(1, time.monotonic() + ttl_seconds)

    @classmethod
    def has_flag(cls, key: str) -> bool:
        return cls._get(key) > 0

    @classmethod
    def revoke_token(cls, jti: str, expires_at: int | float | None) -> None:
        now = datetime.now(UTC)
        expiry = datetime.fromtimestamp(float(expires_at), UTC) if expires_at else now + timedelta(days=1)
        ttl = max(1, int((expiry - now).total_seconds()))
        key = f"auth:revoked:{jti}"
        client = get_sync_redis_client()
        if client is not None:
            try:
                client.set(key, "1", ex=ttl)
                return
            except Exception:
                pass
        with cls._lock:
            cls._counters[key] = _Counter(1, time.monotonic() + ttl)

    @classmethod
    def is_token_revoked(cls, jti: str) -> bool:
        return cls._get(f"auth:revoked:{jti}") > 0

    @classmethod
    def _increment(cls, key: str, ttl: int) -> int:
        client = get_sync_redis_client()
        if client is not None:
            try:
                pipe = client.pipeline()
                pipe.incr(key)
                pipe.expire(key, ttl, nx=True)
                value, _ = pipe.execute()
                return int(value)
            except Exception:
                pass

        now = time.monotonic()
        with cls._lock:
            record = cls._counters.get(key)
            if record is None or record.expires_at <= now:
                record = _Counter(0, now + ttl)
            record.value += 1
            cls._counters[key] = record
            return record.value

    @classmethod
    def _get(cls, key: str) -> int:
        client = get_sync_redis_client()
        if client is not None:
            try:
                value = client.get(key)
                return int(value) if value is not None else 0
            except Exception:
                pass
        now = time.monotonic()
        with cls._lock:
            record = cls._counters.get(key)
            if record is None or record.expires_at <= now:
                cls._counters.pop(key, None)
                return 0
            return record.value


def client_ip(request) -> str:
    forwarded = request.headers.get("x-forwarded-for", "").split(",", 1)[0].strip()
    if forwarded:
        return forwarded
    return request.client.host if request.client else "unknown"
