"""Screen capture — cross-platform, no admin rights required.

Primary method: mss (GDI/BitBlt).
Fallback (Windows): Win32 PrintWindow API — bypasses BitBlt hooks
installed by exam proctoring software (CSCA RecStatus.exe, etc.).
"""

from __future__ import annotations

import logging
import sys

from PIL import Image

log = logging.getLogger(__name__)


def _capture_printwindow() -> Image.Image:
    """Capture the desktop using PrintWindow + win32 API.

    PrintWindow asks the window to paint itself into a memory DC, bypassing
    BitBlt hooks that exam software uses to block screen capture.
    Falls back to CreateDC-based BitBlt if PrintWindow returns a blank image.
    """
    import ctypes
    import ctypes.wintypes

    user32 = ctypes.windll.user32
    gdi32 = ctypes.windll.gdi32

    # Get desktop window and its dimensions
    hwnd = user32.GetDesktopWindow()
    left = user32.GetSystemMetrics(76)   # SM_XVIRTUALSCREEN
    top = user32.GetSystemMetrics(77)    # SM_YVIRTUALSCREEN
    width = user32.GetSystemMetrics(78)  # SM_CXVIRTUALSCREEN
    height = user32.GetSystemMetrics(79) # SM_CYVIRTUALSCREEN

    if width <= 0 or height <= 0:
        width = user32.GetSystemMetrics(0)   # SM_CXSCREEN
        height = user32.GetSystemMetrics(1)  # SM_CYSCREEN
        left, top = 0, 0

    # Create device context and bitmap
    hdc_screen = user32.GetDC(0)
    hdc_mem = gdi32.CreateCompatibleDC(hdc_screen)
    hbmp = gdi32.CreateCompatibleBitmap(hdc_screen, width, height)
    old_bmp = gdi32.SelectObject(hdc_mem, hbmp)

    # Try PrintWindow first (PW_RENDERFULLCONTENT = 2)
    result = user32.PrintWindow(hwnd, hdc_mem, 2)

    if not result:
        # Fallback: direct BitBlt from screen DC
        gdi32.BitBlt(hdc_mem, 0, 0, width, height, hdc_screen, left, top, 0x00CC0020)

    # Read bitmap bits
    class BITMAPINFOHEADER(ctypes.Structure):
        _fields_ = [
            ("biSize", ctypes.wintypes.DWORD),
            ("biWidth", ctypes.wintypes.LONG),
            ("biHeight", ctypes.wintypes.LONG),
            ("biPlanes", ctypes.wintypes.WORD),
            ("biBitCount", ctypes.wintypes.WORD),
            ("biCompression", ctypes.wintypes.DWORD),
            ("biSizeImage", ctypes.wintypes.DWORD),
            ("biXPelsPerMeter", ctypes.wintypes.LONG),
            ("biYPelsPerMeter", ctypes.wintypes.LONG),
            ("biClrUsed", ctypes.wintypes.DWORD),
            ("biClrImportant", ctypes.wintypes.DWORD),
        ]

    bmi = BITMAPINFOHEADER()
    bmi.biSize = ctypes.sizeof(BITMAPINFOHEADER)
    bmi.biWidth = width
    bmi.biHeight = -height  # top-down
    bmi.biPlanes = 1
    bmi.biBitCount = 32
    bmi.biCompression = 0  # BI_RGB

    buf = ctypes.create_string_buffer(width * height * 4)
    gdi32.GetDIBits(hdc_mem, hbmp, 0, height, buf, ctypes.byref(bmi), 0)

    # Cleanup
    gdi32.SelectObject(hdc_mem, old_bmp)
    gdi32.DeleteObject(hbmp)
    gdi32.DeleteDC(hdc_mem)
    user32.ReleaseDC(0, hdc_screen)

    img = Image.frombuffer("RGBA", (width, height), buf, "raw", "BGRA", 0, 1)

    # Check if image is blank (all black) — PrintWindow may return empty
    extrema = img.convert("L").getextrema()
    if extrema == (0, 0):
        log.warning("[screenshot] PrintWindow returned blank image — trying BitBlt fallback")
        raise RuntimeError("PrintWindow returned blank image")

    return img.convert("RGB")


def _capture_foreground_window() -> Image.Image:
    """Capture the foreground window using PrintWindow on the specific window.

    Unlike desktop-wide PrintWindow, this asks the specific window (e.g. CSCA
    exam client) to paint itself. The window must render its own content to
    function, so this bypasses desktop-level capture blocks.
    """
    import ctypes
    import ctypes.wintypes

    user32 = ctypes.windll.user32
    gdi32 = ctypes.windll.gdi32

    # Get the foreground window
    hwnd = user32.GetForegroundWindow()
    if not hwnd:
        raise RuntimeError("No foreground window")

    # Get window dimensions
    class RECT(ctypes.Structure):
        _fields_ = [
            ("left", ctypes.c_long),
            ("top", ctypes.c_long),
            ("right", ctypes.c_long),
            ("bottom", ctypes.c_long),
        ]

    rect = RECT()
    user32.GetClientRect(hwnd, ctypes.byref(rect))
    width = rect.right - rect.left
    height = rect.bottom - rect.top

    if width <= 0 or height <= 0:
        # Try GetWindowRect instead
        user32.GetWindowRect(hwnd, ctypes.byref(rect))
        width = rect.right - rect.left
        height = rect.bottom - rect.top

    if width <= 0 or height <= 0:
        raise RuntimeError("Foreground window has zero size")

    # Create DC and bitmap
    hdc_win = user32.GetDC(hwnd)
    hdc_mem = gdi32.CreateCompatibleDC(hdc_win)
    hbmp = gdi32.CreateCompatibleBitmap(hdc_win, width, height)
    old_bmp = gdi32.SelectObject(hdc_mem, hbmp)

    # Try PrintWindow on the specific window (PW_RENDERFULLCONTENT = 2)
    result = user32.PrintWindow(hwnd, hdc_mem, 2)

    if not result:
        # Fallback: try PW_CLIENTONLY = 1
        result = user32.PrintWindow(hwnd, hdc_mem, 1)

    if not result:
        # Last fallback: BitBlt from window DC
        gdi32.BitBlt(hdc_mem, 0, 0, width, height, hdc_win, 0, 0, 0x00CC0020)

    # Read bitmap bits
    class BITMAPINFOHEADER(ctypes.Structure):
        _fields_ = [
            ("biSize", ctypes.wintypes.DWORD),
            ("biWidth", ctypes.wintypes.LONG),
            ("biHeight", ctypes.wintypes.LONG),
            ("biPlanes", ctypes.wintypes.WORD),
            ("biBitCount", ctypes.wintypes.WORD),
            ("biCompression", ctypes.wintypes.DWORD),
            ("biSizeImage", ctypes.wintypes.DWORD),
            ("biXPelsPerMeter", ctypes.wintypes.LONG),
            ("biYPelsPerMeter", ctypes.wintypes.LONG),
            ("biClrUsed", ctypes.wintypes.DWORD),
            ("biClrImportant", ctypes.wintypes.DWORD),
        ]

    bmi = BITMAPINFOHEADER()
    bmi.biSize = ctypes.sizeof(BITMAPINFOHEADER)
    bmi.biWidth = width
    bmi.biHeight = -height  # top-down
    bmi.biPlanes = 1
    bmi.biBitCount = 32
    bmi.biCompression = 0

    buf = ctypes.create_string_buffer(width * height * 4)
    gdi32.GetDIBits(hdc_mem, hbmp, 0, height, buf, ctypes.byref(bmi), 0)

    # Cleanup
    gdi32.SelectObject(hdc_mem, old_bmp)
    gdi32.DeleteObject(hbmp)
    gdi32.DeleteDC(hdc_mem)
    user32.ReleaseDC(hwnd, hdc_win)

    img = Image.frombuffer("RGBA", (width, height), buf, "raw", "BGRA", 0, 1)

    # Check if blank
    extrema = img.convert("L").getextrema()
    if extrema == (0, 0):
        raise RuntimeError("Foreground window capture returned blank")

    return img.convert("RGB")


def _capture_dxgi() -> Image.Image:
    """Capture screen using DXGI Desktop Duplication via dxcam.

    Reads directly from the GPU framebuffer through DirectX, completely
    bypassing GDI/win32k.sys. This is the same API that OBS and Discord
    use for screen capture.
    """
    import dxcam

    camera = dxcam.create(output_color="RGB")
    frame = camera.grab()
    del camera

    if frame is None:
        raise RuntimeError("DXGI: grab() returned None")

    img = Image.fromarray(frame)

    # Check if blank
    extrema = img.convert("L").getextrema()
    if extrema == (0, 0):
        raise RuntimeError("DXGI: blank frame")

    return img


def _capture_printscreen_key() -> Image.Image:
    """Capture by simulating the PrintScreen key and reading clipboard.

    Some exam software blocks GDI/BitBlt API calls but does not block
    the hardware PrintScreen key. We simulate the key press, wait briefly,
    then grab the screenshot from the clipboard.
    """
    import ctypes
    import time as _time

    user32 = ctypes.windll.user32

    VK_SNAPSHOT = 0x2C  # PrintScreen key
    KEYEVENTF_KEYUP = 0x0002

    # Clear clipboard first
    user32.OpenClipboard(None)
    ctypes.windll.user32.EmptyClipboard()
    user32.CloseClipboard()

    # Simulate PrintScreen key press
    user32.keybd_event(VK_SNAPSHOT, 0, 0, 0)
    user32.keybd_event(VK_SNAPSHOT, 0, KEYEVENTF_KEYUP, 0)

    # Wait for clipboard to be populated
    _time.sleep(0.3)

    # Grab from clipboard using PIL
    from PIL import ImageGrab
    img = ImageGrab.grabclipboard()

    if img is None:
        raise RuntimeError("PrintScreen key: no image in clipboard")

    img = img.convert("RGB")

    # Check if blank
    extrema = img.convert("L").getextrema()
    if extrema == (0, 0):
        raise RuntimeError("PrintScreen key: blank image in clipboard")

    return img


def _capture_mss(monitor_index: int = 1) -> Image.Image:
    """Capture using mss (GDI/BitBlt)."""
    import mss

    with mss.mss() as sct:
        monitors = sct.monitors
        if monitor_index >= len(monitors):
            log.warning(
                "monitor_index=%d out of range (have %d monitors) — using primary",
                monitor_index,
                len(monitors),
            )
            monitor_index = 1 if len(monitors) > 1 else 0

        monitor = monitors[monitor_index]
        raw = sct.grab(monitor)
        img = Image.frombytes("RGBA", raw.size, bytes(raw.bgra), "raw", "BGRA")
        return img.convert("RGB")


def capture_screen(monitor_index: int = 1) -> Image.Image:
    """Capture the specified monitor and return a PIL Image (RGB).

    On Windows: tries mss first, falls back to PrintWindow API if
    the image is blank (blocked by exam software).

    Args:
        monitor_index: Which monitor to capture.
            0  = all monitors combined (virtual screen)
            1  = primary monitor (default)
            2+ = additional monitors

    Returns:
        PIL Image in RGB mode.

    Raises:
        RuntimeError: If all capture methods fail.
    """
    errors: list[str] = []

    # Method 1: mss (fast, reliable in normal conditions)
    try:
        img = _capture_mss(monitor_index)
        # Check if image is blank (exam software may return black screen)
        extrema = img.convert("L").getextrema()
        if extrema != (0, 0):
            return img
        log.warning("[screenshot] mss returned blank image — trying fallback")
        errors.append("mss: blank image")
    except Exception as exc:
        log.warning("[screenshot] mss failed: %s — trying fallback", exc)
        errors.append(f"mss: {exc}")

    # Method 2: PrintWindow (Windows only — bypasses BitBlt hooks)
    if sys.platform == "win32":
        try:
            img = _capture_printwindow()
            log.info("[screenshot] PrintWindow capture succeeded")
            return img
        except Exception as exc:
            log.warning("[screenshot] PrintWindow failed: %s", exc)
            errors.append(f"PrintWindow: {exc}")

    # Method 3: PrintScreen key simulation (Windows only)
    # Simulates the PrintScreen key, then grabs from clipboard.
    # Some exam software blocks API-based capture but not the key itself.
    if sys.platform == "win32":
        try:
            img = _capture_printscreen_key()
            log.info("[screenshot] PrintScreen key capture succeeded")
            return img
        except Exception as exc:
            log.warning("[screenshot] PrintScreen key failed: %s", exc)
            errors.append(f"PrintScreen: {exc}")

    # Method 4: Capture the foreground window directly (Windows only)
    if sys.platform == "win32":
        try:
            img = _capture_foreground_window()
            log.info("[screenshot] Foreground window capture succeeded")
            return img
        except Exception as exc:
            log.warning("[screenshot] Foreground window failed: %s", exc)
            errors.append(f"ForegroundWindow: {exc}")

    # Method 5: DXGI Desktop Duplication (Windows only)
    # Reads directly from the GPU framebuffer via DirectX — completely
    # bypasses GDI and win32k.sys. This is how OBS/Discord capture screens.
    if sys.platform == "win32":
        try:
            img = _capture_dxgi()
            log.info("[screenshot] DXGI capture succeeded")
            return img
        except Exception as exc:
            log.warning("[screenshot] DXGI failed: %s", exc)
            errors.append(f"DXGI: {exc}")

    raise RuntimeError(f"All screenshot methods failed: {'; '.join(errors)}")
