"""Núcleo de segurança: banco, sessões revogáveis, limite de tentativas, senhas, criptografia e auditoria."""
import os
import re
import uuid
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Optional

import bcrypt
import jwt
from cryptography.fernet import Fernet
from dotenv import load_dotenv
from fastapi import Depends, Header, HTTPException, Request
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseModel, Field
from pymongo import ReturnDocument

ROOT_DIR = Path(__file__).parent
load_dotenv(ROOT_DIR / ".env")

client = AsyncIOMotorClient(os.environ["MONGO_URL"])
db = client[os.environ["DB_NAME"]]

JWT_SECRET = os.environ["JWT_SECRET"]
JWT_ALG = "HS256"
ISSUER = "playchannels-api"
AUDIENCE = {"user": "mobile-api", "admin": "admin-web"}
USER_SESSION = timedelta(days=int(os.environ["SESSION_DAYS"]))
ADMIN_SESSION = timedelta(hours=int(os.environ["ADMIN_SESSION_HOURS"]))
LOGIN_MAX = int(os.environ["LOGIN_MAX_ATTEMPTS"])
LOGIN_WINDOW = timedelta(minutes=int(os.environ["LOGIN_WINDOW_MINUTES"]))
TRUST_PROXY = os.environ["TRUST_PROXY"].lower() == "true"
UPLOAD_DIR = Path(os.environ["UPLOAD_DIR"])
MAX_UPLOAD_MB = int(os.environ["MAX_UPLOAD_MB"])
fernet = Fernet(os.environ["VPN_ENCRYPTION_KEY"].encode())

NO_ID = {"_id": 0}
USERNAME_RE = re.compile(r"^[a-z0-9._-]{3,40}$")
COMMON = {"password", "senha", "admin", "admin123", "123456", "12345678", "qwerty", "playchannels"}


def now() -> datetime:
    return datetime.now(timezone.utc)


def now_iso() -> str:
    return now().isoformat()


# ---------------- Passwords ----------------
def hash_password(pw: str) -> str:
    return bcrypt.hashpw(pw.encode(), bcrypt.gensalt()).decode()


def verify_password(pw: str, hashed: str) -> bool:
    try:
        return bcrypt.checkpw(pw.encode(), hashed.encode())
    except ValueError:
        return False


DUMMY_HASH = hash_password(uuid.uuid4().hex)


def password_issues(pw: str, admin: bool) -> list[str]:
    if not admin:
        return [] if 6 <= len(pw) <= 128 else ["entre 6 e 128 caracteres"]
    issues = []
    if not 12 <= len(pw) <= 128:
        issues.append("mínimo de 12 caracteres")
    if not re.search(r"[a-z]", pw):
        issues.append("uma letra minúscula")
    if not re.search(r"[A-Z]", pw):
        issues.append("uma letra maiúscula")
    if not re.search(r"\d", pw):
        issues.append("um número")
    if not re.search(r"[^A-Za-z0-9]", pw):
        issues.append("um símbolo")
    if pw.lower() in COMMON:
        issues.append("não pode ser uma senha comum")
    return issues


def enforce_password(pw: str, admin: bool):
    issues = password_issues(pw, admin)
    if issues:
        raise HTTPException(422, "Senha fraca. Requisitos: " + ", ".join(issues))


# ---------------- Encryption (VPN secrets) ----------------
def encrypt(value: str) -> str:
    return fernet.encrypt(value.encode()).decode()


def decrypt(value: str) -> str:
    return fernet.decrypt(value.encode()).decode()


# ---------------- Login throttling ----------------
def client_ip(request: Request) -> str:
    xff = request.headers.get("x-forwarded-for")
    if TRUST_PROXY and xff:
        return xff.split(",")[0].strip()
    return request.client.host if request.client else "unknown"


def _buckets(username: str, ip: str):
    # (key, limit): por usuário+IP, por usuário e por IP (limite maior para redes compartilhadas)
    return [(f"pair:{username}:{ip}", LOGIN_MAX), (f"u:{username}", LOGIN_MAX * 2), (f"ip:{ip}", LOGIN_MAX * 4)]


async def throttle_check(username: str, ip: str):
    for key, limit in _buckets(username, ip):
        doc = await db.login_limits.find_one({"key": key, "expires_at": {"$gt": now()}})
        if doc and doc["count"] >= limit:
            exp = doc["expires_at"].replace(tzinfo=timezone.utc)
            minutes = max(1, int((exp - now()).total_seconds() // 60) + 1)
            raise HTTPException(429, f"Muitas tentativas de login. Tente novamente em {minutes} minuto(s).")


async def throttle_fail(username: str, ip: str):
    for key, _ in _buckets(username, ip):
        await db.login_limits.delete_many({"key": key, "expires_at": {"$lte": now()}})
        await db.login_limits.find_one_and_update(
            {"key": key},
            {"$inc": {"count": 1}, "$setOnInsert": {"key": key, "expires_at": now() + LOGIN_WINDOW}},
            upsert=True, return_document=ReturnDocument.AFTER)


async def throttle_clear(username: str, ip: str):
    await db.login_limits.delete_many({"key": {"$in": [f"pair:{username}:{ip}", f"u:{username}"]}})


async def security_log(event: str, username: str, ip: str, kind: str, ok: bool):
    await db.security_log.insert_one({"at": now(), "event": event, "username": username, "ip": ip, "kind": kind, "ok": ok})


# ---------------- Sessions ----------------
class LoginIn(BaseModel):
    username: str = Field(min_length=1, max_length=64)
    password: str = Field(min_length=1, max_length=128)


def public_user(u: dict) -> dict:
    keys = ("id", "username", "name", "role", "active", "expires_at", "created_at", "last_login_at", "must_change_password", "notes")
    return {k: u.get(k) for k in keys}


async def authenticate(body: LoginIn, kind: str, request: Request) -> dict:
    username = body.username.strip().lower()
    ip = client_ip(request)
    await throttle_check(username, ip)
    user = await db.users.find_one({"username": username}, NO_ID) if USERNAME_RE.match(username) else None
    valid = verify_password(body.password, user["password_hash"] if user else DUMMY_HASH)
    if not user or not valid or (kind == "admin" and user.get("role") != "admin"):
        await throttle_fail(username, ip)
        await security_log("login_failed", username, ip, kind, False)
        raise HTTPException(401, "Usuário ou senha incorretos")
    if not user.get("active", True):
        raise HTTPException(403, "Acesso bloqueado. Entre em contato com o suporte.")
    if kind == "user" and user.get("expires_at") and user["expires_at"] < now_iso():
        raise HTTPException(403, "Acesso expirado. Renove seu plano para continuar.")
    await throttle_clear(username, ip)
    await security_log("login_ok", username, ip, kind, True)
    await db.users.update_one({"id": user["id"]}, {"$set": {"last_login_at": now_iso()}})
    return user


async def create_session(user: dict, kind: str, request: Request) -> tuple[str, str]:
    issued = now()
    expires = issued + (ADMIN_SESSION if kind == "admin" else USER_SESSION)
    jti = str(uuid.uuid4())
    await db.sessions.insert_one({
        "id": jti, "user_id": user["id"], "username": user["username"], "kind": kind,
        "ip": client_ip(request), "user_agent": request.headers.get("user-agent", "")[:200],
        "created_at": issued, "expires_at": expires, "revoked_at": None})
    token = jwt.encode({"iss": ISSUER, "aud": AUDIENCE[kind], "sub": user["id"], "jti": jti, "kind": kind,
                        "iat": issued, "nbf": issued, "exp": expires}, JWT_SECRET, algorithm=JWT_ALG)
    return token, expires.isoformat()


async def revoke_sessions(user_id: str, kind: Optional[str] = None, except_jti: Optional[str] = None) -> int:
    q: dict = {"user_id": user_id, "revoked_at": None}
    if kind:
        q["kind"] = kind
    if except_jti:
        q["id"] = {"$ne": except_jti}
    res = await db.sessions.update_many(q, {"$set": {"revoked_at": now()}})
    return res.modified_count


async def revoke_current(user: dict):
    await db.sessions.update_one({"id": user["_session"]}, {"$set": {"revoked_at": now()}})


def _auth(kind: str):
    async def dependency(authorization: Optional[str] = Header(None)) -> dict:
        if not authorization or not authorization.lower().startswith("bearer "):
            raise HTTPException(401, "Não autenticado")
        try:
            claims = jwt.decode(authorization[7:], JWT_SECRET, algorithms=[JWT_ALG], issuer=ISSUER, audience=AUDIENCE[kind])
            if claims.get("kind") != kind:
                raise jwt.InvalidTokenError()
        except jwt.InvalidTokenError:
            raise HTTPException(401, "Sessão inválida ou expirada")
        session = await db.sessions.find_one({"id": claims["jti"], "revoked_at": None, "expires_at": {"$gt": now()}})
        if not session:
            raise HTTPException(401, "Sessão encerrada. Faça login novamente.")
        user = await db.users.find_one({"id": claims["sub"]}, NO_ID)
        if not user or not user.get("active", True):
            raise HTTPException(401, "Usuário inativo ou inexistente")
        if kind == "admin" and user.get("role") != "admin":
            raise HTTPException(403, "Acesso restrito ao administrador")
        if kind == "user" and user.get("expires_at") and user["expires_at"] < now_iso():
            raise HTTPException(401, "Acesso expirado")
        user["_session"] = claims["jti"]
        return user
    return dependency


get_current_user = _auth("user")
get_admin_any = _auth("admin")  # permite apenas troca de senha / logout quando a troca é obrigatória


async def require_admin(user: dict = Depends(get_admin_any)) -> dict:
    if user.get("must_change_password"):
        raise HTTPException(403, "Troca de senha obrigatória antes de usar o painel.")
    return user


async def audit(admin: dict, action: str, collection: str, item_id: Optional[str] = None, detail: Optional[str] = None):
    await db.audit_log.insert_one({"at": now(), "admin": admin["username"], "action": action,
                                   "collection": collection, "item_id": item_id, "detail": detail})


async def ensure_indexes():
    await db.users.create_index("username", unique=True)
    await db.users.create_index("id", unique=True)
    await db.sessions.create_index("id", unique=True)
    await db.sessions.create_index("user_id")
    await db.sessions.create_index("expires_at", expireAfterSeconds=0)
    await db.login_limits.create_index("key", unique=True)
    await db.login_limits.create_index("expires_at", expireAfterSeconds=0)
    await db.security_log.create_index("at", expireAfterSeconds=60 * 60 * 24 * 90)
    await db.audit_log.create_index("at", expireAfterSeconds=60 * 60 * 24 * 365)
    await db.vpn_devices.create_index([("server_id", 1), ("address", 1)], unique=True)
    await db.vpn_devices.create_index([("server_id", 1), ("public_key", 1)], unique=True)
