"""AutoAnswerer — screenshot-based automatic exam question answering.

Modes:
  Single press  : answer the current visible question only.
  Double press  : full-auto — answer every question on the page in sequence.
  Press again   : cancel full-auto mode.

Supported question types (detected automatically from screenshot):
  - Choice   (A/B/C/D radio buttons / checkboxes) → presses the key
  - Text      (fill-in / write the answer)         → types the answer

Hotkey: configured via config.auto_answer_hotkey (default: ctrl+shift+f)
"""

from __future__ import annotations

import ctypes
import json
import logging
import re
import sys
import threading
import time
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from ..config import Config
    from ..typing.engine import TypingEngine

log = logging.getLogger(__name__)

# Pre-compiled regex for parsing JSON from AI response
_JSON_OBJECT_RE = re.compile(r"\{[^{}]+\}", re.DOTALL)

# ------------------------------------------------------------------ #
# AI prompt
# ------------------------------------------------------------------ #

_ANSWER_PROMPT = """\
Analyze this exam screenshot.

STEP 1 — Check the current question's answer state:
- Is a radio button / checkbox already selected (filled circle, tick, highlighted option)?
- Is a text field already filled with text?
- Does the question show a score or "correct" mark?
If YES to any of these → the question is ALREADY ANSWERED.

STEP 2 — Based on Step 1, reply with ONLY valid JSON:

Already answered → skip it:
{"type": "done", "value": ""}

Unanswered choice question (radio/checkbox/options A B C D):
{"type": "key", "value": "b"}
(value = LOWERCASE letter of the correct option)

Unanswered fill-in / text question:
{"type": "text", "value": "correct answer here"}

No question visible at all:
{"type": "done", "value": ""}

Answer in the language used by the question.\
"""


# ------------------------------------------------------------------ #
# Win32 helpers (used at module level so they compile once)
# ------------------------------------------------------------------ #

if sys.platform == "win32":  # pragma: no cover

    class _KEYBDINPUT(ctypes.Structure):
        _fields_ = [
            ("wVk", ctypes.c_ushort),
            ("wScan", ctypes.c_ushort),
            ("dwFlags", ctypes.c_ulong),
            ("time", ctypes.c_ulong),
            ("dwExtraInfo", ctypes.POINTER(ctypes.c_ulong)),
        ]

    class _INPUT_UNION(ctypes.Union):
        _fields_ = [("ki", _KEYBDINPUT)]

    class _INPUT(ctypes.Structure):
        _anonymous_ = ("_u",)
        _fields_ = [("type", ctypes.c_ulong), ("_u", _INPUT_UNION)]

    _KEYEVENTF_KEYUP = 0x0002
    _INPUT_KEYBOARD = 1

    def _send_vk(vk_code: int) -> None:
        """Send key-down + key-up via SendInput (global, stealth-safe)."""
        down = _INPUT()
        down.type = _INPUT_KEYBOARD
        down.ki.wVk = vk_code

        up = _INPUT()
        up.type = _INPUT_KEYBOARD
        up.ki.wVk = vk_code
        up.ki.dwFlags = _KEYEVENTF_KEYUP

        arr = (_INPUT * 2)(down, up)
        ctypes.windll.user32.SendInput(2, arr, ctypes.sizeof(_INPUT))

else:

    def _send_vk(vk_code: int) -> None:  # type: ignore[misc]
        pass


# Letter → Windows virtual key code
_VK_LETTERS = {chr(ord("a") + i): 0x41 + i for i in range(26)}
_VK_DIGITS = {str(i): 0x30 + i for i in range(10)}
_VK_MAP = {**_VK_LETTERS, **_VK_DIGITS}
_VK_RETURN = 0x0D
_VK_TAB = 0x09


# ------------------------------------------------------------------ #
# AutoAnswerer
# ------------------------------------------------------------------ #


class AutoAnswerer:
    """Screenshot → Vision AI → keyboard action for exam questions."""

    # Max seconds between two presses to count as "double press"
    _DOUBLE_PRESS_WINDOW = 0.7

    def __init__(self, config: Config, typing_engine: TypingEngine) -> None:
        self._config = config
        self._typing_engine = typing_engine
        self._last_press: float = 0.0
        self._press_lock = threading.Lock()
        self._cancel = threading.Event()
        self._running = False

    # ------------------------------------------------------------------ #
    # Hotkey entry point
    # ------------------------------------------------------------------ #

    def on_hotkey(self) -> None:
        """Called from the hotkey polling thread."""
        if self._running:
            # Cancel ongoing full-auto
            log.info("[auto] Cancelling full-auto")
            self._cancel.set()
            return

        now = time.monotonic()
        with self._press_lock:
            elapsed = now - self._last_press

            if elapsed < self._DOUBLE_PRESS_WINDOW:
                # Double press → full-auto
                self._last_press = 0.0
                self._cancel.clear()
                log.info("[auto] Double-press → starting full-auto mode")
                threading.Thread(target=self._full_auto, daemon=True, name="auto-full").start()
            else:
                self._last_press = now
                # Single press — wait to confirm it's not a double press
                threading.Thread(target=self._single_delayed, daemon=True, name="auto-single").start()

    # ------------------------------------------------------------------ #
    # Modes
    # ------------------------------------------------------------------ #

    def _single_delayed(self) -> None:
        """Wait for double-press window, then execute single answer."""
        time.sleep(self._DOUBLE_PRESS_WINDOW + 0.05)
        if self._last_press == 0.0:
            return  # double press happened while we waited
        self._last_press = 0.0
        self._answer_once(advance=False)

    def _full_auto(self) -> None:
        """Answer questions one by one until done or cancelled."""
        self._running = True
        advance_delay = float(self._config.get("auto_answer.advance_delay", 2.0))
        log.info("[auto] Full-auto started (delay=%.1fs)", advance_delay)
        try:
            for i in range(200):  # safety limit
                if self._cancel.is_set():
                    log.info("[auto] Full-auto cancelled at step %d", i + 1)
                    break
                result = self._answer_once(advance=True)
                if result in ("done", "error"):
                    log.info("[auto] Full-auto stopped: %s (step=%d)", result, i + 1)
                    break
                # Wait for page to update
                time.sleep(advance_delay)
        finally:
            self._running = False
            log.info("[auto] Full-auto ended")

    # ------------------------------------------------------------------ #
    # Core answer logic
    # ------------------------------------------------------------------ #

    def _answer_once(self, advance: bool) -> str:
        """Screenshot → AI → execute. Returns 'ok' | 'done' | 'error'."""
        try:
            from ..screenshot import capture_screen
            from ..utils.image_utils import image_to_base64, resize_image

            img = capture_screen()
            # Cap at 1920x1080; smaller screenshots pass through unchanged.
            img = resize_image(img)
            image_b64 = image_to_base64(img)
            log.info(
                "[auto] Screenshot: %dx%d → %d base64 chars",
                img.size[0],
                img.size[1],
                len(image_b64),
            )

            data = self._ask_ai(image_b64)
            if data is None:
                return "error"

            kind = data.get("type", "done")
            value = str(data.get("value", "")).strip()

            log.info("[auto] AI answer → type=%s value=%r", kind, value[:60] if value else "")

            if kind == "done" or not value:
                return "done"

            if kind == "key":
                key = value[0].lower()
                self._press_key(key)
                if advance:
                    time.sleep(0.4)
                    self._press_advance()

            elif kind == "text":
                self._typing_engine.type_text(value)
                if advance:
                    time.sleep(0.5)
                    self._press_advance()

            return "ok"

        except Exception as exc:
            log.error("[auto] _answer_once error: %s", exc)
            return "error"

    def _ask_ai(self, image_b64: str) -> dict | None:
        """Send screenshot to Vision AI, parse and return JSON dict."""
        from ..ai.vision_adapter import VisionAdapter

        # 60s timeout: vision calls via proxy can be slow in China (packet loss + relay latency)
        adapter = VisionAdapter(self._config, timeout=60.0)
        raw = ""
        try:
            for chunk in adapter.analyze(image_b64, _ANSWER_PROMPT, cancel_event=self._cancel):
                raw += chunk
        except Exception as exc:
            log.error("[auto] Vision AI error: %s", exc)
            return None

        log.debug("[auto] Raw AI response: %r", raw[:300])

        # AI may wrap JSON in ```json ... ``` — extract inner object
        match = _JSON_OBJECT_RE.search(raw)
        if match:
            try:
                return json.loads(match.group())
            except json.JSONDecodeError:
                pass

        try:
            return json.loads(raw.strip())
        except json.JSONDecodeError:
            log.warning("[auto] Cannot parse JSON from: %r", raw[:100])
            return None

    # ------------------------------------------------------------------ #
    # Key helpers
    # ------------------------------------------------------------------ #

    def _press_key(self, key: str) -> None:
        """Press a single letter/digit key globally."""
        vk = _VK_MAP.get(key.lower())
        if not vk:
            log.warning("[auto] Unknown key %r — cannot press", key)
            return
        if sys.platform == "win32":
            _send_vk(vk)
        else:
            try:
                import pyautogui

                pyautogui.press(key)
            except Exception as exc:
                log.warning("[auto] pyautogui.press(%r): %s", key, exc)
        log.info("[auto] Pressed key: %s", key.upper())

    def _press_advance(self) -> None:
        """Press Enter (or configured advance key) to go to next question."""
        advance = str(self._config.get("auto_answer.advance_key", "enter")).lower()
        if advance == "tab":
            vk = _VK_TAB
            label = "Tab"
        else:
            vk = _VK_RETURN
            label = "Enter"

        if sys.platform == "win32":
            _send_vk(vk)
        else:
            try:
                import pyautogui

                pyautogui.press("tab" if advance == "tab" else "enter")
            except Exception as exc:
                log.warning("[auto] advance key (%s) error: %s", label, exc)
        log.info("[auto] Pressed %s to advance", label)
