"""OcrTypist — screenshot → Vision AI → type full answer into CSCA answer panel.

Workflow:
  1. Press hotkey (default: Ctrl+F8)
  2. Foreground window is saved (CSCA answer panel)
  3. Full screenshot is captured
  4. Screenshot sent to Vision AI: analyzes the exam question and writes answer
  5. AI response (plain text) is typed into the CSCA answer panel via
     EM_REPLACESEL / WM_PASTE — bypasses ksHook.exe keyboard monitoring

CSCA forbidden hotkeys (do NOT use): F2, F3, Ctrl+F5
This module uses: Ctrl+F8

Config keys (ocr_typist section):
  enabled   — enable/disable          (default: true)
  hotkey    — hotkey to trigger       (default: "ctrl+f8")
  prompt    — override AI prompt      (default: built-in exam prompt)
"""

from __future__ import annotations

import logging
import sys
import threading
from typing import TYPE_CHECKING

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

log = logging.getLogger(__name__)

# Default prompt — instructs AI to analyze the visible exam question
# and output a ready-to-type answer (no JSON, no preamble)
_DEFAULT_PROMPT = """\
You are an expert exam assistant. Look at this screenshot of an exam interface carefully.

1. Find the question or task visible on screen (may be in the main content area or a dialog box)
2. Read all answer choices if it is multiple choice
3. Compose a complete, correct answer

Output ONLY the answer text that should be typed into the answer field.
- For multiple choice: output the correct letter AND a brief explanation (e.g. "B — Oxygen")
- For open questions: write a full, concise answer
- Do NOT add any preamble like "The answer is..." — just the answer itself
- Respond in the same language as the question
"""

_OCR_TEST_PROMPT = """\
Read ALL visible text from this screenshot exactly as it appears.
Output every word, sentence, and label you can see — preserve the original language.
Do NOT summarize. Do NOT add explanations. Output ONLY the raw text content visible on screen.
"""


class OcrTypist:
    """Hotkey-triggered: screenshot → Vision AI → type answer into CSCA panel."""

    def __init__(self, config: Config, typing_engine: TypingEngine) -> None:
        self._config = config
        self._typing_engine = typing_engine
        self._lock = threading.Lock()
        self._busy = False
        self._answer_window = None

    def set_answer_window(self, answer_window) -> None:
        """Set the AnswerWindow to display results (instead of typing into active window)."""
        self._answer_window = answer_window

    # ------------------------------------------------------------------ #
    # Public entry point
    # ------------------------------------------------------------------ #

    def on_hotkey(self) -> None:
        """Called when Ctrl+F8 is pressed."""
        with self._lock:
            if self._busy:
                log.info("[ocr_typist] Already processing — ignoring")
                return
            self._busy = True

        # Save foreground HWND *before* spawning thread
        # so we can refocus the CSCA answer panel after AI call
        target_hwnd = self._get_foreground_hwnd()

        threading.Thread(
            target=self._run,
            args=(target_hwnd,),
            daemon=True,
            name="ocr-typist-worker",
        ).start()

    def on_test_hotkey(self) -> None:
        """Called when OCR Test hotkey is pressed (Ctrl+Shift+F9) — screenshot → read ALL text → type into CSCA."""
        with self._lock:
            if self._busy:
                log.info("[ocr_typist] Already processing — ignoring test hotkey")
                return
            self._busy = True

        target_hwnd = self._get_foreground_hwnd()
        threading.Thread(
            target=self._run_test,
            args=(target_hwnd,),
            daemon=True,
            name="ocr-test-worker",
        ).start()

    # ------------------------------------------------------------------ #
    # Internal pipeline
    # ------------------------------------------------------------------ #

    def _run(self, target_hwnd: int | None) -> None:
        try:
            self._pipeline(target_hwnd)
        finally:
            with self._lock:
                self._busy = False

    def _run_test(self, target_hwnd: int | None) -> None:
        try:
            self._pipeline(target_hwnd, prompt=_OCR_TEST_PROMPT, max_tokens=1024)
        finally:
            with self._lock:
                self._busy = False

    def _pipeline(self, target_hwnd: int | None, prompt: str | None = None, max_tokens: int = 512) -> None:
        from ..auto_answer.vision_pipeline import (
            beep_result,
            beep_start,
            capture_screenshot,
        )

        log.info("[ocr_typist] Hotkey triggered — starting pipeline")

        # 1. Capture screenshot
        b64, orig_w, orig_h, res_w, res_h = capture_screenshot(
            self._config, "ocr_typist"
        )
        if not b64:
            log.error("[ocr_typist] Screenshot capture failed")
            return

        log.info("[ocr_typist] Screenshot %dx%d → %dx%d", orig_w, orig_h, res_w, res_h)

        # 2. Send to Vision AI — collect raw text (no JSON parsing)
        # Use provided prompt or fall back to config/default
        if prompt is None:
            prompt = str(self._config.get("ocr_typist.prompt", "") or _DEFAULT_PROMPT)
        answer_text = self._call_vision(b64, prompt, max_tokens=max_tokens)

        if not answer_text or not answer_text.strip():
            log.warning("[ocr_typist] Vision AI returned empty response")
            return

        log.info("[ocr_typist] AI answer (%d chars): %r", len(answer_text), answer_text[:120])

        # 4. Show answer in the Ctrl+F1 answer window (never type into active window)
        if self._answer_window:
            self._answer_window.show_text(answer_text.strip(), timeout=20)
            log.info("[ocr_typist] Done — answer shown in answer window")
        else:
            log.warning("[ocr_typist] No answer window set — answer not shown")

    def _call_vision(self, img_b64: str, prompt: str, max_tokens: int = 512) -> str:
        """Send screenshot to Vision AI with provider fallback. Returns raw text."""
        from ..ai.vision_adapter import VisionAdapter

        timeout = float(self._config.get("ocr_typist.ai_timeout", 45.0))
        adapter = VisionAdapter(self._config, timeout=timeout)

        # Build provider fallback chain: aliyun/deepseek first (have embedded keys),
        # then current provider, then any others. Skip ollama (not installed on exam PC).
        current = self._config.vision_provider
        available = adapter.get_available_providers()
        preferred = ["aliyun", "deepseek"]
        seen: set = set()
        chain = []
        for p in [current] + preferred + available:
            if p not in seen and p != "ollama":
                seen.add(p)
                chain.append(p)

        for provider in chain:
            original = self._config.vision_provider
            if provider != original:
                log.info("[ocr_typist] Trying fallback provider: %s", provider)
                self._config._data.setdefault("vision", {})["provider"] = provider

            raw = ""
            try:
                cancel = threading.Event()
                for chunk in adapter.analyze(
                    img_b64,
                    prompt,
                    cancel_event=cancel,
                    temperature=0.3,
                    max_tokens=max_tokens,
                ):
                    raw += chunk
            except Exception as exc:
                log.warning("[ocr_typist] Provider '%s' failed: %s — trying next", provider, exc)
                self._config._data.setdefault("vision", {})["provider"] = original
                continue
            finally:
                self._config._data.setdefault("vision", {})["provider"] = original

            if "[Vision error:" in raw:
                log.warning("[ocr_typist] Provider '%s' error in response — trying next", provider)
                continue

            if raw.strip():
                return raw

        log.error("[ocr_typist] All providers failed")
        return ""

    # ------------------------------------------------------------------ #
    # Helpers
    # ------------------------------------------------------------------ #

    @staticmethod
    def _get_foreground_hwnd() -> int | None:
        if sys.platform != "win32":
            return None
        try:
            import ctypes
            import ctypes.wintypes
            # BUG FIX: must declare restype as HWND (c_void_p) so the full
            # 64-bit handle is returned; without it ctypes defaults to c_int
            # which silently truncates the upper 32 bits on 64-bit Windows.
            gfw = ctypes.windll.user32.GetForegroundWindow  # type: ignore[attr-defined]
            gfw.restype = ctypes.wintypes.HWND
            gfw.argtypes = []
            return gfw()
        except Exception:
            return None

    @staticmethod
    def _refocus(hwnd: int) -> None:
        if sys.platform != "win32":
            return
        try:
            import ctypes
            import ctypes.wintypes
            # BUG FIX: must declare argtypes so the HWND is passed as a
            # full 64-bit pointer, not truncated to 32-bit c_int.
            sfw = ctypes.windll.user32.SetForegroundWindow  # type: ignore[attr-defined]
            sfw.restype = ctypes.wintypes.BOOL
            sfw.argtypes = [ctypes.wintypes.HWND]
            sfw(hwnd)
        except Exception:
            pass
