"""Security primitives: password hashing, JWT, CSRF, rate limiting, audit logging."""
import hashlib
import secrets
import time
from datetime import datetime, timedelta, timezone
from typing import Optional

import jwt
import bcrypt as _bcrypt
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.config import settings

# passlib 1.7.4 is incompatible with bcrypt >= 4.1; use bcrypt directly.
# Hashes remain standard $2b$ bcrypt, fully inter-operable.
_BCRYPT_ROUNDS = 12


def hash_password(p: str) -> str:
    pw = p.encode("utf-8")[:72]  # bcrypt input limit
    return _bcrypt.hashpw(pw, _bcrypt.gensalt(rounds=_BCRYPT_ROUNDS)).decode("ascii")


def verify_password(plain: str, hashed: str) -> bool:
    try:
        return _bcrypt.checkpw(plain.encode("utf-8")[:72], hashed.encode("ascii"))
    except Exception:
        return False

# Sensitive values that must never appear in logs or JSON responses.
SENSITIVE_KEYS = {"password", "client_secret", "refresh_token", "authorization_code",
                  "access_token", "secret", "token", "csrf", "new_password", "old_password"}


def create_access_token(user_id: int, expires_minutes: Optional[int] = None) -> str:
    payload = {
        "sub": str(user_id),
        "iat": int(time.time()),
        "exp": int(time.time()) + 60 * (expires_minutes or settings.ACCESS_TOKEN_EXPIRE_MINUTES),
        "jti": secrets.token_urlsafe(16),
    }
    return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")


def decode_token(token: str) -> Optional[dict]:
    try:
        return jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
    except Exception:
        return None


def sanitize_log_payload(payload: dict) -> dict:
    """Strip secrets from any dict before logging/serialization."""
    out = {}
    for k, v in (payload or {}).items():
        if str(k).lower() in SENSITIVE_KEYS:
            out[k] = "[REDACTED]"
        elif isinstance(v, dict):
            out[k] = sanitize_log_payload(v)
        else:
            out[k] = v
    return out


async def log_audit(db: AsyncSession, user_id: Optional[int], action: str,
                    entity: Optional[str] = None, entity_id: Optional[int] = None,
                    detail: Optional[dict] = None, ip: Optional[str] = None) -> None:
    """Audit log with automatic secret redaction. Imported lazily to avoid cycles."""
    from app.models import AuditLog
    db.add(AuditLog(
        user_id=user_id, action=action, entity=entity, entity_id=entity_id,
        detail=sanitize_log_payload(detail or {}), ip_address=ip,
        created_at=datetime.now(timezone.utc),
    ))
    await db.commit()


# ---------------------------------------------------------------------------
# Simple per-user in-Redis rate limiter (fall back to memory when Redis absent)
# ---------------------------------------------------------------------------
import redis as redis_lib

_rate_redis = None
try:
    _rate_redis = redis_lib.from_url(settings.REDIS_URL, socket_timeout=1)
    _rate_redis.ping()
except Exception:
    _rate_redis = None

_mem_buckets: dict = {}


def rate_limit(key: str, limit: int = None, window: int = 60) -> tuple[bool, int]:
    """Return (allowed, remaining). Fixed-window counter in Redis (or memory)."""
    limit = limit or settings.RATE_LIMIT_PER_MINUTE
    now = int(time.time())
    window_key = f"rl:{key}:{now // window}"
    if _rate_redis is not None:
        try:
            pipe = _rate_redis.pipeline()
            pipe.incr(window_key).expire(window_key, window * 2)
            cnt, _ = pipe.execute()
            return cnt <= limit, max(0, limit - int(cnt))
        except Exception:
            pass
    bucket = _mem_buckets.get(window_key, 0) + 1
    _mem_buckets[window_key] = bucket
    if len(_mem_buckets) > 10000:
        _mem_buckets.clear()
    return bucket <= limit, max(0, limit - bucket)
