"""
Wizard-based Create Assessment dialog.

This adapts the external assessment wizard flow to the EMDS plugin while
keeping EMDS as the owner of assessment/provenance metadata.
"""

from __future__ import annotations

import os
import re
import subprocess
import sys
import tempfile
import uuid
from pathlib import Path
from typing import Any, Dict, List, Optional

from ..qt_compat import (
    QDialog,
    QAbstractItemView,
    QApplication,
    QComboBox,
    QFormLayout,
    QGroupBox,
    QHBoxLayout,
    QHeaderView,
    QLabel,
    QLineEdit,
    QMessageBox,
    QProgressDialog,
    QPushButton,
    QSizePolicy,
    QTableWidget,
    QTableWidgetItem,
    QTextEdit,
    QVBoxLayout,
    QWizard,
    QWizardPage,
    QWidget,
    Qt,
    QColor,
    exec_dialog,
)
from qgis.core import (
    QgsCoordinateTransform,
    QgsCoordinateTransformContext,
    QgsFeature,
    QgsFeatureRequest,
    QgsField,
    QgsGeometry,
    QgsPointXY,
    QgsProject,
    QgsRectangle,
    QgsVectorFileWriter,
    QgsVectorLayer,
    QgsWkbTypes,
)
from qgis.gui import QgsMapCanvas, QgsMapTool, QgsMapToolPan, QgsRubberBand

from .. import debug_logger
from ..comment_formats import apply_sidecar
from ..rich_comment_editor import RichCommentEditor
from ..translator import tr
from ..gdal_capabilities import (
    build_gdal_environment,
    build_geometry_cleanup_command,
    require_layer_algebra_gdal,
)
from ..layer_kind import LayerKindResolver
from ..models import Assessment
from ..onboarding_ui import create_hint_panel
from ..spatialite_manager import SpatialiteManager

try:
    import processing
except Exception:  # pragma: no cover
    processing = None


class FeatureSelectionTool(QgsMapTool):
    """Toggle feature selection by clicking the canvas."""

    def __init__(self, canvas: QgsMapCanvas, layer: QgsVectorLayer, callback):
        super().__init__(canvas)
        self.layer = layer
        self.callback = callback
        self.setCursor(Qt.CursorShape.CrossCursor)

    def canvasReleaseEvent(self, event) -> None:
        point = self.toMapCoordinates(event.pos())
        search_radius = self.canvas().mapUnitsPerPixel() * 5

        layer_point = point
        layer_search_radius = search_radius
        if self.canvas().mapSettings().destinationCrs() != self.layer.crs():
            transform = QgsCoordinateTransform(
                self.canvas().mapSettings().destinationCrs(),
                self.layer.crs(),
                QgsProject.instance(),
            )
            layer_point = transform.transform(point)
            test_point = QgsPointXY(point.x() + search_radius, point.y())
            transformed_test = transform.transform(test_point)
            layer_search_radius = layer_point.distance(transformed_test)

        point_geom = QgsGeometry.fromPointXY(QgsPointXY(layer_point))
        search_rect = QgsRectangle(
            layer_point.x() - layer_search_radius,
            layer_point.y() - layer_search_radius,
            layer_point.x() + layer_search_radius,
            layer_point.y() + layer_search_radius,
        )

        request = QgsFeatureRequest()
        request.setFilterRect(search_rect)

        candidates = []
        for feature in self.layer.getFeatures(request):
            geom = feature.geometry()
            if geom and not geom.isNull():
                distance = geom.distance(point_geom)
                if distance <= layer_search_radius:
                    candidates.append((feature.id(), distance))

        candidates.sort(key=lambda item: item[1])
        if not candidates:
            return

        selected_ids = set(self.layer.selectedFeatureIds())
        clicked_id = candidates[0][0]
        if clicked_id in selected_ids:
            selected_ids.discard(clicked_id)
        else:
            selected_ids.add(clicked_id)

        self.layer.selectByIds(list(selected_ids))
        self.canvas().refresh()
        if self.callback:
            self.callback()


class RectangleSelectTool(QgsMapTool):
    """Select features using a dragged rectangle."""

    def __init__(self, canvas: QgsMapCanvas, layer: QgsVectorLayer, callback):
        super().__init__(canvas)
        self.canvas_ref = canvas
        self.layer = layer
        self.callback = callback
        self.start_point = None
        self.end_point = None
        self.rubber_band = QgsRubberBand(canvas, QgsWkbTypes.PolygonGeometry)
        self.rubber_band.setColor(QColor(255, 0, 0, 100))
        self.rubber_band.setWidth(2)
        self.setCursor(Qt.CursorShape.CrossCursor)

    def canvasPressEvent(self, event) -> None:
        if event.button() != Qt.MouseButton.LeftButton:
            return
        self.start_point = self.toMapCoordinates(event.pos())
        self.end_point = self.start_point
        self.rubber_band.reset(QgsWkbTypes.PolygonGeometry)

    def canvasMoveEvent(self, event) -> None:
        if self.start_point is None:
            return
        self.end_point = self.toMapCoordinates(event.pos())
        self._update_rubber_band()

    def canvasReleaseEvent(self, event) -> None:
        if event.button() != Qt.MouseButton.LeftButton or self.start_point is None:
            return

        self.end_point = self.toMapCoordinates(event.pos())
        self._update_rubber_band()
        rect = QgsRectangle(self.start_point, self.end_point)
        if rect.isEmpty():
            self._reset()
            return

        layer_rect = rect
        if self.canvas_ref.mapSettings().destinationCrs() != self.layer.crs():
            transform = QgsCoordinateTransform(
                self.canvas_ref.mapSettings().destinationCrs(),
                self.layer.crs(),
                QgsProject.instance(),
            )
            layer_rect = transform.transformBoundingBox(rect)

        request = QgsFeatureRequest()
        request.setFilterRect(layer_rect)
        rect_geom = QgsGeometry.fromRect(layer_rect)
        selected_ids = [
            feature.id()
            for feature in self.layer.getFeatures(request)
            if feature.geometry() and feature.geometry().intersects(rect_geom)
        ]

        self.layer.selectByIds(selected_ids)
        self.canvas_ref.refresh()
        if self.callback:
            self.callback()
        self._reset()

    def deactivate(self) -> None:
        self._reset()
        super().deactivate()

    def _update_rubber_band(self) -> None:
        self.rubber_band.reset(QgsWkbTypes.PolygonGeometry)
        if self.start_point is None or self.end_point is None:
            return
        points = [
            self.start_point,
            QgsPointXY(self.start_point.x(), self.end_point.y()),
            self.end_point,
            QgsPointXY(self.end_point.x(), self.start_point.y()),
        ]
        for index, point in enumerate(points):
            self.rubber_band.addPoint(point, index == len(points) - 1)
        self.rubber_band.show()

    def _reset(self) -> None:
        self.start_point = None
        self.end_point = None
        self.rubber_band.reset(QgsWkbTypes.PolygonGeometry)


class CreateAssessmentDialog(QWizard):
    """Wizard for creating an assessment and output overlay layer."""

    STATUS_INCLUDE = "Include in Study Area"
    STATUS_TARGET = "Include as Target"
    STATUS_SELECTOR = "Use as Selector"
    STATUS_SPATIAL_MARKER = "Spatial Marker"
    STATUS_DO_NOT_INCLUDE = "Do not include"
    # Future selector-with-data behavior: first intersect selector + target,
    # then include the selector layer in the identity loop to retain its fields.

    def __init__(
        self,
        available_layers: Optional[List[str]] = None,
        parent=None,
        *,
        database_path: Optional[str] = None,
    ):
        super().__init__(parent)
        self.available_layers = available_layers or []
        self.database_path = str(database_path or "")

        self.output_layer_name = ""
        self.output_table_name = ""
        self.reference_layer_name = ""
        self.reference_table_name = ""
        self.assessment_payload: Dict[str, Any] = {}
        self._assessment: Optional[Assessment] = None

        self.target_layer: Optional[QgsVectorLayer] = None
        self.selector_layer: Optional[QgsVectorLayer] = None
        self.selected_features_layer: Optional[QgsVectorLayer] = None
        self.map_canvas: Optional[QgsMapCanvas] = None
        self.map_tool_select = None
        self.map_tool_rect = None
        self.pan_tool = None

        self.setWindowTitle(tr("create_assessment.window_title"))
        self.setWizardStyle(QWizard.WizardStyle.ModernStyle)
        self.setMinimumSize(980, 720)
        self.currentIdChanged.connect(self._on_page_changed)

        self._build_pages()

    def _build_pages(self) -> None:
        self.page_1 = self._build_configuration_page()
        self.page_2 = self._build_feature_page()
        self.page_3 = self._build_summary_page()
        self.addPage(self.page_1)
        self.addPage(self.page_2)
        self.addPage(self.page_3)

    def _build_configuration_page(self) -> QWizardPage:
        page = QWizardPage()
        page.setTitle(tr("create_assessment.page_initial_title"))
        page.setSubTitle(tr("create_assessment.page_initial_subtitle"))

        layout = QVBoxLayout(page)
        layout.addWidget(
            create_hint_panel(
                tr("create_assessment.hint_initial_title"),
                tr("create_assessment.hint_initial_body"),
                link_key="create_study_area",
                parent=page,
            )
        )

        group = QGroupBox(tr("create_assessment.group_study_area"))
        form = QFormLayout(group)
        self.name_input = QLineEdit()
        self.name_input.setPlaceholderText(tr("create_assessment.name_placeholder"))
        self.description_input = RichCommentEditor(self)
        self.description_input.setPlaceholderText(tr("create_assessment.description_placeholder"))
        form.addRow(tr("create_assessment.row_name"), self.name_input)
        form.addRow(tr("create_assessment.row_description"), self.description_input)
        layout.addWidget(group)

        layout.addWidget(QLabel(tr("create_assessment.lbl_configure_status")))
        self.tableWidget_layers = QTableWidget(0, 4)
        self.tableWidget_layers.setHorizontalHeaderLabels([
            tr("create_assessment.col_layer_name"),
            tr("create_assessment.col_geometry_type"),
            tr("create_assessment.col_status"),
            tr("create_assessment.col_feature_count"),
        ])
        _layers_header = self.tableWidget_layers.horizontalHeader()
        _layers_header.setStretchLastSection(False)
        # Layer Name absorbs the slack (so it is the widest, not truncated);
        # Geometry Type, Status and Features size to their content.
        # Features is LAST so the Status combo stays at column index 2.
        _layers_header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
        _layers_header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
        _layers_header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
        _layers_header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
        self.tableWidget_layers.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        self.tableWidget_layers.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        layout.addWidget(self.tableWidget_layers, 1)

        self._populate_layers()
        page.validatePage = self._validate_page_1
        return page

    def _build_feature_page(self) -> QWizardPage:
        page = QWizardPage()
        page.setTitle(tr("create_assessment.page_features_title"))
        page.setSubTitle(tr("create_assessment.page_features_subtitle"))

        layout = QVBoxLayout(page)
        layout.addWidget(
            create_hint_panel(
                tr("create_assessment.hint_features_title"),
                tr("create_assessment.hint_features_body"),
                link_key="create_study_area",
                parent=page,
            )
        )
        self.label_target_layer_name = QLabel(tr("create_assessment.lbl_target_layer"))
        self.label_target_layer_name.setStyleSheet("font-weight: bold;")
        layout.addWidget(self.label_target_layer_name)

        button_row = QHBoxLayout()
        self.select_all_button = QPushButton(tr("create_assessment.btn_select_all"))
        self.select_all_button.clicked.connect(self._select_all_features)
        button_row.addWidget(self.select_all_button)

        self.clear_selection_button = QPushButton(tr("create_assessment.btn_clear_selection"))
        self.clear_selection_button.clicked.connect(self._clear_selection)
        button_row.addWidget(self.clear_selection_button)

        self.pan_button = QPushButton(tr("create_assessment.btn_pan"))
        self.pan_button.setCheckable(True)
        self.pan_button.clicked.connect(self._activate_pan)
        button_row.addWidget(self.pan_button)

        self.rectangle_select_button = QPushButton(tr("create_assessment.btn_rectangle"))
        self.rectangle_select_button.setCheckable(True)
        self.rectangle_select_button.clicked.connect(self._activate_rectangle_select)
        button_row.addWidget(self.rectangle_select_button)

        button_row.addStretch()
        self.label_selection_count = QLabel(tr("create_assessment.selection_count", count=0))
        button_row.addWidget(self.label_selection_count)
        layout.addLayout(button_row)

        self.map_canvas_container = QWidget()
        self.map_canvas_container.setMinimumHeight(420)
        self.map_canvas_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        self.verticalLayout_map = QVBoxLayout(self.map_canvas_container)
        self.verticalLayout_map.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.map_canvas_container, 1)

        page.validatePage = self._validate_page_2
        return page

    def _build_summary_page(self) -> QWizardPage:
        page = QWizardPage()
        page.setTitle(tr("create_assessment.page_summary_title"))
        page.setSubTitle(tr("create_assessment.page_summary_subtitle"))

        layout = QVBoxLayout(page)

        group = QGroupBox(tr("create_assessment.group_basic_info"))
        form = QFormLayout(group)
        self.label_summary_name = QLabel("")
        self.label_summary_description = QLabel("")
        self.label_summary_description.setWordWrap(True)
        self.label_summary_features = QLabel(tr("create_assessment.summary_features_default"))
        form.addRow(tr("create_assessment.row_study_area_name"), self.label_summary_name)
        form.addRow(tr("create_assessment.row_description"), self.label_summary_description)
        form.addRow(tr("create_assessment.row_selected_features"), self.label_summary_features)
        layout.addWidget(group)

        self.label_complexity_summary = QLabel("")
        self.label_complexity_summary.setWordWrap(True)
        layout.addWidget(self.label_complexity_summary)

        self.tableWidget_summary_layers = QTableWidget(0, 4)
        self.tableWidget_summary_layers.setHorizontalHeaderLabels([
            tr("create_assessment.col_layer_name"),
            tr("create_assessment.col_geometry_type"),
            tr("create_assessment.col_status"),
            tr("create_assessment.col_feature_count"),
        ])
        _summary_header = self.tableWidget_summary_layers.horizontalHeader()
        _summary_header.setStretchLastSection(False)
        _summary_header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
        _summary_header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
        _summary_header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
        _summary_header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
        self.tableWidget_summary_layers.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        layout.addWidget(self.tableWidget_summary_layers, 1)

        return page

    @staticmethod
    def _feature_count_item(layer) -> QTableWidgetItem:
        """Read-only, right-aligned feature-count cell.

        Providers that have not resolved a count yet (and layers we could not
        resolve at all) render as an em dash rather than a wrong number.
        """
        count = LayerKindResolver.feature_count(layer)
        item = QTableWidgetItem(
            tr("create_assessment.count_unknown") if count is None else f"{count:,}"
        )
        item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
        return item

    def _populate_layers(self) -> None:
        self.tableWidget_layers.setRowCount(0)
        # The first featureCount() on a large/unindexed provider can force a
        # scan; this runs before the wizard is shown.
        QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
        try:
            for layer in self._iter_available_vector_layers():
                row = self.tableWidget_layers.rowCount()
                self.tableWidget_layers.insertRow(row)
                self.tableWidget_layers.setItem(row, 0, QTableWidgetItem(layer.name()))
                self.tableWidget_layers.setItem(row, 1, QTableWidgetItem(QgsWkbTypes.displayString(layer.wkbType())))
                combo = QComboBox()
                combo.addItems([
                    self.STATUS_DO_NOT_INCLUDE,
                    self.STATUS_INCLUDE,
                    self.STATUS_TARGET,
                    self.STATUS_SELECTOR,
                    self.STATUS_SPATIAL_MARKER,
                ])
                combo.currentTextChanged.connect(self._on_status_changed)
                self.tableWidget_layers.setCellWidget(row, 2, combo)
                self.tableWidget_layers.setItem(row, 3, self._feature_count_item(layer))
        finally:
            QApplication.restoreOverrideCursor()

    def _iter_available_vector_layers(self) -> List[QgsVectorLayer]:
        layers: List[QgsVectorLayer] = []
        seen: set[str] = set()
        for layer_name in self.available_layers:
            for layer in QgsProject.instance().mapLayersByName(layer_name):
                # Route through LayerKindResolver (raster-support Phase 1)
                # so the vector-only filter shares the single source of
                # truth with the rest of the codebase. Raster layers
                # belonging to this study area's display_layers lane will
                # be presented separately in the Phase 2 dialog update.
                if (
                    LayerKindResolver.is_vector(layer)
                    and layer.isValid()
                    and layer.id() not in seen
                ):
                    layers.append(layer)
                    seen.add(layer.id())
        return layers

    def _on_status_changed(self, text: str) -> None:
        if text not in (self.STATUS_TARGET, self.STATUS_SELECTOR):
            return
        sender = self.sender()
        for row in range(self.tableWidget_layers.rowCount()):
            combo = self.tableWidget_layers.cellWidget(row, 2)
            if combo and combo is not sender and combo.currentText() == text:
                combo.setCurrentText(self.STATUS_DO_NOT_INCLUDE)

    def _selected_layer_configurations(self) -> List[Dict[str, str]]:
        items: List[Dict[str, str]] = []
        for row in range(self.tableWidget_layers.rowCount()):
            layer_name_item = self.tableWidget_layers.item(row, 0)
            combo = self.tableWidget_layers.cellWidget(row, 2)
            if not layer_name_item or not combo:
                continue
            items.append({
                "layer_name": layer_name_item.text(),
                "status": combo.currentText(),
            })
        return items

    def _get_layer_by_name(self, layer_name: str) -> Optional[QgsVectorLayer]:
        for layer in QgsProject.instance().mapLayersByName(layer_name):
            if isinstance(layer, QgsVectorLayer) and layer.isValid():
                return layer
        return None

    def _get_layers_by_status(self, status: str) -> List[QgsVectorLayer]:
        layers: List[QgsVectorLayer] = []
        for config in self._selected_layer_configurations():
            if config["status"] != status:
                continue
            layer = self._get_layer_by_name(config["layer_name"])
            if layer:
                layers.append(layer)
        return layers

    def _resolve_target_layer(self) -> Optional[QgsVectorLayer]:
        target_layers = self._get_layers_by_status(self.STATUS_TARGET)
        if len(target_layers) != 1:
            return None
        return target_layers[0]

    def _resolve_selector_layer(self) -> Optional[QgsVectorLayer]:
        selector_layers = self._get_layers_by_status(self.STATUS_SELECTOR)
        if len(selector_layers) != 1:
            return None
        return selector_layers[0]

    def _is_selector_mode(self) -> bool:
        return self._resolve_selector_layer() is not None

    def _overlay_participating_layers(self) -> List[QgsVectorLayer]:
        target_layer = self.target_layer or self._resolve_target_layer()
        if target_layer is None:
            return []
        return [
            layer
            for layer in self._get_layers_by_status(self.STATUS_INCLUDE)
            if layer.geometryType() == target_layer.geometryType()
        ]

    def _geometry_mismatched_include_layers(self) -> List[QgsVectorLayer]:
        target_layer = self.target_layer or self._resolve_target_layer()
        if target_layer is None:
            return []
        return [
            layer
            for layer in self._get_layers_by_status(self.STATUS_INCLUDE)
            if layer.geometryType() != target_layer.geometryType()
        ]

    def _validate_page_1(self) -> bool:
        title = tr("create_assessment.validation_title")
        if not self.name_input.text().strip():
            QMessageBox.warning(self, title, tr("create_assessment.validation_name_required"))
            return False

        target_layer = self._resolve_target_layer()
        if target_layer is None:
            QMessageBox.warning(self, title, tr("create_assessment.validation_exactly_one_target"))
            return False

        selector_layers = self._get_layers_by_status(self.STATUS_SELECTOR)
        if len(selector_layers) > 1:
            QMessageBox.warning(self, title, tr("create_assessment.validation_at_most_one_selector"))
            return False

        if selector_layers and selector_layers[0].geometryType() != QgsWkbTypes.PolygonGeometry:
            QMessageBox.warning(
                self, title,
                tr("create_assessment.validation_selector_polygon", name=selector_layers[0].name()),
            )
            return False

        mismatched_layers = self._geometry_mismatched_include_layers()
        if mismatched_layers:
            layer_names = ", ".join(layer.name() for layer in mismatched_layers)
            QMessageBox.warning(
                self, title,
                tr("create_assessment.validation_geometry_mismatch", layers=layer_names, target=target_layer.name()),
            )
            return False

        return True

    def _on_page_changed(self, page_id: int) -> None:
        if page_id == 1:
            self._setup_feature_page()
        elif page_id == 2:
            self._setup_summary_page()

    def _setup_feature_page(self) -> None:
        target_layers = self._get_layers_by_status(self.STATUS_TARGET)
        self.target_layer = target_layers[0] if target_layers else None
        self.selector_layer = self._resolve_selector_layer()
        if self.target_layer is None:
            return

        if self.selector_layer is not None:
            self.label_target_layer_name.setText(
                tr("create_assessment.lbl_target_with_selector", target=self.target_layer.name(), selector=self.selector_layer.name())
            )
            self.label_selection_count.setText(tr("create_assessment.selector_mode_msg"))
            self.select_all_button.setEnabled(False)
            self.clear_selection_button.setEnabled(False)
            self.rectangle_select_button.setEnabled(False)
            try:
                self.target_layer.removeSelection()
            except Exception:
                pass
        else:
            self.label_target_layer_name.setText(
                tr("create_assessment.lbl_target_layer_with_value", name=self.target_layer.name())
            )
            self.select_all_button.setEnabled(True)
            self.clear_selection_button.setEnabled(True)
            self.rectangle_select_button.setEnabled(True)

        if self.map_canvas is None:
            self.map_canvas = QgsMapCanvas(self.map_canvas_container)
            self.map_canvas.setCanvasColor(Qt.GlobalColor.white)
            self.map_canvas.setMinimumSize(600, 420)
            self.verticalLayout_map.addWidget(self.map_canvas)

        self.map_canvas.setDestinationCrs(self.target_layer.crs())
        canvas_layers = [self.target_layer]
        if self.selector_layer is not None:
            canvas_layers.insert(0, self.selector_layer)
        self.map_canvas.setLayers(canvas_layers)
        extent = self.target_layer.extent()
        if self.selector_layer is not None and not self.selector_layer.extent().isNull():
            extent.combineExtentWith(self.selector_layer.extent())
        if not extent.isNull() and not extent.isEmpty():
            extent.scale(1.1)
            self.map_canvas.setExtent(extent)
        self.map_canvas.refresh()

        self.map_tool_select = FeatureSelectionTool(self.map_canvas, self.target_layer, self._update_selection_count)
        self.map_tool_rect = RectangleSelectTool(self.map_canvas, self.target_layer, self._update_selection_count)
        self.pan_tool = QgsMapToolPan(self.map_canvas)
        self.map_canvas.setMapTool(self.pan_tool if self.selector_layer is not None else self.map_tool_select)
        self.pan_button.setChecked(False)
        self.rectangle_select_button.setChecked(False)

        try:
            self.target_layer.selectionChanged.disconnect(self._update_selection_count)
        except Exception:
            pass
        self.target_layer.selectionChanged.connect(self._update_selection_count)
        self._update_selection_count()

    def _setup_summary_page(self) -> None:
        self.label_summary_name.setText(self.name_input.text().strip())
        description = self.description_input.toPlainText().strip()
        self.label_summary_description.setText(description or tr("create_assessment.no_description"))
        selector_layer = self._resolve_selector_layer()
        if selector_layer is not None:
            target_count = self.target_layer.featureCount() if self.target_layer else 0
            self.label_summary_features.setText(
                tr("create_assessment.selection_count", count=target_count)
            )
        else:
            selected_count = self.target_layer.selectedFeatureCount() if self.target_layer else 0
            self.label_summary_features.setText(tr("create_assessment.selection_count", count=selected_count))

        included_layers = self._overlay_participating_layers()
        selector_layers = self._get_layers_by_status(self.STATUS_SELECTOR)
        marker_layers = self._get_layers_by_status(self.STATUS_SPATIAL_MARKER)
        if selector_layers:
            summary = tr("create_assessment.selector_summary", name=selector_layers[0].name())
        elif not included_layers and not marker_layers:
            summary = tr("create_assessment.simple_summary")
        else:
            summary = tr("create_assessment.overlay_summary", count=len(included_layers))
        self.label_complexity_summary.setText(summary)

        self.tableWidget_summary_layers.setRowCount(0)
        for config in self._selected_layer_configurations():
            if config["status"] == self.STATUS_DO_NOT_INCLUDE:
                continue
            layer = self._get_layer_by_name(config["layer_name"])
            row = self.tableWidget_summary_layers.rowCount()
            self.tableWidget_summary_layers.insertRow(row)
            self.tableWidget_summary_layers.setItem(row, 0, QTableWidgetItem(config["layer_name"]))
            self.tableWidget_summary_layers.setItem(
                row,
                1,
                QTableWidgetItem(QgsWkbTypes.displayString(layer.wkbType()) if layer else "Unknown"),
            )
            self.tableWidget_summary_layers.setItem(row, 2, QTableWidgetItem(config["status"]))
            self.tableWidget_summary_layers.setItem(row, 3, self._feature_count_item(layer))

    def _select_all_features(self) -> None:
        if self.target_layer:
            self.target_layer.selectAll()
            self._update_selection_count()

    def _clear_selection(self) -> None:
        if self.target_layer:
            self.target_layer.removeSelection()
            self._update_selection_count()

    def _activate_pan(self) -> None:
        if not self.map_canvas or not self.pan_tool:
            return
        if self.pan_button.isChecked():
            self.rectangle_select_button.setChecked(False)
            self.map_canvas.setMapTool(self.pan_tool)
        else:
            self.map_canvas.setMapTool(self.map_tool_select)

    def _activate_rectangle_select(self) -> None:
        if not self.map_canvas or not self.map_tool_rect:
            return
        if self.rectangle_select_button.isChecked():
            self.pan_button.setChecked(False)
            self.map_canvas.setMapTool(self.map_tool_rect)
        else:
            self.map_canvas.setMapTool(self.map_tool_select)

    def _update_selection_count(self) -> None:
        if self._is_selector_mode():
            self.label_selection_count.setText(tr("create_assessment.selector_mode_msg"))
            if self.map_canvas:
                self.map_canvas.refresh()
            return
        count = self.target_layer.selectedFeatureCount() if self.target_layer else 0
        self.label_selection_count.setText(tr("create_assessment.selection_count", count=count))
        if self.map_canvas:
            self.map_canvas.refresh()

    def _validate_page_2(self) -> bool:
        if self.target_layer is None:
            QMessageBox.warning(self, tr("create_assessment.validation_title"), tr("create_assessment.validation_no_target"))
            return False
        if self._is_selector_mode():
            self.selected_features_layer = None
            return True
        if self.target_layer.selectedFeatureCount() == 0:
            QMessageBox.warning(
                self,
                tr("create_assessment.validation_title"),
                tr("create_assessment.validation_no_features"),
            )
            return False
        self.selected_features_layer = self._create_selection_layer(self.target_layer)
        return self.selected_features_layer is not None

    def _create_selection_layer(self, source_layer: QgsVectorLayer) -> Optional[QgsVectorLayer]:
        selected_ids = list(source_layer.selectedFeatureIds())
        if not selected_ids:
            return None

        selection_layer = QgsVectorLayer(
            f"{QgsWkbTypes.displayString(source_layer.wkbType())}?crs={source_layer.crs().authid()}",
            f"{source_layer.name()}_selection",
            "memory",
        )
        if not selection_layer.isValid():
            return None

        provider = selection_layer.dataProvider()
        provider.addAttributes(source_layer.fields())
        selection_layer.updateFields()
        features = []
        for feature_id in selected_ids:
            feature = source_layer.getFeature(feature_id)
            if feature.isValid():
                features.append(feature)
        provider.addFeatures(features)
        selection_layer.updateExtents()
        return selection_layer

    def accept(self) -> None:
        if self.selected_features_layer is None and not self._validate_page_2():
            return

        if not self.database_path:
            QMessageBox.critical(
                self,
                tr("create_assessment.err_workspace_db_title"),
                tr("create_assessment.err_workspace_db_body"),
            )
            return

        progress = QProgressDialog(tr("create_assessment.progress_creating"), None, 0, 0, self)
        progress.setWindowTitle(tr("create_assessment.window_title"))
        progress.setWindowModality(Qt.WindowModality.WindowModal)
        progress.setMinimumDuration(0)
        progress.setAutoClose(False)
        progress.setAutoReset(False)
        progress.show()
        QApplication.processEvents()

        try:
            selector_layer = self._resolve_selector_layer()
            progress.setLabelText(
                tr("create_assessment.progress_overlay_selector")
                if selector_layer is not None
                else tr("create_assessment.progress_overlay_features")
            )
            QApplication.processEvents()
            result_layer = self._build_output_layer()
            if result_layer is None or not result_layer.isValid():
                raise RuntimeError("Could not create Study Area output layer.")

            display_name = f"{self._sanitize_name(self.name_input.text().strip())}_overlay"
            progress.setLabelText(tr("create_assessment.progress_save"))
            QApplication.processEvents()
            spatialite_manager = SpatialiteManager()
            success, table_name = spatialite_manager.copy_vector_layer_to_database(
                display_name,
                result_layer,
                self.database_path,
            )
            if not success or not table_name:
                raise RuntimeError("Could not save Study Area output to the Workspace GeoPackage.")

            self.output_layer_name = table_name
            self.output_table_name = table_name
            self.reference_layer_name = self.target_layer.name() if self.target_layer else ""
            self.reference_table_name = self._get_layer_table_name(self.target_layer) if self.target_layer else ""
            self.target_layer_name = self.target_layer.name() if self.target_layer else ""
            self.target_table_name = self._get_layer_table_name(self.target_layer) if self.target_layer else ""
            self.selector_layer_name = selector_layer.name() if selector_layer else ""
            self.selector_table_name = self._get_layer_table_name(selector_layer) if selector_layer else ""

            assessment_name = self.name_input.text().strip()
            assessment_description = self.description_input.plain_text()
            assessment_description_html = self.description_input.html()
            overlay_layers = self._overlay_participating_layers()
            input_layer_names = [
                layer.name()
                for layer in (
                    [self.target_layer]
                    + ([selector_layer] if selector_layer is not None else [])
                    + overlay_layers
                    + self._get_layers_by_status(self.STATUS_SPATIAL_MARKER)
                )
                if layer is not None
            ]
            input_layers = list(dict.fromkeys(input_layer_names))

            progress.setLabelText(tr("create_assessment.progress_metadata"))
            QApplication.processEvents()
            self.assessment_payload = {
                "study_name": assessment_name,
                "study_comments": assessment_description,
                "assessment_name": assessment_name,
                "assessment_comments": assessment_description,
                "input_layers": input_layers,
                "target_layer_name": self.target_layer_name,
                "target_table_name": self.target_table_name,
                "study_layer_names": [layer.name() for layer in overlay_layers],
                "assessment_layer_names": [layer.name() for layer in overlay_layers],
                "overlay_layer_names": [layer.name() for layer in overlay_layers],
                "spatial_marker_layer_names": [layer.name() for layer in self._get_layers_by_status(self.STATUS_SPATIAL_MARKER)],
                "output_layer_name": self.output_layer_name,
                "output_table_name": self.output_table_name,
                "reference_layer_name": self.reference_layer_name,
                "reference_table_name": self.reference_table_name,
                "database_path": self.database_path,
                "selected_feature_count": (
                    result_layer.featureCount()
                    if selector_layer is not None
                    else self.target_layer.selectedFeatureCount() if self.target_layer else 0
                ),
                "geometry_type": QgsWkbTypes.displayString(result_layer.wkbType()),
                "output_crs_authid": result_layer.crs().authid() if result_layer.crs().isValid() else "",
                "layer_configurations": self._selected_layer_configurations(),
            }
            # Q6b — the rich body rides in the metadata sidecar; the
            # canonical comments column stays plain text.
            self.assessment_payload = apply_sidecar(
                self.assessment_payload, assessment_description_html
            )
            if selector_layer is not None:
                self.assessment_payload.update({
                    "processing_mode": "selector",
                    "selector_layer_name": self.selector_layer_name,
                    "selector_table_name": self.selector_table_name,
                })

            self._assessment = Assessment(
                id=str(uuid.uuid4()),
                name=assessment_name,
                description=assessment_description,
                input_layers=input_layers,
            )
            self._assessment.metadata = dict(self.assessment_payload)

            debug_logger.debug("Assessment wizard output created", {
                "assessment_name": assessment_name,
                "output_layer_name": self.output_layer_name,
                "output_table_name": self.output_table_name,
                "reference_layer_name": self.reference_layer_name,
                "reference_table_name": self.reference_table_name,
                "selector_layer_name": self.selector_layer_name,
                "processing_mode": "selector" if selector_layer is not None else "selection",
                "database_path": self.database_path,
            })

            progress.close()
            self._cleanup_target_selection()
            super().accept()

        except Exception as exc:
            progress.close()
            debug_logger.error("Assessment creation failed", {"exception": str(exc)})
            QMessageBox.critical(self, tr("create_assessment.err_study_area_title"), str(exc))

    def reject(self) -> None:
        self._cleanup_target_selection()
        super().reject()

    def get_assessment(self) -> Optional[Assessment]:
        if exec_dialog(self) == QDialog.DialogCode.Accepted:
            return self._assessment
        return None

    def _build_output_layer(self) -> Optional[QgsVectorLayer]:
        base_layer = self._build_base_output_layer()
        if base_layer is None:
            return None

        output_name = f"{self._sanitize_name(self.name_input.text().strip())}_overlay"
        assessment_layers = self._overlay_participating_layers()
        if assessment_layers:
            final_uri = self._run_ogr_identity_chain(output_name, base_layer)
        elif self._resolve_selector_layer() is not None:
            # Selector-only Study Areas skip the identity chain, but they still
            # come out of `gdal vector layer-algebra --operation intersection`
            # (_run_selector_intersection), which emits the same
            # boundary-collapse artifacts, so they need the same cleanup.
            final_uri = base_layer.source()
        else:
            # No GDAL step ran: base_layer is the in-memory feature selection,
            # which can only contain the target layer's own geometries.
            return base_layer

        final_uri = self._drop_degenerate_geometries(final_uri, output_name)
        result_layer = QgsVectorLayer(final_uri, output_name, "ogr")
        if not result_layer.isValid():
            raise RuntimeError("OGR identity output layer failed to load.")
        if (
            self.target_layer is not None
            and self.target_layer.crs().isValid()
            and not result_layer.crs().isValid()
        ):
            result_layer.setCrs(self.target_layer.crs())
        return result_layer

    def _build_base_output_layer(self) -> Optional[QgsVectorLayer]:
        selector_layer = self._resolve_selector_layer()
        if selector_layer is None:
            return self.selected_features_layer
        if self.target_layer is None:
            raise RuntimeError("No target layer is available.")
        return self._run_selector_intersection(selector_layer, self.target_layer)

    def _run_selector_intersection(
        self,
        selector_layer: QgsVectorLayer,
        target_layer: QgsVectorLayer,
    ) -> QgsVectorLayer:
        output_layer_name = f"{self._sanitize_name(self.name_input.text().strip())}_selector_target"
        gdal_exe = self._find_gdal_executable()
        gdal_env = self._build_gdal_environment(gdal_exe)
        temp_root = Path(tempfile.mkdtemp(prefix="emds_selector_"))

        target_path = temp_root / "target_features.gpkg"
        target_layer_name = "target_features"
        selector_path = temp_root / "selector_mask.gpkg"
        selector_layer_name = "selector_mask"
        output_path = temp_root / "selector_target.gpkg"

        self._export_layer_to_gpkg(target_layer, target_path, target_layer_name)
        self._export_layer_to_gpkg(
            self._create_geometry_only_layer(selector_layer, selector_layer_name),
            selector_path,
            selector_layer_name,
        )

        command = [
            gdal_exe,
            "vector",
            "layer-algebra",
            "--operation",
            "intersection",
            "--input",
            str(target_path),
            "--input-layer",
            target_layer_name,
            "--method",
            str(selector_path),
            "--method-layer",
            selector_layer_name,
            "--output",
            str(output_path),
            "--output-layer",
            output_layer_name,
            "--format",
            "GPKG",
            "--geometry-type",
            "GEOMETRY",
            "--all-input-field",
            "--no-method-field",
            "--overwrite",
            "--lco",
            "LAUNDER=NO",
        ]

        debug_logger.debug(
            "Running GDAL selector intersection",
            {
                "command": subprocess.list2cmdline(command),
                "target_layer": target_layer.name(),
                "selector_layer": selector_layer.name(),
            },
        )
        completed = subprocess.run(
            command,
            capture_output=True,
            text=True,
            check=False,
            env=gdal_env,
            **self._subprocess_window_kwargs(),
        )
        if completed.stdout.strip():
            debug_logger.debug(
                "GDAL selector intersection stdout",
                {"stdout": completed.stdout.strip()},
            )
        if completed.stderr.strip():
            debug_logger.debug(
                "GDAL selector intersection stderr",
                {"stderr": completed.stderr.strip()},
            )
        if completed.returncode != 0:
            raise RuntimeError(
                f"GDAL selector intersection failed for selector layer '{selector_layer.name()}'."
            )

        result_layer = QgsVectorLayer(
            f"{output_path}|layername={output_layer_name}",
            output_layer_name,
            "ogr",
        )
        if not result_layer.isValid():
            raise RuntimeError("Selector-target intersection output layer failed to load.")
        if target_layer.crs().isValid() and not result_layer.crs().isValid():
            result_layer.setCrs(target_layer.crs())
        if result_layer.featureCount() == 0:
            raise RuntimeError(
                f"Selector layer '{selector_layer.name()}' did not intersect any target features."
            )
        return result_layer

    def _run_ogr_identity_chain(
        self,
        output_layer_name: str,
        base_layer: QgsVectorLayer,
    ) -> str:
        include_layers = self._overlay_participating_layers()
        if not include_layers:
            raise RuntimeError("No Study Area layers were selected.")
        if base_layer is None or not base_layer.isValid():
            raise RuntimeError("No Study Area base features are available.")

        gdal_exe = self._find_gdal_executable()
        gdal_env = self._build_gdal_environment(gdal_exe)
        temp_root = Path(tempfile.mkdtemp(prefix="emds_assessment_"))

        debug_logger.debug(
            "Starting assessment OGR identity chain",
            {
                "temp_root": str(temp_root),
                "gdal_exe": gdal_exe,
                "target_layer": base_layer.name(),
                "include_layers": [layer.name() for layer in include_layers],
            },
        )

        current_path = temp_root / "base_features.gpkg"
        current_layer_name = "base_features"
        self._export_layer_to_gpkg(base_layer, current_path, current_layer_name)

        for index, include_layer in enumerate(include_layers, start=1):
            method_path = temp_root / f"include_{index}.gpkg"
            method_layer_name = f"include_{index}"
            output_path = temp_root / f"identity_step_{index}.gpkg"
            next_layer_name = (
                output_layer_name if index == len(include_layers) else f"step_{index}"
            )

            self._export_layer_to_gpkg(include_layer, method_path, method_layer_name)
            command = [
                gdal_exe,
                "vector",
                "layer-algebra",
                "--operation",
                "identity",
                "--input",
                str(current_path),
                "--input-layer",
                current_layer_name,
                "--method",
                str(method_path),
                "--method-layer",
                method_layer_name,
                "--output",
                str(output_path),
                "--output-layer",
                next_layer_name,
                "--format",
                "GPKG",
                "--geometry-type",
                "GEOMETRY",
                "--overwrite",
                "--lco",
                "LAUNDER=NO",
                "--method-prefix",
                f"{self._sanitize_name(include_layer.name())}_",
            ]

            debug_logger.debug(
                "Running GDAL layer algebra identity",
                {
                    "command": subprocess.list2cmdline(command),
                    "include_layer": include_layer.name(),
                },
            )
            completed = subprocess.run(
                command,
                capture_output=True,
                text=True,
                check=False,
                env=gdal_env,
                **self._subprocess_window_kwargs(),
            )
            if completed.stdout.strip():
                debug_logger.debug(
                    "GDAL layer algebra stdout",
                    {"include_layer": include_layer.name(), "stdout": completed.stdout.strip()},
                )
            if completed.stderr.strip():
                debug_logger.debug(
                    "GDAL layer algebra stderr",
                    {"include_layer": include_layer.name(), "stderr": completed.stderr.strip()},
                )
            if completed.returncode != 0:
                raise RuntimeError(
                    f"GDAL identity failed for include layer '{include_layer.name()}'."
                )

            current_path = output_path
            current_layer_name = next_layer_name

        return f"{current_path}|layername={current_layer_name}"

    def _drop_degenerate_geometries(
        self,
        source_uri: str,
        output_layer_name: str,
    ) -> str:
        """Strip overlay artifacts and stamp a conformant geometry type.

        `gdal vector layer-algebra` is invoked with `--geometry-type GEOMETRY`,
        so every boundary-collapse artifact it emits where target and method
        edges coincide survives into the Study Area. Measured on the TCA
        overlay: 17965 POLYGON + 955 MULTILINESTRING + 3 LINESTRING + 1 POINT
        + 1 GEOMETRYCOLLECTION = 18925 rows, i.e. 960 rows with no area. The
        engine scores those rows and every result reads as no-data. QGIS then
        guesses the layer type from the features it samples, which is how the
        GeoPackage came to declare POLYGON while storing non-polygons.

        Returns a layer URI. `spatialite_manager.copy_vector_layer_to_database`
        needs no change: it derives the destination type from
        `source_layer.wkbType()`, which is MultiPolygon after this pass.
        """
        source_layer = QgsVectorLayer(source_uri, "study_area_pre_cleanup", "ogr")
        if not source_layer.isValid():
            raise RuntimeError("Study Area overlay output failed to load for cleanup.")

        if source_layer.geometryType() != QgsWkbTypes.PolygonGeometry:
            # Only polygon Study Areas are cleaned. Forcing a line or point
            # layer to its MULTI type is lossy in the other direction (OGR
            # turns a polygon into its boundary), and identity overlays of
            # non-polygon targets are an untested path today.
            debug_logger.debug(
                "Study Area geometry cleanup skipped: not a polygon Study Area",
                {"wkb_type": QgsWkbTypes.displayString(source_layer.wkbType())},
            )
            return source_uri

        before_count = source_layer.featureCount()
        input_geometry_types = self._geometry_type_histogram(source_layer)

        gdal_exe = self._find_gdal_executable()
        gdal_env = self._build_gdal_environment(gdal_exe)
        temp_root = Path(tempfile.mkdtemp(prefix="emds_cleanup_"))
        output_path = temp_root / "study_area_cleaned.gpkg"

        command = build_geometry_cleanup_command(
            gdal_exe,
            source_uri.split("|", 1)[0],
            self._extract_layername_from_source(source_uri),
            str(output_path),
            output_layer_name,
        )
        debug_logger.debug(
            "Running GDAL Study Area geometry cleanup",
            {"command": subprocess.list2cmdline(command)},
        )
        completed = subprocess.run(
            command,
            capture_output=True,
            text=True,
            check=False,
            env=gdal_env,
            **self._subprocess_window_kwargs(),
        )
        if completed.stdout.strip():
            debug_logger.debug(
                "GDAL geometry cleanup stdout", {"stdout": completed.stdout.strip()}
            )
        if completed.stderr.strip():
            debug_logger.debug(
                "GDAL geometry cleanup stderr", {"stderr": completed.stderr.strip()}
            )
        if completed.returncode != 0:
            raise RuntimeError(
                "GDAL geometry cleanup failed for the Study Area overlay."
            )

        cleaned_uri = f"{output_path}|layername={output_layer_name}"
        cleaned_layer = QgsVectorLayer(cleaned_uri, output_layer_name, "ogr")
        if not cleaned_layer.isValid():
            raise RuntimeError("Study Area geometry cleanup output failed to load.")

        after_count = cleaned_layer.featureCount()
        if after_count == 0:
            raise RuntimeError(
                "Study Area geometry cleanup removed every feature: the overlay "
                "produced no polygons with area."
            )

        debug_logger.debug(
            "Study Area geometry cleanup complete",
            {
                "features_before": before_count,
                "features_after": after_count,
                "features_dropped": before_count - after_count,
                "reason": (
                    "zero-area geometries removed: overlay boundary-collapse "
                    "artifacts (lines, points, collections) and degenerate "
                    "polygons; survivors forced to MULTIPOLYGON"
                ),
                "input_geometry_types": input_geometry_types,
                "output_geometry_type": QgsWkbTypes.displayString(
                    cleaned_layer.wkbType()
                ),
            },
        )
        return cleaned_uri

    @staticmethod
    def _geometry_type_histogram(layer: QgsVectorLayer) -> Dict[str, int]:
        """Count features per WKB type so the cleanup log says WHAT it dropped.

        Geometry-only scan (setNoAttributes): sub-second on the 18925-feature
        TCA overlay, against a ~10 min identity chain.
        """
        request = QgsFeatureRequest()
        request.setNoAttributes()
        histogram: Dict[str, int] = {}
        for feature in layer.getFeatures(request):
            key = QgsWkbTypes.displayString(feature.geometry().wkbType())
            histogram[key] = histogram.get(key, 0) + 1
        return histogram

    @staticmethod
    def _subprocess_window_kwargs() -> Dict[str, Any]:
        if os.name != "nt":
            return {}

        kwargs: Dict[str, Any] = {
            "creationflags": getattr(subprocess, "CREATE_NO_WINDOW", 0),
        }
        startupinfo_cls = getattr(subprocess, "STARTUPINFO", None)
        if startupinfo_cls is not None:
            startupinfo = startupinfo_cls()
            startupinfo.dwFlags |= getattr(subprocess, "STARTF_USESHOWWINDOW", 0)
            kwargs["startupinfo"] = startupinfo
        return kwargs

    @staticmethod
    def _find_gdal_executable() -> str:
        return require_layer_algebra_gdal(sys.platform)

    @staticmethod
    def _build_gdal_environment(gdal_exe: str) -> Dict[str, str]:
        return build_gdal_environment(gdal_exe)

    def _export_layer_to_gpkg(
        self,
        layer: QgsVectorLayer,
        output_path: Path,
        layer_name: str,
    ) -> None:
        if output_path.exists():
            output_path.unlink()

        export_layer = self._prepare_layer_for_persist(layer, layer_name)
        options = QgsVectorFileWriter.SaveVectorOptions()
        options.driverName = "GPKG"
        options.fileEncoding = "UTF-8"
        options.layerName = layer_name

        error, msg, _, _ = QgsVectorFileWriter.writeAsVectorFormatV3(
            export_layer,
            str(output_path),
            QgsCoordinateTransformContext(),
            options,
        )
        if error != QgsVectorFileWriter.NoError:
            raise RuntimeError(
                f"Failed to export layer '{layer.name()}' to '{output_path}': {msg}"
            )

    def _prepare_layer_for_persist(
        self,
        layer: QgsVectorLayer,
        layer_name: str,
    ) -> QgsVectorLayer:
        uri = f"{QgsWkbTypes.displayString(layer.wkbType())}?crs={layer.crs().authid()}"
        prepared = QgsVectorLayer(uri, layer_name, "memory")
        provider = prepared.dataProvider()
        renamed_fields = []
        used_names = set()
        reserved_names = {"fid", "ogc_fid", "rowid"}

        for field in layer.fields():
            new_name = field.name()
            if new_name.lower() in reserved_names:
                new_name = f"{new_name}_attr"

            candidate = new_name
            suffix = 2
            while candidate.lower() in used_names:
                candidate = f"{new_name}_{suffix}"
                suffix += 1

            field_copy = QgsField(field)
            field_copy.setName(candidate)
            renamed_fields.append(field_copy)
            used_names.add(candidate.lower())

        provider.addAttributes(renamed_fields)
        prepared.updateFields()

        features = []
        for source_feature in layer.getFeatures():
            cloned_feature = QgsFeature(prepared.fields())
            cloned_feature.setId(-1)
            cloned_feature.setAttributes(source_feature.attributes())
            cloned_feature.setGeometry(source_feature.geometry())
            features.append(cloned_feature)

        provider.addFeatures(features)
        prepared.updateExtents()
        return prepared

    def _create_geometry_only_layer(
        self,
        source_layer: QgsVectorLayer,
        layer_name: str,
    ) -> QgsVectorLayer:
        geometry_only = QgsVectorLayer(
            f"{QgsWkbTypes.displayString(source_layer.wkbType())}?crs={source_layer.crs().authid()}",
            layer_name,
            "memory",
        )
        if not geometry_only.isValid():
            raise RuntimeError(f"Could not create selector mask for '{source_layer.name()}'.")

        provider = geometry_only.dataProvider()
        features = []
        for source_feature in source_layer.getFeatures():
            cloned_feature = QgsFeature(geometry_only.fields())
            cloned_feature.setId(-1)
            cloned_feature.setGeometry(source_feature.geometry())
            features.append(cloned_feature)

        provider.addFeatures(features)
        geometry_only.updateExtents()
        return geometry_only

    @staticmethod
    def _sanitize_name(value: str) -> str:
        cleaned = re.sub(r"[^A-Za-z0-9_]+", "_", value or "").strip("_")
        return cleaned or "layer"

    @staticmethod
    def _extract_layername_from_source(source: str) -> str:
        match = re.search(r"\|layername=([^|]+)", source or "")
        return match.group(1) if match else ""

    def _get_layer_table_name(self, layer: Optional[QgsVectorLayer]) -> str:
        if layer is None:
            return ""

        explicit_layer_name = self._extract_layername_from_source(layer.source())
        if explicit_layer_name:
            return explicit_layer_name

        database_path = Path(self.database_path) if self.database_path else None
        if database_path and database_path.exists():
            try:
                import sqlite3

                conn = sqlite3.connect(str(database_path))
                cursor = conn.cursor()
                cursor.execute(
                    "SELECT table_name FROM gpkg_contents WHERE lower(table_name) = lower(?)",
                    (layer.name(),),
                )
                row = cursor.fetchone()
                conn.close()
                if row:
                    return row[0]
            except Exception:
                pass

        return self._sanitize_name(layer.name())

    def _cleanup_target_selection(self) -> None:
        if self.target_layer:
            try:
                self.target_layer.removeSelection()
            except Exception:
                pass
