"""
LPA (Locational Priority Assessment) results hotlink viewer.

This viewer mirrors the structure of the NetWeaver/CDP dialogs but stays
engine-agnostic — LPA's Python-side execution path isn't built yet, so
the dialog focuses on what already works for any tabular + spatial
result: a map tab with the result layer, a table tab backed by a
DataFrame, and the shared print/export toolbar.

It also reuses the calculated-field plumbing from ResultsGridDialog so a
user can derive new columns directly on the GeoPackage table.

When the LPA engine lands, this dialog can grow engine-specific tabs
without disturbing the routing — callers always go through
``LPAResultsDialog.from_payload`` and the dialog ignores fields it
doesn't yet understand.
"""

from __future__ import annotations

from typing import Any, Dict, List, Optional

import pandas as pd

from .qt_compat import (
    QAbstractItemView,
    QDialog,
    QHBoxLayout,
    QHeaderView,
    QLabel,
    QLineEdit,
    QMessageBox,
    QPushButton,
    QSplitter,
    QTabWidget,
    QTableView,
    QTextEdit,
    QVBoxLayout,
    QWidget,
    Qt,
    exec_dialog,
)

from . import debug_logger
from .result_export_helpers import (
    TabExportSpec,
    export_dataframe_to_csv,
    export_dataframes_to_xlsx,
    export_widget_as_png,
    print_table_view,
)
from .results_grid_dialog import ResultsFilterProxyModel
from .results_grid_model import DataFrameTableModel
from .translator import tr, add_listener, remove_listener

try:
    from qgis.core import QgsProject, QgsVectorLayer
    from qgis.gui import QgsMapCanvas
except Exception:  # pragma: no cover - QGIS not available in standalone tests
    QgsProject = None
    QgsVectorLayer = None
    QgsMapCanvas = None


class LPAResultsDialog(QDialog):
    """First-pass LPA viewer — table, map, and shared export toolbar."""

    @classmethod
    def from_payload(cls, payload: Dict[str, Any], parent=None) -> "LPAResultsDialog":
        return cls(
            workflow_name=str(payload.get("workflow_name", "")).strip(),
            execution_id=str(payload.get("execution_id", "")).strip(),
            results_df=payload.get("results_df"),
            summary=payload.get("summary") or {},
            database_path=str(payload.get("database_path", "")).strip(),
            output_table_name=str(payload.get("output_table_name", "")).strip(),
            row_key_field=str(payload.get("row_key_field", "")).strip(),
            display_field=str(payload.get("display_field", "")).strip(),
            payload=payload,
            parent=parent,
        )

    def __init__(
        self,
        workflow_name: str,
        execution_id: str,
        results_df: Optional[pd.DataFrame],
        summary: Optional[Dict[str, Any]] = None,
        *,
        database_path: str = "",
        output_table_name: str = "",
        row_key_field: str = "",
        display_field: str = "",
        payload: Optional[Dict[str, Any]] = None,
        parent=None,
    ):
        super().__init__(parent)
        self.workflow_name = workflow_name or tr("lpa_results.default_name")
        self.execution_id = execution_id
        self.summary = dict(summary or {})
        self.payload = dict(payload or {})
        self.database_path = database_path
        self.output_table_name = output_table_name
        self.row_key_field = row_key_field
        self.display_field = display_field

        self.results_df = self._coerce_dataframe(results_df)
        self.layer_name_hint = output_table_name or self.workflow_name
        self._result_layer: Optional["QgsVectorLayer"] = None

        self.setWindowTitle(tr("lpa_results.window_title", name=self.workflow_name))
        self.resize(1280, 820)
        self._init_ui()
        self._refresh_export_buttons_for_current_tab()
        self._populate_summary_text()
        self._refresh_row_details()
        self.retranslateUi()
        add_listener(self.retranslateUi)

    # ------------------------------------------------------------------
    # Layout
    # ------------------------------------------------------------------
    def _init_ui(self) -> None:
        layout = QVBoxLayout()
        self.setLayout(layout)

        self.header_label = QLabel(self._format_header())
        self.header_label.setWordWrap(True)
        layout.addWidget(self.header_label)

        self.tabs = QTabWidget()
        layout.addWidget(self.tabs, 1)

        self._build_table_tab()
        self._build_map_tab()
        self._build_summary_tab()

        self.tabs.setCurrentIndex(0)

        button_row = QHBoxLayout()

        self.btn_print_tab = QPushButton(tr("lpa_results.btn_print"))
        self.btn_print_tab.setToolTip(tr("lpa_results.tip_print"))
        self.btn_print_tab.clicked.connect(self._on_print_current_tab)
        button_row.addWidget(self.btn_print_tab)

        self.btn_export_tab_csv = QPushButton(tr("lpa_results.btn_export_csv"))
        self.btn_export_tab_csv.setToolTip(tr("lpa_results.tip_export_csv"))
        self.btn_export_tab_csv.clicked.connect(self._on_export_current_tab_csv)
        button_row.addWidget(self.btn_export_tab_csv)

        self.btn_export_map_png = QPushButton(tr("lpa_results.btn_export_png"))
        self.btn_export_map_png.setToolTip(tr("lpa_results.tip_export_png"))
        self.btn_export_map_png.clicked.connect(self._on_export_map_png)
        button_row.addWidget(self.btn_export_map_png)

        self.btn_export_all_xlsx = QPushButton(tr("lpa_results.btn_export_xlsx"))
        self.btn_export_all_xlsx.setToolTip(tr("lpa_results.tip_export_xlsx"))
        self.btn_export_all_xlsx.clicked.connect(self._on_export_all_tabs_xlsx)
        button_row.addWidget(self.btn_export_all_xlsx)

        self.btn_add_calc_field = QPushButton(tr("lpa_results.btn_add_calc"))
        self.btn_add_calc_field.clicked.connect(self._on_add_calculated_field)
        button_row.addWidget(self.btn_add_calc_field)

        button_row.addStretch()
        self.close_button = QPushButton(tr("lpa_results.btn_close"))
        self.close_button.clicked.connect(self.close)
        button_row.addWidget(self.close_button)
        layout.addLayout(button_row)

        self.tabs.currentChanged.connect(self._refresh_export_buttons_for_current_tab)

    def _format_header(self) -> str:
        return tr(
            "lpa_results.header",
            name=self.workflow_name,
            exec_id=self.execution_id or tr("lpa_results.execution_unknown"),
            rows=len(self.results_df.index),
            columns=len(self.results_df.columns),
        )

    def retranslateUi(self) -> None:
        self.setWindowTitle(tr("lpa_results.window_title", name=self.workflow_name))
        if hasattr(self, "header_label"):
            self.header_label.setText(self._format_header())
        if hasattr(self, "tabs") and self.tabs.count() >= 3:
            self.tabs.setTabText(0, tr("lpa_results.tab_table"))
            self.tabs.setTabText(1, tr("lpa_results.tab_map"))
            self.tabs.setTabText(2, tr("lpa_results.tab_summary"))
        for attr, key in (
            ("btn_print_tab", "lpa_results.btn_print"),
            ("btn_export_tab_csv", "lpa_results.btn_export_csv"),
            ("btn_export_map_png", "lpa_results.btn_export_png"),
            ("btn_export_all_xlsx", "lpa_results.btn_export_xlsx"),
            ("btn_add_calc_field", "lpa_results.btn_add_calc"),
            ("close_button", "lpa_results.btn_close"),
        ):
            btn = getattr(self, attr, None)
            if btn is not None:
                btn.setText(tr(key))
        for attr, key in (
            ("btn_print_tab", "lpa_results.tip_print"),
            ("btn_export_tab_csv", "lpa_results.tip_export_csv"),
            ("btn_export_map_png", "lpa_results.tip_export_png"),
            ("btn_export_all_xlsx", "lpa_results.tip_export_xlsx"),
        ):
            btn = getattr(self, attr, None)
            if btn is not None:
                btn.setToolTip(tr(key))
        if hasattr(self, "search_lbl"):
            self.search_lbl.setText(tr("lpa_results.lbl_search"))
        if hasattr(self, "search_box"):
            self.search_box.setPlaceholderText(tr("lpa_results.search_placeholder"))
        if hasattr(self, "btn_clear"):
            self.btn_clear.setText(tr("lpa_results.btn_clear"))
        if hasattr(self, "selected_row_label"):
            self.selected_row_label.setText(tr("lpa_results.lbl_selected_row"))
        if hasattr(self, "map_no_qgis_label"):
            self.map_no_qgis_label.setText(tr("lpa_results.map_no_qgis"))
        if hasattr(self, "summary_note_label"):
            # The note text is static, but we generated it via tr() at build time;
            # nothing to refresh dynamically here.
            pass
        # Refresh summary text + row details + map status — they're built from tr().
        self._populate_summary_text()
        self._refresh_row_details()
        self._refresh_export_buttons_for_current_tab()
        if hasattr(self, "map_status") and self._result_layer is not None:
            try:
                self.map_status.setText(
                    tr("lpa_results.map_showing", name=self._result_layer.name(), table_name=self.output_table_name)
                )
            except Exception:
                pass

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

    def _build_table_tab(self) -> None:
        tab = QWidget(self)
        outer = QVBoxLayout()
        tab.setLayout(outer)

        search_row = QHBoxLayout()
        self.search_lbl = QLabel(tr("lpa_results.lbl_search"))
        search_row.addWidget(self.search_lbl)
        self.search_box = QLineEdit()
        self.search_box.setPlaceholderText(tr("lpa_results.search_placeholder"))
        search_row.addWidget(self.search_box, 1)
        self.btn_clear = QPushButton(tr("lpa_results.btn_clear"))
        self.btn_clear.clicked.connect(self.search_box.clear)
        search_row.addWidget(self.btn_clear)
        outer.addLayout(search_row)

        splitter = QSplitter(Qt.Orientation.Vertical)
        outer.addWidget(splitter, 1)

        # Top: the table
        self.table_model = DataFrameTableModel(self.results_df, self)
        self.table_proxy = ResultsFilterProxyModel(self)
        self.table_proxy.setSourceModel(self.table_model)
        self.search_box.textChanged.connect(self.table_proxy.set_filter_text)

        self.table_view = QTableView()
        self.table_view.setModel(self.table_proxy)
        self.table_view.setSortingEnabled(True)
        self.table_view.setAlternatingRowColors(True)
        self.table_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        self.table_view.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
        self.table_view.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        header = self.table_view.horizontalHeader()
        header.setStretchLastSection(True)
        header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
        self.table_view.selectionModel().selectionChanged.connect(self._refresh_row_details)
        splitter.addWidget(self.table_view)

        # Bottom: row inspector
        details_host = QWidget(self)
        details_layout = QVBoxLayout()
        details_host.setLayout(details_layout)
        self.selected_row_label = QLabel(tr("lpa_results.lbl_selected_row"))
        details_layout.addWidget(self.selected_row_label)
        self.row_details = QTextEdit()
        self.row_details.setReadOnly(True)
        self.row_details.setMinimumHeight(140)
        details_layout.addWidget(self.row_details)
        splitter.addWidget(details_host)

        splitter.setStretchFactor(0, 3)
        splitter.setStretchFactor(1, 1)

        self.tabs.addTab(tab, tr("lpa_results.tab_table"))

    def _build_map_tab(self) -> None:
        tab = QWidget(self)
        layout = QVBoxLayout()
        tab.setLayout(layout)

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

        if QgsMapCanvas is None:
            self.map_canvas = None
            self.map_no_qgis_label = QLabel(tr("lpa_results.map_no_qgis"))
            self.map_no_qgis_label.setWordWrap(True)
            layout.addWidget(self.map_no_qgis_label, 1)
            self.tabs.addTab(tab, tr("lpa_results.tab_map"))
            return

        self.map_canvas = QgsMapCanvas(self)
        self.map_canvas.setCanvasColor(Qt.GlobalColor.white)
        self.map_canvas.enableAntiAliasing(True)
        layout.addWidget(self.map_canvas, 1)

        self._load_map_layer_if_possible()
        self.tabs.addTab(tab, tr("lpa_results.tab_map"))

    def _build_summary_tab(self) -> None:
        tab = QWidget(self)
        layout = QVBoxLayout()
        tab.setLayout(layout)

        self.summary_text = QTextEdit()
        self.summary_text.setReadOnly(True)
        layout.addWidget(self.summary_text, 1)

        self.tabs.addTab(tab, tr("lpa_results.tab_summary"))

    # ------------------------------------------------------------------
    # Map / layer
    # ------------------------------------------------------------------
    def _load_map_layer_if_possible(self) -> None:
        if QgsVectorLayer is None or self.map_canvas is None:
            return
        if not (self.database_path and self.output_table_name):
            self.map_status.setText(tr("lpa_results.map_no_gpkg"))
            return

        # Prefer the existing project layer by matching source URI; fall
        # back to opening a fresh handle. Either way we render it in the
        # dialog's local canvas so we don't disturb the main QGIS map.
        candidate: Optional[QgsVectorLayer] = None
        if QgsProject is not None:
            uri = f"{self.database_path}|layername={self.output_table_name}"
            for layer in QgsProject.instance().mapLayers().values():
                if isinstance(layer, QgsVectorLayer) and layer.source() == uri:
                    candidate = layer
                    break
        if candidate is None:
            uri = f"{self.database_path}|layername={self.output_table_name}"
            candidate = QgsVectorLayer(uri, self.layer_name_hint, "ogr")
            if not candidate.isValid():
                self.map_status.setText(
                    tr(
                        "lpa_results.map_open_failed",
                        database_path=self.database_path,
                        table_name=self.output_table_name,
                    )
                )
                return

        self._result_layer = candidate
        try:
            self.map_canvas.setLayers([candidate])
            self.map_canvas.setExtent(candidate.extent())
            self.map_canvas.refresh()
        except Exception as exc:
            debug_logger.warning("Could not bind LPA layer to canvas", {"exception": str(exc)})
            self.map_status.setText(tr("lpa_results.map_drawn_failed", error=exc))
            return

        self.map_status.setText(
            tr("lpa_results.map_showing", name=candidate.name(), table_name=self.output_table_name)
        )

    # ------------------------------------------------------------------
    # Row inspector
    # ------------------------------------------------------------------
    def _refresh_row_details(self, *_args) -> None:
        proxy_index = self.table_view.currentIndex()
        if not proxy_index.isValid():
            self.row_details.clear()
            return
        source_index = self.table_proxy.mapToSource(proxy_index)
        if not source_index.isValid():
            self.row_details.clear()
            return

        df = self.table_model.dataframe()
        if df.empty or source_index.row() >= len(df.index):
            self.row_details.clear()
            return

        row_series = df.iloc[source_index.row()]
        lines = [tr("lpa_results.row_index", value=df.index[source_index.row()])]
        for column_name in df.columns[:24]:
            value = row_series.get(column_name, "")
            if pd.isna(value):
                value = ""
            lines.append(f"{column_name}: {value}")
        remaining = len(df.columns) - min(24, len(df.columns))
        if remaining > 0:
            lines.append("")
            lines.append(tr("lpa_results.row_more_columns", count=remaining))
        self.row_details.setPlainText("\n".join(lines))

    # ------------------------------------------------------------------
    # Summary
    # ------------------------------------------------------------------
    def _populate_summary_text(self) -> None:
        if not hasattr(self, "summary_text"):
            return
        none_text = tr("lpa_results.summary_none")
        unknown_text = tr("lpa_results.execution_unknown")
        lines = [
            tr("lpa_results.summary_pipeline", value=self.workflow_name),
            tr("lpa_results.summary_execution", value=self.execution_id or unknown_text),
            tr("lpa_results.summary_geopackage", value=self.database_path or none_text),
            tr("lpa_results.summary_result_table", value=self.output_table_name or none_text),
            "",
            tr("lpa_results.summary_fields_header"),
        ]
        if self.summary:
            for key, value in self.summary.items():
                lines.append(tr("lpa_results.summary_field_line", field=key, value=value))
        else:
            lines.append(tr("lpa_results.summary_no_fields"))

        lines.append("")
        lines.append(tr("lpa_results.summary_columns_header", count=len(self.results_df.columns)))
        for column_name in self.results_df.columns:
            lines.append(tr("lpa_results.summary_column_line", name=column_name))
        self.summary_text.setPlainText("\n".join(lines))

    # ------------------------------------------------------------------
    # Toolbar handlers
    # ------------------------------------------------------------------
    def _refresh_export_buttons_for_current_tab(self, *_args) -> None:
        index = self.tabs.currentIndex()
        is_table_tab = index == 0
        is_map_tab = index == 1
        self.btn_print_tab.setEnabled(is_table_tab)
        self.btn_export_tab_csv.setEnabled(is_table_tab)
        self.btn_export_map_png.setEnabled(is_map_tab and self.map_canvas is not None)
        # XLSX always works while there's a Table tab DataFrame.
        self.btn_export_all_xlsx.setEnabled(not self.results_df.empty)
        self.btn_add_calc_field.setEnabled(bool(self.database_path and self.output_table_name))
        if not (self.database_path and self.output_table_name):
            self.btn_add_calc_field.setToolTip(tr("lpa_results.tip_calc_disabled"))
        else:
            self.btn_add_calc_field.setToolTip(tr("lpa_results.tip_calc_enabled"))

    def _default_export_basename(self) -> str:
        name_part = (self.workflow_name or "lpa").strip()
        execution_part = (self.execution_id or "").strip()[:8]
        if execution_part:
            return f"{name_part}-{execution_part}".replace(" ", "_")
        return name_part.replace(" ", "_")

    def _on_print_current_tab(self) -> None:
        if self.tabs.currentIndex() != 0:
            return
        title = tr("lpa_results.title_table_print", name=self.workflow_name)
        print_table_view(self.table_view, title=title, parent=self)

    def _on_export_current_tab_csv(self) -> None:
        if self.tabs.currentIndex() != 0:
            return
        default_name = f"{self._default_export_basename()}-table.csv"
        export_dataframe_to_csv(self.table_model.dataframe(), default_name=default_name, parent=self)

    def _on_export_map_png(self) -> None:
        if self.map_canvas is None:
            QMessageBox.information(
                self,
                tr("lpa_results.msg_no_map_title"),
                tr("lpa_results.msg_no_map_body"),
            )
            return
        default_name = f"{self._default_export_basename()}-map.png"
        export_widget_as_png(self.map_canvas, default_name=default_name, parent=self)

    def _on_export_all_tabs_xlsx(self) -> None:
        df = self.table_model.dataframe()
        if df is None or df.empty:
            QMessageBox.information(
                self,
                tr("lpa_results.msg_no_data_title"),
                tr("lpa_results.msg_no_data_body"),
            )
            return
        sheets = [TabExportSpec(name=tr("lpa_results.sheet_results"), dataframe=df)]
        if self.summary:
            sheets.append(
                TabExportSpec(
                    name=tr("lpa_results.sheet_summary"),
                    dataframe=pd.DataFrame(
                        {
                            tr("lpa_results.sheet_col_key"): list(self.summary.keys()),
                            tr("lpa_results.sheet_col_value"): [str(v) for v in self.summary.values()],
                        }
                    ),
                )
            )
        default_name = f"{self._default_export_basename()}-all-tabs.xlsx"
        export_dataframes_to_xlsx(sheets, default_name=default_name, parent=self)

    def _on_add_calculated_field(self) -> None:
        """Reuse the same calculated-field flow that ResultsGridDialog provides."""
        if not (self.database_path and self.output_table_name):
            QMessageBox.information(
                self,
                tr("lpa_results.calc_title"),
                tr("lpa_results.calc_not_backed"),
            )
            return

        if QgsVectorLayer is None:
            QMessageBox.critical(self, tr("lpa_results.calc_title"), tr("lpa_results.calc_no_qgis"))
            return

        uri = f"{self.database_path}|layername={self.output_table_name}"
        layer = QgsVectorLayer(uri, self.layer_name_hint, "ogr")
        if not layer.isValid():
            QMessageBox.critical(
                self,
                tr("lpa_results.calc_title"),
                tr("lpa_results.calc_open_failed", database_path=self.database_path, table_name=self.output_table_name),
            )
            return

        from .calculated_fields import CalculatedFieldError, materialize_calculated_field
        from .dialogs.calculated_field_dialog import CalculatedFieldDialog

        dialog = CalculatedFieldDialog(
            layer=layer,
            existing_field_names=list(self.results_df.columns),
            parent=self,
        )
        if exec_dialog(dialog) != QDialog.DialogCode.Accepted:
            return

        spec = dialog.spec()
        try:
            outcome = materialize_calculated_field(
                layer=layer,
                field_name=spec.name,
                qvariant_type=spec.qvariant_type,
                expression_text=spec.expression,
            )
        except CalculatedFieldError as exc:
            QMessageBox.critical(self, tr("lpa_results.calc_title"), str(exc))
            return
        except Exception as exc:  # noqa: BLE001
            QMessageBox.critical(
                self,
                tr("lpa_results.calc_title"),
                tr("lpa_results.calc_unexpected_error", error=exc),
            )
            return

        self.results_df = outcome.dataframe.fillna("")
        self.table_model.set_dataframe(self.results_df)
        self._populate_summary_text()
        self._refresh_row_details()

        message = tr(
            "lpa_results.calc_success_added",
            name=outcome.field_name,
            table_name=self.output_table_name,
        )
        if outcome.skipped_rows > 0:
            message += tr("lpa_results.calc_success_skipped_note", count=outcome.skipped_rows)
        QMessageBox.information(self, tr("lpa_results.calc_title"), message)

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------
    @staticmethod
    def _coerce_dataframe(value: Any) -> pd.DataFrame:
        if isinstance(value, pd.DataFrame):
            return value.copy().fillna("")
        if value is None:
            return pd.DataFrame()
        try:
            return pd.DataFrame(value).fillna("")
        except Exception:
            return pd.DataFrame()
