"""
wivexx_gaming_mode.py — conservative "free up the PC for gaming" mode  (v1.0)
═══════════════════════════════════════════════════════════════════════
Our OWN gaming mode (no external apps): when ON it
  1. switches Windows to the High-Performance power plan, and
  2. SUSPENDS a curated denylist of known-safe background bloat (browsers,
     Spotify, cloud sync, chat) so they stop eating CPU/RAM/disk while you play.

Everything is fully REVERSIBLE — exit() resumes every process it froze and
restores the previous power plan.  Safety rails:
  • only NAMES on the denylist are ever touched (never system/critical procs,
    never the game, never anti-cheat, never Wivexx itself),
  • the FOREGROUND app is never suspended,
  • anything currently PLAYING AUDIO is exempt — so a Discord call in progress
    or music you want to keep stays alive (this is the "Discord-when-not-in-call"
    rule, generalised),
  • the frozen PID list + previous power plan are written to a temp file so a
    crash can't leave apps stuck suspended — recover() unfreezes them at boot.

No admin needed: we only suspend processes owned by the current user.

Public API (called from wivexx_api / wivexx_gui):
    enter(announce=None, logger=None) -> dict
    exit(announce=None, logger=None) -> dict
    toggle(announce=None, logger=None) -> bool      # returns the NEW state
    is_active() -> bool
    recover(logger=None) -> None                    # crash-recovery at boot
"""

import json
import os
import re
import subprocess
import tempfile
import threading
from typing import Callable, Optional

_NOWIN = getattr(subprocess, "CREATE_NO_WINDOW", 0)
_lock = threading.Lock()
_active = False
_suspended: list = []          # [(pid, name), …] frozen this session
_prev_power: Optional[str] = None
_STATE_FILE = os.path.join(tempfile.gettempdir(), "wivexx_gaming_state.json")

# Known-safe background bloat a game never depends on. Game launchers
# (Steam/Epic/EA) are deliberately LEFT OUT — some titles need them running.
_DENYLIST = {
    # browsers
    "chrome.exe", "msedge.exe", "firefox.exe", "brave.exe", "opera.exe",
    "opera_gx.exe", "vivaldi.exe",
    # music / media
    "spotify.exe",
    # cloud sync
    "onedrive.exe", "dropbox.exe", "megasync.exe", "googledrivefs.exe",
    "googledrivesync.exe",
    # heavy creative-suite helpers safe to pause when unfocused
    "acrobat.exe", "acrotray.exe", "adobedesktopservice.exe",
    "creative cloud.exe", "ccxprocess.exe",
    # NOTE: chat apps (Discord, Slack, Telegram, …) are deliberately NOT here — you
    # usually WANT them alive while gaming (talking to friends), and a suspended one
    # won't open when you click it. Removed on user request.
    # NOTE: generic runtimes (node.exe, python, java) are deliberately EXCLUDED —
    # too broad (a dev server / build / Electron backend you may care about).
}

_HIGH_PERF_GUID = "8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c"


# ── power plan ───────────────────────────────────────────────────────────────
def _get_active_scheme() -> Optional[str]:
    try:
        out = subprocess.run(["powercfg", "/getactivescheme"],
                             capture_output=True, timeout=5, creationflags=_NOWIN)
        m = re.search(r"([0-9a-fA-F]{8}-[0-9a-fA-F-]{27})",
                      out.stdout.decode(errors="ignore"))
        return m.group(1) if m else None
    except Exception:
        return None


def _set_scheme(guid: str) -> bool:
    try:
        out = subprocess.run(["powercfg", "/setactive", guid],
                             capture_output=True, timeout=5, creationflags=_NOWIN)
        return out.returncode == 0
    except Exception:
        return False


# ── which processes are off-limits ───────────────────────────────────────────
def _foreground_pid() -> int:
    try:
        import ctypes
        user32 = ctypes.windll.user32
        hwnd = user32.GetForegroundWindow()
        pid = ctypes.c_ulong(0)
        user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
        return int(pid.value)
    except Exception:
        return 0


def _own_pids() -> set:
    """Wivexx's own process tree — never suspend ourselves."""
    pids = {os.getpid()}
    try:
        import psutil
        me = psutil.Process(os.getpid())
        try:
            pids.add(me.ppid())
        except Exception:
            pass
        for c in me.children(recursive=True):
            pids.add(c.pid)
    except Exception:
        pass
    return pids


def _windowed_pids() -> set:
    """PIDs that own a VISIBLE top-level window — i.e. apps the user has open (on
    screen or minimised to the taskbar). Never suspend these: performance mode must
    never make something you opened yourself (Discord, a browser, …) unresponsive."""
    pids = set()
    try:
        import ctypes
        from ctypes import wintypes
        u = ctypes.windll.user32
        WNDENUMPROC = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)

        def _cb(hwnd, _l):
            try:
                if u.IsWindowVisible(hwnd):
                    pid = wintypes.DWORD()
                    u.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
                    if pid.value:
                        pids.add(int(pid.value))
            except Exception:
                pass
            return True

        u.EnumWindows(WNDENUMPROC(_cb), 0)
    except Exception:
        pass
    return pids


def _audio_pids() -> set:
    """PIDs currently producing sound — exempt (a live call, music you want)."""
    pids = set()
    try:
        from pycaw.pycaw import AudioUtilities, IAudioMeterInformation
        for s in AudioUtilities.GetAllSessions():
            try:
                if float(s._ctl.QueryInterface(IAudioMeterInformation)
                         .GetPeakValue()) >= 0.01 and s.Process:
                    pids.add(s.Process.pid)
            except Exception:
                pass
    except Exception:
        pass
    return pids


# ── state-file (crash recovery) ──────────────────────────────────────────────
def _write_state() -> None:
    try:
        with open(_STATE_FILE, "w", encoding="utf-8") as f:
            json.dump({"suspended": _suspended, "power": _prev_power}, f)
    except Exception:
        pass


def _clear_state() -> None:
    try:
        if os.path.exists(_STATE_FILE):
            os.remove(_STATE_FILE)
    except Exception:
        pass


# ── public API ───────────────────────────────────────────────────────────────
def is_active() -> bool:
    return _active


def enter(announce: Optional[Callable] = None, logger=None) -> dict:
    """Freeze background bloat + go High-Performance. Returns a summary dict."""
    global _active, _suspended, _prev_power
    with _lock:
        if _active:
            return {"already": True, "suspended": len(_suspended)}
        try:
            import psutil
        except Exception:
            if announce:
                announce("I can't run performance mode without psutil, sir.")
            return {"error": "psutil"}

        # never freeze: ourselves, anything playing audio, the foreground app, or
        # anything the user has open in a visible window (so it stays usable)
        protected = _own_pids() | _audio_pids() | _windowed_pids()
        fg = _foreground_pid()
        if fg:
            protected.add(fg)

        frozen = []
        for p in psutil.process_iter(["pid", "name"]):
            try:
                name = (p.info.get("name") or "").lower()
                if name not in _DENYLIST or p.pid in protected:
                    continue
                p.suspend()
                frozen.append([p.pid, name])
            except (psutil.NoSuchProcess, psutil.AccessDenied, Exception):
                pass

        _prev_power = _get_active_scheme()
        power_ok = _set_scheme(_HIGH_PERF_GUID)
        _suspended = frozen
        _active = True
        _write_state()
        if logger:
            logger.info("GAMING MODE on — froze %d apps, power=%s",
                        len(frozen), "high-perf" if power_ok else "unchanged")
        apps = sorted({n for _, n in frozen})
        if announce:
            if frozen:
                announce("Performance mode on, sir — I froze %d background app%s."
                         % (len(frozen), "" if len(frozen) == 1 else "s"))
            else:
                announce("Performance mode on, sir — nothing needed freezing.")
        return {"suspended": len(frozen), "apps": apps, "power": power_ok}


def exit(announce: Optional[Callable] = None, logger=None) -> dict:  # noqa: A003
    """Resume everything we froze + restore the previous power plan."""
    global _active, _suspended, _prev_power
    with _lock:
        if not _active and not _suspended:
            return {"already": True}
        resumed = 0
        try:
            import psutil
            for pid, name in _suspended:
                try:
                    p = psutil.Process(pid)
                    if (p.name() or "").lower() == name:   # guard against PID reuse
                        p.resume()
                        resumed += 1
                except Exception:
                    pass
        except Exception:
            pass
        if _prev_power:
            _set_scheme(_prev_power)
        n = len(_suspended)
        _suspended = []
        _prev_power = None
        _active = False
        _clear_state()
        if logger:
            logger.info("GAMING MODE off — resumed %d/%d apps", resumed, n)
        if announce:
            announce("Performance mode off, sir — your apps are back.")
        return {"resumed": resumed, "total": n}


def toggle(announce: Optional[Callable] = None, logger=None) -> bool:
    if _active:
        exit(announce, logger)
    else:
        enter(announce, logger)
    return _active


def recover(logger=None) -> None:
    """At boot: if a previous run left apps frozen (crash/forced quit), thaw them
    and restore the power plan, so gaming mode can never strand background apps."""
    if not os.path.exists(_STATE_FILE):
        return
    try:
        with open(_STATE_FILE, encoding="utf-8") as f:
            st = json.load(f)
    except Exception:
        _clear_state()
        return
    resumed = 0
    try:
        import psutil
        for pid, name in st.get("suspended", []):
            try:
                p = psutil.Process(pid)
                if (p.name() or "").lower() == name:
                    p.resume()
                    resumed += 1
            except Exception:
                pass
    except Exception:
        pass
    if st.get("power"):
        _set_scheme(st["power"])
    _clear_state()
    if logger and resumed:
        logger.info("GAMING MODE recovery — thawed %d stranded apps from a "
                    "previous run", resumed)


if __name__ == "__main__":
    import time
    print("entering…", enter(announce=print))
    time.sleep(3)
    print("exiting…", exit(announce=print))
