"""
Create Portfolio wizard dialog for the QGIS plugin.

Port of the ArcGIS Pro ``CreateNewPortfolio.xaml`` wizard into a Qt/QWizard
flow that feeds the pure-Python ``PortfolioBridge``. The original five
accordion panes ("Welcome", "Specify Costs and Benefits", "Define Budget
and Strategy", "Funding Categories", "Finish") map 1:1 to QWizardPages
here.

Unlike NetWeaver/CDP, there is no model file to load -- Portfolio runs an
in-process greedy-knapsack algorithm. The wizard's job is purely to
collect the parameter bundle that the executor will hand to the bridge.
"""

from __future__ import annotations

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

from ..qt_compat import (
    QDialog,
    QButtonGroup,
    QCheckBox,
    QComboBox,
    QDoubleSpinBox,
    QFormLayout,
    QGridLayout,
    QGroupBox,
    QHBoxLayout,
    QHeaderView,
    QLabel,
    QLineEdit,
    QMessageBox,
    QPushButton,
    QRadioButton,
    QTableWidget,
    QTableWidgetItem,
    QTextEdit,
    QVBoxLayout,
    QWizard,
    QWizardPage,
    Qt,
    exec_dialog,
)

from .. import debug_logger
from ..models import EngineType, WorkflowStep
from ..project_metadata_manager import ProjectMetadataManager
from ..rich_comment_editor import RichCommentEditor
from ..translator import tr, add_listener, remove_listener


# Keep these in sync with portfolio_engine.STRATEGY_* / DIRECTION_* so the
# wizard stores values the bridge parameter parser will understand without
# further remapping.
STRATEGY_COSTBENEFIT = "costbenefit"
STRATEGY_COST = "cost"
STRATEGY_BENEFIT = "benefit"
STRATEGY_PRIORITIZATION = "prioritization"
STRATEGY_PRIORITIZATION_COST = "prioritization_cost"

DIRECTION_UP = "up"      # Ascending (low to high)
DIRECTION_DOWN = "down"  # Descending (high to low)

def _strategy_options():
    """Return strategy choices (label, value) in the active language."""
    return [
        (tr(f"portfolio_strategies.{STRATEGY_COSTBENEFIT}"), STRATEGY_COSTBENEFIT),
        (tr(f"portfolio_strategies.{STRATEGY_COST}"), STRATEGY_COST),
        (tr(f"portfolio_strategies.{STRATEGY_BENEFIT}"), STRATEGY_BENEFIT),
        (tr(f"portfolio_strategies.{STRATEGY_PRIORITIZATION}"), STRATEGY_PRIORITIZATION),
        (tr(f"portfolio_strategies.{STRATEGY_PRIORITIZATION_COST}"), STRATEGY_PRIORITIZATION_COST),
    ]


# Module-level snapshot for backwards compatibility.
STRATEGY_OPTIONS = _strategy_options()


class CreatePortfolioDialog(QWizard):
    """Wizard for creating a Portfolio task using the pure-Python engine."""

    def __init__(
        self,
        *,
        project_id: str,
        assessment_id: str,
        input_layer_name: str,
        input_table_name: str,
        input_fields: List[str],
        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_fields = list(input_fields or [])
        self.metadata_manager = metadata_manager

        self._result_step: Optional[WorkflowStep] = None
        self._pending_payload: Optional[Dict[str, Any]] = None

        self.setWindowTitle(tr("create_portfolio.window_title"))
        self.setWizardStyle(QWizard.WizardStyle.ClassicStyle)
        self.setOption(QWizard.WizardOption.NoBackButtonOnStartPage, True)
        self.setMinimumSize(820, 620)
        self.currentIdChanged.connect(self._on_page_changed)

        self._wizard_pages = []
        self._tr_labels = {}  # arbitrary widgets keyed by translation key
        self._init_pages()
        self._load_saved_settings_from_store(show_message=False)
        self.retranslateUi()
        add_listener(self.retranslateUi)

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

    def _init_pages(self) -> None:
        for page in (
            self._build_details_page(),
            self._build_costs_benefits_page(),
            self._build_budget_strategy_page(),
            self._build_funding_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_portfolio.page_details_title", "create_portfolio.page_details_subtitle")
        page.setTitle(tr(page.tr_key[0]))
        page.setSubTitle(tr(page.tr_key[1]))

        layout = QVBoxLayout(page)

        self.context_group = QGroupBox(tr("create_portfolio.context_group"))
        context_layout = QGridLayout(self.context_group)
        not_set = tr("create_portfolio.not_set")
        self._tr_labels["lbl_workspace"] = QLabel(tr("create_portfolio.lbl_workspace"))
        self._tr_labels["lbl_study"] = QLabel(tr("create_portfolio.lbl_study"))
        self._tr_labels["lbl_input_layer"] = QLabel(tr("create_portfolio.lbl_input_layer"))
        self._tr_labels["lbl_input_table"] = QLabel(tr("create_portfolio.lbl_input_table"))
        self._input_layer_value_label = QLabel(self.input_layer_name or not_set)
        self._input_table_value_label = QLabel(self.input_table_name or not_set)
        context_layout.addWidget(self._tr_labels["lbl_workspace"], 0, 0)
        context_layout.addWidget(QLabel(self.project_id), 0, 1)
        context_layout.addWidget(self._tr_labels["lbl_study"], 1, 0)
        context_layout.addWidget(QLabel(self.assessment_id), 1, 1)
        context_layout.addWidget(self._tr_labels["lbl_input_layer"], 2, 0)
        context_layout.addWidget(self._input_layer_value_label, 2, 1)
        context_layout.addWidget(self._tr_labels["lbl_input_table"], 3, 0)
        context_layout.addWidget(self._input_table_value_label, 3, 1)
        layout.addWidget(self.context_group)

        form = QFormLayout()
        self.portfolio_name_input = QLineEdit()
        self.portfolio_name_input.setPlaceholderText(tr("create_portfolio.name_placeholder"))
        self._tr_labels["row_name"] = QLabel(tr("create_portfolio.row_name"))
        form.addRow(self._tr_labels["row_name"], self.portfolio_name_input)

        self.comments_input = RichCommentEditor(self)
        self._tr_labels["row_comments"] = QLabel(tr("create_portfolio.row_comments"))
        form.addRow(self._tr_labels["row_comments"], self.comments_input)

        self.unique_field_combo = QComboBox()
        self.unique_field_combo.addItem("")
        self.unique_field_combo.addItems(self.input_fields)
        self._tr_labels["row_unique_field"] = QLabel(tr("create_portfolio.row_unique_field"))
        form.addRow(self._tr_labels["row_unique_field"], self.unique_field_combo)

        layout.addLayout(form)

        self.run_immediately_checkbox = QCheckBox(tr("create_portfolio.run_immediately"))
        self.run_immediately_checkbox.setChecked(True)
        layout.addWidget(self.run_immediately_checkbox)

        layout.addStretch()
        return page

    def _build_costs_benefits_page(self) -> QWizardPage:
        page = QWizardPage()
        page.tr_key = ("create_portfolio.page_costs_title", "create_portfolio.page_costs_subtitle")
        page.setTitle(tr(page.tr_key[0]))
        page.setSubTitle(tr(page.tr_key[1]))

        form = QFormLayout(page)

        self.benefit_field_combo = QComboBox()
        self.benefit_field_combo.addItem("")
        self.benefit_field_combo.addItems(self.input_fields)
        self._tr_labels["row_benefit_field"] = QLabel(tr("create_portfolio.row_benefit_field"))
        form.addRow(self._tr_labels["row_benefit_field"], self.benefit_field_combo)

        self.benefit_units_input = QLineEdit()
        self.benefit_units_input.setPlaceholderText(tr("create_portfolio.benefit_units_placeholder"))
        self._tr_labels["row_benefit_units"] = QLabel(tr("create_portfolio.row_benefit_units"))
        form.addRow(self._tr_labels["row_benefit_units"], self.benefit_units_input)

        self.cost_field_combo = QComboBox()
        self.cost_field_combo.addItem("")
        self.cost_field_combo.addItems(self.input_fields)
        self._tr_labels["row_cost_field"] = QLabel(tr("create_portfolio.row_cost_field"))
        form.addRow(self._tr_labels["row_cost_field"], self.cost_field_combo)

        self.cost_units_input = QLineEdit()
        self.cost_units_input.setPlaceholderText(tr("create_portfolio.cost_units_placeholder"))
        self._tr_labels["row_cost_units"] = QLabel(tr("create_portfolio.row_cost_units"))
        form.addRow(self._tr_labels["row_cost_units"], self.cost_units_input)

        self.prioritization_field_combo = QComboBox()
        self.prioritization_field_combo.addItem("")
        self.prioritization_field_combo.addItems(self.input_fields)
        self._tr_labels["row_prioritization_field"] = QLabel(tr("create_portfolio.row_prioritization_field"))
        form.addRow(self._tr_labels["row_prioritization_field"], self.prioritization_field_combo)

        self.prioritization_units_input = QLineEdit()
        self.prioritization_units_input.setPlaceholderText(tr("create_portfolio.prioritization_units_placeholder"))
        self._tr_labels["row_prioritization_units"] = QLabel(tr("create_portfolio.row_prioritization_units"))
        form.addRow(self._tr_labels["row_prioritization_units"], self.prioritization_units_input)

        self._prioritization_hint = QLabel(tr("create_portfolio.prioritization_hint"))
        self._prioritization_hint.setWordWrap(True)
        form.addRow(self._prioritization_hint)

        return page

    def _build_budget_strategy_page(self) -> QWizardPage:
        page = QWizardPage()
        page.tr_key = ("create_portfolio.page_budget_title", "create_portfolio.page_budget_subtitle")
        page.setTitle(tr(page.tr_key[0]))
        page.setSubTitle(tr(page.tr_key[1]))

        layout = QVBoxLayout(page)

        budget_form = QFormLayout()
        self.overall_budget_spin = QDoubleSpinBox()
        self.overall_budget_spin.setDecimals(2)
        self.overall_budget_spin.setRange(0.0, 1_000_000_000_000.0)
        self.overall_budget_spin.setSingleStep(1000.0)
        self.overall_budget_spin.setGroupSeparatorShown(True)
        self.overall_budget_spin.setValue(100_000.0)
        self._tr_labels["row_budget"] = QLabel(tr("create_portfolio.row_budget"))
        budget_form.addRow(self._tr_labels["row_budget"], self.overall_budget_spin)
        layout.addLayout(budget_form)

        self.strategy_group = QGroupBox(tr("create_portfolio.group_strategy"))
        strategy_layout = QVBoxLayout(self.strategy_group)
        self.strategy_button_group = QButtonGroup(self)
        self._strategy_buttons: Dict[str, QRadioButton] = {}
        for label, value in _strategy_options():
            button = QRadioButton(label)
            strategy_layout.addWidget(button)
            self.strategy_button_group.addButton(button)
            self._strategy_buttons[value] = button
        # Default to Benefit/Cost Ratio (most useful default for POC)
        self._strategy_buttons[STRATEGY_COSTBENEFIT].setChecked(True)

        self.direction_group_box = QGroupBox(tr("create_portfolio.group_sort_order"))
        direction_layout = QVBoxLayout(self.direction_group_box)
        self.direction_button_group = QButtonGroup(self)
        self.direction_desc_button = QRadioButton(tr("create_portfolio.direction_descending"))
        self.direction_asc_button = QRadioButton(tr("create_portfolio.direction_ascending"))
        self.direction_desc_button.setChecked(True)
        direction_layout.addWidget(self.direction_desc_button)
        direction_layout.addWidget(self.direction_asc_button)
        self.direction_button_group.addButton(self.direction_desc_button)
        self.direction_button_group.addButton(self.direction_asc_button)

        radios_row = QHBoxLayout()
        radios_row.addWidget(self.strategy_group, 1)
        radios_row.addWidget(self.direction_group_box, 1)
        layout.addLayout(radios_row)

        layout.addStretch()
        return page

    def _build_funding_page(self) -> QWizardPage:
        page = QWizardPage()
        page.tr_key = ("create_portfolio.page_funding_title", "create_portfolio.page_funding_subtitle")
        page.setTitle(tr(page.tr_key[0]))
        page.setSubTitle(tr(page.tr_key[1]))

        layout = QVBoxLayout(page)

        self.allocation_group_box = QGroupBox(tr("create_portfolio.group_allocation"))
        allocation_layout = QGridLayout(self.allocation_group_box)

        self.allocation_name_input = QLineEdit(tr("create_portfolio.default_allocation"))
        self._tr_labels["row_allocation_name"] = QLabel(tr("create_portfolio.row_allocation_name"))
        allocation_layout.addWidget(self._tr_labels["row_allocation_name"], 0, 0)
        allocation_layout.addWidget(self.allocation_name_input, 0, 1, 1, 2)

        self.allocation_budget_value = QLabel()
        self.allocation_budget_value.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
        self._tr_labels["row_overall_budget"] = QLabel(tr("create_portfolio.row_overall_budget"))
        allocation_layout.addWidget(self._tr_labels["row_overall_budget"], 1, 0)
        allocation_layout.addWidget(self.allocation_budget_value, 1, 1, 1, 2)

        self.bucket_total_value = QLabel()
        self.bucket_total_value.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
        self._tr_labels["row_allocated_total"] = QLabel(tr("create_portfolio.row_allocated_total"))
        allocation_layout.addWidget(self._tr_labels["row_allocated_total"], 2, 0)
        allocation_layout.addWidget(self.bucket_total_value, 2, 1, 1, 2)
        layout.addWidget(self.allocation_group_box)

        self.bucket_group_box = QGroupBox(tr("create_portfolio.group_enabled_buckets"))
        bucket_layout = QVBoxLayout(self.bucket_group_box)
        self._bucket_hint_label = QLabel(tr("create_portfolio.bucket_hint"))
        self._bucket_hint_label.setWordWrap(True)
        bucket_layout.addWidget(self._bucket_hint_label)

        self.bucket_rows: List[Dict[str, Any]] = []
        bucket_specs = [
            ("a", "A", True, 100_000.0),
            ("b", "B", False, 0.0),
            ("c", "C", False, 0.0),
        ]
        for key, letter, enabled, amount in bucket_specs:
            default_name = tr("create_portfolio.bucket_default_name", letter=letter)
            row_group = QGroupBox(default_name)
            row_layout = QFormLayout(row_group)
            enabled_check = QCheckBox(tr("create_portfolio.checkbox_enabled"))
            enabled_check.setChecked(enabled)
            name_input = QLineEdit(default_name)
            amount_spin = self._make_silo_spin(amount)
            enabled_check.toggled.connect(
                lambda checked, row_key=key: self._set_bucket_row_enabled(row_key, checked)
            )
            amount_spin.valueChanged.connect(self._update_bucket_total_label)
            name_row_label = QLabel(tr("create_portfolio.row_bucket_name"))
            amount_row_label = QLabel(tr("create_portfolio.row_bucket_amount"))
            row_layout.addRow(enabled_check)
            row_layout.addRow(name_row_label, name_input)
            row_layout.addRow(amount_row_label, amount_spin)
            bucket_layout.addWidget(row_group)
            self.bucket_rows.append(
                {
                    "key": key,
                    "letter": letter,
                    "group": row_group,
                    "enabled": enabled_check,
                    "name": name_input,
                    "amount": amount_spin,
                    "name_row_label": name_row_label,
                    "amount_row_label": amount_row_label,
                }
            )

        self.silo_a_spin = self.bucket_rows[0]["amount"]
        self.silo_b_spin = self.bucket_rows[1]["amount"]
        self.silo_c_spin = self.bucket_rows[2]["amount"]

        layout.addWidget(self.bucket_group_box)

        self.funding_category_combo = QComboBox()
        self.funding_category_combo.addItem("")
        self.funding_category_combo.addItems(self.input_fields)
        self.funding_category_combo.currentTextChanged.connect(
            self._on_funding_category_changed
        )

        self.category_group_box = QGroupBox(tr("create_portfolio.group_category_mapping"))
        category_layout = QVBoxLayout(self.category_group_box)
        self._category_hint_label = QLabel(tr("create_portfolio.category_hint"))
        self._category_hint_label.setWordWrap(True)
        category_layout.addWidget(self._category_hint_label)

        form = QFormLayout()
        self._tr_labels["row_category_field"] = QLabel(tr("create_portfolio.row_category_field"))
        form.addRow(self._tr_labels["row_category_field"], self.funding_category_combo)
        category_layout.addLayout(form)

        self.percent_group_box = QGroupBox(tr("create_portfolio.group_percent"))
        percent_layout = QVBoxLayout(self.percent_group_box)

        self._percent_hint_label = QLabel(tr("create_portfolio.percent_hint"))
        self._percent_hint_label.setWordWrap(True)
        percent_layout.addWidget(self._percent_hint_label)

        button_row = QHBoxLayout()
        self.add_funding_row_button = QPushButton(tr("create_portfolio.btn_add_row"))
        self.add_funding_row_button.clicked.connect(self._add_funding_row)
        button_row.addWidget(self.add_funding_row_button)

        self.remove_funding_row_button = QPushButton(tr("create_portfolio.btn_remove_row"))
        self.remove_funding_row_button.clicked.connect(self._remove_funding_row)
        button_row.addWidget(self.remove_funding_row_button)
        button_row.addStretch()
        percent_layout.addLayout(button_row)

        self.funding_percent_table = QTableWidget(0, 4)
        self.funding_percent_table.setHorizontalHeaderLabels(self._funding_percent_headers())
        header = self.funding_percent_table.horizontalHeader()
        if hasattr(header, "setSectionResizeMode"):
            header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
            for col in range(1, 4):
                header.setSectionResizeMode(col, QHeaderView.ResizeMode.ResizeToContents)
        percent_layout.addWidget(self.funding_percent_table, 1)

        category_layout.addWidget(self.percent_group_box, 1)
        layout.addWidget(self.category_group_box, 1)

        self._set_bucket_row_enabled("a", True)
        self._set_bucket_row_enabled("b", False)
        self._set_bucket_row_enabled("c", False)
        self.allocation_budget_value.setText(f"${self.overall_budget_spin.value():,.2f}")
        self._update_bucket_total_label()
        self._set_category_controls_enabled(False)
        self._last_overall_budget = float(self.overall_budget_spin.value())
        self.overall_budget_spin.valueChanged.connect(self._on_overall_budget_changed)
        self.bucket_rows[0]["amount"].valueChanged.connect(self._update_bucket_total_label)
        self.bucket_rows[1]["amount"].valueChanged.connect(self._update_bucket_total_label)
        self.bucket_rows[2]["amount"].valueChanged.connect(self._update_bucket_total_label)

        return page

    @staticmethod
    def _funding_percent_headers() -> List[str]:
        return [
            tr("create_portfolio.col_category_code"),
            tr("create_portfolio.col_pct_a"),
            tr("create_portfolio.col_pct_b"),
            tr("create_portfolio.col_pct_c"),
        ]

    def _build_summary_page(self) -> QWizardPage:
        page = QWizardPage()
        page.tr_key = ("create_portfolio.page_summary_title", "create_portfolio.page_summary_subtitle")
        page.setTitle(tr(page.tr_key[0]))
        page.setSubTitle(tr(page.tr_key[1]))

        layout = QVBoxLayout(page)

        self.summary_intro_label = QLabel()
        self.summary_intro_label.setWordWrap(True)
        layout.addWidget(self.summary_intro_label)

        self.summary_mappings_table = QTableWidget(0, 2)
        self.summary_mappings_table.setHorizontalHeaderLabels([
            tr("create_portfolio.col_mapping"),
            tr("create_portfolio.col_value"),
        ])
        mappings_header = self.summary_mappings_table.horizontalHeader()
        if hasattr(mappings_header, "setSectionResizeMode"):
            mappings_header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
            mappings_header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)

        self.mappings_group_box = QGroupBox(tr("create_portfolio.group_mappings"))
        mappings_layout = QVBoxLayout(self.mappings_group_box)
        mappings_layout.addWidget(self.summary_mappings_table)
        layout.addWidget(self.mappings_group_box)

        self.summary_budget_table = QTableWidget(0, 2)
        self.summary_budget_table.setHorizontalHeaderLabels([
            tr("create_portfolio.col_budget_item"),
            tr("create_portfolio.col_value"),
        ])
        budget_header = self.summary_budget_table.horizontalHeader()
        if hasattr(budget_header, "setSectionResizeMode"):
            budget_header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
            budget_header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)

        self.budget_group_box = QGroupBox(tr("create_portfolio.group_budget"))
        budget_layout = QVBoxLayout(self.budget_group_box)
        budget_layout.addWidget(self.summary_budget_table)
        layout.addWidget(self.budget_group_box)

        self.summary_buckets_table = QTableWidget(0, 3)
        self.summary_buckets_table.setHorizontalHeaderLabels([
            tr("create_portfolio.col_bucket"),
            tr("create_portfolio.col_name"),
            tr("create_portfolio.col_amount"),
        ])
        buckets_header = self.summary_buckets_table.horizontalHeader()
        if hasattr(buckets_header, "setSectionResizeMode"):
            buckets_header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
            buckets_header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
            buckets_header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)

        self.buckets_summary_group = QGroupBox(tr("create_portfolio.group_buckets_summary"))
        buckets_layout = QVBoxLayout(self.buckets_summary_group)
        buckets_layout.addWidget(self.summary_buckets_table)
        layout.addWidget(self.buckets_summary_group)

        self.summary_evaluation_table = QTableWidget(0, 2)
        self.summary_evaluation_table.setHorizontalHeaderLabels([
            tr("create_portfolio.col_method"),
            tr("create_portfolio.col_value"),
        ])
        evaluation_header = self.summary_evaluation_table.horizontalHeader()
        if hasattr(evaluation_header, "setSectionResizeMode"):
            evaluation_header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
            evaluation_header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)

        self.evaluation_group_box = QGroupBox(tr("create_portfolio.group_evaluation"))
        evaluation_layout = QVBoxLayout(self.evaluation_group_box)
        evaluation_layout.addWidget(self.summary_evaluation_table)
        layout.addWidget(self.evaluation_group_box)

        layout.addStretch()
        return page

    # ------------------------------------------------------------------
    # Small UI helpers
    # ------------------------------------------------------------------

    def _make_silo_spin(self, default: float) -> QDoubleSpinBox:
        spin = QDoubleSpinBox()
        spin.setDecimals(2)
        spin.setRange(0.0, 1_000_000_000_000.0)
        spin.setSingleStep(1000.0)
        spin.setGroupSeparatorShown(True)
        spin.setValue(default)
        return spin

    def _set_category_controls_enabled(self, enabled: bool) -> None:
        self.add_funding_row_button.setEnabled(enabled)
        self.remove_funding_row_button.setEnabled(enabled)
        self.funding_percent_table.setEnabled(enabled)

    def _on_funding_category_changed(self, text: str) -> None:
        has_category = bool(text.strip())
        self._set_category_controls_enabled(has_category)
        if has_category and self.funding_percent_table.rowCount() == 0:
            self._add_funding_row()

    def _set_bucket_row_enabled(self, key: str, enabled: bool) -> None:
        row = next((row for row in self.bucket_rows if row["key"] == key), None)
        if row is None:
            return
        row["group"].setEnabled(True)
        row["enabled"].setChecked(enabled)
        row["name"].setEnabled(enabled)
        row["amount"].setEnabled(enabled)
        if not enabled:
            row["amount"].setValue(0.0)
        self._update_bucket_total_label()

    def _on_overall_budget_changed(self, value: float) -> None:
        new_budget = round(float(value), 2)
        if len(self._enabled_bucket_rows()) == 1 and self._enabled_bucket_rows()[0]["key"] == "a":
            current_amount = round(float(self.bucket_rows[0]["amount"].value()), 2)
            previous_budget = round(float(getattr(self, "_last_overall_budget", new_budget)), 2)
            if current_amount in (0.0, previous_budget):
                self.bucket_rows[0]["amount"].blockSignals(True)
                self.bucket_rows[0]["amount"].setValue(new_budget)
                self.bucket_rows[0]["amount"].blockSignals(False)
        self._last_overall_budget = new_budget
        self._update_bucket_total_label()

    def _bucket_rows_state(self) -> List[Dict[str, Any]]:
        rows: List[Dict[str, Any]] = []
        for row in self.bucket_rows:
            enabled = row["enabled"].isChecked()
            name = row["name"].text().strip()
            amount = float(row["amount"].value())
            rows.append(
                {
                    "key": row["key"],
                    "enabled": enabled,
                    "name": name,
                    "amount": amount if enabled else 0.0,
                }
            )
        return rows

    def _enabled_bucket_rows(self) -> List[Dict[str, Any]]:
        return [row for row in self._bucket_rows_state() if row["enabled"]]

    def _update_bucket_total_label(self) -> None:
        total = sum(row["amount"].value() if row["enabled"].isChecked() else 0.0 for row in self.bucket_rows)
        overall = float(self.overall_budget_spin.value())
        self.allocation_budget_value.setText(f"${overall:,.2f}")
        self.bucket_total_value.setText(f"${total:,.2f} / ${overall:,.2f}")

    @staticmethod
    def _format_currency(value: float) -> str:
        return f"${value:,.2f}"

    def _populate_summary_page(self) -> None:
        params = self._collect_parameters()
        strategy_label = dict(_strategy_options()).get(params["strategy"], params["strategy"])
        direction_label = (
            tr("create_portfolio.direction_label_ascending")
            if params["direction"] == DIRECTION_UP
            else tr("create_portfolio.direction_label_descending")
        )

        self.summary_intro_label.setText(
            tr(
                "create_portfolio.summary_intro",
                name=params["portfolio_name"] or tr("create_portfolio.unnamed"),
                strategy=strategy_label,
            )
        )

        not_set = tr("create_portfolio.not_set")
        not_used = tr("create_portfolio.not_used")
        mapping_rows = [
            (tr("create_portfolio.summary_unique_field"), params["unique_field"] or not_set),
            (tr("create_portfolio.summary_benefit_field"), params["benefit_field"] or not_set),
            (tr("create_portfolio.summary_cost_field"), params["cost_field"] or not_set),
            (tr("create_portfolio.summary_prioritization_field"), params["prioritization_field"] or not_used),
        ]
        self.summary_mappings_table.setRowCount(len(mapping_rows))
        for row_index, (label, value) in enumerate(mapping_rows):
            self.summary_mappings_table.setItem(row_index, 0, QTableWidgetItem(label))
            self.summary_mappings_table.setItem(row_index, 1, QTableWidgetItem(value))

        default_alloc = tr("create_portfolio.default_allocation")
        budget_rows = [
            (tr("create_portfolio.row_allocation_name"), params.get("allocation_name", "") or default_alloc),
            (tr("create_portfolio.row_overall_budget"), self._format_currency(params["overall_budget"])),
        ]
        self.summary_budget_table.setRowCount(len(budget_rows))
        for row_index, (label, value) in enumerate(budget_rows):
            self.summary_budget_table.setItem(row_index, 0, QTableWidgetItem(label))
            self.summary_budget_table.setItem(row_index, 1, QTableWidgetItem(str(value)))

        enabled_rows = [row for row in (params.get("bucket_rows", []) or []) if row.get("enabled")]
        self.summary_buckets_table.setRowCount(len(enabled_rows))
        for row_index, row in enumerate(enabled_rows):
            bucket_label = tr("create_portfolio.bucket_label", letter=row["key"].upper())
            self.summary_buckets_table.setItem(row_index, 0, QTableWidgetItem(bucket_label))
            self.summary_buckets_table.setItem(row_index, 1, QTableWidgetItem(row["name"]))
            self.summary_buckets_table.setItem(
                row_index,
                2,
                QTableWidgetItem(self._format_currency(float(row["amount"]))),
            )

        evaluation_rows = [
            (tr("create_portfolio.summary_method"), strategy_label),
            (tr("create_portfolio.summary_sort_order"), direction_label),
        ]
        self.summary_evaluation_table.setRowCount(len(evaluation_rows))
        for row_index, (label, value) in enumerate(evaluation_rows):
            self.summary_evaluation_table.setItem(row_index, 0, QTableWidgetItem(label))
            self.summary_evaluation_table.setItem(row_index, 1, QTableWidgetItem(value))

    def retranslateUi(self) -> None:
        self.setWindowTitle(tr("create_portfolio.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]))
        # Tracked label widgets keyed by their tr() key suffix.
        label_keys = {
            "lbl_workspace": "create_portfolio.lbl_workspace",
            "lbl_study": "create_portfolio.lbl_study",
            "lbl_input_layer": "create_portfolio.lbl_input_layer",
            "lbl_input_table": "create_portfolio.lbl_input_table",
            "row_name": "create_portfolio.row_name",
            "row_comments": "create_portfolio.row_comments",
            "row_unique_field": "create_portfolio.row_unique_field",
            "row_benefit_field": "create_portfolio.row_benefit_field",
            "row_benefit_units": "create_portfolio.row_benefit_units",
            "row_cost_field": "create_portfolio.row_cost_field",
            "row_cost_units": "create_portfolio.row_cost_units",
            "row_prioritization_field": "create_portfolio.row_prioritization_field",
            "row_prioritization_units": "create_portfolio.row_prioritization_units",
            "row_budget": "create_portfolio.row_budget",
            "row_allocation_name": "create_portfolio.row_allocation_name",
            "row_overall_budget": "create_portfolio.row_overall_budget",
            "row_allocated_total": "create_portfolio.row_allocated_total",
            "row_category_field": "create_portfolio.row_category_field",
        }
        for attr_key, tr_key in label_keys.items():
            label = self._tr_labels.get(attr_key)
            if label is not None:
                label.setText(tr(tr_key))
        not_set = tr("create_portfolio.not_set")
        if hasattr(self, "_input_layer_value_label"):
            self._input_layer_value_label.setText(self.input_layer_name or not_set)
        if hasattr(self, "_input_table_value_label"):
            self._input_table_value_label.setText(self.input_table_name or not_set)
        if hasattr(self, "context_group"):
            self.context_group.setTitle(tr("create_portfolio.context_group"))
        for attr, key in (
            ("strategy_group", "create_portfolio.group_strategy"),
            ("direction_group_box", "create_portfolio.group_sort_order"),
            ("allocation_group_box", "create_portfolio.group_allocation"),
            ("bucket_group_box", "create_portfolio.group_enabled_buckets"),
            ("category_group_box", "create_portfolio.group_category_mapping"),
            ("percent_group_box", "create_portfolio.group_percent"),
            ("mappings_group_box", "create_portfolio.group_mappings"),
            ("budget_group_box", "create_portfolio.group_budget"),
            ("buckets_summary_group", "create_portfolio.group_buckets_summary"),
            ("evaluation_group_box", "create_portfolio.group_evaluation"),
        ):
            box = getattr(self, attr, None)
            if box is not None:
                box.setTitle(tr(key))
        # Strategy radio button labels.
        if hasattr(self, "_strategy_buttons"):
            for label, value in _strategy_options():
                btn = self._strategy_buttons.get(value)
                if btn is not None:
                    btn.setText(label)
        if hasattr(self, "direction_desc_button"):
            self.direction_desc_button.setText(tr("create_portfolio.direction_descending"))
        if hasattr(self, "direction_asc_button"):
            self.direction_asc_button.setText(tr("create_portfolio.direction_ascending"))
        # Placeholders
        if hasattr(self, "portfolio_name_input"):
            self.portfolio_name_input.setPlaceholderText(tr("create_portfolio.name_placeholder"))
        if hasattr(self, "benefit_units_input"):
            self.benefit_units_input.setPlaceholderText(tr("create_portfolio.benefit_units_placeholder"))
        if hasattr(self, "cost_units_input"):
            self.cost_units_input.setPlaceholderText(tr("create_portfolio.cost_units_placeholder"))
        if hasattr(self, "prioritization_units_input"):
            self.prioritization_units_input.setPlaceholderText(tr("create_portfolio.prioritization_units_placeholder"))
        if hasattr(self, "run_immediately_checkbox"):
            self.run_immediately_checkbox.setText(tr("create_portfolio.run_immediately"))
        if hasattr(self, "_prioritization_hint"):
            self._prioritization_hint.setText(tr("create_portfolio.prioritization_hint"))
        if hasattr(self, "_bucket_hint_label"):
            self._bucket_hint_label.setText(tr("create_portfolio.bucket_hint"))
        if hasattr(self, "_category_hint_label"):
            self._category_hint_label.setText(tr("create_portfolio.category_hint"))
        if hasattr(self, "_percent_hint_label"):
            self._percent_hint_label.setText(tr("create_portfolio.percent_hint"))
        # Funding row buckets
        for row in getattr(self, "bucket_rows", []):
            default_name = tr("create_portfolio.bucket_default_name", letter=row.get("letter", ""))
            if "group" in row:
                row["group"].setTitle(default_name)
            if "enabled" in row:
                row["enabled"].setText(tr("create_portfolio.checkbox_enabled"))
            if "name_row_label" in row:
                row["name_row_label"].setText(tr("create_portfolio.row_bucket_name"))
            if "amount_row_label" in row:
                row["amount_row_label"].setText(tr("create_portfolio.row_bucket_amount"))
        # Buttons
        if hasattr(self, "add_funding_row_button"):
            self.add_funding_row_button.setText(tr("create_portfolio.btn_add_row"))
        if hasattr(self, "remove_funding_row_button"):
            self.remove_funding_row_button.setText(tr("create_portfolio.btn_remove_row"))
        # Table headers
        if hasattr(self, "funding_percent_table"):
            self.funding_percent_table.setHorizontalHeaderLabels(self._funding_percent_headers())
        if hasattr(self, "summary_mappings_table"):
            self.summary_mappings_table.setHorizontalHeaderLabels([
                tr("create_portfolio.col_mapping"),
                tr("create_portfolio.col_value"),
            ])
        if hasattr(self, "summary_budget_table"):
            self.summary_budget_table.setHorizontalHeaderLabels([
                tr("create_portfolio.col_budget_item"),
                tr("create_portfolio.col_value"),
            ])
        if hasattr(self, "summary_buckets_table"):
            self.summary_buckets_table.setHorizontalHeaderLabels([
                tr("create_portfolio.col_bucket"),
                tr("create_portfolio.col_name"),
                tr("create_portfolio.col_amount"),
            ])
        if hasattr(self, "summary_evaluation_table"):
            self.summary_evaluation_table.setHorizontalHeaderLabels([
                tr("create_portfolio.col_method"),
                tr("create_portfolio.col_value"),
            ])

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

    def _add_funding_row(self) -> None:
        row = self.funding_percent_table.rowCount()
        self.funding_percent_table.insertRow(row)
        self.funding_percent_table.setItem(row, 0, QTableWidgetItem(""))
        default_percents = ("100", "0", "0")
        for col, value in enumerate(default_percents, start=1):
            item = QTableWidgetItem(value)
            item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
            self.funding_percent_table.setItem(row, col, item)

    def _remove_funding_row(self) -> None:
        selected_rows = sorted(
            {index.row() for index in self.funding_percent_table.selectedIndexes()},
            reverse=True,
        )
        for row in selected_rows:
            self.funding_percent_table.removeRow(row)

    # ------------------------------------------------------------------
    # Data collection
    # ------------------------------------------------------------------

    def _selected_strategy(self) -> str:
        for value, button in self._strategy_buttons.items():
            if button.isChecked():
                return value
        return STRATEGY_COSTBENEFIT

    def _selected_direction(self) -> str:
        return DIRECTION_UP if self.direction_asc_button.isChecked() else DIRECTION_DOWN

    def _collect_funding_percents(self) -> List[Dict[str, Any]]:
        rows: List[Dict[str, Any]] = []
        for row_index in range(self.funding_percent_table.rowCount()):
            code_item = self.funding_percent_table.item(row_index, 0)
            code = (code_item.text() if code_item else "").strip()
            if not code:
                continue

            percents: List[float] = []
            for col in range(1, 4):
                cell = self.funding_percent_table.item(row_index, col)
                try:
                    percents.append(float((cell.text() if cell else "0").strip() or 0.0))
                except ValueError:
                    percents.append(0.0)
            rows.append(
                {
                    "code": code,
                    "percent_a": percents[0],
                    "percent_b": percents[1],
                    "percent_c": percents[2],
                }
            )
        return rows

    def _collect_parameters(self) -> Dict[str, Any]:
        funding_category = self.funding_category_combo.currentText().strip()
        silo_budgets: Dict[str, float] = {
            row["key"]: float(row["amount"].value()) if row["enabled"].isChecked() else 0.0
            for row in self.bucket_rows
        }
        funding_percents: List[Dict[str, Any]] = []
        if funding_category:
            funding_percents = self._collect_funding_percents()

        return {
            "project_id": self.project_id,
            "assessment_id": self.assessment_id,
            "input_layer_name": self.input_layer_name,
            "input_table_name": self.input_table_name,
            "portfolio_name": self.portfolio_name_input.text().strip(),
            "portfolio_description": self.comments_input.plain_text(),
            "comments": self.comments_input.plain_text(),
            "comments_html": self.comments_input.html(),
            "allocation_name": self.allocation_name_input.text().strip() or "Default Allocation",
            "unique_field": self.unique_field_combo.currentText().strip(),
            "cost_field": self.cost_field_combo.currentText().strip(),
            "benefit_field": self.benefit_field_combo.currentText().strip(),
            "prioritization_field": self.prioritization_field_combo.currentText().strip(),
            "benefit_units": self.benefit_units_input.text().strip(),
            "cost_units": self.cost_units_input.text().strip(),
            "prioritization_units": self.prioritization_units_input.text().strip(),
            "overall_budget": float(self.overall_budget_spin.value()),
            "strategy": self._selected_strategy(),
            "direction": self._selected_direction(),
            "funding_category_field": funding_category,
            "silo_budgets": silo_budgets,
            "bucket_rows": self._bucket_rows_state(),
            "funding_percents": funding_percents,
            "run_immediately": self.run_immediately_checkbox.isChecked(),
        }

    # ------------------------------------------------------------------
    # Saved settings (piggyback on the shared field-mapping store)
    # ------------------------------------------------------------------

    def _settings_identifier(self) -> str:
        """
        Identifier for the saved preset. Portfolio has no model file, so we
        key the preset on the input table name via a synthetic identifier.
        """
        return f"portfolio::{self.input_table_name or 'default'}"

    def _load_saved_settings_from_store(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.TEMPLATE_2.value,
            model_identifier=self._settings_identifier(),
            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_portfolio.saved_title"),
                    tr("create_portfolio.saved_none_found"),
                )
            return
        self._apply_saved_payload(payload)

    def _apply_saved_payload(self, payload: Dict[str, Any]) -> None:
        self.portfolio_name_input.setText(str(payload.get("portfolio_name", "")).strip())
        self.comments_input.set_content(
            str(payload.get("comments", "")).strip(),
            str(payload.get("comments_html", "") or ""),
        )
        self._select_combo_text(self.unique_field_combo, payload.get("unique_field", ""))
        self._select_combo_text(self.benefit_field_combo, payload.get("benefit_field", ""))
        self._select_combo_text(self.cost_field_combo, payload.get("cost_field", ""))
        self._select_combo_text(self.prioritization_field_combo, payload.get("prioritization_field", ""))
        self.benefit_units_input.setText(str(payload.get("benefit_units", "")).strip())
        self.cost_units_input.setText(str(payload.get("cost_units", "")).strip())
        self.prioritization_units_input.setText(str(payload.get("prioritization_units", "")).strip())
        try:
            self.overall_budget_spin.setValue(float(payload.get("overall_budget", 0.0) or 0.0))
        except (TypeError, ValueError):
            pass
        self._last_overall_budget = float(self.overall_budget_spin.value())

        strategy = str(payload.get("strategy", "") or "").strip().lower()
        if strategy in self._strategy_buttons:
            self._strategy_buttons[strategy].setChecked(True)

        direction = str(payload.get("direction", "") or "").strip().lower()
        if direction == DIRECTION_UP:
            self.direction_asc_button.setChecked(True)
        elif direction == DIRECTION_DOWN:
            self.direction_desc_button.setChecked(True)

        funding_category = str(payload.get("funding_category_field", "") or "").strip()
        self._select_combo_text(self.funding_category_combo, funding_category)
        self.allocation_name_input.setText(
            str(payload.get("allocation_name", "Default Allocation") or "Default Allocation").strip()
        )
        silo_budgets = payload.get("silo_budgets") or {}
        if isinstance(silo_budgets, dict):
            for row in self.bucket_rows:
                try:
                    row["amount"].setValue(float(silo_budgets.get(row["key"], 0.0) or 0.0))
                    row["enabled"].setChecked(float(silo_budgets.get(row["key"], 0.0) or 0.0) > 0.0)
                except (TypeError, ValueError):
                    row["amount"].setValue(0.0)

        self.funding_percent_table.setRowCount(0)
        for entry in payload.get("funding_percents", []) or []:
            if not isinstance(entry, dict):
                continue
            code = str(entry.get("code", "")).strip()
            if not code:
                continue
            self._add_funding_row()
            last = self.funding_percent_table.rowCount() - 1
            self.funding_percent_table.setItem(last, 0, QTableWidgetItem(code))
            for col, key in enumerate(("percent_a", "percent_b", "percent_c"), start=1):
                value = entry.get(key, 0)
                try:
                    text = f"{float(value):g}"
                except (TypeError, ValueError):
                    text = "0"
                item = QTableWidgetItem(text)
                item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
                self.funding_percent_table.setItem(last, col, item)

    @staticmethod
    def _select_combo_text(combo: QComboBox, text: Any) -> None:
        value = str(text or "").strip()
        if not value:
            return
        index = combo.findText(value)
        if index >= 0:
            combo.setCurrentIndex(index)
        else:
            combo.setCurrentText(value)

        self._update_bucket_total_label()
        self._set_category_controls_enabled(bool(self.funding_category_combo.currentText().strip()))

    def _save_settings_to_store(self, payload: Dict[str, Any]) -> None:
        if self.metadata_manager is None:
            return
        try:
            self.metadata_manager.save_field_mapping_preset(
                project_id=self.project_id,
                engine_type=EngineType.TEMPLATE_2.value,
                model_identifier=self._settings_identifier(),
                input_table_name=self.input_table_name,
                payload=payload,
            )
        except Exception as exc:
            debug_logger.warning("Could not save Portfolio wizard preset", {"exception": str(exc)})

    # ------------------------------------------------------------------
    # Wizard lifecycle
    # ------------------------------------------------------------------

    def _on_page_changed(self, page_id: int) -> None:
        if page_id == 4:
            self._populate_summary_page()

    def validateCurrentPage(self) -> bool:
        current_id = self.currentId()
        title = tr("create_portfolio.validation_title")

        if current_id == 0:
            if not self.portfolio_name_input.text().strip():
                QMessageBox.warning(self, title, tr("create_portfolio.validation_name_required"))
                return False
            if not self.unique_field_combo.currentText().strip():
                QMessageBox.warning(self, title, tr("create_portfolio.validation_choose_unique"))
                return False

        elif current_id == 1:
            if not self.benefit_field_combo.currentText().strip():
                QMessageBox.warning(self, title, tr("create_portfolio.validation_choose_benefit"))
                return False
            if not self.cost_field_combo.currentText().strip():
                QMessageBox.warning(self, title, tr("create_portfolio.validation_choose_cost"))
                return False
            strategy = self._selected_strategy()
            if strategy in (STRATEGY_PRIORITIZATION, STRATEGY_PRIORITIZATION_COST):
                if not self.prioritization_field_combo.currentText().strip():
                    QMessageBox.warning(self, title, tr("create_portfolio.validation_need_prioritization_field"))
                    return False

        elif current_id == 2:
            if self.overall_budget_spin.value() <= 0:
                QMessageBox.warning(self, title, tr("create_portfolio.validation_budget_positive"))
                return False
            strategy = self._selected_strategy()
            if strategy in (STRATEGY_PRIORITIZATION, STRATEGY_PRIORITIZATION_COST):
                if not self.prioritization_field_combo.currentText().strip():
                    QMessageBox.warning(self, title, tr("create_portfolio.validation_select_prio_prev"))
                    return False

        elif current_id == 3:
            overall_budget = round(float(self.overall_budget_spin.value()), 2)
            enabled_rows = [row for row in self.bucket_rows if row["enabled"].isChecked()]
            if not enabled_rows:
                QMessageBox.warning(self, title, tr("create_portfolio.validation_enable_bucket"))
                return False

            for row in enabled_rows:
                if not row["name"].text().strip():
                    QMessageBox.warning(
                        self, title,
                        tr("create_portfolio.validation_name_for_bucket", letter=row["key"].upper()),
                    )
                    return False

            bucket_total = round(
                sum(float(row["amount"].value()) for row in enabled_rows),
                2,
            )
            if bucket_total != overall_budget:
                QMessageBox.warning(self, title, tr("create_portfolio.validation_bucket_match_budget"))
                return False

            funding_category = self.funding_category_combo.currentText().strip()
            if funding_category:
                rows = self._collect_funding_percents()
                if not rows:
                    QMessageBox.warning(self, title, tr("create_portfolio.validation_need_category_row"))
                    return False
                for row in rows:
                    total = row["percent_a"] + row["percent_b"] + row["percent_c"]
                    if round(total, 2) != 100.0:
                        QMessageBox.warning(
                            self, title,
                            tr("create_portfolio.validation_percent_sum_100", code=row["code"], total=total),
                        )
                        return False

        return super().validateCurrentPage()

    def accept(self) -> None:
        parameters = self._collect_parameters()

        self._result_step = WorkflowStep(
            id=str(uuid.uuid4()),
            name=parameters["portfolio_name"],
            engine_type=EngineType.TEMPLATE_2,
            parameters=parameters,
        )

        self._save_settings_to_store(parameters)

        debug_logger.section("Portfolio wizard accepted")
        debug_logger.debug(
            "Portfolio wizard payload",
            {
                "project_id": self.project_id,
                "assessment_id": self.assessment_id,
                "input_layer_name": self.input_layer_name,
                "input_table_name": self.input_table_name,
                "portfolio_name": parameters["portfolio_name"],
                "allocation_name": parameters["allocation_name"],
                "unique_field": parameters["unique_field"],
                "benefit_field": parameters["benefit_field"],
                "cost_field": parameters["cost_field"],
                "strategy": parameters["strategy"],
                "direction": parameters["direction"],
                "overall_budget": parameters["overall_budget"],
                "silo_budgets": parameters["silo_budgets"],
                "bucket_rows": len(parameters["bucket_rows"]),
                "funding_percent_rows": len(parameters["funding_percents"]),
                "run_immediately": parameters["run_immediately"],
            },
        )

        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# Prime…ConfigureOnlyAsync pattern. Restores simple
        text fields; complex allocation/cost tables depend on the model
        being loaded.
        """
        if step is None:
            return
        try:
            config = getattr(step, "config", None) or {}
            params = config.get("task_parameters") or {}

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

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

            for input_attr, key in (
                ("benefit_units_input", "benefit_units"),
                ("cost_units_input", "cost_units"),
                ("prioritization_units_input", "prioritization_units"),
            ):
                widget = getattr(self, input_attr, None)
                value = str(params.get(key) or "").strip()
                if widget is not None and value:
                    widget.setText(value)

            allocation_name = str(params.get("allocation_name") or "").strip()
            if allocation_name and hasattr(self, "allocation_name_input"):
                self.allocation_name_input.setText(allocation_name)
        except Exception:
            pass
