"""Main PySide6 window emulating the legacy Tk interface with translations and themes."""

from __future__ import annotations
import re
import time
import urllib.request
import urllib.error
import csv
import concurrent.futures
from pathlib import Path
from typing import Callable, Dict, List, Tuple

from PIL import Image
from PySide6 import QtCore, QtGui, QtWidgets

from app.i18n import I18nMixin
from app.logic import SUPPORTED_IMAGE_EXTENSIONS
from .image_processor import QtImageProcessor
from .pattern_puller import PatternPullerDialog

DEFAULT_COLOR = "#763e92"

PRESET_COLORS: List[Tuple[str, str]] = [
    ("palette.swatch.red", "#ff3b30"),
    ("palette.swatch.orange", "#ff9500"),
    ("palette.swatch.yellow", "#ffd60a"),
    ("palette.swatch.green", "#34c759"),
    ("palette.swatch.teal", "#5ac8fa"),
    ("palette.swatch.blue", "#0a84ff"),
    ("palette.swatch.violet", "#af52de"),
    ("palette.swatch.magenta", "#ff2d55"),
    ("palette.swatch.white", "#ffffff"),
    ("palette.swatch.grey", "#8e8e93"),
    ("palette.swatch.black", "#000000"),
]

SLIDER_SPECS: List[Tuple[str, str, int, int]] = [
    ("sliders.hue_min", "hue_min", 0, 360),
    ("sliders.hue_max", "hue_max", 0, 360),
    ("sliders.sat_min", "sat_min", 0, 100),
    ("sliders.val_min", "val_min", 0, 100),
    ("sliders.val_max", "val_max", 0, 100),
    ("sliders.alpha", "alpha", 0, 255),
]

THEMES: Dict[str, Dict[str, str]] = {
    "dark": {
        "window_bg": "#111216",
        "panel_bg": "#16171d",
        "text": "#f7f7fb",
        "text_muted": "rgba(255,255,255,0.68)",
        "text_dim": "rgba(255,255,255,0.45)",
        "accent": "#5168ff",
        "accent_secondary": "#9a4dff",
        "titlebar_bg": "#16171d",
        "border": "rgba(255,255,255,0.08)",
        "highlight": "#e6b84b",
    },
    "light": {
        "window_bg": "#f3f4fb",
        "panel_bg": "#ffffff",
        "text": "#1d1e24",
        "text_muted": "rgba(29,30,36,0.78)",
        "text_dim": "rgba(29,30,36,0.55)",
        "accent": "#5168ff",
        "accent_secondary": "#9a4dff",
        "titlebar_bg": "#e9ebf5",
        "border": "rgba(29,30,36,0.12)",
        "highlight": "#c56217",
    },
}



class ColorSwatch(QtWidgets.QPushButton):
    """Clickable palette swatch."""

    def __init__(self, name: str, hex_code: str, callback: Callable[[str, str], None], parent: QtWidgets.QWidget | None = None):
        super().__init__(parent)
        self.hex_code = hex_code
        self.name_key = name
        self.callback = callback
        self.setCursor(QtCore.Qt.PointingHandCursor)
        self.setFixedSize(28, 28)
        self._apply_color(hex_code)
        self.clicked.connect(lambda: callback(hex_code, self.name_key))

    def _apply_color(self, hex_code: str) -> None:
        self.setStyleSheet(
            f"""
            QPushButton {{
                background-color: {hex_code};
                border: 2px solid rgba(255, 255, 255, 0.18);
                border-radius: 6px;
            }}
            QPushButton:hover {{
                border-color: rgba(255, 255, 255, 0.45);
            }}
            """
        )

    def apply_theme(self, colors: Dict[str, str]) -> None:
        self.setStyleSheet(
            f"""
            QPushButton {{
                background-color: {self.hex_code};
                border: 2px solid {colors['border']};
                border-radius: 6px;
            }}
            QPushButton:hover {{
                border-color: {colors['accent']};
            }}
            """
        )


class SliderControl(QtWidgets.QWidget):
    """Slider with header and editable live value input."""

    value_changed = QtCore.Signal(str, int)

    def __init__(self, title: str, key: str, minimum: int, maximum: int, initial: int):
        super().__init__()
        self.key = key
        self._minimum = minimum
        self._maximum = maximum

        layout = QtWidgets.QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(4)

        header = QtWidgets.QHBoxLayout()
        header.setContentsMargins(0, 0, 0, 0)
        self.title_label = QtWidgets.QLabel(title)
        header.addWidget(self.title_label)
        header.addStretch(1)
        self.value_edit = QtWidgets.QLineEdit(str(initial))
        self.value_edit.setFixedWidth(44)
        self.value_edit.setAlignment(QtCore.Qt.AlignRight)
        self.value_edit.setValidator(QtGui.QIntValidator(minimum, maximum, self))
        self.value_edit.editingFinished.connect(self._commit_edit)
        header.addWidget(self.value_edit)
        layout.addLayout(header)

        self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
        self.slider.setRange(minimum, maximum)
        self.slider.setValue(initial)
        self.slider.setCursor(QtCore.Qt.PointingHandCursor)
        self.slider.valueChanged.connect(self._sync_value)
        layout.addWidget(self.slider)

    def _sync_value(self, value: int) -> None:
        self.value_edit.setText(str(value))
        self.value_changed.emit(self.key, value)

    def _commit_edit(self) -> None:
        text = self.value_edit.text().strip()
        try:
            value = int(text)
        except ValueError:
            value = self.slider.value()
        value = max(self._minimum, min(self._maximum, value))
        self.slider.setValue(value)  # triggers _sync_value -> signal

    def set_value(self, value: int) -> None:
        self.slider.blockSignals(True)
        self.slider.setValue(value)
        self.slider.blockSignals(False)
        self.value_edit.setText(str(value))

    def apply_theme(self, colors: Dict[str, str]) -> None:
        self.title_label.setStyleSheet(f"color: {colors['text_muted']}; font-weight: 500;")
        self.value_edit.setStyleSheet(
            f"color: {colors['text_dim']}; background: transparent; "
            f"border: 1px solid {colors['border']}; border-radius: 4px; padding: 0 2px;"
        )
        self.slider.setStyleSheet(
            f"""
            QSlider::groove:horizontal {{
                border: 1px solid {colors['border']};
                height: 6px;
                background: rgba(255,255,255,0.14);
                border-radius: 4px;
            }}
            QSlider::handle:horizontal {{
                background: {colors['accent_secondary']};
                border: 1px solid rgba(255,255,255,0.2);
                width: 14px;
                margin: -5px 0;
                border-radius: 7px;
            }}
            """
        )


class CanvasView(QtWidgets.QGraphicsView):
    """Interactive canvas for drawing exclusion shapes over the preview image."""

    shapes_changed = QtCore.Signal(list)
    pixel_clicked = QtCore.Signal(int, int)  # x, y in image coordinates

    def __init__(self, parent: QtWidgets.QWidget | None = None) -> None:
        super().__init__(parent)
        self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform)
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setMouseTracking(True)
        self._scene = QtWidgets.QGraphicsScene(self)
        self.setScene(self._scene)

        self._pixmap_item: QtWidgets.QGraphicsPixmapItem | None = None
        self._shape_items: list[QtWidgets.QGraphicsItem] = []
        self._rubber_item: QtWidgets.QGraphicsRectItem | None = None
        self._stroke_item: QtWidgets.QGraphicsPathItem | None = None

        self.shapes: list[dict[str, object]] = []
        self.mode: str = "rect"
        self.pick_mode: bool = False
        self._drawing = False
        self._start_pos = QtCore.QPointF()
        self._last_pos = QtCore.QPointF()
        self._path = QtGui.QPainterPath()
        self._accent = QtGui.QColor("#ffd700")

    def set_pixmap(self, pixmap: QtGui.QPixmap) -> None:
        self._scene.clear()
        self._shape_items.clear()
        self._pixmap_item = self._scene.addPixmap(pixmap)
        self._scene.setSceneRect(pixmap.rect())
        self.resetTransform()
        self.fitInView(self._scene.sceneRect(), QtCore.Qt.KeepAspectRatio)
        self._redraw_shapes()

    def clear_canvas(self) -> None:
        if self._scene:
            self._scene.clear()
        self._pixmap_item = None
        self._shape_items.clear()
        self.shapes = []

    def set_shapes(self, shapes: list[dict[str, object]]) -> None:
        self.shapes = shapes
        self._redraw_shapes()

    def set_mode(self, mode: str) -> None:
        self.mode = mode

    def set_accent(self, color: str) -> None:
        self._accent = QtGui.QColor(color)
        self._redraw_shapes()

    def undo_last(self) -> None:
        if not self.shapes:
            return
        self.shapes.pop()
        self._redraw_shapes()
        self.shapes_changed.emit(self.shapes.copy())

    def clear_shapes(self) -> None:
        self.shapes = []
        self._redraw_shapes()
        self.shapes_changed.emit([])

    # event handling --------------------------------------------------------

    def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
        if event.button() == QtCore.Qt.LeftButton and self.pick_mode and self._pixmap_item:
            scene_pos = self.mapToScene(event.position().toPoint())
            clamped = self._clamp_to_image(scene_pos)
            self.pixel_clicked.emit(int(clamped.x()), int(clamped.y()))
            event.accept()
            return
        if event.button() == QtCore.Qt.RightButton and self._pixmap_item:
            self._drawing = True
            scene_pos = self.mapToScene(event.position().toPoint())
            self._start_pos = self._clamp_to_image(scene_pos)
            if self.mode == "rect":
                pen = QtGui.QPen(self._accent, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
                brush = QtGui.QBrush(QtCore.Qt.NoBrush)
                self._rubber_item = self._scene.addRect(QtCore.QRectF(self._start_pos, self._start_pos), pen, brush)
            else:
                pen = QtGui.QPen(self._accent, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
                self._path = QtGui.QPainterPath(self._start_pos)
                self._stroke_item = self._scene.addPath(self._path, pen)
            event.accept()
            return
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
        if self._drawing and self._pixmap_item:
            scene_pos = self.mapToScene(event.position().toPoint())
            self._last_pos = self._clamp_to_image(scene_pos)
            if self.mode == "rect" and self._rubber_item:
                rect = QtCore.QRectF(self._start_pos, self._last_pos).normalized()
                self._rubber_item.setRect(rect)
            elif self.mode == "free" and self._stroke_item:
                self._path.lineTo(self._last_pos)
                self._stroke_item.setPath(self._path)
            event.accept()
            return
        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None:
        if self._drawing and event.button() == QtCore.Qt.RightButton and self._pixmap_item:
            scene_pos = self.mapToScene(event.position().toPoint())
            end_pos = self._clamp_to_image(scene_pos)
            if self.mode == "rect" and self._rubber_item:
                rect = QtCore.QRectF(self._start_pos, end_pos).normalized()
                if rect.width() > 2 and rect.height() > 2:
                    shape = {
                        "kind": "rect",
                        "coords": (
                            int(rect.left()),
                            int(rect.top()),
                            int(rect.right()),
                            int(rect.bottom()),
                        ),
                    }
                    self.shapes.append(shape)
                self._scene.removeItem(self._rubber_item)
                self._rubber_item = None
            elif self.mode == "free" and self._stroke_item:
                self._path.lineTo(self._start_pos)
                points = [(int(pt.x()), int(pt.y())) for pt in self._path.toFillPolygon()]
                if len(points) >= 3:
                    shape = {"kind": "polygon", "points": points}
                    self.shapes.append(shape)
                self._scene.removeItem(self._stroke_item)
                self._stroke_item = None
            self._drawing = False
            self._redraw_shapes()
            self.shapes_changed.emit(self.shapes.copy())
            event.accept()
            return
        super().mouseReleaseEvent(event)

    # helpers ----------------------------------------------------------------

    def _clamp_to_image(self, pos: QtCore.QPointF) -> QtCore.QPointF:
        if not self._pixmap_item:
            return pos
        pixmap = self._pixmap_item.pixmap()
        x = min(max(0.0, pos.x()), float(pixmap.width() - 1))
        y = min(max(0.0, pos.y()), float(pixmap.height() - 1))
        return QtCore.QPointF(x, y)

    def _redraw_shapes(self) -> None:
        for item in self._shape_items:
            self._scene.removeItem(item)
        self._shape_items.clear()
        if not self._pixmap_item:
            return
        pen = QtGui.QPen(self._accent, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
        pen.setCosmetic(True)
        for shape in self.shapes:
            kind = shape.get("kind")
            if kind == "rect":
                x0, y0, x1, y1 = shape["coords"]  # type: ignore[index]
                rect_item = self._scene.addRect(QtCore.QRectF(x0, y0, x1 - x0, y1 - y0), pen)
                self._shape_items.append(rect_item)
            elif kind == "polygon":
                points = shape.get("points", [])
                if len(points) < 2:
                    continue
                path = QtGui.QPainterPath()
                first = QtCore.QPointF(*points[0])
                path.moveTo(first)
                for px, py in points[1:]:
                    path.lineTo(px, py)
                path.closeSubpath()
                path_item = self._scene.addPath(path, pen)
                self._shape_items.append(path_item)


class OverlayCanvas(QtWidgets.QGraphicsView):
    """Read-only QGraphicsView for displaying the color-match overlay."""

    def __init__(self, parent: QtWidgets.QWidget | None = None) -> None:
        super().__init__(parent)
        self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform)
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self._scene = QtWidgets.QGraphicsScene(self)
        self.setScene(self._scene)
        self._pixmap_item: QtWidgets.QGraphicsPixmapItem | None = None

    def set_pixmap(self, pixmap: QtGui.QPixmap) -> None:
        self._scene.clear()
        self._pixmap_item = self._scene.addPixmap(pixmap)
        self._scene.setSceneRect(pixmap.rect())
        self.resetTransform()
        self.fitInView(self._scene.sceneRect(), QtCore.Qt.KeepAspectRatio)

    def clear_canvas(self) -> None:
        self._scene.clear()
        self._pixmap_item = None

    def resizeEvent(self, event: QtGui.QResizeEvent) -> None:  # type: ignore[override]
        super().resizeEvent(event)
        if self._pixmap_item:
            self.fitInView(self._scene.sceneRect(), QtCore.Qt.KeepAspectRatio)


class TitleBar(QtWidgets.QWidget):
    """Custom title bar with native window controls."""

    HEIGHT = 40

    def __init__(self, window: "MainWindow") -> None:
        super().__init__(window)
        self.window = window
        self.setFixedHeight(self.HEIGHT)
        self.setCursor(QtCore.Qt.ArrowCursor)
        self.setAutoFillBackground(True)

        layout = QtWidgets.QHBoxLayout(self)
        layout.setContentsMargins(12, 8, 12, 8)
        layout.setSpacing(8)

        logo_path = Path(__file__).resolve().parents[1] / "assets" / "logo.png"
        if logo_path.exists():
            pixmap = QtGui.QPixmap(str(logo_path))
            self.logo_label = QtWidgets.QLabel()
            self.logo_label.setPixmap(pixmap.scaled(26, 26, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))
            layout.addWidget(self.logo_label)
        else:
            self.logo_label = None

        self.title_label = QtWidgets.QLabel()
        layout.addWidget(self.title_label)
        layout.addStretch(1)

        self.min_btn = self._create_button("–", "Minimise")
        self.min_btn.clicked.connect(window.showMinimized)
        layout.addWidget(self.min_btn)

        self.max_btn = self._create_button("❐", "Maximise / Restore")
        self.max_btn.clicked.connect(window.toggle_maximise)
        layout.addWidget(self.max_btn)

        self.close_btn = self._create_button("✕", "Close")
        self.close_btn.clicked.connect(window.close)
        layout.addWidget(self.close_btn)

    def _create_button(self, text: str, tooltip: str) -> QtWidgets.QPushButton:
        btn = QtWidgets.QPushButton(text)
        btn.setToolTip(tooltip)
        btn.setFixedSize(36, 24)
        btn.setCursor(QtCore.Qt.ArrowCursor)
        btn.setStyleSheet(
            """
            QPushButton {
                background-color: transparent;
                color: #f7f7fb;
                border: none;
                padding: 4px 10px;
            }
            QPushButton:hover {
                background-color: rgba(255, 255, 255, 0.1);
            }
            """
        )
        return btn

    def apply_theme(self, colors: Dict[str, str]) -> None:
        palette = self.palette()
        palette.setColor(QtGui.QPalette.Window, QtGui.QColor(colors["titlebar_bg"]))
        self.setPalette(palette)
        self.title_label.setStyleSheet(f"color: {colors['text']}; font-weight: 600;")
        hover_bg = "#d0342c" if colors["titlebar_bg"] != "#e9ebf5" else "#e6675a"
        self.close_btn.setStyleSheet(
            f"""
            QPushButton {{
                background-color: transparent;
                color: {colors['text']};
                border: none;
                padding: 4px 10px;
            }}
            QPushButton:hover {{
                background-color: {hover_bg};
                color: #ffffff;
            }}
            """
        )
        for btn in (self.min_btn, self.max_btn):
            btn.setStyleSheet(
                f"""
                QPushButton {{
                    background-color: transparent;
                    color: {colors['text']};
                    border: none;
                    padding: 4px 10px;
                }}
                QPushButton:hover {{
                    background-color: rgba(0, 0, 0, 0.1);
                }}
                """
            )

    def mouseDoubleClickEvent(self, event: QtGui.QMouseEvent) -> None:
        if event.button() == QtCore.Qt.LeftButton:
            self.window.toggle_maximise()
            event.accept()

    def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
        if event.button() == QtCore.Qt.LeftButton:
            self.window.start_system_move(event.globalPosition())
            event.accept()
        super().mousePressEvent(event)


class MainWindow(QtWidgets.QMainWindow, I18nMixin):
    """Main application window containing all controls."""

    def __init__(self, language: str, defaults: dict, reset_exclusions: bool, overlay_color: str | None = None) -> None:
        super().__init__()
        self.init_i18n(language)
        self.setWindowTitle(self._t("app.title"))
        self.setWindowFlag(QtCore.Qt.FramelessWindowHint)
        self.setWindowFlag(QtCore.Qt.Window)
        self.setAttribute(QtCore.Qt.WA_TranslucentBackground, False)
        self.setMinimumSize(1100, 680)

        container = QtWidgets.QWidget()
        container_layout = QtWidgets.QVBoxLayout(container)
        container_layout.setContentsMargins(0, 0, 0, 0)
        container_layout.setSpacing(0)

        self.title_bar = TitleBar(self)
        self.title_bar.title_label.setText(self._t("app.title"))
        container_layout.addWidget(self.title_bar)

        self.content = QtWidgets.QWidget()
        self.processor = QtImageProcessor()
        self.processor.set_defaults(defaults)
        self.processor.reset_exclusions_on_switch = reset_exclusions
        if overlay_color:
            self.processor.set_overlay_color(overlay_color)

        self.content_layout = QtWidgets.QVBoxLayout(self.content)
        self.content_layout.setContentsMargins(24, 0, 24, 24)
        self.content_layout.setSpacing(18)

        self.content_layout.addWidget(self._build_menu_bar())
        
        self.content_layout.addLayout(self._build_palette())
        self.content_layout.addLayout(self._build_sliders())
        self.content_layout.addWidget(self._build_previews(), 1)
        self.content_layout.addLayout(self._build_status_section())

        container_layout.addWidget(self.content, 1)
        self.setCentralWidget(container)

        self._is_maximised = False
        self._current_image_path: Path | None = None
        self._current_color = DEFAULT_COLOR
        self._toolbar_actions: Dict[str, Callable[[], None]] = {}
        self._register_default_actions()

        self.exclude_mode = "rect"
        self._pick_mode = False
        self.image_view.set_mode(self.exclude_mode)
        self.image_view.shapes_changed.connect(self._on_shapes_changed)
        self.image_view.pixel_clicked.connect(self._on_pixel_picked)

        self._sync_sliders_from_processor()
        self._update_color_display(DEFAULT_COLOR, self._t("palette.current"))

        self.current_theme = "dark"
        self._apply_theme(self.current_theme)

        # Drag-and-drop
        self.setAcceptDrops(True)

        # Keyboard shortcuts
        self._setup_shortcuts()

        # Slider debounce timer
        self._slider_timer = QtCore.QTimer(self)
        self._slider_timer.setSingleShot(True)
        self._slider_timer.setInterval(80)
        self._slider_timer.timeout.connect(self._refresh_overlay_only)

        # Restore window geometry
        self._settings = QtCore.QSettings("ICRA", "MainWindow")
        geometry = self._settings.value("geometry")
        if geometry:
            self.restoreGeometry(geometry)

    # Window control helpers -------------------------------------------------

    def toggle_maximise(self) -> None:
        handle = self.windowHandle()
        if handle is None:
            return
        if self._is_maximised:
            self.showNormal()
            self._is_maximised = False
            self.title_bar.max_btn.setText("❐")
        else:
            self.showMaximized()
            self._is_maximised = True
            self.title_bar.max_btn.setText("⧉")

    def start_system_move(self, _global_position: QtCore.QPointF) -> None:
        handle = self.windowHandle()
        if handle:
            handle.startSystemMove()

    # UI builders ------------------------------------------------------------

    def _build_menu_bar(self) -> QtWidgets.QMenuBar:
        self.menu_bar = QtWidgets.QMenuBar(self)

        # File Menu
        file_menu = self.menu_bar.addMenu(self._t("menu.file"))
        file_menu.addAction("🖼 " + self._t("toolbar.open_image"), lambda: self._invoke_action("open_image"), "Ctrl+O")
        file_menu.addAction("📂 " + self._t("toolbar.open_folder"), lambda: self._invoke_action("open_folder"), "Ctrl+Shift+O")
        file_menu.addAction("📊 " + self._t("toolbar.export_folder"), lambda: self._invoke_action("export_folder"))
        file_menu.addSeparator()
        file_menu.addAction("💾 " + self._t("toolbar.save_overlay"), lambda: self._invoke_action("save_overlay"), "Ctrl+S")

        # Edit Menu
        edit_menu = self.menu_bar.addMenu(self._t("menu.edit"))
        edit_menu.addAction("↩ " + self._t("toolbar.undo_exclude"), lambda: self._invoke_action("undo_exclude"), "Ctrl+Z")
        edit_menu.addAction("🧹 " + self._t("toolbar.clear_excludes"), lambda: self._invoke_action("clear_excludes"))
        edit_menu.addSeparator()
        edit_menu.addAction("🔄 " + self._t("toolbar.reset_sliders"), lambda: self._invoke_action("reset_sliders"), "Ctrl+R")

        # Tools Menu
        tools_menu = self.menu_bar.addMenu(self._t("menu.tools"))
        tools_menu.addAction("🎨 " + self._t("toolbar.choose_color"), lambda: self._invoke_action("choose_color"))
        tools_menu.addAction("🖱 " + self._t("toolbar.pick_from_image"), lambda: self._invoke_action("pick_from_image"))
        tools_menu.addAction("△ " + self._t("toolbar.toggle_free_draw"), lambda: self._invoke_action("toggle_free_draw"))
        tools_menu.addSeparator()
        tools_menu.addAction("📥 " + self._t("toolbar.pull_patterns"), lambda: self._invoke_action("pull_patterns"))

        # View Menu
        view_menu = self.menu_bar.addMenu(self._t("menu.view"))
        view_menu.addAction("🌓 " + self._t("toolbar.toggle_theme"), lambda: self._invoke_action("toggle_theme"))

        # Status label logic remains but moved to palette layout or kept minimal
        # We will add it to the palette layout so that it stays on top
        self.status_label = QtWidgets.QLabel(self._t("status.no_file"))
        return self.menu_bar

    def _build_palette(self) -> QtWidgets.QHBoxLayout:
        layout = QtWidgets.QHBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(16)

        current_group = QtWidgets.QHBoxLayout()
        current_group.setSpacing(8)
        self.current_label = QtWidgets.QLabel(self._t("palette.current"))
        current_group.addWidget(self.current_label)

        self.current_color_swatch = QtWidgets.QLabel()
        self.current_color_swatch.setFixedSize(28, 28)
        self.current_color_swatch.setStyleSheet(f"background-color: {DEFAULT_COLOR}; border-radius: 6px;")
        current_group.addWidget(self.current_color_swatch)

        self.current_color_label = QtWidgets.QLabel(f"({DEFAULT_COLOR})")
        current_group.addWidget(self.current_color_label)
        layout.addLayout(current_group)

        self.more_label = QtWidgets.QLabel(self._t("palette.more"))
        layout.addWidget(self.more_label)

        swatch_container = QtWidgets.QHBoxLayout()
        swatch_container.setSpacing(8)
        self.swatch_buttons: List[ColorSwatch] = []
        for name_key, hex_code in PRESET_COLORS:
            swatch = ColorSwatch(self._t(name_key), hex_code, self._update_color_display)
            swatch_container.addWidget(swatch)
            self.swatch_buttons.append(swatch)
        layout.addLayout(swatch_container)
        
        layout.addStretch(1)
        layout.addWidget(self.status_label, 0, QtCore.Qt.AlignRight)
        return layout

    def _build_sliders(self) -> QtWidgets.QHBoxLayout:
        layout = QtWidgets.QHBoxLayout()
        layout.setSpacing(16)
        self._slider_controls: Dict[str, SliderControl] = {}
        for key, attr, minimum, maximum in SLIDER_SPECS:
            initial = int(getattr(self.processor, attr))
            control = SliderControl(self._t(key), attr, minimum, maximum, initial)
            control.value_changed.connect(self._on_slider_change)
            layout.addWidget(control)
            self._slider_controls[attr] = control
        return layout

    def _build_previews(self) -> QtWidgets.QWidget:
        container = QtWidgets.QWidget()
        layout = QtWidgets.QGridLayout(container)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setHorizontalSpacing(16)

        self.prev_button = QtWidgets.QToolButton()
        self.prev_button.setCursor(QtCore.Qt.PointingHandCursor)
        self.prev_button.setAutoRaise(True)
        self.prev_button.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
        self.prev_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_ArrowBack))
        self.prev_button.setIconSize(QtCore.QSize(20, 20))
        self.prev_button.setFixedSize(38, 38)
        self.prev_button.clicked.connect(lambda: self._invoke_action("show_previous_image"))
        layout.addWidget(self.prev_button, 0, 0, QtCore.Qt.AlignVCenter)

        self.image_view = CanvasView()
        layout.addWidget(self.image_view, 0, 1)

        self.overlay_view = OverlayCanvas()
        layout.addWidget(self.overlay_view, 0, 2)

        self.next_button = QtWidgets.QToolButton()
        self.next_button.setCursor(QtCore.Qt.PointingHandCursor)
        self.next_button.setAutoRaise(True)
        self.next_button.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
        self.next_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_ArrowForward))
        self.next_button.setIconSize(QtCore.QSize(20, 20))
        self.next_button.setFixedSize(38, 38)
        self.next_button.clicked.connect(lambda: self._invoke_action("show_next_image"))
        layout.addWidget(self.next_button, 0, 3, QtCore.Qt.AlignVCenter)
        layout.setColumnStretch(1, 1)
        layout.setColumnStretch(2, 1)
        return container

    def _build_status_section(self) -> QtWidgets.QVBoxLayout:
        layout = QtWidgets.QVBoxLayout()
        layout.setSpacing(8)
        layout.setContentsMargins(0, 0, 0, 0)

        self.filename_label = QtWidgets.QLabel("—")
        self.filename_label.setAlignment(QtCore.Qt.AlignCenter)
        layout.addWidget(self.filename_label)

        self.ratio_label = QtWidgets.QLabel(self._t("stats.placeholder"))
        self.ratio_label.setAlignment(QtCore.Qt.AlignCenter)
        layout.addWidget(self.ratio_label)
        return layout

    # Action wiring ----------------------------------------------------------

    def _register_default_actions(self) -> None:
        self._toolbar_actions = {
            "open_image": self.open_image,
            "open_folder": self.open_folder,
            "export_folder": self.export_folder,
            "choose_color": self.choose_color,
            "pick_from_image": self.pick_from_image,
            "save_overlay": self.save_overlay,
            "toggle_free_draw": self.toggle_free_draw,
            "clear_excludes": self.clear_exclusions,
            "undo_exclude": self.undo_exclusion,
            "reset_sliders": self._reset_sliders,
            "toggle_theme": self.toggle_theme,
            "show_previous_image": self.show_previous_image,
            "show_next_image": self.show_next_image,
            "pull_patterns": self.open_pattern_puller,
        }

    def _invoke_action(self, key: str) -> None:
        action = self._toolbar_actions.get(key)
        if action:
            action()

    # Image handling ---------------------------------------------------------

    def open_image(self) -> None:
        filters = "Images (*.png *.jpg *.jpeg *.bmp *.webp)"
        default_dir = str(Path("images").absolute()) if Path("images").exists() else ""
        path_str, _ = QtWidgets.QFileDialog.getOpenFileName(self, self._t("dialog.open_image_title"), default_dir, filters)
        if not path_str:
            return
        path = Path(path_str)
        try:
            loaded_path = self.processor.load_single_image(path)
        except Exception as exc:  # pragma: no cover - user feedback
            QtWidgets.QMessageBox.warning(self, self._t("dialog.error_title"), str(exc))
            return
        self._current_image_path = loaded_path
        if self.processor.reset_exclusions_on_switch:
            self.image_view.clear_shapes()
            self.processor.set_exclusions([])
        self._refresh_views()

    def open_folder(self) -> None:
        default_dir = str(Path("images").absolute()) if Path("images").exists() else ""
        directory = QtWidgets.QFileDialog.getExistingDirectory(self, self._t("dialog.open_folder_title"), default_dir)
        if not directory:
            return
        folder = Path(directory)
        paths = sorted(
            (p for p in folder.iterdir() if p.suffix.lower() in SUPPORTED_IMAGE_EXTENSIONS and p.is_file()),
            key=lambda p: p.name.lower(),
        )
        if not paths:
            QtWidgets.QMessageBox.information(self, self._t("dialog.info_title"), self._t("dialog.folder_empty"))
            return
        try:
            loaded_path = self.processor.load_folder(paths)
        except ValueError as exc:
            QtWidgets.QMessageBox.information(self, self._t("dialog.info_title"), str(exc))
            return
        self._current_image_path = loaded_path
        self._refresh_views()

    def export_folder(self) -> None:
        if not self.processor.preview_paths:
            QtWidgets.QMessageBox.information(self, self._t("dialog.info_title"), self._t("dialog.no_image_loaded"))
            return

        folder_path = self.processor.preview_paths[0].parent
        default_filename = f"icra_stats_{folder_path.name}.csv"
        
        csv_path, _ = QtWidgets.QFileDialog.getSaveFileName(
            self,
            self._t("dialog.export_stats_title"),
            str(folder_path / default_filename),
            self._t("dialog.csv_filter")
        )
        if not csv_path:
            return

        total = len(self.processor.preview_paths)
        
        # Hardcoded to EU format as requested: ; delimiter, , decimal
        delimiter = ";"
        decimal = ","

        headers = [
            "Filename",
            "Color",
            "Matching Pixels",
            "Matching Pixels w/ Exclusions",
            "Excluded Pixels"
        ]
        rows = [headers]

        def process_image(img_path):
            try:
                img = Image.open(img_path)
                s = self.processor.get_stats_headless(img)
                
                pct_all = (s.matches_all / s.total_all * 100) if s.total_all else 0.0
                pct_keep = (s.matches_keep / s.total_keep * 100) if s.total_keep else 0.0
                pct_excl = (s.total_excl / s.total_all * 100) if s.total_all else 0.0

                pct_all_str = f"{pct_all:.2f}".replace(".", decimal)
                pct_keep_str = f"{pct_keep:.2f}".replace(".", decimal)
                pct_excl_str = f"{pct_excl:.2f}".replace(".", decimal)

                img.close()
                return [
                    img_path.name,
                    self._current_color,
                    pct_all_str,
                    pct_keep_str,
                    pct_excl_str
                ]
            except Exception:
                return [img_path.name, self._current_color, "Error", "Error", "Error"]

        results = [None] * total
        with concurrent.futures.ThreadPoolExecutor() as executor:
            future_to_idx = {executor.submit(process_image, p): i for i, p in enumerate(self.processor.preview_paths)}
            done_count = 0
            for future in concurrent.futures.as_completed(future_to_idx):
                idx = future_to_idx[future]
                results[idx] = future.result()
                done_count += 1
                if done_count % 10 == 0 or done_count == total:
                    self.status_label.setText(self._t("status.exporting", current=str(done_count), total=str(total)))
                    QtWidgets.QApplication.processEvents()
        
        rows.extend(results)

        # Compute max width per column for alignment, plus extra space so it's not cramped
        col_widths = [max(len(str(item)) for item in col) + 4 for col in zip(*rows)]

        # Excel on Windows prefers utf-8-sig (with BOM) to identify the encoding correctly.
        with open(csv_path, mode="w", newline="", encoding="utf-8-sig") as f:
            for row in rows:
                # Manual formatting to support both alignment for text editors AND valid CSV for Excel.
                # We pad the strings but keep the delimiter clean.
                padded_cells = [f"{str(item):>{width}}" for item, width in zip(row, col_widths)]
                f.write(delimiter.join(padded_cells) + "\n")

        # Restore overlay state for currently viewed image
        self.processor._rebuild_overlay()
        self.status_label.setText(self._t("status.export_done", path=csv_path))

    def show_previous_image(self) -> None:
        if not self.processor.preview_paths:
            QtWidgets.QMessageBox.information(self, self._t("dialog.info_title"), self._t("dialog.no_image_loaded"))
            return
        path = self.processor.previous_image()
        if path is None:
            return
        self._current_image_path = path
        if self.processor.reset_exclusions_on_switch:
            self.image_view.clear_shapes()
            self.processor.set_exclusions([])
        self._refresh_views()

    def show_next_image(self) -> None:
        if not self.processor.preview_paths:
            QtWidgets.QMessageBox.information(self, self._t("dialog.info_title"), self._t("dialog.no_image_loaded"))
            return
        path = self.processor.next_image()
        if path is None:
            return
        self._current_image_path = path
        if self.processor.reset_exclusions_on_switch:
            self.image_view.clear_shapes()
            self.processor.set_exclusions([])
        self._refresh_views()

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

    def _update_color_display(self, hex_code: str, label: str) -> None:
        self._current_color = hex_code
        self.current_color_swatch.setStyleSheet(f"background-color: {hex_code}; border-radius: 6px;")
        self.current_color_label.setText(f"({hex_code})")
        self.status_label.setText(f"{label}: {hex_code}")

    def _on_slider_change(self, key: str, value: int) -> None:
        self.processor.set_threshold(key, value)
        label = self._slider_title(key)
        self.status_label.setText(f"{label}: {value}")
        self._slider_timer.start()

    def _reset_sliders(self) -> None:
        for _, attr, _, _ in SLIDER_SPECS:
            control = self._slider_controls.get(attr)
            if control:
                default_value = int(self.processor.defaults.get(attr, getattr(self.processor, attr)))
                control.set_value(default_value)
                self.processor.set_threshold(attr, default_value)
        self.status_label.setText(self._t("status.defaults_restored"))
        self._refresh_overlay_only()

    def _coming_soon(self) -> None:
        QtWidgets.QMessageBox.information(
            self,
            self._t("dialog.info_title"),
            "Feature coming soon in the PySide6 migration.",
        )

    # Shortcuts --------------------------------------------------------------

    def _setup_shortcuts(self) -> None:
        shortcuts = [
            (QtGui.QKeySequence.Open, self.open_image),
            (QtGui.QKeySequence("Ctrl+Shift+O"), self.open_folder),
            (QtGui.QKeySequence.Save, self.save_overlay),
            (QtGui.QKeySequence.Undo, self.undo_exclusion),
            (QtGui.QKeySequence("Ctrl+R"), self._reset_sliders),
            (QtGui.QKeySequence(QtCore.Qt.Key_Left), self.show_previous_image),
            (QtGui.QKeySequence(QtCore.Qt.Key_Right), self.show_next_image),
            (QtGui.QKeySequence(QtCore.Qt.Key_Escape), self._exit_pick_mode),
        ]
        for seq, slot in shortcuts:
            sc = QtGui.QShortcut(seq, self)
            sc.activated.connect(slot)

    # Pick-mode / eyedropper -------------------------------------------------

    def pick_from_image(self) -> None:
        if self.processor.preview_img is None:
            QtWidgets.QMessageBox.information(
                self, self._t("dialog.info_title"), self._t("dialog.no_image_loaded")
            )
            return
        self._pick_mode = True
        self.image_view.pick_mode = True
        self.image_view.setCursor(QtCore.Qt.CrossCursor)
        self.status_label.setText(self._t("status.pick_mode_ready"))

    def _exit_pick_mode(self) -> None:
        self._pick_mode = False
        self.image_view.pick_mode = False
        self.image_view.setCursor(QtCore.Qt.ArrowCursor)
        self.status_label.setText(self._t("status.pick_mode_ended"))

    def _on_pixel_picked(self, x: int, y: int) -> None:
        result = self.processor.pick_color(x, y)
        if result is None:
            self._exit_pick_mode()
            return
        hue, sat, val = result

        # Derive a narrow hue range (±15°) and minimum sat/val from the pixel
        margin = 15
        hue_min = max(0, int(hue) - margin)
        hue_max = min(360, int(hue) + margin)
        sat_min = max(0, int(sat) - 20)
        val_min = max(0, int(val) - 30)
        val_max = 100

        for attr, value in [
            ("hue_min", hue_min), ("hue_max", hue_max),
            ("sat_min", sat_min), ("val_min", val_min), ("val_max", val_max),
        ]:
            ctrl = self._slider_controls.get(attr)
            if ctrl:
                ctrl.set_value(value)
            self.processor.set_threshold(attr, value)

        # Update color swatch to the picked pixel color
        h_norm = hue / 360.0
        s_norm = sat / 100.0
        v_norm = val / 100.0
        import colorsys
        r, g, b = colorsys.hsv_to_rgb(h_norm, s_norm, v_norm)
        hex_code = "#{:02x}{:02x}{:02x}".format(int(r * 255), int(g * 255), int(b * 255))
        self._update_color_display(hex_code, "")

        self.status_label.setText(
            self._t("status.pick_mode_from_image", hue=hue, saturation=sat, value=val)
        )
        self._refresh_overlay_only()

    def open_pattern_puller(self) -> None:
        dialog = PatternPullerDialog(self.language, parent=self)
        dialog.exec()

    # Drag-and-drop ----------------------------------------------------------

    def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None:
        if event.mimeData().hasUrls():
            event.acceptProposedAction()

    def dropEvent(self, event: QtGui.QDropEvent) -> None:
        urls = event.mimeData().urls()
        if not urls:
            return
        path = Path(urls[0].toLocalFile())
        if path.is_dir():
            paths = sorted(
                (p for p in path.iterdir() if p.suffix.lower() in SUPPORTED_IMAGE_EXTENSIONS and p.is_file()),
                key=lambda p: p.name.lower(),
            )
            if not paths:
                QtWidgets.QMessageBox.information(self, self._t("dialog.info_title"), self._t("dialog.folder_empty"))
                return
            try:
                loaded_path = self.processor.load_folder(paths)
            except ValueError as exc:
                QtWidgets.QMessageBox.information(self, self._t("dialog.info_title"), str(exc))
                return
            self._current_image_path = loaded_path
        elif path.suffix.lower() in SUPPORTED_IMAGE_EXTENSIONS:
            try:
                loaded_path = self.processor.load_single_image(path)
            except Exception as exc:
                QtWidgets.QMessageBox.warning(self, self._t("dialog.error_title"), str(exc))
                return
            self._current_image_path = loaded_path
            if self.processor.reset_exclusions_on_switch:
                self.image_view.clear_shapes()
                self.processor.set_exclusions([])
        else:
            return
        self._refresh_views()
        event.acceptProposedAction()

    # Window lifecycle -------------------------------------------------------

    def closeEvent(self, event: QtGui.QCloseEvent) -> None:
        self._settings.setValue("geometry", self.saveGeometry())
        super().closeEvent(event)

    def choose_color(self) -> None:
        color = QtWidgets.QColorDialog.getColor(parent=self, title=self._t("dialog.choose_color_title"))
        if not color.isValid():
            return
        hex_code = color.name()
        self._update_color_display(hex_code, self._t("dialog.choose_color_title"))

    def save_overlay(self) -> None:
        pixmap = self.processor.overlay_pixmap()
        if pixmap.isNull():
            QtWidgets.QMessageBox.information(self, self._t("dialog.info_title"), self._t("dialog.no_preview_available"))
            return
        filename, _ = QtWidgets.QFileDialog.getSaveFileName(
            self,
            self._t("dialog.save_overlay_title"),
            "overlay.png",
            "PNG (*.png)",
        )
        if not filename:
            return
        if not pixmap.save(filename, "PNG"):
            QtWidgets.QMessageBox.warning(self, self._t("dialog.error_title"), self._t("dialog.image_open_failed", error="Unable to save file"))
            return
        self.status_label.setText(self._t("dialog.overlay_saved", path=filename))

    def toggle_free_draw(self) -> None:
        self.exclude_mode = "free" if self.exclude_mode == "rect" else "rect"
        self.image_view.set_mode(self.exclude_mode)
        message_key = "status.free_draw_enabled" if self.exclude_mode == "free" else "status.free_draw_disabled"
        self.status_label.setText(self._t(message_key))

    def clear_exclusions(self) -> None:
        self.image_view.clear_shapes()
        self.processor.set_exclusions([])
        self.status_label.setText(self._t("toolbar.clear_excludes"))
        self._refresh_overlay_only()

    def undo_exclusion(self) -> None:
        self.image_view.undo_last()
        self.status_label.setText(self._t("toolbar.undo_exclude"))

    def toggle_theme(self) -> None:
        self.current_theme = "light" if self.current_theme == "dark" else "dark"
        self._apply_theme(self.current_theme)

    def _apply_theme(self, mode: str) -> None:
        colors = THEMES[mode]
        self.content.setStyleSheet(f"background-color: {colors['window_bg']};")
        self.image_view.setStyleSheet(
            f"background-color: {colors['panel_bg']}; border: 1px solid {colors['border']}; border-radius: 12px;"
        )
        self.image_view.set_accent(colors["highlight"])
        self.overlay_view.setStyleSheet(
            f"background-color: {colors['panel_bg']}; border: 1px solid {colors['border']}; border-radius: 12px;"
        )

        self.status_label.setStyleSheet(f"color: {colors['text_muted']}; font-weight: 500;")
        self.current_label.setStyleSheet(f"color: {colors['text_muted']}; font-weight: 500;")
        self.current_color_label.setStyleSheet(f"color: {colors['text_dim']};")
        self.more_label.setStyleSheet(f"color: {colors['text_muted']}; font-weight: 500;")
        self.filename_label.setStyleSheet(f"color: {colors['text']}; font-weight: 600;")
        self.ratio_label.setStyleSheet(f"color: {colors['highlight']}; font-weight: 600;")

        # Style MenuBar
        self.menu_bar.setStyleSheet(
            f"""
            QMenuBar {{
                background-color: {colors['window_bg']};
                color: {colors['text']};
                font-weight: 500;
                font-size: 13px;
                border-bottom: 1px solid {colors['border']};
            }}
            QMenuBar::item {{
                spacing: 8px;
                padding: 6px 12px;
                background: transparent;
                border-radius: 4px;
            }}
            QMenuBar::item:selected {{
                background: rgba(128, 128, 128, 0.2);
            }}
            QMenu {{
                background-color: {colors['panel_bg']};
                color: {colors['text']};
                border: 1px solid {colors['border']};
            }}
            QMenu::item {{
                padding: 6px 24px;
            }}
            QMenu::item:selected {{
                background-color: {colors['highlight']};
                color: #ffffff;
            }}
            """
        )

        for swatch in self.swatch_buttons:
            swatch.apply_theme(colors)
        for control in self._slider_controls.values():
            control.apply_theme(colors)

        self._style_nav_button(self.prev_button)
        self._style_nav_button(self.next_button)

        self.title_bar.apply_theme(colors)

    def _sync_sliders_from_processor(self) -> None:
        for _, attr, _, _ in SLIDER_SPECS:
            control = self._slider_controls.get(attr)
            if control:
                control.set_value(int(getattr(self.processor, attr)))

    def _slider_title(self, key: str) -> str:
        for title_key, attr, _, _ in SLIDER_SPECS:
            if attr == key:
                return self._t(title_key)
        return key

    def _style_nav_button(self, button: QtWidgets.QToolButton) -> None:
        colors = THEMES[self.current_theme]
        button.setStyleSheet(
            f"QToolButton {{ border-radius: 19px; background-color: {colors['panel_bg']}; "
            f"border: 1px solid {colors['border']}; color: {colors['text']}; }}"
            f"QToolButton:hover {{ background-color: {colors['accent_secondary']}; color: white; }}"
        )
        button.setIconSize(QtCore.QSize(20, 20))
        if button is getattr(self, "prev_button", None):
            button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_ArrowBack))
        elif button is getattr(self, "next_button", None):
            button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_ArrowForward))


    def _refresh_views(self) -> None:
        preview_pix = self.processor.preview_pixmap()
        if preview_pix.isNull():
            self.image_view.clear_canvas()
            self.image_view.set_shapes([])
            self.overlay_view.clear_canvas()
        else:
            self.image_view.set_pixmap(preview_pix)
            self.image_view.set_shapes(self.processor.exclude_shapes.copy())
            overlay_pix = self.processor.overlay_pixmap()
            overlay_pix = self._overlay_with_outlines(overlay_pix)
            self.overlay_view.set_pixmap(overlay_pix)
        if self._current_image_path and self.processor.preview_img:
            width, height = self.processor.preview_img.size
            total = len(self.processor.preview_paths)
            position = f" [{self.processor.current_index + 1}/{total}]" if total > 1 else ""
            dimensions = f"{width}×{height}"
            self.status_label.setText(
                self._t("status.loaded", name=self._current_image_path.name, dimensions=dimensions, position=position)
            )
            self.filename_label.setText(
                self._t("status.filename_label", name=self._current_image_path.name, dimensions=dimensions, position=position)
            )
        self.ratio_label.setText(self.processor.stats.summary(self._t))

    def _refresh_overlay_only(self) -> None:
        if self.processor.preview_img is None:
            return
        pix = self.processor.overlay_pixmap()
        if pix.isNull():
            self.overlay_view.clear_canvas()
        else:
            self.overlay_view.set_pixmap(self._overlay_with_outlines(pix))
        self.ratio_label.setText(self.processor.stats.summary(self._t))

    def _on_shapes_changed(self, shapes: list[dict[str, object]]) -> None:
        self.processor.set_exclusions(shapes)
        self._refresh_overlay_only()

    def _overlay_with_outlines(self, pixmap: QtGui.QPixmap) -> QtGui.QPixmap:
        if pixmap.isNull() or not self.processor.exclude_shapes:
            return pixmap
        result = QtGui.QPixmap(pixmap)
        painter = QtGui.QPainter(result)
        color = QtGui.QColor(THEMES[self.current_theme]["highlight"])
        pen = QtGui.QPen(color)
        pen.setWidth(3)
        pen.setCosmetic(True)
        pen.setCapStyle(QtCore.Qt.RoundCap)
        pen.setJoinStyle(QtCore.Qt.RoundJoin)
        painter.setPen(pen)
        for shape in self.processor.exclude_shapes:
            kind = shape.get("kind")
            if kind == "rect":
                x0, y0, x1, y1 = shape["coords"]  # type: ignore[index]
                painter.drawRect(QtCore.QRectF(x0, y0, x1 - x0, y1 - y0))
            elif kind == "polygon":
                points = shape.get("points", [])
                if len(points) >= 2:
                    path = QtGui.QPainterPath()
                    first = QtCore.QPointF(*points[0])
                    path.moveTo(first)
                    for px, py in points[1:]:
                        path.lineTo(px, py)
                    path.closeSubpath()
                    painter.drawPath(path)
        painter.end()
        return result
