"""Helper sesi pengguna (berjalan sebagai siswa yang sedang login, otomatis saat login lewat Scheduled Task).

Tugas: layar kunci, pesan guru, BANNER PEMANTAUAN (selalu tampil saat layar dipantau / dikendalikan), tangkap layar,
input remote assistance, logout/lock Windows, dan membuka browser terkelola. Berbicara dengan service lewat 127.0.0.1.
"""
from __future__ import annotations

import base64
import ctypes
import io
import json
import os
import queue
import socket
import subprocess
import sys
import threading
import time
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
from config import IPC_PORT, agent_home  # noqa: E402

IS_WIN = os.name == "nt"


def user_downloads() -> str:
    if IS_WIN:
        try:
            from ctypes import wintypes
            guid = ctypes.c_char * 16
            buf = ctypes.c_wchar_p()
            # FOLDERID_Downloads = {374DE290-123F-4565-9164-39C4925E467B}
            fid = (ctypes.c_ubyte * 16)(*bytes.fromhex("90E24D373F126545916439C4925E467B"))
            if ctypes.windll.shell32.SHGetKnownFolderPath(ctypes.byref(fid), 0, None, ctypes.byref(buf)) == 0:
                return buf.value
        except Exception:
            pass
    return str(Path.home() / "Downloads")


def idle_seconds() -> float:
    if not IS_WIN:
        return 0.0

    class LASTINPUTINFO(ctypes.Structure):
        _fields_ = [("cbSize", ctypes.c_uint), ("dwTime", ctypes.c_uint)]
    li = LASTINPUTINFO()
    li.cbSize = ctypes.sizeof(li)
    ctypes.windll.user32.GetLastInputInfo(ctypes.byref(li))
    return (ctypes.windll.kernel32.GetTickCount() - li.dwTime) / 1000.0


class UI:
    """Semua widget Tk berjalan di thread utama; thread lain mengirim tugas lewat antrean."""

    def __init__(self):
        import tkinter as tk
        self.tk = tk
        self.root = tk.Tk()
        self.root.withdraw()
        self.q: queue.Queue = queue.Queue()
        self.lock_win = None
        self.banner = None
        self.banner_text = ""
        self.root.after(100, self._pump)

    def post(self, fn, *a):
        self.q.put((fn, a))

    def _pump(self):
        try:
            while True:
                fn, a = self.q.get_nowait()
                fn(*a)
        except queue.Empty:
            pass
        if self.lock_win is not None:
            try:
                self.lock_win.attributes("-topmost", True)
                self.lock_win.lift()
                self.lock_win.focus_force()
            except Exception:
                pass
        self.root.after(250, self._pump)

    # ---- layar kunci -----------------------------------------------------------------------------------------------
    def lock(self, message: str):
        tk = self.tk
        if self.lock_win is not None:
            self.lock_label.config(text=message or "Komputer dikunci oleh guru")
            return
        w = tk.Toplevel(self.root)
        w.configure(bg="#0b1f3a")
        w.attributes("-fullscreen", True)
        w.attributes("-topmost", True)
        w.overrideredirect(True)
        w.protocol("WM_DELETE_WINDOW", lambda: None)
        tk.Label(w, text="\U0001F512", font=("Segoe UI Emoji", 72), bg="#0b1f3a", fg="white").pack(expand=True, pady=(0, 0))
        self.lock_label = tk.Label(w, text=message or "Komputer dikunci oleh guru", font=("Segoe UI", 28, "bold"), bg="#0b1f3a", fg="white", wraplength=900)
        self.lock_label.pack()
        tk.Label(w, text="Tunggu hingga guru membuka kunci", font=("Segoe UI", 16), bg="#0b1f3a", fg="#8fb4e8").pack(pady=(8, 120), expand=True)
        w.bind("<Key>", lambda e: "break")
        w.focus_force()
        w.grab_set()
        self.lock_win = w

    def unlock(self):
        if self.lock_win is not None:
            try:
                self.lock_win.grab_release()
                self.lock_win.destroy()
            finally:
                self.lock_win = None

    # ---- pesan / toast -----------------------------------------------------------------------------------------------
    def toast(self, text: str, seconds: int = 8, title: str = "Pesan dari sekolah"):
        tk = self.tk
        w = tk.Toplevel(self.root)
        w.overrideredirect(True)
        w.attributes("-topmost", True)
        w.configure(bg="#143a73")
        sw = self.root.winfo_screenwidth()
        tk.Label(w, text=title, font=("Segoe UI", 10, "bold"), bg="#143a73", fg="#9cc4ff", anchor="w").pack(fill="x", padx=14, pady=(10, 0))
        tk.Label(w, text=text, font=("Segoe UI", 12), bg="#143a73", fg="white", wraplength=380, justify="left", anchor="w").pack(fill="x", padx=14, pady=(2, 12))
        w.update_idletasks()
        w.geometry(f"+{sw - w.winfo_reqwidth() - 20}+{60 + 90 * len([1 for _ in range(0)])}")
        w.after(max(3, seconds) * 1000, w.destroy)

    def message(self, text: str, sender: str, seconds: int):
        self.toast(text, seconds, f"Pesan dari {sender or 'Guru'}")
        self.beep()

    @staticmethod
    def beep():
        if IS_WIN:
            try:
                ctypes.windll.user32.MessageBeep(0x40)
            except Exception:
                pass

    # ---- banner pemantauan -----------------------------------------------------------------------------------------------
    def set_banner(self, text: str | None):
        tk = self.tk
        if not text:
            if self.banner is not None:
                self.banner.destroy()
                self.banner = None
            return
        if self.banner is None:
            b = tk.Toplevel(self.root)
            b.overrideredirect(True)
            b.attributes("-topmost", True)
            b.configure(bg="#b3261e")
            self.banner_label = tk.Label(b, text=text, font=("Segoe UI", 10, "bold"), bg="#b3261e", fg="white", padx=14, pady=4)
            self.banner_label.pack()
            b.update_idletasks()
            sw = self.root.winfo_screenwidth()
            b.geometry(f"+{(sw - b.winfo_reqwidth()) // 2}+0")
            self.banner = b
        else:
            self.banner_label.config(text=text)

    def run(self):
        self.root.mainloop()


class Helper:
    def __init__(self, ui: UI | None):
        self.ui = ui
        self.sock: socket.socket | None = None
        self.send_lock = threading.Lock()
        self.screen_thread: threading.Thread | None = None
        self.screen_stop = threading.Event()
        self.viewer = ""
        self.controlling = False
        self.locked = False
        self.last_control = 0.0

    def ipc_key(self) -> str:
        return (agent_home() / "ipc.key").read_text().strip()

    def send(self, msg: dict) -> None:
        with self.send_lock:
            if self.sock:
                self.sock.sendall((json.dumps(msg) + "\n").encode())

    def update_banner(self):
        if not self.ui:
            return
        parts = []
        if self.controlling:
            parts.append(f"{self.viewer} sedang MENGENDALIKAN komputer ini")
        elif self.screen_thread and self.screen_thread.is_alive():
            parts.append(f"Layar Anda sedang DIPANTAU oleh {self.viewer}")
        self.ui.post(self.ui.set_banner, "  ●  " + parts[0] if parts else None)

    # ---- layar ------------------------------------------------------------------------------------------------------------
    def screen_loop(self, fps: float, quality: int, max_w: int):
        import mss
        from PIL import Image
        interval = 1.0 / max(0.5, min(fps, 5))
        with mss.mss() as sct:
            mon = sct.monitors[1]
            while not self.screen_stop.is_set():
                t0 = time.time()
                shot = sct.grab(mon)
                img = Image.frombytes("RGB", shot.size, shot.bgra, "raw", "BGRX")
                if img.width > max_w:
                    img = img.resize((max_w, int(img.height * max_w / img.width)))
                buf = io.BytesIO()
                img.save(buf, "JPEG", quality=max(20, min(quality, 90)))
                try:
                    self.send({"type": "frame", "jpeg": base64.b64encode(buf.getvalue()).decode(), "w": img.width, "h": img.height})
                except OSError:
                    return
                self.screen_stop.wait(max(0.05, interval - (time.time() - t0)))

    def start_screen(self, m: dict):
        self.stop_screen()
        self.viewer = m.get("viewer", "Guru")
        self.screen_stop.clear()
        self.screen_thread = threading.Thread(target=self.screen_loop, args=(float(m.get("fps", 2)), int(m.get("quality", 50)), int(m.get("max_width", 1280))), daemon=True)
        self.screen_thread.start()
        self.update_banner()

    def stop_screen(self):
        self.screen_stop.set()
        if self.screen_thread:
            self.screen_thread.join(timeout=2)
        self.screen_thread, self.controlling = None, False
        self.update_banner()

    # ---- kendali jarak jauh (hanya selagi sesi dipantau & banner tampil) -----------------------------------------------
    def control_input(self, ev: dict):
        if not self.controlling or self.locked:
            return
        import pyautogui
        pyautogui.FAILSAFE = False
        t = ev.get("t")
        sw, sh = pyautogui.size()
        x, y = int(float(ev.get("x", 0)) * sw), int(float(ev.get("y", 0)) * sh)
        if t == "move":
            pyautogui.moveTo(x, y)
        elif t == "click":
            pyautogui.click(x, y, button=ev.get("button", "left") if ev.get("button") in ("left", "right", "middle") else "left")
        elif t == "dblclick":
            pyautogui.doubleClick(x, y)
        elif t == "scroll":
            pyautogui.scroll(int(ev.get("dy", 0)))
        elif t == "key" and isinstance(ev.get("key"), str) and len(ev["key"]) < 20:
            pyautogui.press(ev["key"])
        elif t == "text" and isinstance(ev.get("text"), str):
            pyautogui.write(ev["text"][:200], interval=0.01)

    # ---- pesan dari service ---------------------------------------------------------------------------------------------------
    def handle(self, m: dict):
        c = m.get("cmd")
        ui = self.ui
        if c == "lock":
            self.locked = True
            ui and ui.post(ui.lock, m.get("message", ""))
            self.send({"type": "state", "state": "locked", "user": os.environ.get("USERNAME", "")})
        elif c == "unlock":
            self.locked = False
            ui and ui.post(ui.unlock)
        elif c == "message":
            ui and ui.post(ui.message, m.get("text", ""), m.get("from", ""), int(m.get("seconds", 15)))
        elif c == "toast":
            ui and ui.post(ui.toast, m.get("text", ""), int(m.get("seconds", 8)))
        elif c == "screen_start":
            self.start_screen(m)
        elif c == "screen_stop":
            self.stop_screen()
        elif c == "control_on":
            if self.screen_thread and self.screen_thread.is_alive():
                self.controlling = True
                self.viewer = m.get("viewer", self.viewer)
                self.update_banner()
                self.send({"type": "control_state", "state": "on"})
        elif c == "control_off":
            self.controlling = False
            self.update_banner()
            self.send({"type": "control_state", "state": "off"})
        elif c == "control_input":
            self.control_input(m.get("event") or {})
        elif c == "logout" and IS_WIN:
            subprocess.Popen(["shutdown", "/l"], creationflags=0x08000000)
        elif c == "lock_workstation" and IS_WIN:
            ctypes.windll.user32.LockWorkStation()
        elif c == "launch_browser":
            args = m.get("args") or []
            if args and isinstance(args, list) and all(isinstance(a, str) for a in args):
                subprocess.Popen(args, close_fds=True)

    def state_loop(self):
        while True:
            time.sleep(5)
            st = "locked" if self.locked else ("in_use" if idle_seconds() < 300 else "idle")
            try:
                self.send({"type": "state", "state": st, "user": os.environ.get("USERNAME") or os.environ.get("USER", "")})
            except OSError:
                return

    def connect_loop(self):
        while True:
            try:
                s = socket.create_connection(("127.0.0.1", IPC_PORT), timeout=5)
                s.settimeout(None)
                self.sock = s
                self.send({"type": "hello", "key": self.ipc_key(), "downloads_dir": user_downloads(), "user": os.environ.get("USERNAME") or os.environ.get("USER", "")})
                threading.Thread(target=self.state_loop, daemon=True).start()
                buf = b""
                while True:
                    chunk = s.recv(65536)
                    if not chunk:
                        break
                    buf += chunk
                    while b"\n" in buf:
                        line, buf = buf.split(b"\n", 1)
                        if line.strip():
                            try:
                                self.handle(json.loads(line))
                            except Exception as e:  # satu pesan buruk tidak menjatuhkan helper
                                print("handle error:", e, file=sys.stderr)
            except (OSError, FileNotFoundError):
                pass
            finally:
                self.sock = None
                self.stop_screen()
            time.sleep(3)


def main():
    headless = "--headless" in sys.argv or (not IS_WIN and not os.environ.get("DISPLAY"))
    ui = None if headless else UI()
    h = Helper(ui)
    t = threading.Thread(target=h.connect_loop, daemon=True)
    t.start()
    if ui:
        ui.run()
    else:
        t.join()


if __name__ == "__main__":
    main()
