import secrets
import time
from pathlib import Path

import bcrypt
from fastapi import Depends, Form, HTTPException, Request
from sqlalchemy.orm import Session

from app import models
from app.database import BASE_DIR, get_db
from app.permissions import verrouiller_fichier
from app.sauvegarde import creer_sauvegarde
from app.version import obtenir_version

SECRET_KEY_PATH = BASE_DIR / "secret.key"

MAX_TENTATIVES = 5
FENETRE_TENTATIVES_SECONDES = 15 * 60

DUREE_INACTIVITE_MAX_SECONDES = 30 * 60  # déconnexion automatique après 30 min d'inactivité

_tentatives_echouees: dict[str, list[float]] = {}


class AccesRefuse(Exception):
    """Levée quand une session n'est pas authentifiée (ou pas admin) ; interceptée
    dans main.py pour rediriger vers la page de connexion."""

    def __init__(self, redirection: str = "/connexion"):
        self.redirection = redirection


def obtenir_cle_secrete() -> str:
    if SECRET_KEY_PATH.exists():
        verrouiller_fichier(SECRET_KEY_PATH)
        return SECRET_KEY_PATH.read_text(encoding="utf-8").strip()
    cle = secrets.token_hex(32)
    SECRET_KEY_PATH.write_text(cle, encoding="utf-8")
    verrouiller_fichier(SECRET_KEY_PATH)
    return cle


# ---------------------------------------------------------------------------
# Mots de passe
# ---------------------------------------------------------------------------
def hacher_mot_de_passe(mot_de_passe: str) -> str:
    return bcrypt.hashpw(mot_de_passe.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")


def verifier_mot_de_passe(mot_de_passe: str, hash_stocke: str) -> bool:
    try:
        return bcrypt.checkpw(mot_de_passe.encode("utf-8"), hash_stocke.encode("utf-8"))
    except ValueError:
        return False


# ---------------------------------------------------------------------------
# Anti brute-force (en mémoire, process unique)
# ---------------------------------------------------------------------------
def trop_de_tentatives(identifiant: str) -> bool:
    maintenant = time.time()
    horodatages = _tentatives_echouees.get(identifiant, [])
    horodatages = [t for t in horodatages if maintenant - t < FENETRE_TENTATIVES_SECONDES]
    _tentatives_echouees[identifiant] = horodatages
    return len(horodatages) >= MAX_TENTATIVES


def enregistrer_echec(identifiant: str) -> None:
    _tentatives_echouees.setdefault(identifiant, []).append(time.time())


def reinitialiser_tentatives(identifiant: str) -> None:
    _tentatives_echouees.pop(identifiant, None)


# ---------------------------------------------------------------------------
# Session / utilisateur courant
# ---------------------------------------------------------------------------
def sauvegarder_sans_bloquer() -> None:
    """Déclenche une sauvegarde (zip local + envoi Google Drive si connecté) sans jamais
    faire échouer l'appelant : une session qui se termine (déconnexion ou inactivité) ne
    doit pas rester bloquée si le disque est plein ou Drive indisponible."""
    try:
        creer_sauvegarde()
    except OSError:
        pass


def utilisateur_courant(request: Request, db: Session = Depends(get_db)) -> models.Utilisateur | None:
    user_id = request.session.get("user_id")
    if not user_id:
        return None

    maintenant = time.time()
    derniere_activite = request.session.get("derniere_activite")
    if derniere_activite is not None and maintenant - derniere_activite > DUREE_INACTIVITE_MAX_SECONDES:
        request.session.clear()
        sauvegarder_sans_bloquer()
        return None

    utilisateur = db.get(models.Utilisateur, user_id)
    if utilisateur is None or not utilisateur.actif:
        return None

    request.session["derniere_activite"] = maintenant
    return utilisateur


def exiger_connexion(request: Request, db: Session = Depends(get_db)) -> models.Utilisateur:
    utilisateur = utilisateur_courant(request, db)
    if utilisateur is None:
        raise AccesRefuse(f"/connexion?suivant={request.url.path}")
    return utilisateur


def injecter_contexte(request: Request, db: Session = Depends(get_db)) -> None:
    """Dépendance globale (voir FastAPI(dependencies=...) dans main.py) qui expose
    request.state.utilisateur et request.state.csrf_token à tous les templates
    (accessibles via `request`, déjà présent dans tous les contextes Jinja2).

    Volontairement une dépendance FastAPI plutôt qu'un BaseHTTPMiddleware : ce dernier
    exécute l'application interne dans une tâche asyncio séparée et peut désynchroniser
    les mutations de request.session sur les requêtes POST, provoquant des déconnexions
    aléatoires."""
    request.state.utilisateur = utilisateur_courant(request, db)
    request.state.csrf_token = obtenir_jeton_csrf(request)
    request.state.version = obtenir_version()


def exiger_admin(utilisateur: models.Utilisateur = Depends(exiger_connexion)) -> models.Utilisateur:
    if not utilisateur.est_admin:
        raise AccesRefuse("/?erreur=Accès réservé aux administrateurs")
    return utilisateur


# ---------------------------------------------------------------------------
# CSRF
# ---------------------------------------------------------------------------
def obtenir_jeton_csrf(request: Request) -> str:
    jeton = request.session.get("csrf_token")
    if not jeton:
        jeton = secrets.token_hex(32)
        request.session["csrf_token"] = jeton
    return jeton


def verifier_csrf(request: Request, jeton_soumis: str | None) -> bool:
    jeton_session = request.session.get("csrf_token")
    return bool(jeton_session) and bool(jeton_soumis) and secrets.compare_digest(jeton_session, jeton_soumis)


def verifier_jeton_csrf(request: Request, csrf_token: str = Form(...)) -> None:
    """Dépendance à ajouter (dependencies=[Depends(verifier_jeton_csrf)]) sur chaque route
    POST : lit le champ csrf_token du même formulaire que la route (un seul parsing du
    corps de requête, contrairement à une lecture en middleware qui viderait le flux)."""
    if not verifier_csrf(request, csrf_token):
        raise HTTPException(status_code=403, detail="Jeton de sécurité invalide ou expiré. Rechargez la page et réessayez.")
