"""
Wizard-based Create Study Area dialog for **raster** studies.

Parallel to create_assessment_dialog.py (the vector equivalent), but
wired to the raster engine path:

  - Input picker shows only QgsRasterLayer instances.
  - Per-input editor lets the user choose which bands and which RAT
    columns to carry forward into the overlay output.
  - Alignment panel captures snap-to / resampling / output config.
  - On OK the dialog assembles a Study(kind=RASTER, input_layer_refs=[…],
    raster_overlay=RasterOverlaySpec(...)) and dispatches the overlay
    via RasterOverlay.run — direct analogue of ArcGIS Pro's Spatial
    Analyst "Combine" tool, implemented here via GRASS r.cross.

The pure-Python builder `build_raster_study_from_form_state` is
factored out so it can be unit-tested without Qt / QGIS. Everything
that touches Qt or QGIS APIs sits behind that boundary.
"""

from __future__ import annotations

from typing import Dict, List, Optional

from ..qt_compat import (
    QDialog,
    QAbstractItemView,
    QCheckBox,
    QComboBox,
    QFileDialog,
    QFormLayout,
    QGroupBox,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QListWidget,
    QListWidgetItem,
    QMessageBox,
    QPushButton,
    QSizePolicy,
    QTextEdit,
    QVBoxLayout,
    QWidget,
    QWizard,
    QWizardPage,
    Qt,
    exec_dialog,
)

import html as _html

from .. import debug_logger
from ..layer_kind import LayerKindResolver
from ..models import Assessment
from ..rat_io import RatReader
from ..rich_comment_editor import RichCommentEditor
from ..raster_overlay import RasterOverlay, RasterOverlayError

# Pure-Python builder + form-state live in raster_study_form so the
# validation rules stay testable without Qt. The dialog snapshots
# widget state into RasterStudyFormState then runs it through the
# builder on Accept.
from ..raster_study_form import (
    InputCarryForward as _InputCarryForward,
    OUTPUT_FORMAT_OPTIONS,
    RESAMPLING_OPTIONS,
    RasterStudyFormError,
    RasterStudyFormState,
    build_raster_study_from_form_state,
)
from ..translator import tr, add_listener, remove_listener


# ---- Qt dialog -------------------------------------------------------


class CreateRasterStudyAreaDialog(QWizard):
    """Three-page wizard for building a raster study area."""

    def __init__(self, parent=None) -> None:
        super().__init__(parent)
        self.setWindowTitle(tr("create_raster.window_title"))
        self.setWizardStyle(QWizard.WizardStyle.ModernStyle)
        self.setMinimumSize(900, 680)
        # Page handles + row labels stored on self so retranslateUi() can find them.
        self._inputs_page: Optional[QWizardPage] = None
        self._alignment_page: Optional[QWizardPage] = None
        self._summary_page: Optional[QWizardPage] = None
        self._inputs_row_labels: Dict[str, QLabel] = {}
        self._alignment_row_labels: Dict[str, QLabel] = {}
        self._available_layers_label: Optional[QLabel] = None
        self._browse_button: Optional[QPushButton] = None

        # Form widgets (built in _build_pages)
        self._name_edit: Optional[QLineEdit] = None
        self._description_edit: Optional[QTextEdit] = None
        self._input_picker: Optional[QListWidget] = None
        self._per_input_box: Optional[QGroupBox] = None
        self._per_input_layout: Optional[QVBoxLayout] = None
        self._per_input_widgets: Dict[str, "_PerInputEditor"] = {}
        self._snap_to_combo: Optional[QComboBox] = None
        self._resampling_combo: Optional[QComboBox] = None
        self._output_format_combo: Optional[QComboBox] = None
        self._output_path_edit: Optional[QLineEdit] = None
        self._summary_label: Optional[QLabel] = None

        # Returned via get_study() after Accept.
        self._study: Optional[Assessment] = None
        self._overlay_result_path: str = ""

        self._build_pages()
        self.retranslateUi()
        add_listener(self.retranslateUi)

    # ---- Page construction --------------------------------------------

    def _build_pages(self) -> None:
        self._inputs_page = self._build_inputs_page()
        self.addPage(self._inputs_page)
        self._alignment_page = self._build_alignment_page()
        self.addPage(self._alignment_page)
        self._summary_page = self._build_summary_page()
        self.addPage(self._summary_page)
        self.currentIdChanged.connect(self._on_page_changed)

    def _build_inputs_page(self) -> QWizardPage:
        page = QWizardPage()
        page.setTitle(tr("create_raster.page_inputs_title"))
        page.setSubTitle(tr("create_raster.page_inputs_subtitle"))
        outer = QVBoxLayout()

        # Name + description
        form = QFormLayout()
        self._name_edit = QLineEdit()
        self._name_edit.setPlaceholderText(tr("create_raster.name_placeholder"))
        lbl_name = QLabel(tr("create_raster.row_name"))
        self._inputs_row_labels["name"] = lbl_name
        form.addRow(lbl_name, self._name_edit)
        self._description_edit = RichCommentEditor(self)
        lbl_desc = QLabel(tr("create_raster.row_description"))
        self._inputs_row_labels["description"] = lbl_desc
        form.addRow(lbl_desc, self._description_edit)
        outer.addLayout(form)

        # Raster input picker
        self._available_layers_label = QLabel(tr("create_raster.lbl_available_layers"))
        outer.addWidget(self._available_layers_label)
        self._input_picker = QListWidget()
        self._input_picker.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
        for name in self._collect_raster_layer_names():
            QListWidgetItem(name, self._input_picker)
        self._input_picker.itemSelectionChanged.connect(self._rebuild_per_input_editors)
        outer.addWidget(self._input_picker)

        # Per-input carry-forward editors (filled dynamically)
        self._per_input_box = QGroupBox(tr("create_raster.group_carry_forward"))
        self._per_input_layout = QVBoxLayout()
        self._per_input_box.setLayout(self._per_input_layout)
        outer.addWidget(self._per_input_box)

        page.setLayout(outer)
        return page

    def _build_alignment_page(self) -> QWizardPage:
        page = QWizardPage()
        page.setTitle(tr("create_raster.page_alignment_title"))
        page.setSubTitle(tr("create_raster.page_alignment_subtitle"))
        form = QFormLayout()
        self._snap_to_combo = QComboBox()
        lbl_snap = QLabel(tr("create_raster.row_snap_to"))
        self._alignment_row_labels["snap_to"] = lbl_snap
        form.addRow(lbl_snap, self._snap_to_combo)
        self._resampling_combo = QComboBox()
        for opt in RESAMPLING_OPTIONS:
            self._resampling_combo.addItem(opt)
        lbl_resample = QLabel(tr("create_raster.row_resampling"))
        self._alignment_row_labels["resampling"] = lbl_resample
        form.addRow(lbl_resample, self._resampling_combo)
        self._output_format_combo = QComboBox()
        for opt in OUTPUT_FORMAT_OPTIONS:
            self._output_format_combo.addItem(opt)
        lbl_format = QLabel(tr("create_raster.row_output_format"))
        self._alignment_row_labels["output_format"] = lbl_format
        form.addRow(lbl_format, self._output_format_combo)

        path_row = QHBoxLayout()
        self._output_path_edit = QLineEdit()
        self._output_path_edit.setPlaceholderText(tr("create_raster.output_path_placeholder"))
        path_row.addWidget(self._output_path_edit)
        self._browse_button = QPushButton(tr("create_raster.btn_browse"))
        self._browse_button.clicked.connect(self._browse_output_path)
        path_row.addWidget(self._browse_button)
        lbl_output = QLabel(tr("create_raster.row_output_raster"))
        self._alignment_row_labels["output_raster"] = lbl_output
        form.addRow(lbl_output, path_row)

        outer = QVBoxLayout()
        outer.addLayout(form)
        outer.addStretch(1)
        page.setLayout(outer)
        return page

    def _build_summary_page(self) -> QWizardPage:
        page = QWizardPage()
        page.setTitle(tr("create_raster.page_summary_title"))
        page.setSubTitle(tr("create_raster.page_summary_subtitle"))
        outer = QVBoxLayout()
        self._summary_label = QLabel("")
        self._summary_label.setWordWrap(True)
        self._summary_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
        outer.addWidget(self._summary_label)
        outer.addStretch(1)
        page.setLayout(outer)
        return page

    def retranslateUi(self) -> None:
        self.setWindowTitle(tr("create_raster.window_title"))
        if self._inputs_page is not None:
            self._inputs_page.setTitle(tr("create_raster.page_inputs_title"))
            self._inputs_page.setSubTitle(tr("create_raster.page_inputs_subtitle"))
        if self._alignment_page is not None:
            self._alignment_page.setTitle(tr("create_raster.page_alignment_title"))
            self._alignment_page.setSubTitle(tr("create_raster.page_alignment_subtitle"))
        if self._summary_page is not None:
            self._summary_page.setTitle(tr("create_raster.page_summary_title"))
            self._summary_page.setSubTitle(tr("create_raster.page_summary_subtitle"))
        if self._name_edit is not None:
            self._name_edit.setPlaceholderText(tr("create_raster.name_placeholder"))
        if self._output_path_edit is not None:
            self._output_path_edit.setPlaceholderText(tr("create_raster.output_path_placeholder"))
        if self._browse_button is not None:
            self._browse_button.setText(tr("create_raster.btn_browse"))
        if self._available_layers_label is not None:
            self._available_layers_label.setText(tr("create_raster.lbl_available_layers"))
        if self._per_input_box is not None:
            self._per_input_box.setTitle(tr("create_raster.group_carry_forward"))
        for key, label in self._inputs_row_labels.items():
            label.setText(tr(f"create_raster.row_{key}"))
        for key, label in self._alignment_row_labels.items():
            label.setText(tr(f"create_raster.row_{key}"))
        for editor in self._per_input_widgets.values():
            editor.retranslateUi()
        # Refresh summary if it's currently visible.
        if self._summary_label is not None and self.currentPage() is self._summary_page:
            self._summary_label.setText(self._render_summary_text())

    def closeEvent(self, event):
        remove_listener(self.retranslateUi)
        super().closeEvent(event)

    # ---- Per-input editor management ----------------------------------

    def _rebuild_per_input_editors(self) -> None:
        # Tear down old editors that are no longer selected.
        selected_names = [item.text() for item in self._input_picker.selectedItems()]
        for name in list(self._per_input_widgets.keys()):
            if name not in selected_names:
                editor = self._per_input_widgets.pop(name)
                editor.setParent(None)

        # Add editors for newly selected layers.
        for name in selected_names:
            if name in self._per_input_widgets:
                continue
            editor = _PerInputEditor(name)
            self._per_input_widgets[name] = editor
            self._per_input_layout.addWidget(editor)

        # Refresh snap-to combo to match the current input set.
        if self._snap_to_combo is not None:
            self._snap_to_combo.clear()
            for name in selected_names:
                self._snap_to_combo.addItem(name)

    # ---- Page-change hook + summary rendering -------------------------

    def _on_page_changed(self, _page_id: int) -> None:
        # Refresh the summary every time we land on the summary page.
        if self.currentPage() is self.page(self.pageIds()[-1]):
            self._summary_label.setText(self._render_summary_text())

    def _render_summary_text(self) -> str:
        try:
            state = self._snapshot_form_state()
        except Exception as exc:
            return tr("create_raster.summary_cannot_build", error=exc)
        # User-entered values are ESCAPED — this string renders as rich text,
        # so a name/description containing markup must not inject into it.
        lines = [
            f"<b>{tr('create_raster.summary_name')}</b> {_html.escape(state.name)}",
            f"<b>{tr('create_raster.summary_description')}</b> "
            f"{_html.escape(state.description) or tr('create_raster.summary_none')}",
            f"<b>{tr('create_raster.summary_inputs')}</b>",
        ]
        for inp in state.inputs:
            bits = []
            if inp.bands_to_carry:
                bits.append(f"bands={inp.bands_to_carry}")
            if inp.rat_columns_to_carry:
                bits.append(f"rat={inp.rat_columns_to_carry}")
            lines.append(f"&nbsp;&nbsp;• {inp.name} ({inp.role}): {', '.join(bits) or '—'}")
        lines += [
            f"<b>{tr('create_raster.summary_snap_to')}</b> {state.snap_to or tr('create_raster.summary_snap_first_input')}",
            f"<b>{tr('create_raster.summary_resampling')}</b> {state.resampling}",
            f"<b>{tr('create_raster.summary_output')}</b> {state.output_format} → {state.output_path or tr('create_raster.summary_output_unset')}",
        ]
        return "<br>".join(lines)

    # ---- Form-state extraction (Qt → pure dataclass) ------------------

    def _snapshot_form_state(self) -> RasterStudyFormState:
        inputs: List[_InputCarryForward] = []
        for item in self._input_picker.selectedItems():
            name = item.text()
            editor = self._per_input_widgets.get(name)
            if editor is None:
                continue
            inputs.append(editor.snapshot())
        return RasterStudyFormState(
            name=self._name_edit.text() if self._name_edit else "",
            description=(
                self._description_edit.plain_text()
                if self._description_edit else ""
            ),
            description_html=(
                self._description_edit.html()
                if self._description_edit else ""
            ),
            inputs=inputs,
            snap_to=(
                self._snap_to_combo.currentText()
                if self._snap_to_combo else ""
            ),
            resampling=(
                self._resampling_combo.currentText()
                if self._resampling_combo else "nearest"
            ),
            output_format=(
                self._output_format_combo.currentText()
                if self._output_format_combo else "GTiff"
            ),
            output_path=(
                self._output_path_edit.text() if self._output_path_edit else ""
            ),
        )

    # ---- Wizard finish: run the overlay -------------------------------

    def accept(self) -> None:
        try:
            state = self._snapshot_form_state()
            study = build_raster_study_from_form_state(state)
        except RasterStudyFormError as exc:
            QMessageBox.warning(self, tr("create_raster.err_cannot_create_title"), str(exc))
            return

        try:
            # Run the overlay synchronously for the initial cut.
            # QgsTask wrapping is a polish item (added when we see how
            # long real workspaces take). The dialog blocks during the
            # run; on large rasters QGIS will show its native busy
            # cursor.
            overlay = RasterOverlay()
            result = overlay.run(study)
            self._overlay_result_path = result.output_raster_path
            debug_logger.success(
                "Raster overlay completed",
                {
                    "output_path": result.output_raster_path,
                    "unique_combinations": result.unique_combinations,
                },
            )
        except RasterOverlayError as exc:
            QMessageBox.critical(self, tr("create_raster.err_overlay_failed_title"), str(exc))
            return
        except Exception as exc:  # pragma: no cover — defensive
            QMessageBox.critical(
                self,
                tr("create_raster.err_overlay_failed_title"),
                tr("create_raster.err_overlay_unexpected", error=exc),
            )
            return

        self._study = study
        super().accept()

    # ---- Public accessors ---------------------------------------------

    def get_study(self) -> Optional[Assessment]:
        """Return the persisted Study after exec(), or None on cancel."""
        if exec_dialog(self) != QDialog.DialogCode.Accepted:
            return None
        return self._study

    @property
    def overlay_result_path(self) -> str:
        """Filesystem path of the overlay raster produced on Accept."""
        return self._overlay_result_path

    # ---- QGIS helpers --------------------------------------------------

    @staticmethod
    def _collect_raster_layer_names() -> List[str]:
        """List names of raster layers currently in the QGIS project.

        Defensive: returns [] if QGIS isn't importable (test contexts).
        """
        try:
            from qgis.core import QgsProject  # type: ignore
        except ImportError:
            return []
        return sorted(
            layer.name()
            for layer in QgsProject.instance().mapLayers().values()
            if LayerKindResolver.is_raster(layer)
        )

    def _browse_output_path(self) -> None:
        path, _ = QFileDialog.getSaveFileName(
            self,
            tr("create_raster.file_dialog_title"),
            self._output_path_edit.text() if self._output_path_edit else "",
            tr("create_raster.file_dialog_filter"),
        )
        if path and self._output_path_edit is not None:
            self._output_path_edit.setText(path)


# ---- Per-input editor widget -----------------------------------------


class _PerInputEditor(QWidget):
    """Bands + RAT columns picker for a single input raster layer."""

    def __init__(self, layer_name: str, parent=None) -> None:
        super().__init__(parent)
        self._layer_name = layer_name
        self._band_indices: List[int] = list(self._enumerate_bands(layer_name))
        self._rat_empty: bool = False
        outer = QVBoxLayout()
        outer.setContentsMargins(4, 4, 4, 4)

        header = QHBoxLayout()
        header.addWidget(QLabel(f"<b>{layer_name}</b>"))
        self._role_combo = QComboBox()
        # Phase 3.5b: canonical role vocabulary; default to "overlay".
        # Items store role identifiers (overlay/target) as data; labels are translated.
        self._role_combo.addItem(tr("create_raster.role_overlay"), "overlay")
        self._role_combo.addItem(tr("create_raster.role_target"), "target")
        self._role_label = QLabel(tr("create_raster.role_label"))
        header.addWidget(self._role_label)
        header.addWidget(self._role_combo)
        header.addStretch(1)
        outer.addLayout(header)

        # Bands picker — populated from the live layer at construction.
        bands_row = QHBoxLayout()
        self._bands_label = QLabel(tr("create_raster.lbl_carry_bands"))
        bands_row.addWidget(self._bands_label)
        self._band_checkboxes: List[QCheckBox] = []
        for band_index in self._band_indices:
            cb = QCheckBox(tr("create_raster.band_label", index=band_index))
            cb.setProperty("band_index", band_index)
            if band_index == 1:
                cb.setChecked(True)  # sensible default
            self._band_checkboxes.append(cb)
            bands_row.addWidget(cb)
        bands_row.addStretch(1)
        outer.addLayout(bands_row)

        # RAT columns picker — populated from the layer's RAT if it has one.
        self._rat_header_label = QLabel(tr("create_raster.lbl_carry_rat"))
        outer.addWidget(self._rat_header_label)
        self._rat_list = QListWidget()
        self._rat_list.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
        self._rat_list.setMaximumHeight(120)
        for col in self._enumerate_rat_columns(layer_name):
            QListWidgetItem(col, self._rat_list)
        if self._rat_list.count() == 0:
            self._rat_list.addItem(QListWidgetItem(tr("create_raster.no_rat_detected")))
            self._rat_list.setEnabled(False)
            self._rat_empty = True
        outer.addWidget(self._rat_list)

        self.setLayout(outer)
        self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)

    def retranslateUi(self) -> None:
        if hasattr(self, "_role_label"):
            self._role_label.setText(tr("create_raster.role_label"))
        if hasattr(self, "_role_combo"):
            self._role_combo.setItemText(0, tr("create_raster.role_overlay"))
            self._role_combo.setItemText(1, tr("create_raster.role_target"))
        if hasattr(self, "_bands_label"):
            self._bands_label.setText(tr("create_raster.lbl_carry_bands"))
        for cb in self._band_checkboxes:
            idx = cb.property("band_index")
            if idx is not None:
                cb.setText(tr("create_raster.band_label", index=idx))
        if hasattr(self, "_rat_header_label"):
            self._rat_header_label.setText(tr("create_raster.lbl_carry_rat"))
        if self._rat_empty and self._rat_list.count() >= 1:
            self._rat_list.item(0).setText(tr("create_raster.no_rat_detected"))

    def snapshot(self) -> _InputCarryForward:
        bands_to_carry = [
            int(cb.property("band_index"))
            for cb in self._band_checkboxes
            if cb.isChecked() and cb.property("band_index") is not None
        ]
        rat_columns_to_carry: List[str] = []
        if self._rat_list.isEnabled():
            for item in self._rat_list.selectedItems():
                rat_columns_to_carry.append(item.text())
        return _InputCarryForward(
            name=self._layer_name,
            bands_to_carry=bands_to_carry,
            rat_columns_to_carry=rat_columns_to_carry,
            role=str(self._role_combo.currentData() or "overlay"),
        )

    # ---- QGIS helpers --------------------------------------------------

    @staticmethod
    def _enumerate_bands(layer_name: str) -> List[int]:
        """Return 1-based band indices for the named layer (or [1] fallback)."""
        try:
            from qgis.core import QgsProject  # type: ignore
        except ImportError:
            return [1]
        layers = QgsProject.instance().mapLayersByName(layer_name)
        if not layers:
            return [1]
        layer = layers[0]
        try:
            return list(range(1, int(layer.bandCount()) + 1))
        except Exception:
            return [1]

    @staticmethod
    def _enumerate_rat_columns(layer_name: str) -> List[str]:
        """Read the RAT once at construction time so the picker is populated.

        Returns [] if the layer has no RAT, no value column, or QGIS
        isn't importable.
        """
        try:
            from qgis.core import QgsProject  # type: ignore
        except ImportError:
            return []
        layers = QgsProject.instance().mapLayersByName(layer_name)
        if not layers:
            return []
        try:
            df = RatReader().read(layers[0], band=1)
        except Exception:
            return []
        if df is None or df.empty:
            return []
        # Hide the join-key column from the user picker — it's always
        # carried implicitly as the per-input value column in the output.
        return [col for col in df.columns if col.lower() != "value"]
