"""Keamanan Atlas Histo: sandi, CSRF, peran, dan hak lihat/sunting.

Modul ini sengaja tidak mengimpor `app` di tingkat atas: `app` mengimpor modul
ini saat start, jadi impor DB dilakukan di dalam fungsi (lazy) untuk menghindari
siklus impor.
"""

from __future__ import annotations

import re
import secrets
from functools import wraps

from flask import abort, g, redirect, request, session, url_for
from markupsafe import Markup
from werkzeug.security import check_password_hash, generate_password_hash

COOKIE_CSRF = "_csrf"
PERAN = ("admin", "dosen", "mahasiswa")
POLA_PENGGUNA = re.compile(r"^[a-zA-Z0-9._-]{3,40}$")


def hash_sandi(sandi: str) -> str:
    # scrypt: lebih tahan tebakan per detik daripada pbkdf2, dan di CPU i5 lama
    # pbkdf2 bawaan Werkzeug (1 juta iterasi) butuh ~8 detik per login.
    return generate_password_hash((sandi or "").strip(), method="scrypt")


def cocok_sandi(sandi: str, hash_tersimpan: str | None) -> bool:
    if not hash_tersimpan:
        return False
    try:
        return check_password_hash(hash_tersimpan, (sandi or "").strip())
    except (ValueError, TypeError):
        return False


def csrf_token() -> str:
    token = session.get(COOKIE_CSRF)
    if not token:
        token = secrets.token_urlsafe(24)
        session[COOKIE_CSRF] = token
    return token


def csrf_input() -> Markup:
    return Markup(f'<input type="hidden" name="{COOKIE_CSRF}" value="{csrf_token()}">')


def cek_csrf() -> None:
    """Tolak POST tanpa token sesi yang sah."""
    if request.method not in ("POST", "PUT", "PATCH", "DELETE"):
        return
    tersimpan = session.get(COOKIE_CSRF) or ""
    dikirim = request.form.get(COOKIE_CSRF) or request.headers.get("X-CSRF-Token") or ""
    if not tersimpan or not dikirim or not secrets.compare_digest(dikirim, tersimpan):
        abort(400, description="Token formulir tidak sah. Muat ulang halaman lalu coba lagi.")


def pengguna_sekarang() -> dict | None:
    """Baris pengguna aktif dari sesi (di-cache per request di flask.g)."""
    if "pengguna" in g:
        return g.pengguna
    pid = session.get("pengguna_id")
    g.pengguna = None
    if pid:
        from app import kueri
        g.pengguna = kueri(
            "SELECT * FROM pengguna WHERE id = %s AND aktif = 1", (pid,), satu=True)
        if not g.pengguna:
            session.pop("pengguna_id", None)
    return g.pengguna


def peran_sekarang() -> str | None:
    pengguna = pengguna_sekarang()
    return (pengguna or {}).get("peran")


def butuh(*peran: str):
    """Dekorator: tamu → /masuk, peran salah → 403."""
    izin = set(peran) or set(PERAN)

    def hias(fungsi):
        @wraps(fungsi)
        def bungkus(*a, **kw):
            pengguna = pengguna_sekarang()
            if not pengguna:
                return redirect(url_for("auth.masuk", lanjut=request.full_path.rstrip("?")))
            if pengguna["peran"] not in izin:
                abort(403)
            return fungsi(*a, **kw)
        return bungkus
    return hias


def boleh_sunting(gbr: dict | None, pengguna: dict | None) -> bool:
    if not gbr or not pengguna:
        return False
    if pengguna["peran"] == "admin":
        return True
    return pengguna["peran"] == "dosen" and gbr.get("dosen_id") == pengguna["id"]


def kelas_mahasiswa(pengguna_id: int) -> list[int]:
    from app import kueri
    baris = kueri("SELECT kelas_id FROM kelas_anggota WHERE pengguna_id = %s", (pengguna_id,))
    return [r["kelas_id"] for r in baris]


def boleh_lihat(gbr: dict | None, pengguna: dict | None) -> bool:
    """Admin & dosen melihat semua; mahasiswa hanya materi kelasnya."""
    if not gbr or not pengguna:
        return False
    if gbr.get("aktif") == 0:
        return pengguna["peran"] == "admin" or gbr.get("dosen_id") == pengguna["id"]
    if pengguna["peran"] in ("admin", "dosen"):
        return True
    if gbr.get("publik"):
        return True
    from app import kueri
    baris = kueri(
        "SELECT 1 AS ada FROM kelas_anggota ka JOIN kelas k ON k.id = ka.kelas_id "
        "WHERE ka.pengguna_id = %s AND k.dosen_id = %s LIMIT 1",
        (pengguna["id"], gbr.get("dosen_id")), satu=True)
    return bool(baris)


def sql_lihat_preparat(pengguna: dict | None) -> tuple[str, tuple]:
    """Potongan WHERE untuk daftar preparat sesuai peran."""
    if not pengguna:
        return ("0 = 1", ())
    if pengguna["peran"] in ("admin", "dosen"):
        return ("g.aktif = 1", ())
    return ("g.aktif = 1 AND (g.publik = 1 OR g.dosen_id IN ("
            "SELECT k.dosen_id FROM kelas k JOIN kelas_anggota ka ON ka.kelas_id = k.id "
            "WHERE ka.pengguna_id = %s))", (pengguna["id"],))
