"""Autentikasi: masuk, keluar, bootstrap admin pertama, ganti sandi, daftar.

Tamu hanya boleh menyentuh rute di blueprint ini (plus /health dan berkas statis).
"""

from __future__ import annotations

import secrets
import time

from flask import (Blueprint, flash, redirect, render_template, request, session,
                   url_for)

from keamanan import (COOKIE_CSRF, POLA_PENGGUNA, cocok_sandi, hash_sandi,
                      pengguna_sekarang)
from konfigurasi import ADMIN_PASS

bp = Blueprint("auth", __name__)

_GAGAL: dict[str, list[float]] = {}
MIN_SANDI = 8


def _lanjut_aman(nilai: str | None) -> str:
    """Hanya izinkan path relatif; tolak URL absolut (open redirect)."""
    if not nilai or not nilai.startswith("/") or nilai.startswith("//"):
        return url_for("index")
    return nilai


def _catat_gagal(kunci: str) -> None:
    sekarang = time.time()
    daftar = [t for t in _GAGAL.get(kunci, []) if sekarang - t < 300]
    daftar.append(sekarang)
    _GAGAL[kunci] = daftar


def _terlalu_sering(kunci: str) -> bool:
    sekarang = time.time()
    daftar = [t for t in _GAGAL.get(kunci, []) if sekarang - t < 60]
    return len(daftar) >= 6


@bp.route("/masuk", methods=["GET", "POST"])
def masuk():
    from app import kueri

    if pengguna_sekarang():
        return redirect(_lanjut_aman(request.args.get("lanjut")))

    if not kueri("SELECT COUNT(*) c FROM pengguna", satu=True)["c"]:
        return redirect(url_for("auth.pertama"))

    if request.method == "POST":
        nama_pengguna = (request.form.get("pengguna") or "").strip()
        sandi = request.form.get("sandi") or ""
        if _terlalu_sering(nama_pengguna.lower()):
            time.sleep(1)
            flash("Terlalu banyak percobaan. Tunggu satu menit lalu coba lagi.", "galat")
            return render_template("masuk.html", lanjut=request.form.get("lanjut"))

        baris = kueri("SELECT * FROM pengguna WHERE pengguna = %s OR email = %s",
                      (nama_pengguna, nama_pengguna), satu=True)
        if not baris or not cocok_sandi(sandi, baris["sandi_hash"]):
            _catat_gagal(nama_pengguna.lower())
            time.sleep(0.6)
            flash("Nama pengguna atau sandi tidak cocok.", "galat")
        elif not baris["aktif"]:
            flash("Akun ini tidak aktif. Hubungi admin atau dosen Anda.", "galat")
        else:
            session.clear()
            session[COOKIE_CSRF] = secrets.token_urlsafe(24)
            session["pengguna_id"] = baris["id"]
            session["peran"] = baris["peran"]
            session["nama"] = baris["nama"]
            kueri("UPDATE pengguna SET masuk_terakhir = NOW() WHERE id = %s", (baris["id"],))
            if baris["ganti_sandi"]:
                flash("Ganti sandi dulu sebelum memakai atlas.", "ok")
                return redirect(url_for("auth.ganti_sandi"))
            return redirect(_lanjut_aman(request.form.get("lanjut")))

    return render_template("masuk.html", lanjut=request.args.get("lanjut"))


@bp.route("/masuk/pertama", methods=["GET", "POST"])
def pertama():
    """Buat akun admin pertama; hanya sekali, dan butuh HIST_ADMIN_PASS bila diset."""
    from app import kueri

    if kueri("SELECT COUNT(*) c FROM pengguna", satu=True)["c"]:
        return redirect(url_for("auth.masuk"))

    if request.method == "POST":
        nama = (request.form.get("nama") or "").strip()
        nama_pengguna = (request.form.get("pengguna") or "").strip()
        sandi = request.form.get("sandi") or ""
        sandi2 = request.form.get("sandi2") or ""
        kunci = request.form.get("kunci") or ""
        galat = []
        if ADMIN_PASS and not secrets.compare_digest(kunci, ADMIN_PASS):
            galat.append("Kunci admin (HIST_ADMIN_PASS) tidak cocok.")
        if not nama:
            galat.append("Nama wajib diisi.")
        if not POLA_PENGGUNA.match(nama_pengguna):
            galat.append("Nama pengguna 3–40 karakter: huruf, angka, titik, garis bawah.")
        if len(sandi) < MIN_SANDI:
            galat.append(f"Sandi minimal {MIN_SANDI} karakter.")
        if sandi != sandi2:
            galat.append("Ulangi sandi tidak sama.")
        if not galat:
            kueri("INSERT INTO pengguna (nama, pengguna, sandi_hash, peran, aktif, ganti_sandi) "
                  "VALUES (%s, %s, %s, 'admin', 1, 0)",
                  (nama, nama_pengguna, hash_sandi(sandi)))
            flash("Akun admin dibuat. Silakan masuk.", "ok")
            return redirect(url_for("auth.masuk"))
        for pesan in galat:
            flash(pesan, "galat")
    return render_template("pertama.html", ada_kunci=bool(ADMIN_PASS))


@bp.post("/keluar")
def keluar():
    session.clear()
    session[COOKIE_CSRF] = secrets.token_urlsafe(24)
    flash("Sudah keluar.", "ok")
    return redirect(url_for("auth.masuk"))


@bp.route("/ganti-sandi", methods=["GET", "POST"])
def ganti_sandi():
    from app import kueri

    pengguna = pengguna_sekarang()
    if not pengguna:
        return redirect(url_for("auth.masuk"))

    if request.method == "POST":
        lama = request.form.get("sandi_lama") or ""
        baru = request.form.get("sandi") or ""
        baru2 = request.form.get("sandi2") or ""
        galat = []
        if not pengguna["ganti_sandi"] and not cocok_sandi(lama, pengguna["sandi_hash"]):
            galat.append("Sandi lama tidak cocok.")
        if len(baru) < MIN_SANDI:
            galat.append(f"Sandi baru minimal {MIN_SANDI} karakter.")
        if baru != baru2:
            galat.append("Ulangi sandi baru tidak sama.")
        if baru and cocok_sandi(baru, pengguna["sandi_hash"]):
            galat.append("Sandi baru harus berbeda dari sandi lama.")
        if galat:
            for pesan in galat:
                flash(pesan, "galat")
        else:
            kueri("UPDATE pengguna SET sandi_hash=%s, ganti_sandi=0 WHERE id=%s",
                  (hash_sandi(baru), pengguna["id"]))
            flash("Sandi diperbarui.", "ok")
            return redirect(url_for("index"))
    return render_template("ganti_sandi.html", pengguna=pengguna)


@bp.route("/daftar", methods=["GET", "POST"])
def daftar():
    """Pendaftaran mahasiswa dengan kode kelas wajib."""
    from app import kueri

    if request.method == "POST":
        nama = (request.form.get("nama") or "").strip()
        nama_pengguna = (request.form.get("pengguna") or "").strip()
        sandi = request.form.get("sandi") or ""
        kode = (request.form.get("kode") or "").strip().upper()
        galat = []
        kelas = kueri("SELECT * FROM kelas WHERE kode_gabung = %s AND aktif = 1",
                      (kode,), satu=True)
        if not kelas:
            galat.append("Kode kelas tidak dikenal. Minta kode terbaru ke dosen Anda.")
        if not nama:
            galat.append("Nama lengkap wajib diisi.")
        if not POLA_PENGGUNA.match(nama_pengguna):
            galat.append("Nama pengguna 3–40 karakter: huruf, angka, titik, garis bawah.")
        if len(sandi) < MIN_SANDI:
            galat.append(f"Sandi minimal {MIN_SANDI} karakter.")
        if kueri("SELECT id FROM pengguna WHERE pengguna = %s", (nama_pengguna,), satu=True):
            galat.append("Nama pengguna sudah dipakai.")
        if not galat:
            pid = kueri("INSERT INTO pengguna (nama, pengguna, sandi_hash, peran, aktif, ganti_sandi) "
                        "VALUES (%s, %s, %s, 'mahasiswa', 1, 0)",
                        (nama, nama_pengguna, hash_sandi(sandi)))
            kueri("INSERT IGNORE INTO kelas_anggota (kelas_id, pengguna_id) VALUES (%s, %s)",
                  (kelas["id"], pid))
            flash("Akun dibuat. Silakan masuk.", "ok")
            return redirect(url_for("auth.masuk"))
        for pesan in galat:
            flash(pesan, "galat")
    return render_template("daftar.html")


@bp.route("/kelas/gabung", methods=["GET", "POST"])
def gabung():
    from app import kueri

    pengguna = pengguna_sekarang()
    if not pengguna:
        return redirect(url_for("auth.masuk"))
    if request.method == "POST":
        kode = (request.form.get("kode") or "").strip().upper()
        kelas = kueri("SELECT * FROM kelas WHERE kode_gabung = %s AND aktif = 1",
                      (kode,), satu=True)
        if not kelas:
            flash("Kode kelas tidak dikenal.", "galat")
        else:
            kueri("INSERT IGNORE INTO kelas_anggota (kelas_id, pengguna_id) VALUES (%s, %s)",
                  (kelas["id"], pengguna["id"]))
            flash(f"Bergabung ke kelas “{kelas['nama']}”.", "ok")
            return redirect(url_for("index"))
    return render_template("gabung.html")
