from __future__ import annotations

from datetime import datetime

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

from ..database import get_db
from ..models import (AllowedWebsite, ApplicationPolicy, BlockedWebsite, BrowserPolicy, Classroom, Computer, InternetPolicy,
                      PolicyAssignment, Schedule)
from ..schemas.schemas import (ApplicationPolicyIn, AssignmentIn, BrowserPolicyIn, InternetPolicyIn, ScheduleIn)
from ..services import audit
from ..services.categories import CATEGORIES
from ..services.policy import effective_policy
from ..websocket.manager import hub
from .deps import Principal, client_ip, current_user, visible_computers

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

PERM = {"internet": "internet", "browser": "browser", "application": "apps"}
MODEL = {"internet": InternetPolicy, "browser": BrowserPolicy, "application": ApplicationPolicy}


def push_all(db: Session) -> int:
    n = 0
    for c in db.execute(select(Computer).where(Computer.is_online.is_(True))).scalars():
        pol = effective_policy(db, c)
        if hub.send_agent_threadsafe(c.id, {"type": "policy_update", "policy": pol}):
            n += 1
    return n


def _need(pr: Principal, kind: str, write: bool = False) -> None:
    pr.require(PERM[kind])
    if write and not pr.is_admin and not pr.can(PERM[kind]):
        raise HTTPException(403, "Tidak diizinkan")


def _ser_internet(p: InternetPolicy) -> dict:
    return {"id": p.id, "name": p.name, "mode": p.mode, "block_social": p.block_social, "block_games": p.block_games,
            "block_entertainment": p.block_entertainment, "is_active": p.is_active, "created_by": p.created_by,
            "allowed": sorted(a.domain for a in p.allowed), "blocked": sorted(b.domain for b in p.blocked)}


def _ser(p) -> dict:
    return {c.name: getattr(p, c.name) for c in p.__table__.columns if c.name not in ("created_at", "updated_at")}


@router.get("/categories")
def categories(pr: Principal = Depends(current_user)):
    return CATEGORIES


# ---- Internet ---------------------------------------------------------------------------
@router.get("/policies/internet")
def list_internet(pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    pr.require("internet")
    return [_ser_internet(p) for p in db.execute(select(InternetPolicy).order_by(InternetPolicy.name)).scalars()]


def _apply_internet(p: InternetPolicy, body: InternetPolicyIn) -> None:
    p.name, p.mode, p.is_active = body.name, body.mode, body.is_active
    p.block_social, p.block_games, p.block_entertainment = body.block_social, body.block_games, body.block_entertainment
    if body.allowed is not None:
        p.allowed = [AllowedWebsite(domain=d) for d in body.allowed]
    if body.blocked is not None:
        p.blocked = [BlockedWebsite(domain=d) for d in body.blocked]


@router.post("/policies/internet")
def create_internet(body: InternetPolicyIn, request: Request, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    pr.require("internet")
    if db.execute(select(InternetPolicy).where(InternetPolicy.name == body.name)).scalar_one_or_none():
        raise HTTPException(409, "Nama kebijakan sudah dipakai")
    p = InternetPolicy(created_by=pr.name)
    _apply_internet(p, body)
    db.add(p)
    db.commit()
    audit.log(db, pr.role, pr.name, "policy.internet.create", actor_id=pr.id, detail={"id": p.id, "name": p.name}, ip=client_ip(request))
    return _ser_internet(p)


@router.put("/policies/internet/{pid}")
def update_internet(pid: int, body: InternetPolicyIn, request: Request, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    pr.require("internet")
    p = db.get(InternetPolicy, pid)
    if not p:
        raise HTTPException(404, "Kebijakan tidak ditemukan")
    _apply_internet(p, body)
    db.commit()
    audit.log(db, pr.role, pr.name, "policy.internet.update", actor_id=pr.id, detail={"id": p.id, "name": p.name, "mode": p.mode}, ip=client_ip(request))
    return {**_ser_internet(p), "pushed_to": push_all(db)}


@router.delete("/policies/internet/{pid}")
def delete_internet(pid: int, request: Request, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    return _delete(db, pr, "internet", pid, request)


# ---- Browser & Aplikasi -------------------------------------------------------------------
@router.get("/policies/browser")
def list_browser(pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    pr.require("browser")
    return [_ser(p) for p in db.execute(select(BrowserPolicy).order_by(BrowserPolicy.name)).scalars()]


@router.get("/policies/application")
def list_app(pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    pr.require("apps")
    return [_ser(p) for p in db.execute(select(ApplicationPolicy).order_by(ApplicationPolicy.name)).scalars()]


def _upsert(db, pr, request, kind, model, body, pid):
    pr.require(PERM[kind])
    p = db.get(model, pid) if pid else model()
    if pid and not p:
        raise HTTPException(404, "Kebijakan tidak ditemukan")
    dup = db.execute(select(model).where(model.name == body.name)).scalar_one_or_none()
    if dup and dup.id != pid:
        raise HTTPException(409, "Nama kebijakan sudah dipakai")
    for k, v in body.model_dump().items():
        setattr(p, k, v)
    db.add(p)
    db.commit()
    audit.log(db, pr.role, pr.name, f"policy.{kind}.{'update' if pid else 'create'}", actor_id=pr.id,
              detail={"id": p.id, "name": p.name}, ip=client_ip(request))
    return {**_ser(p), "pushed_to": push_all(db) if pid else 0}


@router.post("/policies/browser")
def create_browser(body: BrowserPolicyIn, request: Request, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    return _upsert(db, pr, request, "browser", BrowserPolicy, body, None)


@router.put("/policies/browser/{pid}")
def update_browser(pid: int, body: BrowserPolicyIn, request: Request, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    return _upsert(db, pr, request, "browser", BrowserPolicy, body, pid)


@router.delete("/policies/browser/{pid}")
def delete_browser(pid: int, request: Request, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    return _delete(db, pr, "browser", pid, request)


@router.post("/policies/application")
def create_app(body: ApplicationPolicyIn, request: Request, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    return _upsert(db, pr, request, "application", ApplicationPolicy, body, None)


@router.put("/policies/application/{pid}")
def update_app(pid: int, body: ApplicationPolicyIn, request: Request, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    return _upsert(db, pr, request, "application", ApplicationPolicy, body, pid)


@router.delete("/policies/application/{pid}")
def delete_app(pid: int, request: Request, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    return _delete(db, pr, "application", pid, request)


def _delete(db, pr, kind, pid, request):
    pr.require(PERM[kind])
    p = db.get(MODEL[kind], pid)
    if not p:
        raise HTTPException(404, "Kebijakan tidak ditemukan")
    db.execute(delete(PolicyAssignment).where(PolicyAssignment.policy_type == kind, PolicyAssignment.policy_id == pid))
    db.execute(delete(Schedule).where(Schedule.kind == kind, Schedule.policy_id == pid))
    db.delete(p)
    db.commit()
    audit.log(db, pr.role, pr.name, f"policy.{kind}.delete", actor_id=pr.id, detail={"id": pid}, ip=client_ip(request))
    return {"ok": True, "pushed_to": push_all(db)}


# ---- Penugasan -----------------------------------------------------------------------------
@router.get("/assignments")
def list_assignments(pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    names = {"computer": {c.id: c.label or c.hostname for c in db.execute(select(Computer)).scalars()},
             "classroom": {c.id: c.name for c in db.execute(select(Classroom)).scalars()}, "all": {0: "Semua komputer"}}
    pol = {k: {p.id: p.name for p in db.execute(select(m)).scalars()} for k, m in MODEL.items()}
    out = []
    for a in db.execute(select(PolicyAssignment).order_by(PolicyAssignment.id)).scalars():
        if not pr.is_admin:
            if a.target_type == "all":
                pass
            elif a.target_type == "classroom" and a.target_id not in (pr.classroom_ids or set()):
                continue
            elif a.target_type == "computer":
                c = db.get(Computer, a.target_id)
                if not c or not pr.can_see(c):
                    continue
        out.append({"id": a.id, "policy_type": a.policy_type, "policy_id": a.policy_id, "policy_name": pol[a.policy_type].get(a.policy_id),
                    "target_type": a.target_type, "target_id": a.target_id, "target_name": names[a.target_type].get(a.target_id)})
    return out


@router.post("/assignments")
def assign(body: AssignmentIn, request: Request, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    pr.require(PERM[body.policy_type])
    if not db.get(MODEL[body.policy_type], body.policy_id):
        raise HTTPException(404, "Kebijakan tidak ditemukan")
    tid = 0 if body.target_type == "all" else body.target_id
    if body.target_type == "all" and not pr.is_admin:
        raise HTTPException(403, "Hanya admin yang boleh menerapkan ke semua komputer")
    if body.target_type == "classroom":
        if not db.get(Classroom, tid) or (pr.classroom_ids is not None and tid not in pr.classroom_ids):
            raise HTTPException(403, "Kelas tidak ditemukan / bukan kelas Anda")
    if body.target_type == "computer":
        c = db.get(Computer, tid)
        if not c or not pr.can_see(c):
            raise HTTPException(403, "Komputer tidak ditemukan / bukan kelas Anda")
    row = db.execute(select(PolicyAssignment).where(PolicyAssignment.policy_type == body.policy_type,
                                                    PolicyAssignment.target_type == body.target_type,
                                                    PolicyAssignment.target_id == tid)).scalar_one_or_none()
    if row:
        row.policy_id = body.policy_id
    else:
        db.add(PolicyAssignment(policy_type=body.policy_type, policy_id=body.policy_id, target_type=body.target_type, target_id=tid))
    db.commit()
    audit.log(db, pr.role, pr.name, "policy.assign", actor_id=pr.id, detail=body.model_dump(), ip=client_ip(request))
    return {"ok": True, "pushed_to": push_all(db)}


@router.delete("/assignments/{aid}")
def unassign(aid: int, request: Request, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    a = db.get(PolicyAssignment, aid)
    if not a:
        raise HTTPException(404, "Penugasan tidak ditemukan")
    pr.require(PERM[a.policy_type])
    if a.target_type == "all" and not pr.is_admin:
        raise HTTPException(403, "Hanya admin")
    if a.target_type == "classroom" and pr.classroom_ids is not None and a.target_id not in pr.classroom_ids:
        raise HTTPException(403, "Bukan kelas Anda")
    db.delete(a)
    db.commit()
    audit.log(db, pr.role, pr.name, "policy.unassign", actor_id=pr.id, detail={"id": aid}, ip=client_ip(request))
    return {"ok": True, "pushed_to": push_all(db)}


@router.get("/computers/{cid}/effective-policy")
def effective(cid: int, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    c = db.get(Computer, cid)
    if not c or not pr.can_see(c):
        raise HTTPException(404, "Komputer tidak ditemukan")
    return effective_policy(db, c)


# ---- Jadwal -----------------------------------------------------------------------------------
@router.get("/schedules")
def list_schedules(pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    out = []
    for s in db.execute(select(Schedule).order_by(Schedule.id)).scalars():
        if not pr.is_admin:
            if s.classroom_id is not None and s.classroom_id not in (pr.classroom_ids or set()):
                continue
            if s.computer_id is not None:
                c = db.get(Computer, s.computer_id)
                if not c or not pr.can_see(c):
                    continue
            if s.classroom_id is None and s.computer_id is None:
                continue
        out.append(_ser(s))
    return out


def _check_sched(db: Session, pr: Principal, body: ScheduleIn) -> None:
    pr.require({"internet": "internet", "browser": "browser", "application": "apps", "usage": "lock"}[body.kind])
    if (body.classroom_id is None) == (body.computer_id is None):
        raise HTTPException(422, "Pilih tepat satu target: kelas atau komputer")
    if body.kind != "usage":
        if not body.policy_id or not db.get(MODEL[body.kind], body.policy_id):
            raise HTTPException(422, "Pilih kebijakan yang valid untuk jadwal ini")
    if body.classroom_id is not None and (not db.get(Classroom, body.classroom_id) or
                                          (pr.classroom_ids is not None and body.classroom_id not in pr.classroom_ids)):
        raise HTTPException(403, "Kelas tidak valid / bukan kelas Anda")
    if body.computer_id is not None:
        c = db.get(Computer, body.computer_id)
        if not c or not pr.can_see(c):
            raise HTTPException(403, "Komputer tidak valid / bukan kelas Anda")


@router.post("/schedules")
def create_schedule(body: ScheduleIn, request: Request, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    _check_sched(db, pr, body)
    s = Schedule(**body.model_dump())
    db.add(s)
    db.commit()
    audit.log(db, pr.role, pr.name, "schedule.create", actor_id=pr.id, detail={"id": s.id, "name": s.name}, ip=client_ip(request))
    push_all(db)
    return _ser(s)


@router.put("/schedules/{sid}")
def update_schedule(sid: int, body: ScheduleIn, request: Request, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    s = db.get(Schedule, sid)
    if not s:
        raise HTTPException(404, "Jadwal tidak ditemukan")
    _check_sched(db, pr, body)
    for k, v in body.model_dump().items():
        setattr(s, k, v)
    db.commit()
    audit.log(db, pr.role, pr.name, "schedule.update", actor_id=pr.id, detail={"id": s.id}, ip=client_ip(request))
    push_all(db)
    return _ser(s)


@router.delete("/schedules/{sid}")
def delete_schedule(sid: int, request: Request, pr: Principal = Depends(current_user), db: Session = Depends(get_db)):
    s = db.get(Schedule, sid)
    if not s:
        raise HTTPException(404, "Jadwal tidak ditemukan")
    pr.require({"internet": "internet", "browser": "browser", "application": "apps", "usage": "lock"}[s.kind])
    if not pr.is_admin and (s.classroom_id not in (pr.classroom_ids or set()) and s.computer_id is None):
        raise HTTPException(403, "Bukan jadwal kelas Anda")
    db.delete(s)
    db.commit()
    audit.log(db, pr.role, pr.name, "schedule.delete", actor_id=pr.id, detail={"id": sid}, ip=client_ip(request))
    push_all(db)
    return {"ok": True}
