"""Pengawas proses: aplikasi terlarang, installer tanpa izin, Microsoft Store, dan browser yang tidak dikelola."""
from __future__ import annotations

import logging
import os
import re
import time
from dataclasses import dataclass, field

import psutil

log = logging.getLogger("agent.watcher")

BROWSER_EXE = {"chrome": "chrome.exe", "edge": "msedge.exe", "firefox": "firefox.exe", "opera": "opera.exe", "brave": "brave.exe",
               "vivaldi": "vivaldi.exe", "tor": "tor.exe", "yandex": "browser.exe"}
EXE_TO_BROWSER = {v: k for k, v in BROWSER_EXE.items()}
STORE_EXES = {"winstore.app.exe", "winstore.mobile.exe"}
PROTECTED = {"explorer.exe", "system", "svchost.exe", "csrss.exe", "winlogon.exe", "lsass.exe", "services.exe", "wininit.exe", "dwm.exe",
             "classroomagent.exe", "python.exe", "pythonw.exe", "classroomagent-helper.exe", "cms-browser.exe"}
RISKY_BROWSER_FLAGS = ("--disable-web-security", "--proxy-server", "--load-extension", "--incognito", "--inprivate", "--user-data-dir=c:\\users",
                       "--remote-allow-origins")
USER_DIRS = re.compile(r"\\users\\[^\\]+\\(downloads|desktop|documents|appdata\\local\\temp|appdata\\roaming)\\|\\temp\\", re.I)
INSTALLER_NAME = re.compile(r"(setup|install|installer|unins|update)[^\\]*\.(exe|msi)$|\.msi$", re.I)
SAFE_PREFIXES = ("c:\\windows\\", "c:\\program files\\", "c:\\program files (x86)\\", "c:\\programdata\\")


@dataclass
class Action:
    kind: str  # kill | request_install | request_store | launch_managed
    pid: int
    name: str
    reason: str
    detail: str = ""
    cmdline: list[str] = field(default_factory=list)


def browser_main_process(cmdline: list[str]) -> bool:
    return not any(a.startswith("--type=") for a in cmdline)


def classify(info: dict, policy: dict, cfg_ports: dict[str, int], permit_active: bool, started_before: float, managed_dirs: tuple[str, ...]) -> Action | None:
    """Keputusan murni (tanpa efek samping) agar mudah diuji. info: name, pid, exe, cmdline, create_time, username."""
    name = (info.get("name") or "").lower()
    if not name or name in PROTECTED:
        return None
    exe = (info.get("exe") or "").lower()
    cmd = info.get("cmdline") or []
    apps, br = policy.get("applications", {}), policy.get("browser", {})
    pid = info["pid"]
    is_new = info.get("create_time", 0) >= started_before

    # 1. Browser
    if name in EXE_TO_BROWSER:
        kind = EXE_TO_BROWSER[name]
        allowed = br.get("allowed_browsers", ["chrome", "edge"])
        if kind not in allowed:
            return Action("kill", pid, name, f"browser '{kind}' tidak diizinkan")
        if kind in cfg_ports and browser_main_process(cmd):
            joined = " ".join(cmd).lower()
            port_ok = f"--remote-debugging-port={cfg_ports[kind]}" in joined
            dir_ok = any(d.lower() in joined for d in managed_dirs)
            if not (port_ok and dir_ok):
                return Action("launch_managed", pid, name, "browser tidak dikelola (gunakan 'Browser Sekolah')", detail=kind, cmdline=cmd)
            if any(f in joined for f in RISKY_BROWSER_FLAGS if f != "--user-data-dir=c:\\users"):
                return Action("kill", pid, name, "opsi browser berisiko terdeteksi")
        return None

    # 2. Store
    if name in STORE_EXES and apps.get("block_store"):
        return Action("request_store", pid, name, "Microsoft Store diblokir", detail="Microsoft Store")

    # 3. Aplikasi terlarang
    if name in [a.lower() for a in apps.get("blocked_apps", [])]:
        return Action("kill", pid, name, "aplikasi dilarang", detail=name)

    user = (info.get("username") or "").lower()
    system_owned = user.endswith("system") or user.endswith("service") or not user

    # 4. Installer (hanya proses baru milik pengguna)
    if apps.get("block_installers") and not permit_active and is_new and not system_owned:
        if name == "msiexec.exe" and any(a.lower() in ("/i", "-i", "/package", "/a") or a.lower().endswith(".msi") for a in cmd[1:]):
            target = next((a for a in cmd[1:] if a.lower().endswith(".msi")), "paket MSI")
            return Action("request_install", pid, name, "instalasi MSI tanpa izin", detail=os.path.basename(target.replace("\\", "/")))
        if exe and USER_DIRS.search(exe) and (INSTALLER_NAME.search(exe) or "\\downloads\\" in exe):
            return Action("request_install", pid, name, "installer dari folder pengguna tanpa izin", detail=os.path.basename(exe.replace("\\", "/")))
        if name == "winget.exe":
            return Action("request_install", pid, name, "winget diblokir", detail="winget")

    # 5. Mode allowlist: program di luar lokasi sistem yang tidak terdaftar
    if apps.get("mode") == "allowlist" and exe and not system_owned and not exe.startswith(SAFE_PREFIXES):
        if name not in [a.lower() for a in apps.get("allowed_apps", [])]:
            return Action("kill", pid, name, "aplikasi tidak ada di daftar izin", detail=name)
    return None


class Watcher:
    def __init__(self, ports: dict[str, int], managed_dirs: tuple[str, ...]):
        self.ports, self.managed_dirs = ports, managed_dirs
        self.started = time.time() - 5
        self.permit_until = 0.0
        self.policy: dict = {}
        self._reported: dict[str, float] = {}

    def set_policy(self, policy: dict) -> None:
        self.policy = policy

    @property
    def permit_active(self) -> bool:
        return time.time() < self.permit_until

    def grant_permit(self, minutes: int) -> None:
        self.permit_until = time.time() + minutes * 60

    def scan(self) -> list[Action]:
        if not self.policy:
            return []
        out = []
        for p in psutil.process_iter(["name", "pid", "exe", "cmdline", "create_time", "username"]):
            try:
                a = classify(p.info, self.policy, self.ports, self.permit_active, self.started, self.managed_dirs)
            except Exception:
                continue
            if a:
                out.append(a)
        return out

    def should_report(self, key: str, every: float = 60.0) -> bool:
        now = time.time()
        if now - self._reported.get(key, 0) < every:
            return False
        self._reported[key] = now
        return True

    @staticmethod
    def kill(pid: int) -> bool:
        try:
            p = psutil.Process(pid)
            for c in p.children(recursive=True):
                try:
                    c.kill()
                except psutil.Error:
                    pass
            p.kill()
            return True
        except psutil.Error:
            return False

    @staticmethod
    def kill_by_name(name: str) -> int:
        n = 0
        if name.lower() in PROTECTED:
            return 0
        for p in psutil.process_iter(["name", "pid"]):
            if (p.info["name"] or "").lower() == name.lower() and Watcher.kill(p.info["pid"]):
                n += 1
        return n
