from __future__ import annotations

import hmac
import secrets
from datetime import timedelta

from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy import func, select
from sqlalchemy.orm import Session

from ..config import DATA_DIR, settings
from ..database import get_db
from ..models import Administrator, RefreshToken, Teacher, utcnow
from ..schemas.schemas import ChangePasswordIn, LoginIn, RefreshIn, SetupIn
from ..security import (create_access_token, hash_password, new_token, password_strong, sha256, verify_password)
from ..services import audit
from ..services.policy import seed_defaults
from ..services.settings_service import get_setting, set_setting
from .deps import Principal, client_ip, current_user

router = APIRouter(tags=["auth"])
SETUP_FILE = DATA_DIR / "setup_code.txt"
_DUMMY = hash_password("dummy-password-for-timing")


def ensure_setup_code(db: Session) -> None:
    """Dipanggil saat server start: jika belum ada admin, buat kode setup satu-kali dan tampilkan di konsol."""
    if db.execute(select(func.count(Administrator.id))).scalar() == 0:
        if not SETUP_FILE.exists():
            SETUP_FILE.write_text(secrets.token_hex(4).upper())
        print(f"\n=== SETUP AWAL ===\nKode setup (dipakai sekali untuk membuat akun admin pertama): {SETUP_FILE.read_text().strip()}"
              f"\nBuka dashboard di browser lalu isi form setup. File: {SETUP_FILE}\n==================\n")


def _issue(db: Session, role: str, user) -> dict:
    refresh = new_token(48)
    db.add(RefreshToken(token_hash=sha256(refresh), role=role, user_id=user.id,
                        expires_at=utcnow() + timedelta(days=settings.refresh_token_days)))
    db.commit()
    return {"access_token": create_access_token(user.username, role, user.id, user.full_name), "refresh_token": refresh,
            "token_type": "bearer", "user": {"id": user.id, "username": user.username, "full_name": user.full_name, "role": role}}


@router.get("/api/setup/status")
def setup_status(db: Session = Depends(get_db)):
    return {"needs_setup": db.execute(select(func.count(Administrator.id))).scalar() == 0}


@router.post("/api/setup")
def setup(body: SetupIn, request: Request, db: Session = Depends(get_db)):
    if db.execute(select(func.count(Administrator.id))).scalar() != 0:
        raise HTTPException(409, "Setup sudah dilakukan")
    if not SETUP_FILE.exists() or not hmac.compare_digest(body.setup_code.strip().upper(), SETUP_FILE.read_text().strip().upper()):
        raise HTTPException(403, "Kode setup salah (lihat konsol server / file data/setup_code.txt)")
    if not password_strong(body.password):
        raise HTTPException(422, "Password minimal 8 karakter dan mengandung huruf serta angka")
    admin = Administrator(username=body.username, full_name=body.full_name, password_hash=hash_password(body.password))
    db.add(admin)
    db.commit()
    if body.school_name:
        set_setting(db, "school_name", body.school_name)
    get_setting(db, "enrollment_key")
    seed_defaults(db)
    SETUP_FILE.unlink(missing_ok=True)
    audit.log(db, "admin", admin.full_name, "setup.admin_created", actor_id=admin.id, ip=client_ip(request))
    return _issue(db, "admin", admin)


@router.post("/api/auth/login")
def login(body: LoginIn, request: Request, db: Session = Depends(get_db)):
    ip = client_ip(request)
    role, user = None, db.execute(select(Administrator).where(Administrator.username == body.username)).scalar_one_or_none()
    if user:
        role = "admin"
    else:
        user = db.execute(select(Teacher).where(Teacher.username == body.username)).scalar_one_or_none()
        role = "guru" if user else None
    ok = verify_password(body.password, user.password_hash if user else _DUMMY) and user is not None and user.is_active
    if not ok:
        audit.log(db, "system", body.username[:60], "auth.login", status="failed", ip=ip, detail={"reason": "kredensial salah"})
        raise HTTPException(401, "Username atau password salah")
    user.last_login_at = utcnow()
    db.commit()
    audit.log(db, role, user.full_name, "auth.login", actor_id=user.id, ip=ip)
    return _issue(db, role, user)


@router.post("/api/auth/refresh")
def refresh(body: RefreshIn, db: Session = Depends(get_db)):
    rt = db.execute(select(RefreshToken).where(RefreshToken.token_hash == sha256(body.refresh_token))).scalar_one_or_none()
    if not rt or rt.revoked or rt.expires_at < utcnow():
        if rt and rt.revoked:  # pemakaian ulang token lama => cabut seluruh token pengguna (deteksi pencurian)
            for t in db.execute(select(RefreshToken).where(RefreshToken.user_id == rt.user_id, RefreshToken.role == rt.role)).scalars():
                t.revoked = True
            db.commit()
        raise HTTPException(401, "Refresh token tidak valid")
    rt.revoked = True  # rotasi
    db.commit()
    user = db.get(Administrator if rt.role == "admin" else Teacher, rt.user_id)
    if not user or not user.is_active:
        raise HTTPException(401, "Akun tidak aktif")
    return _issue(db, rt.role, user)


@router.post("/api/auth/logout")
def logout(body: RefreshIn, db: Session = Depends(get_db)):
    rt = db.execute(select(RefreshToken).where(RefreshToken.token_hash == sha256(body.refresh_token))).scalar_one_or_none()
    if rt:
        rt.revoked = True
        db.commit()
    return {"ok": True}


@router.get("/api/auth/me")
def me(pr: Principal = Depends(current_user)):
    return {"id": pr.id, "name": pr.name, "role": pr.role, "permissions": pr.perms,
            "classroom_ids": None if pr.classroom_ids is None else sorted(pr.classroom_ids)}


@router.post("/api/auth/change-password")
def change_password(body: ChangePasswordIn, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    user = db.get(Administrator if pr.is_admin else Teacher, pr.id)
    if not verify_password(body.old_password, user.password_hash):
        raise HTTPException(400, "Password lama salah")
    if not password_strong(body.new_password):
        raise HTTPException(422, "Password minimal 8 karakter dan mengandung huruf serta angka")
    user.password_hash = hash_password(body.new_password)
    for t in db.execute(select(RefreshToken).where(RefreshToken.user_id == pr.id, RefreshToken.role == pr.role)).scalars():
        t.revoked = True
    db.commit()
    audit.log(db, pr.role, pr.name, "auth.change_password", actor_id=pr.id)
    return {"ok": True}
