from __future__ import annotations

from datetime import timedelta

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

from ..config import settings
from ..database import get_db
from ..models import (ApplicationRequest, Classroom, Computer, ComputerCommand, ComputerSession, DeviceToken, DownloadRequest,
                      InstalledApplication, Student, utcnow)
from ..schemas.schemas import CommandIn, ComputerPatch
from ..services import audit, commands as cmdsvc
from ..services.policy import effective_policy
from ..websocket.manager import hub
from .deps import Principal, admin_only, client_ip, current_user, visible_computers

router = APIRouter(prefix="/api", tags=["computers"])
METRIC_HISTORY: dict[int, list] = {}


def serialize(c: Computer, student: Student | None = None) -> dict:
    cls = c.classroom
    return {"id": c.id, "hostname": c.hostname, "label": c.label, "name": c.label or c.hostname, "ip_address": c.ip_address,
            "mac_address": c.mac_address, "windows_user": c.windows_user, "os_name": c.os_name, "agent_version": c.agent_version,
            "classroom_id": c.classroom_id, "classroom": cls.name if cls else None, "is_online": c.is_online,
            "usage_state": c.usage_state if c.is_online else "offline", "is_locked": c.is_locked,
            "internet_override": c.internet_override, "exam_lock": c.exam_lock, "metrics": c.metrics,
            "last_seen_at": c.last_seen_at.isoformat() if c.last_seen_at else None,
            "policy_applied_at": c.policy_applied_at.isoformat() if c.policy_applied_at else None,
            "student": ({"id": student.id, "full_name": student.full_name, "nis": student.nis} if student else None)}


def _students(db: Session) -> dict[int, Student]:
    return {s.computer_id: s for s in db.execute(select(Student).where(Student.computer_id.is_not(None))).scalars()}


@router.get("/computers")
def list_computers(classroom_id: int | None = None, status: str | None = Query(default=None, pattern="^(online|offline|locked|in_use)$"),
                   q: str | None = None, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    st = _students(db)
    out = []
    for c in visible_computers(db, pr):
        if classroom_id is not None and c.classroom_id != classroom_id:
            continue
        if status == "online" and not c.is_online or status == "offline" and c.is_online:
            continue
        if status == "locked" and not c.is_locked or status == "in_use" and not (c.is_online and c.usage_state == "in_use"):
            continue
        if q and q.lower() not in " ".join(filter(None, [c.hostname, c.label, c.ip_address, c.windows_user,
                                                         st.get(c.id).full_name if st.get(c.id) else ""])).lower():
            continue
        out.append(serialize(c, st.get(c.id)))
    return out


@router.get("/computers/{cid}")
def get_computer(cid: int, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    c = _get(db, pr, cid)
    d = serialize(c, _students(db).get(c.id))
    d["metrics_history"] = METRIC_HISTORY.get(c.id, [])[-60:]
    d["usage_seconds_today"] = _usage_today(db, c.id)
    return d


def _get(db: Session, pr: Principal, cid: int) -> Computer:
    c = db.get(Computer, cid)
    if not c or c.released or not pr.can_see(c):
        raise HTTPException(404, "Komputer tidak ditemukan")
    return c


def _usage_today(db: Session, cid: int) -> int:
    start = utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
    total = 0
    for s in db.execute(select(ComputerSession).where(ComputerSession.computer_id == cid, ComputerSession.kind == "usage",
                                                      ComputerSession.started_at >= start)).scalars():
        total += int(((s.ended_at or utcnow()) - s.started_at).total_seconds())
    return total


@router.patch("/computers/{cid}")
def patch_computer(cid: int, body: ComputerPatch, request: Request, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    c = _get(db, pr, cid)
    changes = {}
    if body.label is not None:
        c.label = body.label.strip() or None
        changes["label"] = c.label
    if body.set_classroom:
        if not pr.is_admin:
            raise HTTPException(403, "Hanya admin yang boleh memindahkan komputer antar kelas")
        if body.classroom_id is not None and not db.get(Classroom, body.classroom_id):
            raise HTTPException(404, "Kelas tidak ditemukan")
        c.classroom_id = body.classroom_id
        changes["classroom_id"] = body.classroom_id
    if body.set_student:
        if not pr.is_admin:
            raise HTTPException(403, "Hanya admin yang boleh menghubungkan siswa ke komputer")
        for s in db.execute(select(Student).where(Student.computer_id == c.id)).scalars():
            s.computer_id = None
        if body.student_id is not None:
            s = db.get(Student, body.student_id)
            if not s:
                raise HTTPException(404, "Siswa tidak ditemukan")
            s.computer_id = c.id
        changes["student_id"] = body.student_id
    if body.internet_override is not None:
        pr.require("internet")
        c.internet_override = None if body.internet_override == "none" else body.internet_override
        changes["internet_override"] = c.internet_override
    if body.exam_lock is not None:
        pr.require("browser")
        c.exam_lock = body.exam_lock
        changes["exam_lock"] = c.exam_lock
    db.commit()
    audit.log(db, pr.role, pr.name, "computer.update", actor_id=pr.id, computer=c, detail=changes, ip=client_ip(request))
    push_policy(db, c)
    hub.emit({"type": "computer_update", "data": serialize(c, _students(db).get(c.id))}, classroom_id=c.classroom_id)
    return serialize(c, _students(db).get(c.id))


def push_policy(db: Session, c: Computer) -> None:
    if hub.is_online(c.id):
        pol = effective_policy(db, c)
        hub.send_agent_threadsafe(c.id, {"type": "policy_update", "policy": pol})


@router.post("/commands")
def send_commands(body: CommandIn, request: Request, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    perm = cmdsvc.PERMISSION_FOR[body.action]
    if perm == "admin":
        if not pr.is_admin:
            raise HTTPException(403, "Hanya administrator")
    else:
        pr.require(perm)
    if body.action in cmdsvc.RISKY and not body.confirm:
        raise HTTPException(409, detail={"code": "confirmation_required", "message": f"Tindakan '{body.action}' berdampak pada komputer siswa. Kirim ulang dengan confirm=true."})
    try:
        params = cmdsvc.validate_params(body.action, body.params)
        if body.action == "message":
            params["from"] = pr.name
    except (ValueError, TypeError) as e:
        raise HTTPException(422, str(e))
    comps = visible_computers(db, pr, body.computer_ids)
    if len(comps) != len(set(body.computer_ids)):
        raise HTTPException(403, "Sebagian komputer tidak ada atau di luar kelas Anda")
    results = []
    for c in comps:
        cmd = cmdsvc.execute(db, c, body.action, params, pr.role, pr.id, pr.name, body.reason, client_ip(request))
        if body.action == "release":
            for t in db.execute(select(DeviceToken).where(DeviceToken.computer_id == c.id)).scalars():
                t.revoked = True
            db.commit()
        if body.action in ("lock", "unlock"):  # status kunci disimpan di server -> diterapkan saat komputer online kembali
            c.is_locked = body.action == "lock"
            db.commit()
            push_policy(db, c)
        results.append(cmdsvc.serialize(cmd))
    return {"results": results}


@router.get("/commands")
def list_commands(limit: int = Query(100, le=500), computer_id: int | None = None, pr: Principal = Depends(current_user),
                  db: Session = Depends(get_db)):
    q = select(ComputerCommand).order_by(ComputerCommand.id.desc()).limit(limit)
    if computer_id:
        q = q.where(ComputerCommand.computer_id == computer_id)
    ids = {c.id for c in visible_computers(db, pr)}
    return [cmdsvc.serialize(c) for c in db.execute(q).scalars() if c.computer_id in ids]


@router.get("/computers/{cid}/applications")
def installed_apps(cid: int, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    c = _get(db, pr, cid)
    rows = db.execute(select(InstalledApplication).where(InstalledApplication.computer_id == c.id).order_by(InstalledApplication.name)).scalars()
    return [{"id": r.id, "name": r.name, "version": r.version, "publisher": r.publisher, "install_date": r.install_date,
             "first_seen_at": r.first_seen_at.isoformat()} for r in rows]


@router.get("/stats/overview")
def overview(pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    comps = visible_computers(db, pr)
    ids = [c.id for c in comps]
    pending = db.execute(select(func.count(DownloadRequest.id)).where(DownloadRequest.status == "menunggu",
                                                                      DownloadRequest.computer_id.in_(ids or [-1]))).scalar()
    pending_apps = db.execute(select(func.count(ApplicationRequest.id)).where(ApplicationRequest.status == "menunggu",
                                                                             ApplicationRequest.computer_id.in_(ids or [-1]))).scalar()
    online = [c for c in comps if c.is_online]
    cpu = [c.metrics.get("cpu", 0) for c in online if c.metrics]
    return {"total": len(comps), "online": len(online), "offline": len(comps) - len(online),
            "in_use": sum(1 for c in online if c.usage_state == "in_use"), "locked": sum(1 for c in comps if c.is_locked),
            "pending_downloads": pending, "pending_apps": pending_apps, "avg_cpu": round(sum(cpu) / len(cpu), 1) if cpu else 0,
            "by_classroom": _by_class(db, comps)}


def _by_class(db: Session, comps: list[Computer]) -> list[dict]:
    names = {c.id: c.name for c in db.execute(select(Classroom)).scalars()}
    agg: dict[str, dict] = {}
    for c in comps:
        k = names.get(c.classroom_id, "Tanpa kelas")
        a = agg.setdefault(k, {"classroom": k, "online": 0, "offline": 0})
        a["online" if c.is_online else "offline"] += 1
    return list(agg.values())


@router.get("/enrollment")
def enrollment_info(pr: Principal = Depends(admin_only), db: Session = Depends(get_db)):
    from ..services.settings_service import get_setting
    return {"enrollment_key": get_setting(db, "enrollment_key")}


@router.post("/enrollment/rotate")
def rotate_enrollment(request: Request, pr: Principal = Depends(admin_only), db: Session = Depends(get_db)):
    import secrets

    from ..services.settings_service import set_setting
    key = secrets.token_urlsafe(9)
    set_setting(db, "enrollment_key", key)
    audit.log(db, "admin", pr.name, "enrollment.rotate", actor_id=pr.id, ip=client_ip(request))
    return {"enrollment_key": key}
