"""
CDP Prioritization wizard dialog for the QGIS plugin.
"""

from __future__ import annotations

import uuid
from pathlib import Path
from typing import Any, Dict, List, Optional

from ..qt_compat import (
    QDialog,
    QApplication,
    QComboBox,
    QFileDialog,
    QFormLayout,
    QGridLayout,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QMessageBox,
    QProgressDialog,
    QPushButton,
    QTableWidget,
    QTableWidgetItem,
    QTextEdit,
    QVBoxLayout,
    QWizard,
    QWizardPage,
    Qt,
    exec_dialog,
)
from .. import debug_logger
from ..bridge_loader import CDPBridge
from ..bridge_worker import BridgeWorker
from ..models import EngineType, WorkflowStep
from ..parameter_mapper import ModelVariableExtractor
from ..project_metadata_manager import ProjectMetadataManager
from ..rich_comment_editor import RichCommentEditor
from ..translator import tr, add_listener, remove_listener


def _unrated_options():
    return [
        (tr("create_prioritization.unrated_raise"), 0),
        (tr("create_prioritization.unrated_unrated"), 1),
        (tr("create_prioritization.unrated_scale_avg"), 2),
        (tr("create_prioritization.unrated_scale_high"), 3),
        (tr("create_prioritization.unrated_scale_low"), 4),
    ]


def _offscale_options():
    return [
        (tr("create_prioritization.offscale_raise"), 0),
        (tr("create_prioritization.offscale_unrated"), 1),
        (tr("create_prioritization.offscale_clamp"), 2),
    ]


def _unknown_options():
    return _unrated_options()


class CreatePrioritizationDialog(QWizard):
    """Lean CDP prioritization wizard built around the mapping-based bridge contract."""

    # Kept for backwards compatibility; prefer the module-level helpers above.
    UNRATED_OPTIONS = _unrated_options()
    OFFSCALE_OPTIONS = _offscale_options()
    UNKNOWN_OPTIONS = _unknown_options()

    def __init__(
        self,
        *,
        project_id: str,
        assessment_id: str,
        input_layer_name: str,
        input_table_name: str,
        input_fields: List[str],
        engine_path: str,
        input_field_options: Optional[List[Dict[str, Any]]] = None,
        metadata_manager: Optional[ProjectMetadataManager] = None,
        parent=None,
    ):
        super().__init__(parent)
        self.project_id = project_id
        self.assessment_id = assessment_id
        self.input_layer_name = input_layer_name
        self.input_table_name = input_table_name
        self.input_field_options = self._normalize_field_options(input_fields, input_field_options)
        self.input_fields = [option["name"] for option in self.input_field_options]
        self.engine_path = engine_path
        self.metadata_manager = metadata_manager

        self.model_info: Dict[str, Any] = {}
        self.criteria_rows: List[Dict[str, Any]] = []
        self._bridge: Optional[CDPBridge] = None
        self._progress_dialog: Optional[QProgressDialog] = None
        self._active_workers: list = []
        self._result_step: Optional[WorkflowStep] = None
        self._pending_mapping_payload: Optional[Dict[str, Any]] = None
        # Set when the model came from the central registry (provenance item 6):
        # the embedded+referenced model_snapshots id, so the created task is
        # linked to the verified registry version.
        self._registry_model_snapshot_id: str = ""

        self.setWindowTitle(tr("create_prioritization.window_title"))
        # ClassicStyle adapts better to QGIS dark/light themes than ModernStyle.
        self.setWizardStyle(QWizard.WizardStyle.ClassicStyle)
        self.setOption(QWizard.WizardOption.NoBackButtonOnStartPage, True)
        self.setMinimumSize(920, 720)
        self.currentIdChanged.connect(self._on_page_changed)

        # Page handles + retranslatable widgets registered as we build pages.
        self._wizard_pages = []
        self._row_labels = {}
        self._context_labels = {}
        self._init_pages()
        self.retranslateUi()
        add_listener(self.retranslateUi)

    @staticmethod
    def _normalize_field_options(
        input_fields: List[str],
        input_field_options: Optional[List[Dict[str, Any]]],
    ) -> List[Dict[str, Any]]:
        options: List[Dict[str, Any]] = []
        seen: set[str] = set()
        for option in input_field_options or []:
            name = str(option.get("name", "")).strip()
            if not name or name.lower() in seen:
                continue
            seen.add(name.lower())
            options.append({
                "name": name,
                "label": str(option.get("label", name) or name),
                "data_type": str(option.get("data_type", "") or ""),
                "source_type": str(option.get("source_type", "input") or "input"),
                "source_label": str(option.get("source_label", "") or ""),
            })
        for field_name in input_fields or []:
            name = str(field_name).strip()
            if not name or name.lower() in seen:
                continue
            seen.add(name.lower())
            options.append({
                "name": name,
                "label": name,
                "data_type": "",
                "source_type": "input",
                "source_label": "",
            })
        return options

    def _init_pages(self) -> None:
        for page in (
            self._build_details_page(),
            self._build_model_page(),
            self._build_mapping_page(),
            self._build_validation_page(),
            self._build_rules_page(),
            self._build_summary_page(),
        ):
            self.addPage(page)
            self._wizard_pages.append(page)

    def _build_details_page(self) -> QWizardPage:
        page = QWizardPage()
        page.tr_key = ("create_prioritization.page_details_title", "create_prioritization.page_details_subtitle")
        page.setTitle(tr(page.tr_key[0]))
        page.setSubTitle(tr(page.tr_key[1]))
        layout = QVBoxLayout(page)

        context_layout = QGridLayout()
        not_set = tr("create_prioritization.not_set")
        self._context_labels["workspace_label"] = QLabel(tr("create_prioritization.lbl_workspace"))
        self._context_labels["study_label"] = QLabel(tr("create_prioritization.lbl_study"))
        self._context_labels["input_layer_label"] = QLabel(tr("create_prioritization.lbl_input_layer"))
        self._context_labels["input_table_label"] = QLabel(tr("create_prioritization.lbl_input_table"))
        self._context_labels["input_layer_value"] = QLabel(self.input_layer_name or not_set)
        self._context_labels["input_table_value"] = QLabel(self.input_table_name or not_set)
        context_layout.addWidget(self._context_labels["workspace_label"], 0, 0)
        context_layout.addWidget(QLabel(self.project_id), 0, 1)
        context_layout.addWidget(self._context_labels["study_label"], 1, 0)
        context_layout.addWidget(QLabel(self.assessment_id), 1, 1)
        context_layout.addWidget(self._context_labels["input_layer_label"], 2, 0)
        context_layout.addWidget(self._context_labels["input_layer_value"], 2, 1)
        context_layout.addWidget(self._context_labels["input_table_label"], 3, 0)
        context_layout.addWidget(self._context_labels["input_table_value"], 3, 1)
        layout.addLayout(context_layout)

        form = QFormLayout()
        self.prioritization_name_input = QLineEdit()
        self._row_labels["name"] = QLabel(tr("create_prioritization.row_name"))
        form.addRow(self._row_labels["name"], self.prioritization_name_input)
        self.comments_input = RichCommentEditor(self)
        self._row_labels["comments"] = QLabel(tr("create_prioritization.row_comments"))
        form.addRow(self._row_labels["comments"], self.comments_input)
        layout.addLayout(form)

        self.run_immediately_checkbox = QComboBox()
        self.run_immediately_checkbox.addItem(tr("create_prioritization.exec_run"), True)
        self.run_immediately_checkbox.addItem(tr("create_prioritization.exec_create_only"), False)
        self._row_labels["execution"] = QLabel(tr("create_prioritization.row_execution"))
        form.addRow(self._row_labels["execution"], self.run_immediately_checkbox)

        layout.addStretch()
        return page

    def _build_model_page(self) -> QWizardPage:
        page = QWizardPage()
        page.tr_key = ("create_prioritization.page_model_title", "create_prioritization.page_model_subtitle")
        page.setTitle(tr(page.tr_key[0]))
        page.setSubTitle(tr(page.tr_key[1]))
        layout = QVBoxLayout(page)

        row = QHBoxLayout()
        self.model_file_input = QLineEdit()
        self.model_file_input.setPlaceholderText(tr("create_prioritization.model_file_placeholder"))
        row.addWidget(self.model_file_input)
        self.browse_button = QPushButton(tr("create_prioritization.btn_browse"))
        self.browse_button.clicked.connect(self._browse_model_file)
        row.addWidget(self.browse_button)
        self.pick_registry_button = QPushButton(tr("create_prioritization.btn_pick_registry"))
        self.pick_registry_button.setToolTip(tr("create_prioritization.tip_pick_registry"))
        self.pick_registry_button.clicked.connect(self._pick_registry_model)
        row.addWidget(self.pick_registry_button)
        self.load_model_button = QPushButton(tr("create_prioritization.btn_reload_model"))
        self.load_model_button.clicked.connect(self._load_model_schema)
        row.addWidget(self.load_model_button)
        self.load_model_button.hide()
        layout.addLayout(row)

        self.model_summary_label = QLabel(tr("create_prioritization.model_not_loaded"))
        self.model_summary_label.setWordWrap(True)
        layout.addWidget(self.model_summary_label)
        layout.addStretch()
        return page

    def _build_mapping_page(self) -> QWizardPage:
        page = QWizardPage()
        page.tr_key = ("create_prioritization.page_mapping_title", "create_prioritization.page_mapping_subtitle")
        page.setTitle(tr(page.tr_key[0]))
        page.setSubTitle(tr(page.tr_key[1]))
        layout = QVBoxLayout(page)

        button_row = QHBoxLayout()
        self.load_mapping_button = QPushButton(tr("create_prioritization.btn_load_mapping"))
        self.load_mapping_button.clicked.connect(self._load_saved_mapping)
        button_row.addWidget(self.load_mapping_button)
        self.save_mapping_button = QPushButton(tr("create_prioritization.btn_save_mapping"))
        self.save_mapping_button.clicked.connect(self._save_mapping)
        button_row.addWidget(self.save_mapping_button)
        button_row.addStretch()
        layout.addLayout(button_row)

        selector_form = QFormLayout()
        self.display_field_combo = QComboBox()
        self.display_field_combo.addItem("")
        self.display_field_combo.addItems(self.input_fields)
        self._row_labels["display_field"] = QLabel(tr("create_prioritization.row_display_field"))
        selector_form.addRow(self._row_labels["display_field"], self.display_field_combo)
        self.primary_key_combo = QComboBox()
        self.primary_key_combo.addItem("")
        self.primary_key_combo.addItems(self.input_fields)
        self._row_labels["primary_key"] = QLabel(tr("create_prioritization.row_primary_key"))
        selector_form.addRow(self._row_labels["primary_key"], self.primary_key_combo)
        layout.addLayout(selector_form)

        self.mapping_table = QTableWidget(0, 4)
        self.mapping_table.setHorizontalHeaderLabels(self._mapping_table_headers())
        self.mapping_table.horizontalHeader().setStretchLastSection(True)
        layout.addWidget(self.mapping_table, 1)
        return page

    @staticmethod
    def _mapping_table_headers() -> List[str]:
        return [
            tr("create_prioritization.col_model_criterion"),
            tr("create_prioritization.col_criterion_hint"),
            tr("create_prioritization.col_input_field"),
            tr("create_prioritization.col_input_field_source"),
        ]

    def _build_validation_page(self) -> QWizardPage:
        page = QWizardPage()
        page.tr_key = ("create_prioritization.page_validation_title", "create_prioritization.page_validation_subtitle")
        page.setTitle(tr(page.tr_key[0]))
        page.setSubTitle(tr(page.tr_key[1]))
        layout = QFormLayout(page)

        self.validation_hint_label = QLabel(tr("create_prioritization.validation_hint"))
        self.validation_hint_label.setWordWrap(True)
        layout.addRow(self.validation_hint_label)

        self.validation_unrated_combo = QComboBox()
        for label, value in _unrated_options():
            self.validation_unrated_combo.addItem(label, value)
        self.validation_unknown_combo = QComboBox()
        for label, value in _unknown_options():
            self.validation_unknown_combo.addItem(label, value)
        self.validation_offscale_combo = QComboBox()
        for label, value in _offscale_options():
            self.validation_offscale_combo.addItem(label, value)
        self.validation_unrated_combo.setCurrentIndex(2)
        self.validation_unknown_combo.setCurrentIndex(2)
        self.validation_offscale_combo.setCurrentIndex(2)

        self.validation_unrated_combo.setToolTip(tr("create_prioritization.tip_unrated"))
        self.validation_unknown_combo.setToolTip(tr("create_prioritization.tip_unknown"))
        self.validation_offscale_combo.setToolTip(tr("create_prioritization.tip_offscale"))

        self._row_labels["unrated"] = QLabel(tr("create_prioritization.row_unrated"))
        self._row_labels["unknown"] = QLabel(tr("create_prioritization.row_unknown"))
        self._row_labels["offscale"] = QLabel(tr("create_prioritization.row_offscale"))
        layout.addRow(self._row_labels["unrated"], self.validation_unrated_combo)
        layout.addRow(self._row_labels["unknown"], self.validation_unknown_combo)
        layout.addRow(self._row_labels["offscale"], self.validation_offscale_combo)
        return page

    def _build_rules_page(self) -> QWizardPage:
        page = QWizardPage()
        page.tr_key = ("create_prioritization.page_rules_title", "create_prioritization.page_rules_subtitle")
        page.setTitle(tr(page.tr_key[0]))
        page.setSubTitle(tr(page.tr_key[1]))
        layout = QVBoxLayout(page)

        rule_form = QFormLayout()
        self.rules_combo = QComboBox()
        self.rules_combo.addItem(tr("create_prioritization.rule_no_selection"), "")
        self.rules_combo.currentIndexChanged.connect(self._on_rule_changed)
        self._row_labels["rule"] = QLabel(tr("create_prioritization.row_rule"))
        rule_form.addRow(self._row_labels["rule"], self.rules_combo)
        layout.addLayout(rule_form)

        self.rules_description = QTextEdit()
        self.rules_description.setReadOnly(True)
        self.rules_description.setMinimumHeight(160)
        self.rules_description.setPlainText(tr("create_prioritization.rules_initial_help"))
        layout.addWidget(self.rules_description, 1)
        return page

    def _build_summary_page(self) -> QWizardPage:
        page = QWizardPage()
        page.tr_key = ("create_prioritization.page_summary_title", "create_prioritization.page_summary_subtitle")
        page.setTitle(tr(page.tr_key[0]))
        page.setSubTitle(tr(page.tr_key[1]))
        layout = QVBoxLayout(page)
        self.summary_label = QLabel(tr("create_prioritization.summary_default"))
        self.summary_label.setWordWrap(True)
        layout.addWidget(self.summary_label)

        self.weights_header_label = QLabel(tr("create_prioritization.lbl_model_weights"))
        layout.addWidget(self.weights_header_label)
        self.weights_table = QTableWidget(0, 0)
        self.weights_table.setMinimumHeight(140)
        layout.addWidget(self.weights_table, 1)
        layout.addStretch()
        return page

    def retranslateUi(self) -> None:
        self.setWindowTitle(tr("create_prioritization.window_title"))
        for page in self._wizard_pages:
            keys = getattr(page, "tr_key", None)
            if keys:
                page.setTitle(tr(keys[0]))
                page.setSubTitle(tr(keys[1]))
        for key, label in self._row_labels.items():
            tr_key = {
                "name": "create_prioritization.row_name",
                "comments": "create_prioritization.row_comments",
                "execution": "create_prioritization.row_execution",
                "display_field": "create_prioritization.row_display_field",
                "primary_key": "create_prioritization.row_primary_key",
                "unrated": "create_prioritization.row_unrated",
                "unknown": "create_prioritization.row_unknown",
                "offscale": "create_prioritization.row_offscale",
                "rule": "create_prioritization.row_rule",
            }.get(key)
            if tr_key:
                label.setText(tr(tr_key))
        for key, label in self._context_labels.items():
            tr_key = {
                "workspace_label": "create_prioritization.lbl_workspace",
                "study_label": "create_prioritization.lbl_study",
                "input_layer_label": "create_prioritization.lbl_input_layer",
                "input_table_label": "create_prioritization.lbl_input_table",
            }.get(key)
            if tr_key:
                label.setText(tr(tr_key))
        not_set = tr("create_prioritization.not_set")
        if "input_layer_value" in self._context_labels:
            self._context_labels["input_layer_value"].setText(self.input_layer_name or not_set)
        if "input_table_value" in self._context_labels:
            self._context_labels["input_table_value"].setText(self.input_table_name or not_set)
        if hasattr(self, "run_immediately_checkbox"):
            self.run_immediately_checkbox.setItemText(0, tr("create_prioritization.exec_run"))
            self.run_immediately_checkbox.setItemText(1, tr("create_prioritization.exec_create_only"))
        if hasattr(self, "model_file_input"):
            self.model_file_input.setPlaceholderText(tr("create_prioritization.model_file_placeholder"))
        if hasattr(self, "browse_button"):
            self.browse_button.setText(tr("create_prioritization.btn_browse"))
        if hasattr(self, "load_model_button"):
            self.load_model_button.setText(tr("create_prioritization.btn_reload_model"))
        if hasattr(self, "load_mapping_button"):
            self.load_mapping_button.setText(tr("create_prioritization.btn_load_mapping"))
        if hasattr(self, "save_mapping_button"):
            self.save_mapping_button.setText(tr("create_prioritization.btn_save_mapping"))
        if hasattr(self, "mapping_table"):
            self.mapping_table.setHorizontalHeaderLabels(self._mapping_table_headers())
        if hasattr(self, "validation_hint_label"):
            self.validation_hint_label.setText(tr("create_prioritization.validation_hint"))
        for combo, opts_fn, tip_key in (
            (getattr(self, "validation_unrated_combo", None), _unrated_options, "create_prioritization.tip_unrated"),
            (getattr(self, "validation_unknown_combo", None), _unknown_options, "create_prioritization.tip_unknown"),
            (getattr(self, "validation_offscale_combo", None), _offscale_options, "create_prioritization.tip_offscale"),
        ):
            if combo is None:
                continue
            current = combo.currentData()
            combo.blockSignals(True)
            combo.clear()
            for label, value in opts_fn():
                combo.addItem(label, value)
            idx = combo.findData(current)
            if idx >= 0:
                combo.setCurrentIndex(idx)
            combo.blockSignals(False)
            combo.setToolTip(tr(tip_key))
        if hasattr(self, "weights_header_label"):
            self.weights_header_label.setText(tr("create_prioritization.lbl_model_weights"))

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

    def _on_rule_changed(self, _index: int) -> None:
        rule = self.rules_combo.currentData()
        if not isinstance(rule, dict):
            self.rules_description.setPlainText(
                tr("create_prioritization.rules_none_defined_msg")
                if self.rules_combo.count() <= 1
                else tr("create_prioritization.rules_select_msg")
            )
            return
        description = (
            rule.get("Description")
            or rule.get("description")
            or rule.get("RuleType")
            or rule.get("Type")
            or ""
        )
        self.rules_description.setPlainText(str(description) or tr("create_prioritization.rules_no_description"))

    def _populate_rules(self, rules: list) -> None:
        self.rules_combo.blockSignals(True)
        self.rules_combo.clear()
        if not rules:
            self.rules_combo.addItem(tr("create_prioritization.rules_none_defined_combo"), "")
            self.rules_combo.setEnabled(False)
            self.rules_description.setPlainText(tr("create_prioritization.rules_none_defined_msg"))
            self.rules_combo.blockSignals(False)
            return
        self.rules_combo.setEnabled(True)
        self.rules_combo.addItem(tr("create_prioritization.rule_no_selection"), "")
        for rule in rules:
            name = str(
                rule.get("Name")
                or rule.get("name")
                or rule.get("RuleName")
                or tr("create_prioritization.rules_unnamed")
            )
            self.rules_combo.addItem(name, rule)
        self.rules_combo.blockSignals(False)
        self.rules_combo.setCurrentIndex(0)
        self._on_rule_changed(0)

    def _populate_weights(self, weights: list) -> None:
        if not weights:
            self.weights_table.setRowCount(0)
            self.weights_table.setColumnCount(1)
            self.weights_table.setHorizontalHeaderLabels([tr("create_prioritization.weights_header_default")])
            self.weights_table.setRowCount(1)
            self.weights_table.setItem(
                0, 0,
                QTableWidgetItem(tr("create_prioritization.weights_none_defined")),
            )
            return
        columns: list = []
        for entry in weights:
            if isinstance(entry, dict):
                for key in entry.keys():
                    if key not in columns:
                        columns.append(key)
        if not columns:
            columns = [tr("create_prioritization.weights_col_default")]
        self.weights_table.setColumnCount(len(columns))
        self.weights_table.setHorizontalHeaderLabels(columns)
        self.weights_table.setRowCount(len(weights))
        for r, entry in enumerate(weights):
            for c, key in enumerate(columns):
                value = entry.get(key, "") if isinstance(entry, dict) else entry
                self.weights_table.setItem(r, c, QTableWidgetItem(str(value)))
        self.weights_table.resizeColumnsToContents()

    def _show_busy_dialog(self, *, title: str, label: str) -> None:
        if self._progress_dialog is None:
            self._progress_dialog = QProgressDialog(label, None, 0, 0, self)
            self._progress_dialog.setWindowTitle(title)
            self._progress_dialog.setWindowModality(Qt.WindowModality.WindowModal)
            self._progress_dialog.setMinimumDuration(0)
            self._progress_dialog.setAutoClose(False)
            self._progress_dialog.setAutoReset(False)
            self._progress_dialog.show()
        else:
            self._progress_dialog.setWindowTitle(title)
            self._progress_dialog.setLabelText(label)
        QApplication.processEvents()

    def _close_busy_dialog(self) -> None:
        if self._progress_dialog is not None:
            self._progress_dialog.close()
            self._progress_dialog = None
            QApplication.processEvents()

    def _on_worker_progress(self, message: str) -> None:
        if self._progress_dialog is not None:
            self._progress_dialog.setLabelText(message)
            QApplication.processEvents()

    def _browse_model_file(self) -> None:
        model_file, _ = QFileDialog.getOpenFileName(
            self,
            tr("create_prioritization.file_dialog_select"),
            "",
            tr("create_prioritization.file_dialog_filter"),
        )
        if model_file:
            # A manually chosen file is not a registry version; drop any
            # previously-picked registry snapshot linkage.
            self._registry_model_snapshot_id = ""
            self.model_file_input.setText(model_file)
            self._load_model_schema()

    def _pick_registry_model(self) -> None:
        """Pick a published model version from the central registry (item 6).

        On selection: import the version as an embedded+referenced
        model_snapshots row, materialize its bytes to a temp file, set that as
        the chosen model path, and remember the snapshot id so the created task
        is linked to the verified registry model.
        """
        from .model_registry_picker_dialog import ModelRegistryPickerDialog
        from .. import model_registry

        dialog = ModelRegistryPickerDialog(model_kind="cdp", parent=self)
        version = dialog.get_selected_version()
        if version is None:
            return

        snapshot_id = ""
        if self.metadata_manager is not None:
            try:
                snapshot_id = model_registry.import_version_as_snapshot(
                    self.metadata_manager,
                    version,
                    registry_path=dialog.registry_path,
                ) or ""
            except Exception as exc:
                debug_logger.error("Registry snapshot import failed", {"exception": str(exc)})

        try:
            model_path = model_registry.materialize_version(version)
        except Exception as exc:
            QMessageBox.warning(
                self,
                tr("create_prioritization.err_model_required_title"),
                tr("create_prioritization.registry_materialize_failed", error=str(exc)),
            )
            return

        self._registry_model_snapshot_id = snapshot_id
        self.model_file_input.setText(model_path)
        self._load_model_schema()

    def _normalize_model_path(self) -> Path:
        return Path(self.model_file_input.text().strip().strip('"').strip("'")).expanduser()

    def _load_model_schema(self) -> None:
        model_path = self._normalize_model_path()
        if not str(model_path):
            QMessageBox.warning(self, tr("create_prioritization.err_model_required_title"), tr("create_prioritization.err_model_required_body"))
            return
        if not Path(self.engine_path).exists():
            QMessageBox.critical(
                self,
                tr("create_prioritization.err_engine_missing_title"),
                tr("create_prioritization.err_engine_missing_body", path=self.engine_path),
            )
            return
        if not model_path.exists():
            QMessageBox.critical(
                self,
                tr("create_prioritization.err_model_missing_title"),
                tr("create_prioritization.err_model_missing_body", path=model_path),
            )
            return

        self._bridge = CDPBridge(self.engine_path)
        self._show_busy_dialog(
            title=tr("create_prioritization.busy_title_loading"),
            label=tr("create_prioritization.busy_label_loading"),
        )
        self.load_model_button.setEnabled(False)
        worker = BridgeWorker(self._bridge.get_mapping_schema, str(model_path))
        worker.signals.finished.connect(self._on_model_schema_received)
        worker.signals.error.connect(self._on_model_schema_error)
        worker.signals.progress.connect(self._on_worker_progress)
        self._active_workers.append(worker)
        worker.start()

    def _on_model_schema_received(self, result) -> None:
        self._active_workers = [worker for worker in self._active_workers if worker.isRunning()]
        self._close_busy_dialog()
        self.load_model_button.setEnabled(True)
        self.model_info = result or {}
        self.criteria_rows = self.model_info.get("Criteria", []) or []
        self._populate_mapping_table()
        self.model_summary_label.setText(
            tr(
                "create_prioritization.model_loaded",
                name=self.model_info.get("Name", self._normalize_model_path().name),
                count=len(self.criteria_rows),
            )
        )
        self._load_saved_mapping(show_message=False)

        # Best-effort load of model rules and weights for the Rules and
        # Summary pages. Older CDP CLI builds may not implement these
        # commands; the bridge returns [] so the wizard shows placeholders.
        rules: list = []
        weights: list = []
        if self._bridge is not None:
            try:
                rules = self._bridge.get_rules(str(self._normalize_model_path()))
            except Exception:
                rules = []
            try:
                weights = self._bridge.get_weights(str(self._normalize_model_path()))
            except Exception:
                weights = []
        self._populate_rules(rules)
        self._populate_weights(weights)

    def _on_model_schema_error(self, error_message: str) -> None:
        self._active_workers = [worker for worker in self._active_workers if worker.isRunning()]
        self._close_busy_dialog()
        self.load_model_button.setEnabled(True)
        QMessageBox.critical(self, tr("create_prioritization.err_load_failed_title"), error_message)

    def _populate_mapping_table(self) -> None:
        self.mapping_table.setRowCount(len(self.criteria_rows))
        for row_index, criterion in enumerate(self.criteria_rows):
            criterion_name = (
                str(criterion.get("Name", "")).strip()
                or str(criterion.get("ModelFieldName", "")).strip()
                or tr("create_prioritization.criterion_unnamed", index=row_index + 1)
            )
            item = QTableWidgetItem(criterion_name)
            item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
            self.mapping_table.setItem(row_index, 0, item)

            hint_item = QTableWidgetItem(self._format_criterion_hint(criterion))
            hint_item.setFlags(hint_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
            hint_item.setToolTip(self._format_criterion_tooltip(criterion))
            self.mapping_table.setItem(row_index, 1, hint_item)

            combo = QComboBox()
            combo.addItem("")
            combo.addItems(self.input_fields)
            suggested_field = self._suggest_field_for_criterion(criterion_name, criterion)
            if suggested_field:
                combo.setCurrentText(suggested_field)
            combo.currentTextChanged.connect(lambda _text, row=row_index: self._refresh_mapping_source_cell(row))
            self.mapping_table.setCellWidget(row_index, 2, combo)

            source_item = QTableWidgetItem("")
            source_item.setFlags(source_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
            self.mapping_table.setItem(row_index, 3, source_item)
            self._refresh_mapping_source_cell(row_index)
        self.mapping_table.resizeColumnsToContents()

    @staticmethod
    def _field_key(value: str) -> str:
        return "".join(ch for ch in str(value or "").lower() if ch.isalnum())

    def _field_lookup(self) -> Dict[str, str]:
        lookup: Dict[str, str] = {}
        for field_name in self.input_fields:
            lookup.setdefault(str(field_name).strip().lower(), field_name)
            lookup.setdefault(self._field_key(field_name), field_name)
        return lookup

    def _suggest_field_for_criterion(self, criterion_name: str, criterion: Dict[str, Any]) -> str:
        lookup = self._field_lookup()
        hint_values = [
            criterion.get("XID"),
            criterion.get("ModelFieldName"),
            criterion.get("FieldName"),
            criterion_name,
        ]
        for value in hint_values:
            raw = str(value or "").strip()
            if not raw:
                continue
            match = lookup.get(raw.lower()) or lookup.get(self._field_key(raw))
            if match:
                return match

        criterion_key = self._field_key(criterion_name)
        if criterion_key:
            for field_name in self.input_fields:
                field_key = self._field_key(field_name)
                if field_key and (field_key.startswith(criterion_key) or criterion_key.startswith(field_key)):
                    return field_name
            for field_name in self.input_fields:
                field_key = self._field_key(field_name)
                if field_key and len(field_key) > 3 and (field_key in criterion_key or criterion_key in field_key):
                    return field_name

        suggestions = ModelVariableExtractor.suggest_mappings(self.input_fields, [criterion_name])
        return suggestions[0][0] if suggestions else ""

    @staticmethod
    def _format_criterion_hint(criterion: Dict[str, Any]) -> str:
        parts = []
        xid = str(criterion.get("XID", "") or "").strip()
        scale = str(criterion.get("PrimaryScaleName", "") or criterion.get("ScaleName", "") or "").strip()
        hblock = str(criterion.get("HBlockID", "") or "").strip()
        if xid:
            parts.append(f"XID: {xid}")
        if scale:
            parts.append(scale)
        if hblock:
            parts.append(f"HBlockID {hblock}")
        return "; ".join(parts)

    @staticmethod
    def _format_criterion_tooltip(criterion: Dict[str, Any]) -> str:
        lines = []
        for key in ("Name", "XID", "PrimaryScaleName", "ModelWeights", "HBlockID"):
            value = criterion.get(key)
            if value not in (None, ""):
                lines.append(f"{key}: {value}")
        return "\n".join(lines)

    def _field_option(self, field_name: str) -> Dict[str, Any]:
        normalized = str(field_name or "").strip().lower()
        for option in self.input_field_options:
            if option["name"].lower() == normalized:
                return option
        return {
            "name": field_name,
            "label": field_name,
            "data_type": "",
            "source_type": "input",
            "source_label": "",
        }

    def _field_source_text(self, field_name: str) -> str:
        if not field_name:
            return ""
        option = self._field_option(field_name)
        data_type = option.get("data_type", "")
        suffix = f" ({data_type})" if data_type else ""
        source_type = str(option.get("source_type", "input") or "input").lower()
        source_label = str(option.get("source_label", "") or self.input_table_name or self.input_layer_name)
        if source_type == "netweaver":
            return tr("create_prioritization.source_netweaver", source=source_label, suffix=suffix)
        if source_label:
            return tr("create_prioritization.source_input", source=source_label, suffix=suffix)
        return tr("create_prioritization.source_input_only", suffix=suffix)

    def _refresh_mapping_source_cell(self, row_index: int) -> None:
        field_combo = self.mapping_table.cellWidget(row_index, 2)
        source_item = self.mapping_table.item(row_index, 3)
        if field_combo is None or source_item is None:
            return
        field_name = field_combo.currentText().strip()
        source_item.setText(self._field_source_text(field_name))

    def _collect_field_mappings(self) -> List[Dict[str, Any]]:
        mappings: List[Dict[str, Any]] = []
        for row_index in range(self.mapping_table.rowCount()):
            criterion_item = self.mapping_table.item(row_index, 0)
            field_combo = self.mapping_table.cellWidget(row_index, 2)
            if criterion_item is None or field_combo is None:
                continue
            source_column = field_combo.currentText().strip()
            if source_column:
                mappings.append(
                    {
                        "target_variable": criterion_item.text().strip(),
                        "source_column": source_column,
                    }
                )
        return mappings

    def _mapping_payload(self) -> Dict[str, Any]:
        return {
            "prioritization_name": self.prioritization_name_input.text().strip(),
            "comments": self.comments_input.plain_text(),
            "comments_html": self.comments_input.html(),
            "field_mappings": self._collect_field_mappings(),
            "display_field": self.display_field_combo.currentText().strip(),
            "primary_key": self.primary_key_combo.currentText().strip(),
            "validation_settings": {
                "unrated": self.validation_unrated_combo.currentData(),
                "unknown": self.validation_unknown_combo.currentData(),
                "offscale": self.validation_offscale_combo.currentData(),
            },
        }

    def _load_saved_mapping(self, *, show_message: bool = True) -> None:
        if self.metadata_manager is None:
            return
        preset = self.metadata_manager.get_field_mapping_preset(
            project_id=self.project_id,
            engine_type=EngineType.CDP.value,
            model_identifier=str(self._normalize_model_path()),
            input_table_name=self.input_table_name,
        )
        payload = preset.get("payload", {}) if preset else {}
        if not payload:
            if show_message:
                QMessageBox.information(
                    self,
                    tr("create_prioritization.saved_mapping_title"),
                    tr("create_prioritization.saved_no_mapping_found"),
                )
            return
        self.display_field_combo.setCurrentText(str(payload.get("display_field", "")).strip())
        self.primary_key_combo.setCurrentText(str(payload.get("primary_key", "")).strip())
        validation = payload.get("validation_settings", {}) or {}
        for combo, key in (
            (self.validation_unrated_combo, "unrated"),
            (self.validation_unknown_combo, "unknown"),
            (self.validation_offscale_combo, "offscale"),
        ):
            for index in range(combo.count()):
                if combo.itemData(index) == validation.get(key, 2):
                    combo.setCurrentIndex(index)
                    break
        lookup = {
            str(mapping.get("target_variable", "")).strip(): str(mapping.get("source_column", "")).strip()
            for mapping in payload.get("field_mappings", []) or []
        }
        for row_index in range(self.mapping_table.rowCount()):
            criterion_item = self.mapping_table.item(row_index, 0)
            field_combo = self.mapping_table.cellWidget(row_index, 2)
            if criterion_item is None or field_combo is None:
                continue
            field_combo.setCurrentText(lookup.get(criterion_item.text().strip(), ""))
            self._refresh_mapping_source_cell(row_index)
        if show_message:
            QMessageBox.information(
                self,
                tr("create_prioritization.saved_mapping_title"),
                tr("create_prioritization.saved_loaded"),
            )

    def _save_mapping(self) -> None:
        title = tr("create_prioritization.saved_mapping_title")
        if self.metadata_manager is None:
            QMessageBox.warning(self, title, tr("create_prioritization.saved_no_metadata_manager"))
            return
        payload = self._mapping_payload()
        if not payload["field_mappings"]:
            QMessageBox.warning(self, title, tr("create_prioritization.saved_map_at_least_one"))
            return
        if not self.metadata_manager.save_field_mapping_preset(
            project_id=self.project_id,
            engine_type=EngineType.CDP.value,
            model_identifier=str(self._normalize_model_path()),
            input_table_name=self.input_table_name,
            payload=payload,
        ):
            QMessageBox.warning(self, title, tr("create_prioritization.saved_could_not_save"))
            return
        QMessageBox.information(self, title, tr("create_prioritization.saved_saved"))

    def _on_page_changed(self, page_id: int) -> None:
        if page_id == 4:
            mappings = self._collect_field_mappings()
            unnamed = tr("create_prioritization.unnamed")
            not_selected = tr("create_prioritization.not_selected")
            not_set = tr("create_prioritization.not_set")
            self.summary_label.setText(
                "\n".join(
                    [
                        tr("create_prioritization.summary_prioritization", value=self.prioritization_name_input.text().strip() or unnamed),
                        tr("create_prioritization.summary_model", value=self.model_file_input.text().strip() or not_selected),
                        tr("create_prioritization.summary_mapped_criteria", count=len(mappings)),
                        tr("create_prioritization.summary_display_field", value=self.display_field_combo.currentText().strip() or not_set),
                        tr("create_prioritization.summary_primary_key", value=self.primary_key_combo.currentText().strip() or not_set),
                        tr("create_prioritization.summary_input_table", value=self.input_table_name or not_set),
                    ]
                )
            )

    def validateCurrentPage(self) -> bool:
        current_id = self.currentId()
        title = tr("create_prioritization.validation_title")
        if current_id == 0 and not self.prioritization_name_input.text().strip():
            QMessageBox.warning(self, title, tr("create_prioritization.validation_name_required"))
            return False
        if current_id == 1 and not self.criteria_rows:
            QMessageBox.warning(self, title, tr("create_prioritization.validation_load_model"))
            return False
        if current_id == 2:
            if not self._collect_field_mappings():
                QMessageBox.warning(self, title, tr("create_prioritization.validation_map_at_least_one"))
                return False
            if not self.display_field_combo.currentText().strip():
                QMessageBox.warning(self, title, tr("create_prioritization.validation_choose_display"))
                return False
        return super().validateCurrentPage()

    def accept(self) -> None:
        parameters = {
            "project_id": self.project_id,
            "assessment_id": self.assessment_id,
            "input_layer_name": self.input_layer_name,
            "input_table_name": self.input_table_name,
            "model_file": self.model_file_input.text().strip(),
            "prioritization_name": self.prioritization_name_input.text().strip(),
            "comments": self.comments_input.plain_text(),
            "comments_html": self.comments_input.html(),
            "field_mappings": self._collect_field_mappings(),
            "display_field": self.display_field_combo.currentText().strip(),
            "primary_key": self.primary_key_combo.currentText().strip(),
            "validation_settings": {
                "unrated": self.validation_unrated_combo.currentData(),
                "unknown": self.validation_unknown_combo.currentData(),
                "offscale": self.validation_offscale_combo.currentData(),
            },
            "run_immediately": bool(self.run_immediately_checkbox.currentData()),
        }
        if self._registry_model_snapshot_id:
            parameters["model_snapshot_id"] = self._registry_model_snapshot_id

        self._result_step = WorkflowStep(
            id=str(uuid.uuid4()),
            name=parameters["prioritization_name"],
            engine_type=EngineType.CDP,
            parameters=parameters,
        )
        if self._registry_model_snapshot_id:
            self._result_step.metadata["model_snapshot_id"] = self._registry_model_snapshot_id

        if self.metadata_manager is not None:
            self.metadata_manager.save_field_mapping_preset(
                project_id=self.project_id,
                engine_type=EngineType.CDP.value,
                model_identifier=str(self._normalize_model_path()),
                input_table_name=self.input_table_name,
                payload=self._mapping_payload(),
            )

        super().accept()

    def get_workflow_step(self) -> Optional[WorkflowStep]:
        if exec_dialog(self) == QDialog.DialogCode.Accepted:
            return self._result_step
        return None

    def prime_from_step(self, step) -> None:
        """Hydrate the wizard fields from an existing WorkflowDefinitionStep.

        Mirrors the C# PrimeCdpConfigureOnlyAsync pattern. Restores the
        simple text/combo fields; the rules-driven field mapping table
        depends on the model being loaded — the user can click "Load Model"
        to repopulate that.
        """
        if step is None:
            return
        try:
            config = getattr(step, "config", None) or {}
            params = config.get("task_parameters") or {}

            name = str(params.get("prioritization_name") or step.name or "").strip()
            if name:
                self.prioritization_name_input.setText(name)

            model_file = str(params.get("model_file") or config.get("model_reference") or "").strip()
            if model_file:
                self.model_file_input.setText(model_file)

            comments = str(params.get("comments") or "").strip()
            comments_html = str(params.get("comments_html") or "")
            if comments or comments_html:
                self.comments_input.set_content(comments, comments_html)

            display_field = str(params.get("display_field") or "").strip()
            if display_field:
                idx = self.display_field_combo.findText(display_field)
                if idx >= 0:
                    self.display_field_combo.setCurrentIndex(idx)

            primary_key = str(params.get("primary_key") or "").strip()
            if primary_key:
                idx = self.primary_key_combo.findText(primary_key)
                if idx >= 0:
                    self.primary_key_combo.setCurrentIndex(idx)

            validation = params.get("validation_settings") or {}
            for combo_attr, key in (
                ("validation_unrated_combo", "unrated"),
                ("validation_unknown_combo", "unknown"),
                ("validation_offscale_combo", "offscale"),
            ):
                combo = getattr(self, combo_attr, None)
                value = validation.get(key)
                if combo is None or value is None:
                    continue
                idx = combo.findData(value)
                if idx >= 0:
                    combo.setCurrentIndex(idx)

            run_immediately = params.get("run_immediately")
            if run_immediately is not None:
                idx = self.run_immediately_checkbox.findData(bool(run_immediately))
                if idx >= 0:
                    self.run_immediately_checkbox.setCurrentIndex(idx)
        except Exception:
            pass
