"""Endpoint yang dipakai Client Agent di komputer siswa (autentikasi: token perangkat)."""
from __future__ import annotations

import hmac
import time
from datetime import timedelta

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

from ..database import get_db
from ..models import (ApplicationRequest, Computer, DeviceToken, DownloadApproval, DownloadRequest, InstalledApplication, Student,
                      utcnow)
from ..schemas.schemas import AgentAppRequestIn, AgentDownloadIn, AgentEventIn, AgentRegisterIn
from ..security import device_signing_key, new_token, sha256
from ..services import audit
from ..services.notify import notify
from ..services.policy import effective_policy
from ..services.settings_service import csv_set, get_int, get_setting
from .deps import agent_auth, client_ip
from .requests_api import ser_app, ser_dl
from ..websocket.manager import hub

router = APIRouter(prefix="/api/agent", tags=["agent"])
TOKEN_DAYS = 30
_event_last: dict[tuple, float] = {}


def issue_token(db: Session, comp: Computer) -> str:
    tok = new_token(40)
    db.add(DeviceToken(computer_id=comp.id, token_hash=sha256(tok), expires_at=utcnow() + timedelta(days=TOKEN_DAYS)))
    db.commit()
    return tok


@router.post("/register")
def register(body: AgentRegisterIn, request: Request, db: Session = Depends(get_db)):
    if not hmac.compare_digest(body.enrollment_key.strip(), get_setting(db, "enrollment_key")):
        audit.log(db, "system", body.hostname, "agent.register", status="failed", ip=client_ip(request), detail={"reason": "enrollment key salah"})
        raise HTTPException(403, "Kunci pendaftaran salah")
    comp = db.execute(select(Computer).where(Computer.device_uid == body.device_uid)).scalar_one_or_none()
    new = comp is None
    if new:
        comp = Computer(device_uid=body.device_uid, hostname=body.hostname)
        db.add(comp)
    comp.hostname, comp.ip_address, comp.mac_address = body.hostname, body.ip_address or client_ip(request), body.mac_address
    comp.os_name, comp.windows_user, comp.agent_version, comp.released = body.os_name, body.windows_user, body.agent_version, False
    db.commit()
    for t in db.execute(select(DeviceToken).where(DeviceToken.computer_id == comp.id, DeviceToken.revoked.is_(False))).scalars():
        t.revoked = True
    db.commit()
    token = issue_token(db, comp)
    audit.log(db, "agent", comp.hostname, "agent.register", computer=comp, ip=client_ip(request), detail={"new": new})
    if new:
        notify(db, "computer_new", f"Komputer baru terhubung: {comp.hostname}", f"IP {comp.ip_address}", severity="info", computer=comp)
    return {"computer_id": comp.id, "token": token, "signing_key": device_signing_key(comp.device_uid), "token_days": TOKEN_DAYS,
            "server_time": int(time.time())}


@router.post("/rotate")
def rotate(request: Request, comp: Computer = Depends(agent_auth), db: Session = Depends(get_db)):
    """Rotasi token: token lama tetap berlaku 2 menit (masa tenggang) supaya agent aman dari crash saat menyimpan token baru."""
    for t in db.execute(select(DeviceToken).where(DeviceToken.computer_id == comp.id, DeviceToken.revoked.is_(False))).scalars():
        if t.expires_at > utcnow() + timedelta(minutes=2):
            t.expires_at = utcnow() + timedelta(minutes=2)
    db.commit()
    return {"token": issue_token(db, comp), "token_days": TOKEN_DAYS}


@router.get("/policy")
def get_policy(comp: Computer = Depends(agent_auth), db: Session = Depends(get_db)):
    return effective_policy(db, comp)


def _student(db: Session, comp: Computer) -> Student | None:
    return db.execute(select(Student).where(Student.computer_id == comp.id)).scalars().first()


@router.post("/downloads")
def request_download(body: AgentDownloadIn, comp: Computer = Depends(agent_auth), db: Session = Depends(get_db)):
    # Cegah banjir: permintaan sama (URL & berkas) yang masih menunggu dipakai ulang
    dup = db.execute(select(DownloadRequest).where(DownloadRequest.computer_id == comp.id, DownloadRequest.source_url == body.source_url,
                                                   DownloadRequest.file_name == body.file_name,
                                                   DownloadRequest.status == "menunggu")).scalars().first()
    if dup:
        return ser_dl(dup)
    st = _student(db, comp)
    ext = body.file_name.rsplit(".", 1)[-1].lower() if "." in body.file_name else ""
    expiry = get_int(db, "download_expiry_minutes", 10)
    r = DownloadRequest(computer_id=comp.id, student_id=st.id if st else None, student_name=st.full_name if st else comp.windows_user,
                        file_name=body.file_name, file_size=body.file_size, file_type=body.file_type or ext or None,
                        source_url=body.source_url, browser=body.browser, expires_at=utcnow() + timedelta(minutes=expiry))
    max_mb = get_int(db, "download_max_mb", 0)
    if ext in csv_set(get_setting(db, "download_auto_reject_ext")):
        r.status, r.auto_decision, r.decision_note, r.decided_by, r.decided_at = "ditolak", True, f"Jenis berkas .{ext} dilarang oleh kebijakan", "sistem", utcnow()
    elif max_mb and body.file_size and body.file_size > max_mb * 1024 * 1024:
        r.status, r.auto_decision, r.decision_note, r.decided_by, r.decided_at = "ditolak", True, f"Ukuran melebihi {max_mb} MB", "sistem", utcnow()
    elif ext and ext in csv_set(get_setting(db, "download_auto_approve_ext")):
        r.status, r.auto_decision, r.decision_note, r.decided_by, r.decided_at = "disetujui", True, "Otomatis disetujui (jenis berkas diizinkan)", "sistem", utcnow()
    db.add(r)
    db.commit()
    if r.status == "disetujui":
        db.add(DownloadApproval(request_id=r.id, approved_by="sistem", valid_until=utcnow() + timedelta(minutes=get_int(db, "download_approval_minutes", 5))))
        db.commit()
    r.computer = comp
    audit.log(db, "siswa", r.student_name or comp.hostname, "download.request", computer=comp, status="success",
              detail={"request_id": r.id, "file": r.file_name, "url": r.source_url, "size": r.file_size, "auto": r.status if r.auto_decision else None})
    if r.status == "menunggu":
        notify(db, "download_request", f"Permintaan download dari {comp.label or comp.hostname}", f"{r.file_name} — {r.source_url[:120]}",
               severity="warning", computer=comp)
    hub.emit({"type": "download_update", "data": ser_dl(r)}, classroom_id=comp.classroom_id)
    return ser_dl(r)


@router.get("/downloads/{rid}")
def download_status(rid: int, comp: Computer = Depends(agent_auth), db: Session = Depends(get_db)):
    r = db.get(DownloadRequest, rid)
    if not r or r.computer_id != comp.id:
        raise HTTPException(404, "Permintaan tidak ditemukan")
    r.computer = comp
    d = ser_dl(r)
    ap = db.execute(select(DownloadApproval).where(DownloadApproval.request_id == rid)).scalar_one_or_none()
    d["valid_until"] = ap.valid_until.isoformat() if ap else None
    d["used"] = bool(ap and ap.used_at)
    return d


@router.post("/downloads/{rid}/used")
def mark_used(rid: int, comp: Computer = Depends(agent_auth), db: Session = Depends(get_db)):
    """Agent melapor bahwa izin sudah dipakai (izin sekali pakai)."""
    r = db.get(DownloadRequest, rid)
    ap = db.execute(select(DownloadApproval).where(DownloadApproval.request_id == rid)).scalar_one_or_none()
    if not r or r.computer_id != comp.id or not ap:
        raise HTTPException(404, "Persetujuan tidak ditemukan")
    if ap.used_at and ap.single_use:
        raise HTTPException(409, "Izin sudah dipakai")
    if ap.valid_until < utcnow():
        raise HTTPException(410, "Izin kedaluwarsa")
    ap.used_at = utcnow()
    db.commit()
    audit.log(db, "agent", comp.hostname, "download.used", computer=comp, detail={"request_id": rid, "file": r.file_name})
    return {"ok": True}


@router.post("/app-requests")
def request_app(body: AgentAppRequestIn, comp: Computer = Depends(agent_auth), db: Session = Depends(get_db)):
    dup = db.execute(select(ApplicationRequest).where(ApplicationRequest.computer_id == comp.id, ApplicationRequest.app_name == body.app_name,
                                                      ApplicationRequest.status == "menunggu")).scalars().first()
    if dup:
        return ser_app(dup)
    st = _student(db, comp)
    r = ApplicationRequest(computer_id=comp.id, student_name=st.full_name if st else comp.windows_user, app_name=body.app_name,
                           request_type=body.request_type, detail=body.detail)
    db.add(r)
    db.commit()
    r.computer = comp
    audit.log(db, "siswa", r.student_name or comp.hostname, "app.request", computer=comp, detail={"request_id": r.id, "app": r.app_name, "type": r.request_type})
    notify(db, "app_request", f"Permintaan instalasi dari {comp.label or comp.hostname}", r.app_name, severity="warning", computer=comp)
    hub.emit({"type": "app_request_update", "data": ser_app(r)}, classroom_id=comp.classroom_id)
    return ser_app(r)


@router.get("/app-requests/{rid}")
def app_request_status(rid: int, comp: Computer = Depends(agent_auth), db: Session = Depends(get_db)):
    r = db.get(ApplicationRequest, rid)
    if not r or r.computer_id != comp.id:
        raise HTTPException(404, "Permintaan tidak ditemukan")
    r.computer = comp
    return ser_app(r)


@router.post("/inventory")
def inventory(body: dict, comp: Computer = Depends(agent_auth), db: Session = Depends(get_db)):
    apps = body.get("applications") or []
    if not isinstance(apps, list) or len(apps) > 3000:
        raise HTTPException(422, "Format inventaris salah")
    existing = {(a.name, a.version or ""): a for a in db.execute(select(InstalledApplication).where(InstalledApplication.computer_id == comp.id)).scalars()}
    seen, now, first_run, added = set(), utcnow(), not existing, []
    for a in apps:
        name = str(a.get("name", ""))[:255].strip()
        if not name:
            continue
        key = (name, str(a.get("version") or "")[:80])
        seen.add(key)
        row = existing.get(key)
        if row:
            row.reported_at = now
        else:
            db.add(InstalledApplication(computer_id=comp.id, name=name, version=key[1] or None, publisher=str(a.get("publisher") or "")[:160] or None,
                                        install_date=str(a.get("install_date") or "")[:20] or None))
            added.append(name)
    for key, row in existing.items():
        if key not in seen:
            db.delete(row)
    db.commit()
    if added and not first_run:
        audit.log(db, "agent", comp.hostname, "app.installed", computer=comp, detail={"added": added[:20]})
        notify(db, "app_installed", f"Aplikasi baru terpasang di {comp.label or comp.hostname}", ", ".join(added[:5]), severity="warning", computer=comp)
    return {"ok": True, "count": len(seen), "added": len(added)}


@router.post("/events")
def event(body: AgentEventIn, comp: Computer = Depends(agent_auth), db: Session = Depends(get_db)):
    d = body.detail
    key = (comp.id, body.type, str(d.get("domain") or d.get("app") or ""))
    now = time.time()
    if now - _event_last.get(key, 0) < 30:  # debounce notifikasi berulang
        return {"ok": True, "debounced": True}
    _event_last[key] = now
    st = _student(db, comp)
    who = st.full_name if st else (comp.windows_user or comp.hostname)
    titles = {"blocked_site": ("Situs terlarang diakses", "warning"), "blocked_app": ("Aplikasi terlarang dijalankan", "warning"),
              "tab_limit": ("Batas tab terlampaui", "info"), "policy_error": ("Kebijakan gagal diterapkan", "error"),
              "tamper": ("Percobaan mengubah/menghentikan agent", "error"), "info": ("Info", "info")}
    title, sev = titles[body.type]
    audit.log(db, "siswa", who, f"event.{body.type}", computer=comp, status="failed" if sev == "error" else "success", detail=d)
    if body.type != "info" and (body.type != "blocked_site" or get_setting(db, "blocked_attempt_notify") == "true"):
        notify(db, body.type, f"{title}: {comp.label or comp.hostname}", f"{who} — {d.get('domain') or d.get('app') or d.get('message') or ''}"[:300],
               severity=sev, computer=comp)
    return {"ok": True}
