"""Transparent desktop shell for the coco Web GUI."""

from __future__ import annotations

import json
import functools
import ctypes
import os
from ctypes import wintypes
import sys
import threading
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

from PyQt6.QtCore import QObject, QPoint, QTimer, Qt, QUrl, pyqtSignal, pyqtSlot
from PyQt6.QtGui import QColor, QDesktopServices, QIcon, QKeySequence, QShortcut
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtWebEngineCore import QWebEnginePage, QWebEngineSettings
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
    QApplication,
    QFrame,
    QGraphicsDropShadowEffect,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QVBoxLayout,
)

from .runtime import load_or_create_bridge_token, project_root, resource_root


DRAG_HOOK_SCRIPT = r"""
(() => {
    if (window.__cocoDesktopHook) return;
    window.__cocoDesktopHook = true;

    function installDesktopHook() {
        const dragRegion = document.querySelector('.system');
        if (!dragRegion || !window.qt || !qt.webChannelTransport || !window.QWebChannel) return;

        new QWebChannel(qt.webChannelTransport, channel => {
            const desktop = channel.objects.cocoDesktop;
            if (!desktop) return;

            document.documentElement.style.overflow = 'hidden';
            document.body.style.overflow = 'hidden';

            let resizeQueued = false;
            const resizeToContent = () => {
                resizeQueued = false;
                const nodes = [
                    document.querySelector('.coco'),
                    document.querySelector('.profile'),
                    document.querySelector('.control'),
                    ...document.querySelectorAll('.cont.active')
                ].filter(Boolean);

                let bottom = 0;
                for (const node of nodes) {
                    const rect = node.getBoundingClientRect();
                    bottom = Math.max(bottom, rect.bottom);
                }

                desktop.resizeToContent(Math.ceil(bottom + 20));
            };
            const queueResize = () => {
                if (resizeQueued) return;
                resizeQueued = true;
                requestAnimationFrame(resizeToContent);
            };

            new ResizeObserver(queueResize).observe(document.body);
            document.querySelectorAll('.coco, .profile, .control, .cont').forEach(node => {
                new ResizeObserver(queueResize).observe(node);
            });
            new MutationObserver(queueResize).observe(document.body, {
                attributes: true,
                childList: true,
                subtree: true,
                attributeFilter: ['class', 'style', 'hidden']
            });
            document.addEventListener('click', queueResize, true);
            window.addEventListener('load', queueResize);
            queueResize();

            dragRegion.style.cursor = 'move';
            dragRegion.addEventListener('pointerdown', event => {
                if (event.button !== undefined && event.button !== 0) return;
                event.preventDefault();

                desktop.startDrag(event.screenX, event.screenY);

                const move = moveEvent => {
                    desktop.dragTo(moveEvent.screenX, moveEvent.screenY);
                };
                const end = () => {
                    desktop.endDrag();
                    window.removeEventListener('pointermove', move, true);
                    window.removeEventListener('pointerup', end, true);
                    window.removeEventListener('pointercancel', end, true);
                };

                window.addEventListener('pointermove', move, true);
                window.addEventListener('pointerup', end, true);
                window.addEventListener('pointercancel', end, true);
            }, true);
        });
    }

    if (window.QWebChannel) {
        installDesktopHook();
        return;
    }

    const script = document.createElement('script');
    script.src = 'qrc:///qtwebchannel/qwebchannel.js';
    script.onload = installDesktopHook;
    document.head.appendChild(script);
})();
"""

VK_CONTROL = 0x11
VK_OEM_3 = 0xC0
OFFICIAL_SITE_URL = "https://coco.mssay.com/"


class StaticHandler(SimpleHTTPRequestHandler):
    bridge_base_url = "http://127.0.0.1:8765"
    bridge_token = ""

    def do_GET(self):
        request_path = self.path.split("?", 1)[0].split("#", 1)[0]
        if request_path in {"", "/", "/index.html"}:
            self._send_index()
            return
        super().do_GET()

    def _send_index(self):
        index_path = Path(self.directory) / "index.html"
        if not index_path.exists():
            self.send_error(404, "index.html not found")
            return

        config = json.dumps({
            "baseUrl": self.bridge_base_url,
            "token": self.bridge_token,
        })
        injection = f"<script>window.__COCO_BRIDGE__ = {config};</script>"
        html = index_path.read_text(encoding="utf-8")
        if "<head>" in html:
            html = html.replace("<head>", f"<head>\n{injection}", 1)
        else:
            html = f"{injection}\n{html}"

        body = html.encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, format: str, *args):  # noqa: A002 - inherited API name
        return


class DesktopBridge(QObject):
    def __init__(self, window: QMainWindow):
        super().__init__(window)
        self._window = window
        self._start_mouse: QPoint | None = None
        self._start_window: QPoint | None = None

    @pyqtSlot(float, float)
    def startDrag(self, screen_x: float, screen_y: float):
        self._start_mouse = QPoint(round(screen_x), round(screen_y))
        self._start_window = self._window.pos()

    @pyqtSlot(float, float)
    def dragTo(self, screen_x: float, screen_y: float):
        if self._start_mouse is None or self._start_window is None:
            return
        current = QPoint(round(screen_x), round(screen_y))
        self._window.move(self._start_window + current - self._start_mouse)

    @pyqtSlot()
    def endDrag(self):
        self._start_mouse = None
        self._start_window = None

    @pyqtSlot(float)
    def resizeToContent(self, height: float):
        target_height = max(1, round(height))
        current = self._window.size()
        if current.height() == target_height:
            return
        self._window.resize(current.width(), target_height)


class cocoWebView(QWebEngineView):
    def contextMenuEvent(self, event):
        self._context_menu = DesktopContextMenu(self)
        self._context_menu.show_at(event.globalPos())
        event.accept()


class DesktopContextAction(QFrame):
    triggered = pyqtSignal()

    def __init__(self, text: str, shortcut: str, parent=None):
        super().__init__(parent)
        self.setObjectName("desktopContextAction")
        self.setCursor(Qt.CursorShape.PointingHandCursor)
        self.setAttribute(Qt.WidgetAttribute.WA_Hover, True)
        self.setFixedHeight(32)
        self.setMinimumWidth(174)

        layout = QHBoxLayout(self)
        layout.setContentsMargins(15, 0, 15, 0)
        layout.setSpacing(18)

        label = QLabel(text, self)
        label.setObjectName("desktopContextActionText")
        label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
        shortcut_label = QLabel(shortcut, self)
        shortcut_label.setObjectName("desktopContextShortcut")
        shortcut_label.setAlignment(
            Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
        )
        shortcut_label.setAttribute(
            Qt.WidgetAttribute.WA_TransparentForMouseEvents,
            True,
        )

        layout.addWidget(label)
        layout.addStretch(1)
        layout.addWidget(shortcut_label)

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton and self.rect().contains(event.pos()):
            self.triggered.emit()
            event.accept()
            return
        super().mouseReleaseEvent(event)


class DesktopContextMenu(QFrame):
    def __init__(self, view: cocoWebView):
        super().__init__(
            view,
            Qt.WindowType.Popup
            | Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.NoDropShadowWindowHint,
        )
        self.view = view
        self.setObjectName("desktopContextMenu")
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)

        layout = QVBoxLayout(self)
        layout.setContentsMargins(10, 0, 10, 20)
        layout.setSpacing(0)

        panel = QFrame(self)
        panel.setObjectName("desktopContextPanel")
        layout.addWidget(panel)

        shadow = QGraphicsDropShadowEffect(panel)
        shadow.setBlurRadius(15)
        shadow.setOffset(0, 5)
        shadow.setColor(QColor(0, 0, 0, 26))
        panel.setGraphicsEffect(shadow)

        self.setStyleSheet("""
            QFrame#desktopContextMenu {
                background: transparent;
                border: none;
            }
            QFrame#desktopContextPanel {
                background: white;
                border: none;
                border-radius: 5px;
            }
            QFrame#desktopContextAction,
            QFrame#firstAction,
            QFrame#lastAction {
                background: transparent;
                border: none;
            }
            QFrame#desktopContextAction:hover,
            QFrame#firstAction:hover,
            QFrame#lastAction:hover {
                background: #f7f7f7;
            }
            QFrame#firstAction:hover {
                border-top-left-radius: 5px;
                border-top-right-radius: 5px;
            }
            QFrame#lastAction:hover {
                border-bottom-left-radius: 5px;
                border-bottom-right-radius: 5px;
            }
            QLabel#desktopContextActionText {
                background: transparent;
                color: #333;
                font-size: 12px;
            }
            QLabel#desktopContextShortcut {
                background: transparent;
                color: #ccc;
                font-size: 12px;
            }
        """)

        panel_layout = QVBoxLayout(panel)
        panel_layout.setContentsMargins(0, 0, 0, 0)
        panel_layout.setSpacing(0)

        window = self.view.window()
        self._add_action(
            panel_layout,
            "重新加载",
            "Ctrl+R",
            self.view.reload,
            "firstAction",
        )
        self._add_action(
            panel_layout,
            "取消置顶" if window.is_always_on_top() else "置顶",
            "Ctrl+T",
            window.toggle_always_on_top,
        )
        self._add_action(
            panel_layout,
            "最小化",
            "Ctrl+~",
            window.toggle_minimized_or_shown,
        )
        self._add_action(
            panel_layout,
            "前往官方",
            "",
            window.open_official_site,
        )
        self._add_action(
            panel_layout,
            "退出",
            "Ctrl+Q",
            window.close,
            "lastAction",
        )

    def show_at(self, global_pos: QPoint):
        self.adjustSize()
        self.move(global_pos - QPoint(10, 0))
        self.show()

    def _add_action(
        self,
        layout: QVBoxLayout,
        text: str,
        shortcut: str,
        callback,
        name: str = "",
    ):
        button = DesktopContextAction(text, shortcut, self)
        if name:
            button.setObjectName(name)
        button.triggered.connect(lambda: self._trigger(callback))
        layout.addWidget(button)

    def _trigger(self, callback):
        self.close()
        QTimer.singleShot(30, callback)


class cocoDesktopWindow(QMainWindow):
    def __init__(self, url: str, icon: QIcon, width: int = 540, height: int = 700):
        super().__init__()
        self.setWindowTitle("coco")
        self.setWindowIcon(icon)
        self.resize(width, height)
        self.setWindowFlags(
            Qt.WindowType.Window
            | Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.WindowMinimizeButtonHint
        )
        self._always_on_top = False
        self._shortcuts: list[QShortcut] = []
        self._get_async_key_state = None
        self._visibility_hotkey_down = False
        self._visibility_hotkey_timer: QTimer | None = None
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)

        self.view = cocoWebView(self)
        self.view.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
        self.view.setStyleSheet("background: transparent;")
        self.view.page().setBackgroundColor(QColor(0, 0, 0, 0))
        self.view.settings().setAttribute(
            QWebEngineSettings.WebAttribute.PlaybackRequiresUserGesture,
            False,
        )
        self.view.page().featurePermissionRequested.connect(self._handle_feature_permission)

        self.channel = QWebChannel(self.view.page())
        self.bridge = DesktopBridge(self)
        self.channel.registerObject("cocoDesktop", self.bridge)
        self.view.page().setWebChannel(self.channel)
        self.view.loadFinished.connect(self._inject_desktop_hooks)
        self.view.setUrl(QUrl(url))

        self.setCentralWidget(self.view)
        self._install_shortcuts()
        self._install_visibility_hotkey_poll()

    def _inject_desktop_hooks(self, ok: bool):
        if ok:
            self.view.page().runJavaScript(DRAG_HOOK_SCRIPT)

    def _handle_feature_permission(self, security_origin: QUrl, feature: QWebEnginePage.Feature):
        allowed = {
            QWebEnginePage.Feature.MediaAudioCapture,
            QWebEnginePage.Feature.MediaAudioVideoCapture,
        }
        policy = (
            QWebEnginePage.PermissionPolicy.PermissionGrantedByUser
            if feature in allowed
            else QWebEnginePage.PermissionPolicy.PermissionDeniedByUser
        )
        self.view.page().setFeaturePermission(security_origin, feature, policy)

    def is_always_on_top(self) -> bool:
        return self._always_on_top

    def toggle_minimized_or_shown(self):
        if self.isMinimized() or not self.isVisible():
            self.showNormal()
            self.raise_()
            self.activateWindow()
            return

        self.showMinimized()

    def open_official_site(self):
        QDesktopServices.openUrl(QUrl(OFFICIAL_SITE_URL))

    def toggle_always_on_top(self):
        self._always_on_top = not self._always_on_top
        if self._apply_windows_topmost(self._always_on_top):
            return

        flags = self.windowFlags()
        if self._always_on_top:
            flags |= Qt.WindowType.WindowStaysOnTopHint
        else:
            flags &= ~Qt.WindowType.WindowStaysOnTopHint
        self.setWindowFlags(flags)
        self.show()

    def _apply_windows_topmost(self, enabled: bool) -> bool:
        if sys.platform != "win32":
            return False

        hwnd = int(self.winId())
        set_window_pos = ctypes.windll.user32.SetWindowPos
        set_window_pos.argtypes = [
            wintypes.HWND,
            wintypes.HWND,
            ctypes.c_int,
            ctypes.c_int,
            ctypes.c_int,
            ctypes.c_int,
            ctypes.c_uint,
        ]
        set_window_pos.restype = wintypes.BOOL

        hwnd_insert_after = wintypes.HWND(-1 if enabled else -2)  # HWND_TOPMOST / HWND_NOTOPMOST
        flags = 0x0001 | 0x0002 | 0x0010  # NOMOVE, NOSIZE, NOACTIVATE
        return bool(set_window_pos(
            wintypes.HWND(hwnd),
            hwnd_insert_after,
            0,
            0,
            0,
            0,
            flags,
        ))

    def _install_shortcuts(self):
        self._add_shortcut("Ctrl+R", self.view.reload)
        self._add_shortcut("Ctrl+T", self.toggle_always_on_top)
        self._add_shortcut("Ctrl+Q", self.close)
        if sys.platform != "win32":
            self._add_shortcut("Ctrl+~", self.toggle_minimized_or_shown)
            self._add_shortcut(
                QKeySequence(
                    Qt.KeyboardModifier.ControlModifier | Qt.Key.Key_QuoteLeft
                ),
                self.toggle_minimized_or_shown,
            )

    def _add_shortcut(self, sequence: str | QKeySequence, callback):
        shortcut = QShortcut(QKeySequence(sequence), self)
        shortcut.setContext(Qt.ShortcutContext.ApplicationShortcut)
        shortcut.activated.connect(callback)
        self._shortcuts.append(shortcut)

    def _install_visibility_hotkey_poll(self):
        if sys.platform != "win32":
            return

        self._get_async_key_state = ctypes.windll.user32.GetAsyncKeyState
        self._get_async_key_state.argtypes = [ctypes.c_int]
        self._get_async_key_state.restype = ctypes.c_short
        self._visibility_hotkey_timer = QTimer(self)
        self._visibility_hotkey_timer.setInterval(50)
        self._visibility_hotkey_timer.timeout.connect(self._poll_visibility_hotkey)
        self._visibility_hotkey_timer.start()

    def _is_key_down(self, virtual_key: int) -> bool:
        if self._get_async_key_state is None:
            return False
        return bool(self._get_async_key_state(virtual_key) & 0x8000)

    def _poll_visibility_hotkey(self):
        pressed = self._is_key_down(VK_CONTROL) and self._is_key_down(VK_OEM_3)
        if pressed and not self._visibility_hotkey_down:
            self._visibility_hotkey_down = True
            self.toggle_minimized_or_shown()
            return

        if not pressed:
            self._visibility_hotkey_down = False

    def closeEvent(self, event):
        if self._visibility_hotkey_timer is not None:
            self._visibility_hotkey_timer.stop()
        super().closeEvent(event)


def _set_windows_app_id():
    if sys.platform != "win32":
        return
    try:
        ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
            "mixice.coco.desktop"
        )
    except Exception:
        pass


def _start_static_server(
    gui_dir: Path,
    bridge_base_url: str,
    bridge_token: str,
) -> tuple[ThreadingHTTPServer, str]:
    class Handler(StaticHandler):
        pass

    Handler.bridge_base_url = bridge_base_url
    Handler.bridge_token = bridge_token
    handler = functools.partial(Handler, directory=str(gui_dir))
    server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
    host, port = server.server_address
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    return server, f"http://{host}:{port}/index.html"


def _gui_dir() -> Path:
    candidates = [
        project_root() / "gui",
        resource_root() / "gui",
        Path(__file__).resolve().parent.parent / "gui",
    ]
    for candidate in candidates:
        if (candidate / "index.html").exists():
            return candidate
    return candidates[0]


def run_desktop(width: int = 540, height: int = 700):
    _set_windows_app_id()
    gui_dir = _gui_dir()
    bridge_token = load_or_create_bridge_token()
    os.environ.setdefault("coco_bridge_token", bridge_token)
    bridge_base_url = os.environ.get("coco_bridge_url", "http://127.0.0.1:8765").rstrip("/")
    icon_paths = [
        gui_dir / "images" / "ico.ico",
        gui_dir / "images" / "ico.png",
    ]
    server, url = _start_static_server(gui_dir, bridge_base_url, bridge_token)
    app = QApplication.instance() or QApplication(sys.argv)
    icon = next(
        (candidate for candidate in (QIcon(str(path)) for path in icon_paths) if not candidate.isNull()),
        QIcon(),
    )
    app.setWindowIcon(icon)
    window = cocoDesktopWindow(url, icon=icon, width=width, height=height)
    window.show()

    try:
        return app.exec()
    finally:
        server.shutdown()
        server.server_close()
