"""Windows AI response overlay — PyQt6 frameless window.

Floats above all other windows. Shows streaming AI responses.
Supports drag-to-move, insert, copy, cancel and close actions.
"""

from __future__ import annotations

import ctypes
import logging
import sys
from collections.abc import Callable

from PyQt6.QtCore import QPoint, QRectF, Qt, QTimer, pyqtSignal
from PyQt6.QtGui import QBrush, QColor, QFont, QPainter, QPainterPath, QPen
from PyQt6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QPushButton,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)

from ..i18n import _

log = logging.getLogger(__name__)

_STYLE_DARK = """
QWidget#overlay {
    background-color: transparent;
}
QTextEdit {
    background-color: transparent;
    color: #e0e0e0;
    border: none;
    font-size: 13px;
    padding: 4px;
}
QPushButton {
    background-color: #444;
    color: #ddd;
    border: 1px solid #666;
    border-radius: 4px;
    padding: 4px 10px;
    font-size: 11px;
}
QPushButton:hover {
    background-color: #555;
}
QPushButton:disabled {
    background-color: #333;
    color: #777;
}
QPushButton#cancel_btn {
    background-color: #6b2d2d;
    color: #faa;
    border-color: #933;
}
QPushButton#cancel_btn:hover {
    background-color: #7d3333;
}
QLabel#header {
    color: #888;
    font-size: 11px;
    padding: 2px 4px;
}
QLabel#streaming_indicator {
    color: #5baaff;
    font-size: 11px;
    padding: 2px 4px;
}
"""

_STYLE_LIGHT = """
QWidget#overlay {
    background-color: transparent;
}
QTextEdit {
    background-color: transparent;
    color: #1a1a1a;
    border: none;
    font-size: 13px;
    padding: 4px;
}
QPushButton {
    background-color: #e0e0e0;
    color: #1a1a1a;
    border: 1px solid #aaa;
    border-radius: 4px;
    padding: 4px 10px;
    font-size: 11px;
}
QPushButton:hover {
    background-color: #d0d0d0;
}
QPushButton:disabled {
    background-color: #ececec;
    color: #999;
}
QPushButton#cancel_btn {
    background-color: #f0d0d0;
    color: #c00;
    border-color: #d08080;
}
QPushButton#cancel_btn:hover {
    background-color: #e0c0c0;
}
QLabel#header {
    color: #555;
    font-size: 11px;
    padding: 2px 4px;
}
QLabel#streaming_indicator {
    color: #0060cc;
    font-size: 11px;
    padding: 2px 4px;
}
"""

# Default (backward compat)
_STYLE = _STYLE_DARK


class OverlayWindow(QWidget):
    """Floating overlay that displays streaming AI output."""

    # Thread-safe signals for updates from non-Qt threads
    append_signal = pyqtSignal(str)
    show_signal = pyqtSignal(str)
    hide_signal = pyqtSignal()
    set_streaming_signal = pyqtSignal(bool)
    apply_theme_signal = pyqtSignal(str)

    def __init__(
        self,
        on_insert: Callable[[str], None] | None = None,
        on_cancel: Callable[[], None] | None = None,
    ) -> None:
        super().__init__()
        self._on_insert = on_insert
        self._on_cancel = on_cancel
        self._drag_pos: QPoint | None = None
        self._full_text = ""
        self._dot_count = 0

        self._current_theme = "dark"
        self._setup_ui()
        self._connect_signals()

        # Dot animation timer — fires every 500 ms while streaming
        self._dot_timer = QTimer(self)
        self._dot_timer.timeout.connect(self._animate_indicator)

        # Z-order re-assertion timer — keeps overlay above topmost exam windows
        # 50ms for aggressive CSCA kiosk competition (was 100ms)
        self._topmost_timer = QTimer(self)
        self._topmost_timer.timeout.connect(self._assert_topmost)
        self._topmost_timer.start(50)

    def _setup_ui(self) -> None:
        self.setObjectName("overlay")
        self.setWindowFlags(
            Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.WindowStaysOnTopHint
            | Qt.WindowType.Tool
            | Qt.WindowType.WindowDoesNotAcceptFocus  # WS_EX_NOACTIVATE equivalent
        )
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
        self.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating)
        initial_style = _STYLE_DARK
        try:
            from .theme_manager import ThemeManager

            self._current_theme = ThemeManager.get_instance().get_current_theme()
            initial_style = _STYLE_DARK if self._current_theme == "dark" else _STYLE_LIGHT
        except (RuntimeError, ImportError):
            pass
        self.setStyleSheet(initial_style)
        self.setMinimumWidth(360)
        self.setMaximumWidth(560)
        self.setMinimumHeight(80)
        self.setMaximumHeight(440)

        layout = QVBoxLayout(self)
        layout.setContentsMargins(8, 6, 8, 6)
        layout.setSpacing(4)

        # Header bar with streaming indicator
        header_row = QHBoxLayout()
        self._header = QLabel(_("overlay.header"))
        self._header.setObjectName("header")
        header_row.addWidget(self._header)

        self._streaming_label = QLabel(_("overlay.streaming"))
        self._streaming_label.setObjectName("streaming_indicator")
        self._streaming_label.hide()
        header_row.addWidget(self._streaming_label)
        header_row.addStretch()
        layout.addLayout(header_row)

        # Response text area
        self._text_edit = QTextEdit()
        self._text_edit.setReadOnly(True)
        self._text_edit.setFont(QFont("Segoe UI", 11))
        layout.addWidget(self._text_edit)

        # Action buttons
        btn_row = QHBoxLayout()
        btn_row.setSpacing(6)

        self._btn_insert = QPushButton(_("overlay.btn_insert"))
        self._btn_insert.clicked.connect(self._on_insert_clicked)

        self._btn_copy = QPushButton(_("overlay.btn_copy"))
        self._btn_copy.clicked.connect(self._on_copy_clicked)

        self._btn_cancel = QPushButton(_("overlay.btn_cancel"))
        self._btn_cancel.setObjectName("cancel_btn")
        self._btn_cancel.setToolTip(_("overlay.tooltip_cancel"))
        self._btn_cancel.clicked.connect(self._on_cancel_clicked)
        self._btn_cancel.hide()

        self._btn_close = QPushButton("✕")
        self._btn_close.setMaximumWidth(32)
        self._btn_close.clicked.connect(self.hide)

        btn_row.addWidget(self._btn_insert)
        btn_row.addWidget(self._btn_copy)
        btn_row.addWidget(self._btn_cancel)
        btn_row.addStretch()
        btn_row.addWidget(self._btn_close)
        layout.addLayout(btn_row)

    def _connect_signals(self) -> None:
        self.append_signal.connect(self._append_text)
        self.show_signal.connect(self._show_with_text)
        self.hide_signal.connect(self.hide)
        self.set_streaming_signal.connect(self._set_streaming)
        self.apply_theme_signal.connect(self._do_apply_theme)

    def apply_theme(self, theme: str) -> None:
        """Thread-safe: switch the overlay's colour scheme. theme: 'light' | 'dark'."""
        self.apply_theme_signal.emit(theme)

    def _do_apply_theme(self, theme: str) -> None:
        """Apply overlay QSS for the given theme (Qt main thread only)."""
        self._current_theme = theme
        self.setStyleSheet(_STYLE_DARK if theme == "dark" else _STYLE_LIGHT)
        self.update()

    def retranslate_ui(self) -> None:
        """Update all widget texts to the current locale."""
        self._header.setText(_("overlay.header"))
        self._streaming_label.setText(_("overlay.streaming"))
        self._btn_insert.setText(_("overlay.btn_insert"))
        self._btn_copy.setText(_("overlay.btn_copy"))
        self._btn_cancel.setText(_("overlay.btn_cancel"))
        self._btn_cancel.setToolTip(_("overlay.tooltip_cancel"))

    # ------------------------------------------------------------------ #
    # Public API (thread-safe via signals)
    # ------------------------------------------------------------------ #

    def show_response(self, initial_text: str = "") -> None:
        """Show the overlay, optionally with initial text."""
        self.show_signal.emit(initial_text)

    def append_chunk(self, chunk: str) -> None:
        """Append a streaming chunk. Thread-safe."""
        self.append_signal.emit(chunk)

    def hide_overlay(self) -> None:
        """Hide the overlay. Thread-safe."""
        self.hide_signal.emit()

    def get_text(self) -> str:
        """Return the full accumulated response text."""
        return self._full_text

    # ------------------------------------------------------------------ #
    # Slot implementations (Qt thread only)
    # ------------------------------------------------------------------ #

    def _show_with_text(self, text: str) -> None:
        self._full_text = text
        self._text_edit.setPlainText(text)
        self._position_bottom_right()
        self.show()
        self.raise_()

    def _append_text(self, chunk: str) -> None:
        self._full_text += chunk
        cursor = self._text_edit.textCursor()
        cursor.movePosition(cursor.MoveOperation.End)
        self._text_edit.setTextCursor(cursor)
        self._text_edit.insertPlainText(chunk)
        # Auto-scroll to bottom
        scrollbar = self._text_edit.verticalScrollBar()
        if scrollbar is not None:
            scrollbar.setValue(scrollbar.maximum())
        self.adjustSize()

    def _set_streaming(self, active: bool) -> None:
        """Show/hide streaming indicator and cancel button."""
        self._btn_cancel.setVisible(active)
        self._streaming_label.setVisible(active)
        self._btn_insert.setEnabled(not active)
        if active:
            self._dot_count = 0
            self._dot_timer.start(500)
        else:
            self._dot_timer.stop()
            self._streaming_label.hide()

    def _animate_indicator(self) -> None:
        """Cycle the streaming indicator dots."""
        self._dot_count = (self._dot_count + 1) % 4
        dots = "." * self._dot_count
        self._streaming_label.setText(_("overlay.streaming_dots", dots=dots))

    def _on_insert_clicked(self) -> None:
        if self._on_insert and self._full_text:
            try:
                self._on_insert(self._full_text)
            except Exception as exc:
                log.error("Insert callback error: %s", exc)

    def _on_copy_clicked(self) -> None:
        if self._full_text:
            cb = QApplication.clipboard()
            if cb is not None:
                cb.setText(self._full_text)

    def _on_cancel_clicked(self) -> None:
        log.debug("Cancel button clicked")
        if self._on_cancel:
            try:
                self._on_cancel()
            except Exception as exc:
                log.error("Cancel callback error: %s", exc)
        # Immediately hide the cancel UI elements
        self._set_streaming(False)

    def _position_bottom_right(self) -> None:
        """Place the overlay in the bottom-right corner of the primary screen."""
        screen = QApplication.primaryScreen()
        if screen is None:
            return
        geom = screen.availableGeometry()
        w, h = self.width() or 400, self.height() or 200
        x = geom.right() - w - 20
        y = geom.bottom() - h - 60
        self.move(x, y)

    def paintEvent(self, event) -> None:
        """Draw rounded rectangle background to avoid square border artifact."""
        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        rect = QRectF(0.5, 0.5, self.width() - 1, self.height() - 1)
        path = QPainterPath()
        path.addRoundedRect(rect, 8, 8)
        if self._current_theme == "dark":
            painter.fillPath(path, QBrush(QColor(30, 30, 30, 240)))
            painter.setPen(QPen(QColor(85, 85, 85), 1))
        else:
            painter.fillPath(path, QBrush(QColor(240, 240, 240, 240)))
            painter.setPen(QPen(QColor(187, 187, 187), 1))
        painter.drawPath(path)
        painter.end()

    def showEvent(self, event) -> None:
        """Apply CSCA stealth on every show."""
        super().showEvent(event)
        self._apply_csca_stealth()
        self._register_with_cbt_hook()

    # ------------------------------------------------------------------ #
    # Drag to move
    # ------------------------------------------------------------------ #

    def mousePressEvent(self, event) -> None:
        if event.button() == Qt.MouseButton.LeftButton:
            self._drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft()

    def mouseMoveEvent(self, event) -> None:
        if self._drag_pos is not None and event.buttons() == Qt.MouseButton.LeftButton:
            self.move(event.globalPosition().toPoint() - self._drag_pos)

    def mouseReleaseEvent(self, event) -> None:
        self._drag_pos = None

    # ------------------------------------------------------------------ #
    # Z-order maintenance (stealth compatibility)
    # ------------------------------------------------------------------ #

    _HWND_TOPMOST = -1
    _SWP_NOMOVE = 0x0002
    _SWP_NOSIZE = 0x0001
    _SWP_NOACTIVATE = 0x0010
    # SetWindowDisplayAffinity: excludes window from screen captures (Windows 10 2004+)
    _WDA_EXCLUDEFROMCAPTURE = 0x00000011
    # Window extended styles for hiding from taskbar/Alt+Tab
    _GWL_EXSTYLE = -20
    _WS_EX_TOOLWINDOW = 0x00000080
    _WS_EX_NOACTIVATE = 0x08000000
    _WS_EX_LAYERED = 0x00080000

    def _apply_csca_stealth(self) -> None:
        """Apply CSCA stealth properties to the window (Windows only).

        1. SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE) — window invisible
           to CSCA/SEB screen capture tools (screenshots show black instead).
        2. WS_EX_TOOLWINDOW — hide from Alt+Tab and taskbar.
        3. WS_EX_NOACTIVATE — never steal input focus.
        """
        if sys.platform != "win32":
            return
        try:
            hwnd = int(self.winId())
            user32 = ctypes.windll.user32  # type: ignore[attr-defined]
            # Exclude from screen capture
            user32.SetWindowDisplayAffinity(hwnd, self._WDA_EXCLUDEFROMCAPTURE)
            # Hide from Alt+Tab + taskbar (WS_EX_TOOLWINDOW) + no focus
            ex_style = self._WS_EX_TOOLWINDOW | self._WS_EX_NOACTIVATE | self._WS_EX_LAYERED
            user32.SetWindowLongW(hwnd, self._GWL_EXSTYLE, ex_style)
            # Clear window title — harder to find via FindWindow/EnumWindows
            user32.SetWindowTextW(hwnd, "")
            # DWM cloak — hide from EnumWindows/FindWindow programmatic enumeration
            try:
                from ..seb.window_cloak import cloak_window
                cloak_window(hwnd)
            except Exception:
                pass
        except (OSError, TypeError, ValueError, AttributeError):
            pass

    def _assert_topmost(self) -> None:
        """Re-assert window level every 100ms to stay above exam windows.

        On macOS sets NSWindowLevel to 1100 (above SEB kiosk at ~1000).
        On Windows re-asserts HWND_TOPMOST so we stay above exam windows.
        Also re-applies CSCA stealth properties (screen capture exclusion).
        """
        if not self.isVisible():
            return
        if sys.platform == "darwin":
            self._assert_topmost_macos()
        else:
            try:
                hwnd = int(self.winId())
                user32 = ctypes.windll.user32  # type: ignore[attr-defined]
                user32.SetWindowPos(
                    hwnd,
                    self._HWND_TOPMOST,
                    0,
                    0,
                    0,
                    0,
                    self._SWP_NOMOVE | self._SWP_NOSIZE | self._SWP_NOACTIVATE,
                )
                # Re-apply screen capture exclusion (CSCA may reset it)
                user32.SetWindowDisplayAffinity(hwnd, self._WDA_EXCLUDEFROMCAPTURE)
            except (OSError, TypeError, ValueError, AttributeError):
                pass

    def _register_with_cbt_hook(self) -> None:
        """Register this window with the CBT hook system for CSCA protection."""
        if sys.platform != "win32":
            return
        try:
            from ..seb.window_cloak import register_protected_hwnd, install_cbt_hook
            hwnd = int(self.winId())
            register_protected_hwnd(hwnd)
            install_cbt_hook()
        except Exception:
            pass

    def _assert_topmost_macos(self) -> None:
        """Set NSWindowLevel to 1100 — above SEB kiosk level (~1000)."""
        try:
            import ctypes.util

            objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("objc"))
            objc.objc_getClass.restype = ctypes.c_void_p
            objc.sel_registerName.restype = ctypes.c_void_p
            objc.objc_msgSend.restype = ctypes.c_void_p
            objc.objc_msgSend.argtypes = [ctypes.c_void_p, ctypes.c_void_p]

            win_id = int(self.winId())
            nsview = ctypes.c_void_p(win_id)
            sel_window = objc.sel_registerName(b"window")
            nswindow = objc.objc_msgSend(nsview, sel_window)
            if nswindow:
                sel_set_level = objc.sel_registerName(b"setLevel:")
                objc.objc_msgSend.argtypes = [
                    ctypes.c_void_p,
                    ctypes.c_void_p,
                    ctypes.c_long,
                ]
                objc.objc_msgSend(nswindow, sel_set_level, 1100)
        except Exception:
            pass
