"""
Core WebView class for rendering forms.

This module contains the main WebView class that manages the WebView2
lifecycle and handles communication with the frontend.
"""

import ctypes
import json
import os
from ctypes import wintypes
from pathlib import Path
import sys
from typing import Any, Callable, Dict, Optional

if sys.version_info >= (3, 8):
    from typing import Literal
else:
    from typing_extensions import Literal

import win32gui

from .._internal.logging import debug_log
from ..exceptions import WebViewError, _InternalCancelSignal, _InternalExitSignal
from ..resources import get_resource_path
from .handlers import (
    ControllerCompletedHandler,
    EnvironmentCompletedHandler,
    ProcessFailedHandler,
    WebMessageReceivedHandler,
)
from .interfaces import ICoreWebView2, ICoreWebView2Controller, ICoreWebView2Environment
from .loader import create_environment_with_options

_MODULE = "webview"


class WebView:
    """
    WebView2 wrapper for rendering JSON Schema forms.

    This class manages the lifecycle of a WebView2 instance, including:
    - Environment and controller creation
    - Event handling (web messages, process failures)
    - Communication with the frontend

    Attributes:
        result: The form data returned when user confirms, or None if not yet submitted.
        schema: The JSON Schema configuration for the form.
        debug: Whether debug mode is enabled.
        theme: The color theme ("light" or "dark").
    """

    def __init__(
        self,
        schema: Optional[Dict[str, Any]] = None,
        debug: bool = False,
        theme: Literal["light", "dark"] = "light",
    ):
        """
        Initialize a new WebView instance.

        Args:
            schema: The JSON Schema configuration for the form.
            debug: If True, enables DevTools and context menu.
            theme: Color theme, either "light" or "dark".
        """
        self.hwnd: Optional[int] = None
        # COM interface pointers (typed at runtime via ctypes.cast)
        self.env: Optional[Any] = None  # ICoreWebView2Environment pointer
        self.controller: Optional[Any] = None  # ICoreWebView2Controller pointer
        self.core_webview: Optional[Any] = None  # ICoreWebView2 pointer
        self.result: Optional[Dict[str, Any]] = None
        self.schema = schema
        self.debug = debug
        self.theme = theme
        self._schema_sent = False
        self._context_menu_blocker_added = False

        # Callbacks for window management
        self._on_ready_callback: Optional[Callable[[], None]] = None
        self._on_cancel_callback: Optional[Callable[[], None]] = None
        self._on_confirm_callback: Optional[Callable[[Dict[str, Any]], None]] = None
        self._on_start_drag_callback: Optional[Callable[[], None]] = None

    def set_callbacks(
        self,
        on_ready: Optional[Callable[[], None]] = None,
        on_cancel: Optional[Callable[[], None]] = None,
        on_confirm: Optional[Callable[[Dict[str, Any]], None]] = None,
        on_start_drag: Optional[Callable[[], None]] = None,
    ) -> None:
        """
        Set callback functions for various events.

        Args:
            on_ready: Called when frontend signals it's ready.
            on_cancel: Called when user cancels the form.
            on_confirm: Called when user confirms with data.
            on_start_drag: Called when user starts dragging the window.
        """
        self._on_ready_callback = on_ready
        self._on_cancel_callback = on_cancel
        self._on_confirm_callback = on_confirm
        self._on_start_drag_callback = on_start_drag

    def create(self, hwnd: int) -> None:
        """
        Create the WebView2 instance for the given window.

        Args:
            hwnd: The window handle to host the WebView.

        Raises:
            WebViewError: If environment creation fails.
        """
        debug_log("WebView.create() start", _MODULE)
        self.hwnd = hwnd

        handler = EnvironmentCompletedHandler(self)
        hr = create_environment_with_options(handler)

        if hr != 0:
            debug_log(f"CreateCoreWebView2EnvironmentWithOptions failed: {hr}", _MODULE)
            raise WebViewError(
                f"WebView2 environment creation failed with HRESULT: {hr}"
            )

    def _on_env_created(self, env_ptr: Any) -> None:
        """
        Handle environment creation completion.

        Args:
            env_ptr: Raw COM pointer to the created environment.
        """
        self.env = ctypes.cast(env_ptr, ctypes.POINTER(ICoreWebView2Environment))
        debug_log("CoreWebView2 environment created", _MODULE)

        handler = ControllerCompletedHandler(self)
        self.env.CreateCoreWebView2Controller(self.hwnd, handler)

    def _on_controller_created(self, controller_ptr: Any) -> None:
        """
        Handle controller creation completion.

        Args:
            controller_ptr: Raw COM pointer to the created controller.
        """
        self.controller = ctypes.cast(
            controller_ptr, ctypes.POINTER(ICoreWebView2Controller)
        )
        debug_log("CoreWebView2 controller created", _MODULE)

        # Resize to fit window
        rect = wintypes.RECT()
        try:
            ctypes.windll.user32.GetClientRect(self.hwnd, ctypes.byref(rect))
        except Exception as exc:
            debug_log(f"GetClientRect failed: {exc!r}", _MODULE)
            raise

        try:
            self.controller.put_Bounds(rect)
        except Exception as exc:
            debug_log(f"controller.put_Bounds failed: {exc!r}", _MODULE)
            raise

        try:
            self.controller.put_IsVisible(True)
        except Exception as exc:
            debug_log(f"controller.put_IsVisible failed: {exc!r}", _MODULE)
            raise

        # Get CoreWebView2
        try:
            core_ptr = self.controller.get_CoreWebView2()
            self.core_webview = ctypes.cast(core_ptr, ctypes.POINTER(ICoreWebView2))
        except Exception as exc:
            debug_log(f"controller.get_CoreWebView2 failed: {exc!r}", _MODULE)
            raise

        self._configure_settings()
        self._register_events()
        self._load_content()

    def _register_events(self) -> None:
        """Register WebView2 event handlers."""
        # WebMessageReceived
        msg_handler = WebMessageReceivedHandler(self)
        try:
            self.core_webview.add_WebMessageReceived(msg_handler)
        except Exception as exc:
            debug_log(f"add_WebMessageReceived failed: {exc!r}", _MODULE)
            raise

        # ProcessFailed
        fail_handler = ProcessFailedHandler(self)
        try:
            self.core_webview.add_ProcessFailed(fail_handler)
        except Exception as exc:
            debug_log(f"add_ProcessFailed failed: {exc!r}", _MODULE)
            raise

    def _load_content(self) -> None:
        """Load the frontend HTML content."""
        html_path = get_resource_path("index.html")
        if not os.path.exists(html_path):
            debug_log(f"index.html not found at: {html_path}", _MODULE)
            raise _InternalExitSignal(f"Frontend not found: {html_path}")

        # Convert to file URI
        uri = Path(html_path).resolve().as_uri()
        debug_log(f"Navigating to: {uri}", _MODULE)

        try:
            self.core_webview.Navigate(uri)
        except Exception as exc:
            debug_log(f"Navigate failed: {exc!r}", _MODULE)
            raise

    def _configure_settings(self) -> None:
        """Configure WebView2 settings."""
        try:
            settings = self.core_webview.get_Settings()
            # Enable DevTools only in debug mode
            settings.put_AreDevToolsEnabled(self.debug)
            # Try to disable context menu via settings
            try:
                settings.put_AreDefaultContextMenusEnabled(self.debug)
            except Exception:
                pass
            debug_log(f"WebView settings: debug={self.debug}", _MODULE)
        except Exception as exc:
            debug_log(f"configure settings failed: {exc!r}", _MODULE)

        # Inject JavaScript to disable right-click context menu (more reliable)
        if not self.debug:
            self._inject_context_menu_blocker()

    def _inject_context_menu_blocker(self) -> None:
        """Inject JavaScript to block the context menu."""
        if not self.core_webview or self._context_menu_blocker_added:
            return

        script = (
            "(function(){"
            "document.addEventListener('contextmenu', function(e){"
            "e.preventDefault();"
            "}, {capture:true});"
            "})();"
        )

        try:
            self.core_webview.AddScriptToExecuteOnDocumentCreated(script, None)
            self._context_menu_blocker_added = True
            debug_log("Context menu blocker injected on document created", _MODULE)
        except Exception as exc:
            debug_log(f"AddScriptToExecuteOnDocumentCreated failed: {exc!r}", _MODULE)
            # Fallback: try to inject immediately after load
            try:
                self.core_webview.ExecuteScript(script, None)
                self._context_menu_blocker_added = True
                debug_log("Context menu blocker injected via ExecuteScript", _MODULE)
            except Exception as exc2:
                debug_log(f"ExecuteScript failed: {exc2!r}", _MODULE)

    def _post_schema(self) -> None:
        """Send the schema to the frontend."""
        if self._schema_sent:
            return
        if not self.core_webview:
            raise _InternalExitSignal("Cannot post schema: WebView not initialized")

        payload = self.schema if isinstance(self.schema, dict) else None
        # Include theme in the message
        message = {"type": "schema", "payload": payload, "theme": self.theme}

        try:
            self.core_webview.PostWebMessageAsJson(json.dumps(message))
            self._schema_sent = True
        except Exception as exc:
            debug_log(f"PostWebMessageAsJson failed: {exc!r}", _MODULE)
            raise _InternalExitSignal(f"Failed to send schema: {exc}")

    def _extract_json_string(self, raw_value: Any) -> Optional[str]:
        """
        Extract string from various WebView2 return formats.

        WebView2 COM methods may return values in different formats depending
        on the binding layer. This method normalizes them to a plain string.

        Args:
            raw_value: The raw return value from WebView2 COM call.

        Returns:
            Extracted string, or None if extraction failed.
        """
        if raw_value is None:
            return None
        # Some COM bindings return tuples (value, HRESULT)
        if isinstance(raw_value, tuple):
            raw_value = raw_value[0]
        # comtypes may wrap strings in a special object with .value attribute
        if hasattr(raw_value, "value"):
            raw_value = raw_value.value
        return str(raw_value) if raw_value is not None else None

    def _parse_web_message(self, json_str: str) -> Optional[Dict[str, Any]]:
        """
        Parse JSON string, handling double-encoded strings.

        Some WebView2 versions may double-encode JSON strings, so we need
        to handle that case gracefully.

        Args:
            json_str: The JSON string to parse.

        Returns:
            Parsed dictionary, or None if parsing failed or result is not a dict.
        """
        try:
            data = json.loads(json_str)
            # Handle double-encoded JSON from some WebView2 versions
            if isinstance(data, str):
                data = json.loads(data)
            return data if isinstance(data, dict) else None
        except json.JSONDecodeError as exc:
            debug_log(f"WebMessage parse failed: {exc!r}; raw={json_str!r}", _MODULE)
            return None

    def _on_web_message(self, args: Any) -> None:
        """
        Handle web messages from the frontend.

        Args:
            args: WebMessageReceivedEventArgs COM object.
        """
        try:
            raw = args.get_WebMessageAsJson()
        except Exception as exc:
            debug_log(f"get_WebMessageAsJson failed: {exc!r}", _MODULE)
            return

        json_str = self._extract_json_string(raw)
        if json_str is None:
            debug_log("WebMessage JSON is None", _MODULE)
            return

        data = self._parse_web_message(json_str)
        if data is None:
            return

        msg_type = data.get("type")

        if msg_type == "ready":
            self._post_schema()
            # Notify that frontend is ready
            if self._on_ready_callback:
                self._on_ready_callback()

        elif msg_type == "confirm":
            self.result = data.get("payload")
            # Notify confirmation
            if self._on_confirm_callback:
                self._on_confirm_callback(self.result)
            # Close window and exit message loop
            if self.hwnd:
                try:
                    win32gui.DestroyWindow(self.hwnd)
                except Exception:
                    pass
            ctypes.windll.user32.PostQuitMessage(0)

        elif msg_type == "cancel":
            # Signal cancellation
            if self._on_cancel_callback:
                self._on_cancel_callback()
            # Close window and exit message loop (same as confirm)
            if self.hwnd:
                try:
                    win32gui.DestroyWindow(self.hwnd)
                except Exception:
                    pass
            ctypes.windll.user32.PostQuitMessage(0)
            raise _InternalCancelSignal("User cancelled the form")

        elif msg_type == "startDrag":
            # Start window drag for frameless windows
            if self._on_start_drag_callback:
                self._on_start_drag_callback()

    def resize(self, width: int, height: int) -> None:
        """
        Resize the WebView to match the window size.

        Args:
            width: New width in pixels.
            height: New height in pixels.
        """
        if self.controller:
            rect = wintypes.RECT(0, 0, width, height)
            self.controller.put_Bounds(rect)
