"""Siswa, kelas, pengguna (admin/guru), notifikasi, log, laporan, pengaturan."""
from __future__ import annotations

import csv
import io
from datetime import datetime, timedelta

from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import StreamingResponse
from sqlalchemy import delete, func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from ..database import get_db
from ..models import (ActivityLog, Administrator, ApplicationRequest, Classroom, Computer, ComputerCommand, DownloadRequest,
                      Notification, Student, Teacher, utcnow)
from ..schemas.schemas import (TEACHER_PERMS, ClassIn, SettingsIn, StudentIn, UserIn)
from ..security import hash_password, password_strong
from ..services import audit
from ..services.settings_service import DEFAULTS, EDITABLE_BY_ADMIN, all_settings, set_setting
from .deps import Principal, admin_only, client_ip, current_user, visible_computers

router = APIRouter(prefix="/api", tags=["management"])


# ---- Kelas -------------------------------------------------------------------------------
def _cls(c: Classroom, counts: dict) -> dict:
    return {"id": c.id, "name": c.name, "description": c.description, "teacher_id": c.teacher_id,
            "teacher": c.teacher.full_name if c.teacher else None, "computers": counts.get(c.id, 0)}


@router.get("/classrooms")
def list_classrooms(pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    counts = dict(db.execute(select(Computer.classroom_id, func.count(Computer.id)).where(Computer.released.is_(False)).group_by(Computer.classroom_id)).all())
    rows = db.execute(select(Classroom).order_by(Classroom.name)).scalars()
    return [_cls(c, counts) for c in rows if pr.classroom_ids is None or c.id in pr.classroom_ids]


@router.post("/classrooms")
def create_classroom(body: ClassIn, request: Request, pr: Principal = Depends(admin_only), db: Session = Depends(get_db)):
    c = Classroom(**body.model_dump())
    db.add(c)
    try:
        db.commit()
    except IntegrityError:
        db.rollback()
        raise HTTPException(409, "Nama kelas sudah ada / guru tidak valid")
    audit.log(db, "admin", pr.name, "classroom.create", actor_id=pr.id, detail={"id": c.id, "name": c.name}, ip=client_ip(request))
    return _cls(c, {})


@router.put("/classrooms/{cid}")
def update_classroom(cid: int, body: ClassIn, request: Request, pr: Principal = Depends(admin_only), db: Session = Depends(get_db)):
    c = db.get(Classroom, cid)
    if not c:
        raise HTTPException(404, "Kelas tidak ditemukan")
    for k, v in body.model_dump().items():
        setattr(c, k, v)
    try:
        db.commit()
    except IntegrityError:
        db.rollback()
        raise HTTPException(409, "Nama kelas sudah ada / guru tidak valid")
    audit.log(db, "admin", pr.name, "classroom.update", actor_id=pr.id, detail={"id": c.id}, ip=client_ip(request))
    return _cls(c, {})


@router.delete("/classrooms/{cid}")
def delete_classroom(cid: int, request: Request, pr: Principal = Depends(admin_only), db: Session = Depends(get_db)):
    c = db.get(Classroom, cid)
    if not c:
        raise HTTPException(404, "Kelas tidak ditemukan")
    db.delete(c)
    db.commit()
    audit.log(db, "admin", pr.name, "classroom.delete", actor_id=pr.id, detail={"id": cid, "name": c.name}, ip=client_ip(request))
    return {"ok": True}


# ---- Siswa -------------------------------------------------------------------------------
def _stu(s: Student, db: Session) -> dict:
    c = db.get(Classroom, s.classroom_id) if s.classroom_id else None
    comp = db.get(Computer, s.computer_id) if s.computer_id else None
    return {"id": s.id, "nis": s.nis, "full_name": s.full_name, "classroom_id": s.classroom_id, "classroom": c.name if c else None,
            "computer_id": s.computer_id, "computer": (comp.label or comp.hostname) if comp else None, "is_active": s.is_active}


@router.get("/students")
def list_students(classroom_id: int | None = None, q: str | None = None, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    query = select(Student).order_by(Student.full_name)
    if classroom_id:
        query = query.where(Student.classroom_id == classroom_id)
    if pr.classroom_ids is not None:
        query = query.where(Student.classroom_id.in_(pr.classroom_ids or {-1}))
    if q:
        query = query.where(Student.full_name.ilike(f"%{q}%") | Student.nis.ilike(f"%{q}%"))
    return [_stu(s, db) for s in db.execute(query).scalars()]


def _save_student(db: Session, s: Student, body: StudentIn) -> None:
    if body.classroom_id is not None and not db.get(Classroom, body.classroom_id):
        raise HTTPException(404, "Kelas tidak ditemukan")
    if body.computer_id is not None:
        if not db.get(Computer, body.computer_id):
            raise HTTPException(404, "Komputer tidak ditemukan")
        for o in db.execute(select(Student).where(Student.computer_id == body.computer_id, Student.id != (s.id or -1))).scalars():
            o.computer_id = None
    for k, v in body.model_dump().items():
        setattr(s, k, v)


@router.post("/students")
def create_student(body: StudentIn, request: Request, pr: Principal = Depends(admin_only), db: Session = Depends(get_db)):
    s = Student()
    _save_student(db, s, body)
    db.add(s)
    try:
        db.commit()
    except IntegrityError:
        db.rollback()
        raise HTTPException(409, "NIS sudah dipakai")
    audit.log(db, "admin", pr.name, "student.create", actor_id=pr.id, detail={"id": s.id, "name": s.full_name}, ip=client_ip(request))
    return _stu(s, db)


@router.put("/students/{sid}")
def update_student(sid: int, body: StudentIn, request: Request, pr: Principal = Depends(admin_only), db: Session = Depends(get_db)):
    s = db.get(Student, sid)
    if not s:
        raise HTTPException(404, "Siswa tidak ditemukan")
    _save_student(db, s, body)
    try:
        db.commit()
    except IntegrityError:
        db.rollback()
        raise HTTPException(409, "NIS sudah dipakai")
    audit.log(db, "admin", pr.name, "student.update", actor_id=pr.id, detail={"id": s.id}, ip=client_ip(request))
    return _stu(s, db)


@router.delete("/students/{sid}")
def delete_student(sid: int, request: Request, pr: Principal = Depends(admin_only), db: Session = Depends(get_db)):
    s = db.get(Student, sid)
    if not s:
        raise HTTPException(404, "Siswa tidak ditemukan")
    db.delete(s)
    db.commit()
    audit.log(db, "admin", pr.name, "student.delete", actor_id=pr.id, detail={"id": sid, "name": s.full_name}, ip=client_ip(request))
    return {"ok": True}


@router.get("/students/{sid}/activity")
def student_activity(sid: int, limit: int = Query(100, le=500), pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    s = db.get(Student, sid)
    if not s or (pr.classroom_ids is not None and s.classroom_id not in pr.classroom_ids):
        raise HTTPException(404, "Siswa tidak ditemukan")
    q = select(ActivityLog).where(ActivityLog.actor_name == s.full_name)
    if s.computer_id:
        q = select(ActivityLog).where((ActivityLog.actor_name == s.full_name) | (ActivityLog.computer_id == s.computer_id))
    return [_log(r) for r in db.execute(q.order_by(ActivityLog.id.desc()).limit(limit)).scalars()]


# ---- Pengguna (admin & guru) ------------------------------------------------------------------
def _usr(u, role: str) -> dict:
    d = {"id": u.id, "role": role, "username": u.username, "full_name": u.full_name, "email": u.email, "is_active": u.is_active,
         "last_login_at": u.last_login_at.isoformat() if u.last_login_at else None}
    if role == "guru":
        d.update({"nip": u.nip, "permissions": {k: bool((u.permissions or {}).get(k, True)) for k in TEACHER_PERMS},
                  "classrooms": [c.name for c in u.classrooms]})
    return d


@router.get("/users")
def list_users(pr: Principal = Depends(admin_only), db: Session = Depends(get_db)):
    return ([_usr(u, "admin") for u in db.execute(select(Administrator).order_by(Administrator.username)).scalars()] +
            [_usr(u, "guru") for u in db.execute(select(Teacher).order_by(Teacher.username)).scalars()])


@router.post("/users/{role}")
def create_user(role: str, body: UserIn, request: Request, pr: Principal = Depends(admin_only), db: Session = Depends(get_db)):
    if role not in ("admin", "guru"):
        raise HTTPException(404, "Peran tidak dikenal")
    if not body.password or not password_strong(body.password):
        raise HTTPException(422, "Password minimal 8 karakter dan mengandung huruf serta angka")
    if db.execute(select(Administrator).where(Administrator.username == body.username)).first() or \
            db.execute(select(Teacher).where(Teacher.username == body.username)).first():
        raise HTTPException(409, "Username sudah dipakai")
    if role == "admin":
        u = Administrator(username=body.username, full_name=body.full_name, email=body.email, password_hash=hash_password(body.password), is_active=body.is_active)
    else:
        u = Teacher(username=body.username, full_name=body.full_name, email=body.email, nip=body.nip, password_hash=hash_password(body.password),
                    permissions={k: (body.permissions or {}).get(k, True) for k in TEACHER_PERMS}, is_active=body.is_active)
    db.add(u)
    db.commit()
    audit.log(db, "admin", pr.name, f"user.create.{role}", actor_id=pr.id, detail={"username": u.username}, ip=client_ip(request))
    return _usr(u, role)


@router.put("/users/{role}/{uid}")
def update_user(role: str, uid: int, body: UserIn, request: Request, pr: Principal = Depends(admin_only), db: Session = Depends(get_db)):
    if role not in ("admin", "guru"):
        raise HTTPException(404, "Peran tidak dikenal")
    u = db.get(Administrator if role == "admin" else Teacher, uid)
    if not u:
        raise HTTPException(404, "Pengguna tidak ditemukan")
    if role == "admin" and u.id == pr.id and not body.is_active:
        raise HTTPException(400, "Tidak bisa menonaktifkan akun sendiri")
    u.full_name, u.email, u.is_active = body.full_name, body.email, body.is_active
    if body.password:
        if not password_strong(body.password):
            raise HTTPException(422, "Password minimal 8 karakter dan mengandung huruf serta angka")
        u.password_hash = hash_password(body.password)
    if role == "guru":
        u.nip = body.nip
        if body.permissions is not None:
            u.permissions = {k: bool(body.permissions.get(k, False)) for k in TEACHER_PERMS}
    db.commit()
    audit.log(db, "admin", pr.name, f"user.update.{role}", actor_id=pr.id, detail={"id": uid}, ip=client_ip(request))
    return _usr(u, role)


@router.delete("/users/{role}/{uid}")
def delete_user(role: str, uid: int, request: Request, pr: Principal = Depends(admin_only), db: Session = Depends(get_db)):
    if role not in ("admin", "guru"):
        raise HTTPException(404, "Peran tidak dikenal")
    if role == "admin":
        if uid == pr.id:
            raise HTTPException(400, "Tidak bisa menghapus akun sendiri")
        if db.execute(select(func.count(Administrator.id)).where(Administrator.is_active.is_(True))).scalar() <= 1:
            raise HTTPException(400, "Harus ada minimal satu administrator aktif")
    u = db.get(Administrator if role == "admin" else Teacher, uid)
    if not u:
        raise HTTPException(404, "Pengguna tidak ditemukan")
    db.delete(u)
    db.commit()
    audit.log(db, "admin", pr.name, f"user.delete.{role}", actor_id=pr.id, detail={"id": uid, "username": u.username}, ip=client_ip(request))
    return {"ok": True}


# ---- Notifikasi -------------------------------------------------------------------------------
@router.get("/notifications")
def list_notifications(unread: bool = False, limit: int = Query(100, le=500), pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    q = select(Notification).order_by(Notification.id.desc()).limit(limit)
    if unread:
        q = q.where(Notification.is_read.is_(False))
    if pr.classroom_ids is not None:
        q = q.where(Notification.classroom_id.in_(pr.classroom_ids or {-1}))
    return [{"id": n.id, "type": n.type, "title": n.title, "message": n.message, "severity": n.severity, "computer_id": n.computer_id,
             "is_read": n.is_read, "created_at": n.created_at.isoformat()} for n in db.execute(q).scalars()]


@router.post("/notifications/read-all")
def read_all(pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    q = select(Notification).where(Notification.is_read.is_(False))
    if pr.classroom_ids is not None:
        q = q.where(Notification.classroom_id.in_(pr.classroom_ids or {-1}))
    for n in db.execute(q).scalars():
        n.is_read = True
    db.commit()
    return {"ok": True}


# ---- Log ----------------------------------------------------------------------------------------
def _log(r: ActivityLog) -> dict:
    return {"id": r.id, "actor_type": r.actor_type, "actor_name": r.actor_name, "action": r.action, "computer_id": r.computer_id,
            "computer_name": r.computer_name, "status": r.status, "detail": r.detail, "reason": r.reason, "ip_address": r.ip_address,
            "created_at": r.created_at.isoformat()}


def _log_query(pr: Principal, db: Session, computer_id=None, action=None, status=None, since=None, until=None, actor=None):
    q = select(ActivityLog).order_by(ActivityLog.id.desc())
    if pr.classroom_ids is not None:
        ids = [c.id for c in visible_computers(db, pr)]
        q = q.where(ActivityLog.computer_id.in_(ids or [-1]))
    if computer_id:
        q = q.where(ActivityLog.computer_id == computer_id)
    if action:
        q = q.where(ActivityLog.action.like(f"{action}%"))
    if status:
        q = q.where(ActivityLog.status == status)
    if actor:
        q = q.where(ActivityLog.actor_name.ilike(f"%{actor}%"))
    if since:
        q = q.where(ActivityLog.created_at >= since)
    if until:
        q = q.where(ActivityLog.created_at < until + timedelta(days=1))
    return q


@router.get("/logs")
def list_logs(computer_id: int | None = None, action: str | None = None, status: str | None = None, actor: str | None = None,
              since: datetime | None = None, until: datetime | None = None, limit: int = Query(200, le=2000), offset: int = 0,
              pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    return [_log(r) for r in db.execute(_log_query(pr, db, computer_id, action, status, since, until, actor).limit(limit).offset(offset)).scalars()]


@router.get("/logs/export.csv")
def export_logs(since: datetime | None = None, until: datetime | None = None, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    buf = io.StringIO()
    w = csv.writer(buf)
    w.writerow(["id", "waktu(UTC)", "pelaku", "peran", "aksi", "komputer", "status", "alasan", "detail"])
    for r in db.execute(_log_query(pr, db, since=since, until=until).limit(50000)).scalars():
        w.writerow([r.id, r.created_at.isoformat(), r.actor_name, r.actor_type, r.action, r.computer_name or "", r.status, r.reason or "", r.detail])
    buf.seek(0)
    return StreamingResponse(iter([buf.getvalue()]), media_type="text/csv", headers={"Content-Disposition": "attachment; filename=audit_log.csv"})


@router.get("/logs/verify")
def verify_logs(pr: Principal = Depends(admin_only), db: Session = Depends(get_db)):
    return audit.verify_chain(db)


# ---- Laporan --------------------------------------------------------------------------------------
@router.get("/reports/summary")
def report(days: int = Query(7, ge=1, le=365), pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    since = utcnow() - timedelta(days=days)
    comps = visible_computers(db, pr)
    ids = [c.id for c in comps] or [-1]
    names = {c.id: c.label or c.hostname for c in comps}

    def count(model, *conds):
        return db.execute(select(func.count(model.id)).where(*conds)).scalar()

    dl = dict(db.execute(select(DownloadRequest.status, func.count(DownloadRequest.id)).where(DownloadRequest.computer_id.in_(ids),
                                                                                              DownloadRequest.requested_at >= since).group_by(DownloadRequest.status)).all())
    ap = dict(db.execute(select(ApplicationRequest.status, func.count(ApplicationRequest.id)).where(ApplicationRequest.computer_id.in_(ids),
                                                                                                    ApplicationRequest.requested_at >= since).group_by(ApplicationRequest.status)).all())
    cmds = dict(db.execute(select(ComputerCommand.action, func.count(ComputerCommand.id)).where(ComputerCommand.computer_id.in_(ids),
                                                                                                ComputerCommand.created_at >= since).group_by(ComputerCommand.action)).all())
    blocked = db.execute(select(ActivityLog).where(ActivityLog.action == "event.blocked_site", ActivityLog.created_at >= since,
                                                   ActivityLog.computer_id.in_(ids))).scalars().all()
    doms: dict[str, int] = {}
    per_comp: dict[str, int] = {}
    for r in blocked:
        d = (r.detail or {}).get("domain", "?")
        doms[d] = doms.get(d, 0) + 1
        per_comp[names.get(r.computer_id, "?")] = per_comp.get(names.get(r.computer_id, "?"), 0) + 1
    top = lambda m: sorted(({"name": k, "count": v} for k, v in m.items()), key=lambda x: -x["count"])[:10]
    return {"days": days, "computers": len(comps), "downloads": dl, "app_requests": ap, "commands": cmds,
            "blocked_attempts": len(blocked), "top_blocked_domains": top(doms), "top_blocked_computers": top(per_comp)}


# ---- Pengaturan ---------------------------------------------------------------------------------------
@router.get("/settings")
def get_settings(pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    return all_settings(db)


@router.put("/settings")
def put_settings(body: SettingsIn, request: Request, pr: Principal = Depends(admin_only), db: Session = Depends(get_db)):
    bad = [k for k in body.values if k not in EDITABLE_BY_ADMIN]
    if bad:
        raise HTTPException(422, f"Pengaturan tidak dikenal: {', '.join(bad)}")
    for k, v in body.values.items():
        if len(v) > 500:
            raise HTTPException(422, f"Nilai '{k}' terlalu panjang")
        set_setting(db, k, v.strip())
    audit.log(db, "admin", pr.name, "settings.update", actor_id=pr.id, detail=body.values, ip=client_ip(request))
    return all_settings(db)


@router.post("/logs/purge")
def purge_logs(request: Request, days: int = Query(ge=30), confirm: bool = False, pr: Principal = Depends(admin_only), db: Session = Depends(get_db)):
    """Pembersihan log lama: hanya admin, minimal 30 hari, butuh konfirmasi. Rantai hash dimulai ulang setelah titik pemotongan."""
    if not confirm:
        raise HTTPException(409, detail={"code": "confirmation_required", "message": "Kirim confirm=true untuk menghapus log lama"})
    cutoff = utcnow() - timedelta(days=days)
    last_old = db.execute(select(ActivityLog.id).where(ActivityLog.created_at < cutoff).order_by(ActivityLog.id.desc()).limit(1)).scalar()
    n = 0
    if last_old:
        nxt = db.execute(select(ActivityLog).where(ActivityLog.id > last_old).order_by(ActivityLog.id).limit(1)).scalar_one_or_none()
        n = db.execute(delete(ActivityLog).where(ActivityLog.id <= last_old)).rowcount
        if nxt:
            nxt.prev_hash = None
            # hash baris pertama dihitung ulang tanpa prev supaya rantai tetap valid
            nxt.row_hash = audit._hash(None, nxt)
            rest = db.execute(select(ActivityLog).where(ActivityLog.id > nxt.id).order_by(ActivityLog.id)).scalars().all()
            prev = nxt.row_hash
            for r in rest:
                r.prev_hash, r.row_hash = prev, audit._hash(prev, r)
                prev = r.row_hash
        db.commit()
    audit.log(db, "admin", pr.name, "logs.purge", actor_id=pr.id, detail={"older_than_days": days, "deleted": n}, ip=client_ip(request))
    return {"deleted": n}
