"""Anti-debugging protections for runtime integrity.

Detects attached debuggers via multiple heuristics and provides
a background watchdog that can trigger countermeasures.
"""
from __future__ import annotations

import ctypes
import gc
import logging
import platform
import struct
import sys
import threading
import time
from typing import Callable

logger = logging.getLogger(__name__)

_watchdog_started = False
_watchdog_lock = threading.Lock()


def _is_windows() -> bool:
    """Return True if running on Windows."""
    return platform.system() == "Windows"


def _check_is_debugger_present() -> bool:
    """Windows-only: call kernel32.IsDebuggerPresent()."""
    if not _is_windows():
        return False
    try:
        kernel32 = ctypes.windll.kernel32  # type: ignore[attr-defined]
        return bool(kernel32.IsDebuggerPresent())
    except Exception:
        return False


def _check_nt_global_flag() -> bool:
    """Windows x64-only: read NtGlobalFlag from PEB for debugger traces.

    When a process is created under a debugger the OS sets
    FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK |
    FLG_HEAP_VALIDATE_PARAMETERS  (combined 0x70) in the PEB.
    """
    if not _is_windows():
        return False
    if struct.calcsize("P") * 8 != 64:
        # Only implemented for x64
        return False
    try:
        ntdll = ctypes.windll.ntdll  # type: ignore[attr-defined]

        class PROCESS_BASIC_INFORMATION(ctypes.Structure):
            _fields_ = [
                ("Reserved1", ctypes.c_void_p),
                ("PebBaseAddress", ctypes.c_void_p),
                ("Reserved2", ctypes.c_void_p * 2),
                ("UniqueProcessId", ctypes.POINTER(ctypes.c_ulong)),
                ("Reserved3", ctypes.c_void_p),
            ]

        pbi = PROCESS_BASIC_INFORMATION()
        ret_length = ctypes.c_ulong(0)
        status = ntdll.NtQueryInformationProcess(
            ctypes.c_void_p(-1),  # current process handle
            0,  # ProcessBasicInformation
            ctypes.byref(pbi),
            ctypes.sizeof(pbi),
            ctypes.byref(ret_length),
        )
        if status != 0 or not pbi.PebBaseAddress:
            return False

        # NtGlobalFlag is at offset 0xBC in the PEB on x64
        nt_global_flag_addr = pbi.PebBaseAddress + 0xBC
        flag_value = ctypes.c_ulong()
        ctypes.memmove(ctypes.byref(flag_value), nt_global_flag_addr, 4)
        return (flag_value.value & 0x70) != 0
    except Exception:
        return False


def _check_sys_trace() -> bool:
    """Cross-platform: detect if a trace function is set (debugger / profiler)."""
    return sys.gettrace() is not None


def _check_timing() -> bool:
    """Cross-platform: timing heuristic — a trivial loop should complete quickly.

    If a debugger is single-stepping, the elapsed time will exceed the
    threshold significantly.
    """
    start = time.perf_counter()
    total = 0
    for i in range(100_000):
        total += i
    elapsed_ms = (time.perf_counter() - start) * 1000.0
    return elapsed_ms > 500.0


def check_debugger() -> bool:
    """Return True if any debugger heuristic fires.

    Combines Windows-specific and cross-platform checks.
    """
    checks: list[tuple[str, Callable[[], bool]]] = [
        ("IsDebuggerPresent", _check_is_debugger_present),
        ("NtGlobalFlag", _check_nt_global_flag),
        ("sys.gettrace", _check_sys_trace),
        ("timing", _check_timing),
    ]
    for name, fn in checks:
        try:
            if fn():
                logger.warning("Debugger detected via %s", name)
                return True
        except Exception:
            # Swallow individual check failures
            pass
    return False


def _scrub_sensitive_data() -> None:
    """Overwrite sensitive strings (API keys) found in live objects.

    Walks the garbage-collector's tracked objects looking for dicts that
    contain common secret key names and replaces their values in-place.
    """
    secret_keys = {
        "api_key",
        "api_secret",
        "secret_key",
        "access_token",
        "token",
        "license_key",
        "password",
    }
    try:
        for obj in gc.get_objects():
            if not isinstance(obj, dict):
                continue
            for key in list(obj.keys()):
                if isinstance(key, str) and key.lower() in secret_keys:
                    val = obj[key]
                    if isinstance(val, str) and val:
                        # Overwrite with zeros of same length
                        obj[key] = "\x00" * len(val)
    except Exception:
        pass


def start_anti_debug_watchdog(
    interval: float = 15.0,
    on_detected: Callable[[], None] | None = None,
) -> None:
    """Launch a daemon thread that periodically checks for debuggers.

    Idempotent — calling multiple times has no additional effect.

    Args:
        interval: Seconds between checks.
        on_detected: Optional callback invoked when a debugger is found.
    """
    global _watchdog_started

    with _watchdog_lock:
        if _watchdog_started:
            return
        _watchdog_started = True

    def _loop() -> None:
        while True:
            try:
                if check_debugger():
                    logger.warning(
                        "Anti-debug watchdog triggered — scrubbing secrets"
                    )
                    _scrub_sensitive_data()
                    if on_detected is not None:
                        try:
                            on_detected()
                        except Exception:
                            logger.exception("on_detected callback failed")
            except Exception:
                logger.exception("Anti-debug watchdog iteration failed")
            time.sleep(interval)

    t = threading.Thread(target=_loop, name="anti-debug-watchdog", daemon=True)
    t.start()
    logger.debug("Anti-debug watchdog started (interval=%.1fs)", interval)
