"""Menghitung kebijakan efektif untuk satu komputer.

Prioritas: override cepat di komputer > jadwal (komputer > kelas) > penugasan (komputer > kelas > semua) > bawaan.
Hasilnya dikirim ke agent, yang menerjemahkannya menjadi aturan Windows Firewall, hosts, kebijakan Chrome/Edge, dll.
"""
from __future__ import annotations

import hashlib
import json
from datetime import datetime

from sqlalchemy import select
from sqlalchemy.orm import Session

from ..models import (AllowedWebsite, ApplicationPolicy, BlockedWebsite, BrowserPolicy, Computer, InternetPolicy,
                      PolicyAssignment, Schedule)
from .categories import BYPASS, CATEGORIES, DEFAULT_BLOCKED_APPS

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


def in_window(s: Schedule, now: datetime) -> bool:
    if now.weekday() not in (s.days or []):
        return False
    cur = now.strftime("%H:%M")
    a, b = s.start_time, s.end_time
    return a <= cur < b if a <= b else (cur >= a or cur < b)


def _active_schedules(db: Session, comp: Computer, kind: str, now: datetime) -> list[Schedule]:
    q = select(Schedule).where(Schedule.kind == kind, Schedule.is_active.is_(True))
    rows = [s for s in db.execute(q).scalars() if s.computer_id == comp.id
            or (s.computer_id is None and s.classroom_id is not None and s.classroom_id == comp.classroom_id)]
    return sorted(rows, key=lambda s: (s.computer_id is None))  # komputer lebih dulu


def resolve_policy(db: Session, comp: Computer, ptype: str, now: datetime):
    model = MODEL[ptype]
    for s in _active_schedules(db, comp, ptype, now):
        if s.policy_id and in_window(s, now):
            p = db.get(model, s.policy_id)
            if p and p.is_active:
                return p, f"jadwal:{s.name}"
    for ttype, tid in (("computer", comp.id), ("classroom", comp.classroom_id), ("all", 0)):
        if ttype == "classroom" and tid is None:
            continue
        a = db.execute(select(PolicyAssignment).where(PolicyAssignment.policy_type == ptype, PolicyAssignment.target_type == ttype,
                                                      PolicyAssignment.target_id == tid)).scalar_one_or_none()
        if a:
            p = db.get(model, a.policy_id)
            if p and p.is_active:
                return p, f"penugasan:{ttype}"
    return None, "bawaan"


def _dedupe(seq):
    seen, out = set(), []
    for x in seq:
        x = x.strip().lower()
        if x and x not in seen:
            seen.add(x)
            out.append(x)
    return out


def compile_internet(p: InternetPolicy | None, override: str | None, exam: bool, extra_allow: list[str]) -> dict:
    mode, src_domains_block, src_domains_allow = "allow_all", [], []
    if p:
        mode = p.mode
        allow = [a.domain for a in p.allowed]
        block = [b.domain for b in p.blocked]
        if p.block_social:
            block += CATEGORIES["social"]
        if p.block_games:
            block += CATEGORIES["games"]
        if p.block_entertainment:
            block += CATEGORIES["entertainment"]
        src_domains_allow, src_domains_block = _dedupe(allow), _dedupe(block)
    if override in ("block_all", "allow_all"):
        mode = override
    if exam and mode in ("allow_all", "blocklist"):
        mode = "allowlist"
    allow_set = set(src_domains_allow) | set(extra_allow)
    block_domains = [d for d in src_domains_block if d not in allow_set] if mode == "blocklist" else []
    if mode != "allow_all":
        block_domains = _dedupe(block_domains + BYPASS)  # VPN/proxy/DoH publik selalu diblokir kecuali allow_all
    return {"mode": mode, "allow_domains": _dedupe(list(allow_set)) if mode in ("allowlist", "blocklist") else [],
            "block_domains": block_domains if mode != "allow_all" else []}


def compile_browser(p: BrowserPolicy | None, exam: bool) -> dict:
    d = {"max_tabs": p.max_tabs if p else 0, "allowed_browsers": list(p.allowed_browsers) if p else ["chrome", "edge"],
         "homepage": (p.homepage if p else None) or "", "block_incognito": p.block_incognito if p else True,
         "downloads_require_approval": p.downloads_require_approval if p else True,
         "exam_mode": bool(exam or (p and p.exam_mode))}
    return d


def compile_apps(p: ApplicationPolicy | None) -> dict:
    if not p:
        return {"mode": "blocklist", "blocked_apps": [a.lower() for a in DEFAULT_BLOCKED_APPS], "allowed_apps": [],
                "block_store": True, "block_installers": True}
    return {"mode": p.mode, "blocked_apps": _dedupe(list(p.blocked_apps or [])), "allowed_apps": _dedupe(list(p.allowed_apps or [])),
            "block_store": p.block_store, "block_installers": p.block_installers}


def usage_state(db: Session, comp: Computer, now: datetime) -> tuple[bool, str]:
    scheds = _active_schedules(db, comp, "usage", now)
    if not scheds:
        return True, ""
    if any(in_window(s, now) for s in scheds):
        return True, ""
    return False, "Di luar jadwal penggunaan komputer"


def effective_policy(db: Session, comp: Computer, now: datetime | None = None, server_host: str = "") -> dict:
    now = now or datetime.now()
    ip, isrc = resolve_policy(db, comp, "internet", now)
    bp, bsrc = resolve_policy(db, comp, "browser", now)
    ap, asrc = resolve_policy(db, comp, "application", now)
    allowed_now, why = usage_state(db, comp, now)
    browser = compile_browser(bp, comp.exam_lock)
    internet = compile_internet(ip, comp.internet_override, browser["exam_mode"], [])
    pol = {
        "internet": {**internet, "source": "override" if comp.internet_override else isrc,
                     "policy": ip.name if ip else None},
        "browser": {**browser, "source": bsrc, "policy": bp.name if bp else None},
        "applications": {**compile_apps(ap), "source": asrc, "policy": ap.name if ap else None},
        "lock": {"locked": bool(comp.is_locked or not allowed_now),
                 "message": why or ("Komputer dikunci oleh guru" if comp.is_locked else "")},
        "exam_lock": bool(comp.exam_lock),
    }
    pol["version"] = hashlib.sha256(json.dumps(pol, sort_keys=True).encode()).hexdigest()[:16]
    return pol


# ---- Data awal -------------------------------------------------------------------------
def seed_defaults(db: Session) -> None:
    if db.execute(select(InternetPolicy.id).limit(1)).first():
        return
    web = InternetPolicy(name="Default Sekolah", mode="blocklist", block_social=True, block_games=True,
                         block_entertainment=True, created_by="sistem")
    for dom, cat in (("classroom.google.com", "pembelajaran"), ("google.com", "pembelajaran"), ("docs.google.com", "pembelajaran"),
                     ("drive.google.com", "pembelajaran"), ("kemdikbud.go.id", "pendidikan"), ("belajar.id", "pendidikan"),
                     ("wikipedia.org", "referensi"), ("ruangguru.com", "pembelajaran")):
        web.allowed.append(AllowedWebsite(domain=dom, category=cat))
    open_p = InternetPolicy(name="Internet Terbuka (tanpa batas)", mode="allow_all", created_by="sistem")
    only_learn = InternetPolicy(name="Hanya Website Pembelajaran", mode="allowlist", created_by="sistem")
    for dom, cat in (("classroom.google.com", "pembelajaran"), ("accounts.google.com", "pembelajaran"),
                     ("docs.google.com", "pembelajaran"), ("drive.google.com", "pembelajaran"), ("kemdikbud.go.id", "pendidikan"),
                     ("belajar.id", "pendidikan"), ("wikipedia.org", "referensi")):
        only_learn.allowed.append(AllowedWebsite(domain=dom, category=cat))
    blocked_all = InternetPolicy(name="Blokir Semua Internet", mode="block_all", created_by="sistem")
    db.add_all([web, open_p, only_learn, blocked_all])

    b_default = BrowserPolicy(name="Default Browser", max_tabs=5, allowed_browsers=["chrome", "edge"], homepage="https://classroom.google.com")
    b_exam = BrowserPolicy(name="Mode Ujian", max_tabs=1, allowed_browsers=["chrome", "edge"], exam_mode=True)
    db.add_all([b_default, b_exam])

    a_default = ApplicationPolicy(name="Default Aplikasi", mode="blocklist", blocked_apps=DEFAULT_BLOCKED_APPS,
                                  block_store=True, block_installers=True)
    db.add(a_default)
    db.flush()
    for ptype, pol in (("internet", web), ("browser", b_default), ("application", a_default)):
        db.add(PolicyAssignment(policy_type=ptype, policy_id=pol.id, target_type="all", target_id=0))
    db.commit()
