"""
Reusable results table viewer for EMDS workflows.
"""

from __future__ import annotations

from typing import Optional

import pandas as pd
from .theme import muted_text_color
from .qt_compat import (
    QApplication,
    QAbstractItemView,
    QComboBox,
    QDialog,
    QFileDialog,
    QHeaderView,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QMessageBox,
    QPushButton,
    QSortFilterProxyModel,
    QSplitter,
    QTableView,
    QTextEdit,
    QVBoxLayout,
    QWidget,
    Qt,
    exec_dialog,
)

from .results_grid_model import DataFrameTableModel


class ResultsFilterProxyModel(QSortFilterProxyModel):
    """Filter rows by searching across all columns."""

    def __init__(self, parent=None):
        super().__init__(parent)
        self._filter_text = ""
        self._allowed_source_rows: Optional[set[int]] = None

    def set_filter_text(self, text: str) -> None:
        self._filter_text = text.strip().lower()
        self.invalidateFilter()

    def set_source_rows_filter(self, source_rows: Optional[set[int]]) -> None:
        self._allowed_source_rows = set(source_rows) if source_rows else None
        self.invalidateFilter()

    def clear_source_rows_filter(self) -> None:
        self._allowed_source_rows = None
        self.invalidateFilter()

    def filterAcceptsRow(self, source_row: int, source_parent) -> bool:  # noqa: N802
        if self._allowed_source_rows is not None and source_row not in self._allowed_source_rows:
            return False

        if not self._filter_text:
            return True

        model = self.sourceModel()
        if model is None:
            return True

        column_count = model.columnCount()
        for column in range(column_count):
            index = model.index(source_row, column, source_parent)
            value = model.data(index, Qt.ItemDataRole.DisplayRole)
            if self._filter_text in str(value).lower():
                return True
        return False


class ResultsGridDialog(QDialog):
    """Generic, reusable results table viewer."""

    def __init__(
        self,
        dataframe: Optional[pd.DataFrame],
        title: str = "Results Grid",
        subtitle: str = "",
        parent=None,
        *,
        database_path: str = "",
        table_name: str = "",
        layer_name_hint: str = "",
    ):
        super().__init__(parent)
        self.dataframe = dataframe.copy().fillna("") if dataframe is not None else pd.DataFrame()
        self.title_text = title
        self.subtitle_text = subtitle
        # Optional GeoPackage backing — when both are present, the user
        # can add calculated fields that are written into the underlying
        # data.gpkg table. Without these, the calculated-field button is
        # disabled with a tooltip explaining why.
        self.database_path = str(database_path or "").strip()
        self.table_name = str(table_name or "").strip()
        self.layer_name_hint = str(layer_name_hint or table_name or title or "Results").strip()
        self.model = DataFrameTableModel(self.dataframe, self)
        self.proxy_model = ResultsFilterProxyModel(self)
        self.proxy_model.setSourceModel(self.model)

        self.setWindowTitle(self.title_text)
        self.resize(1200, 780)

        self._init_ui()
        self._refresh_row_details()

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

        title_label = QLabel(self.title_text)
        title_label.setWordWrap(True)
        layout.addWidget(title_label)

        if self.subtitle_text:
            subtitle_label = QLabel(self.subtitle_text)
            subtitle_label.setWordWrap(True)
            subtitle_label.setStyleSheet(f"color: {muted_text_color()};")
            layout.addWidget(subtitle_label)

        search_row = QHBoxLayout()
        search_row.addWidget(QLabel("Search"))
        self.search_box = QLineEdit()
        self.search_box.setPlaceholderText("Filter rows across all columns")
        self.search_box.textChanged.connect(self._on_search_changed)
        search_row.addWidget(self.search_box, 1)

        self.btn_clear_search = QPushButton("Clear")
        self.btn_clear_search.clicked.connect(self._clear_search)
        search_row.addWidget(self.btn_clear_search)
        layout.addLayout(search_row)

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

        table_host = QWidget()
        table_layout = QVBoxLayout()
        table_host.setLayout(table_layout)

        self.table_view = QTableView()
        self.table_view.setModel(self.proxy_model)
        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)
        self.table_view.selectionModel().selectionChanged.connect(self._refresh_row_details)
        self.table_view.doubleClicked.connect(self._copy_current_cell_value)
        table_layout.addWidget(self.table_view)
        splitter.addWidget(table_host)

        details_host = QWidget()
        details_layout = QVBoxLayout()
        details_host.setLayout(details_layout)
        details_layout.addWidget(QLabel("Selected Row"))

        self.row_details = QTextEdit()
        self.row_details.setReadOnly(True)
        self.row_details.setMinimumHeight(180)
        details_layout.addWidget(self.row_details)
        splitter.addWidget(details_host)
        splitter.setStretchFactor(0, 3)
        splitter.setStretchFactor(1, 1)

        button_row = QHBoxLayout()
        layout.addLayout(button_row)

        self.summary_label = QLabel(self._build_summary_text())
        self.summary_label.setWordWrap(True)
        button_row.addWidget(self.summary_label, 1)

        self.btn_add_calculated = QPushButton("Add Calculated Field…")
        self.btn_add_calculated.clicked.connect(self._add_calculated_field)
        button_row.addWidget(self.btn_add_calculated)

        self.btn_export = QPushButton("Export CSV")
        self.btn_export.clicked.connect(self._export_csv)
        button_row.addWidget(self.btn_export)

        self.btn_copy = QPushButton("Copy Selection")
        self.btn_copy.clicked.connect(self._copy_selection)
        button_row.addWidget(self.btn_copy)

        close_button = QPushButton("Close")
        close_button.clicked.connect(self.accept)
        button_row.addWidget(close_button)

        self._configure_headers()
        self._update_calculated_field_button_state()

    def _configure_headers(self) -> None:
        header = self.table_view.horizontalHeader()
        header.setStretchLastSection(True)
        header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)

    def _build_summary_text(self) -> str:
        row_count = len(self.dataframe.index)
        column_count = len(self.dataframe.columns)
        if row_count == 0 or column_count == 0:
            return "No results available."
        return f"Rows: {row_count}    Columns: {column_count}"

    def _on_search_changed(self, text: str) -> None:
        self.proxy_model.set_filter_text(text)
        self._refresh_row_details()

    def _clear_search(self) -> None:
        self.search_box.clear()

    def _current_source_index(self):
        proxy_index = self.table_view.currentIndex()
        if not proxy_index.isValid():
            return None
        return self.proxy_model.mapToSource(proxy_index)

    def _refresh_row_details(self, *args) -> None:
        index = self._current_source_index()
        if index is None or not index.isValid():
            self.row_details.clear()
            return

        dataframe = self.model.dataframe()
        row_series = dataframe.iloc[index.row()]
        lines = [f"Row Index: {dataframe.index[index.row()]}"]

        columns = list(dataframe.columns[:16])
        for column_name in columns:
            value = row_series.get(column_name, "")
            if pd.isna(value):
                value = ""
            lines.append(f"{column_name}: {value}")

        remaining = len(dataframe.columns) - len(columns)
        if remaining > 0:
            lines.append("")
            lines.append(f"... {remaining} more columns not shown")

        self.row_details.setPlainText("\n".join(lines))

    def _copy_current_cell_value(self) -> None:
        index = self._current_source_index()
        if index is None or not index.isValid():
            return

        dataframe = self.model.dataframe()
        column_name = dataframe.columns[index.column()]
        value = dataframe.iloc[index.row(), index.column()]
        if pd.isna(value):
            value = ""
        QApplication.clipboard().setText(f"{column_name}: {value}")

    def _copy_selection(self) -> None:
        selected_indexes = self.table_view.selectionModel().selectedRows()
        if not selected_indexes:
            QMessageBox.information(self, "Copy Selection", "Select one or more rows first.")
            return

        rows = []
        dataframe = self.model.dataframe()
        for proxy_index in selected_indexes:
            source_index = self.proxy_model.mapToSource(proxy_index)
            row_series = dataframe.iloc[source_index.row()]
            values = []
            for column in dataframe.columns:
                value = row_series.get(column, "")
                if pd.isna(value):
                    value = ""
                values.append(str(value))
            rows.append("\t".join(values))

        QApplication.clipboard().setText("\n".join(rows))

    def _export_csv(self) -> None:
        if self.dataframe.empty:
            QMessageBox.information(self, "Export CSV", "There is no data to export.")
            return

        file_path, _ = QFileDialog.getSaveFileName(
            self,
            "Export Results to CSV",
            "",
            "CSV Files (*.csv)",
        )
        if not file_path:
            return

        try:
            self.dataframe.to_csv(file_path, index=False)
            QMessageBox.information(self, "Export CSV", f"Results exported to:\n{file_path}")
        except Exception as exc:
            QMessageBox.critical(self, "Export CSV", f"Could not export results:\n{exc}")

    # ------------------------------------------------------------------
    # Calculated fields
    # ------------------------------------------------------------------
    def _update_calculated_field_button_state(self) -> None:
        """Enable the calc-field button only when we have a GeoPackage to write to."""
        button = getattr(self, "btn_add_calculated", None)
        if button is None:
            return
        if self.database_path and self.table_name:
            button.setEnabled(True)
            button.setToolTip(
                "Add a calculated column to the underlying GeoPackage table using "
                "a QGIS expression. The new column is persisted and reloaded with the table."
            )
        else:
            button.setEnabled(False)
            button.setToolTip(
                "This view was opened without a GeoPackage backing, so calculated "
                "fields cannot be saved. Open the result from a workflow task to enable this."
            )

    def _resolve_result_layer(self):
        """Open the GeoPackage table as a fresh QgsVectorLayer for editing.

        Each call returns a new QgsVectorLayer so the GeoPackage handle is
        released cleanly when the calculated-field operation is done.
        """
        try:
            from qgis.core import QgsVectorLayer
        except ImportError as exc:
            raise RuntimeError(
                "QGIS Python bindings are not available — calculated fields require QGIS."
            ) from exc

        if not self.database_path or not self.table_name:
            return None
        uri = f"{self.database_path}|layername={self.table_name}"
        layer = QgsVectorLayer(uri, self.layer_name_hint, "ogr")
        if not layer.isValid():
            return None
        return layer

    def _add_calculated_field(self) -> None:
        if not (self.database_path and self.table_name):
            QMessageBox.information(
                self,
                "Add Calculated Field",
                "This view is not backed by a GeoPackage, so calculated fields cannot "
                "be saved. Open the result from a workflow task to enable this feature.",
            )
            return

        layer = self._resolve_result_layer()
        if layer is None:
            QMessageBox.critical(
                self,
                "Add Calculated Field",
                f"Could not open the result table at:\n{self.database_path}\n"
                f"layer: {self.table_name}",
            )
            return

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

        existing = list(self.dataframe.columns)
        dialog = CalculatedFieldDialog(
            layer=layer,
            existing_field_names=existing,
            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, "Add Calculated Field", str(exc))
            return
        except Exception as exc:  # noqa: BLE001
            QMessageBox.critical(
                self,
                "Add Calculated Field",
                f"An unexpected error occurred while adding the column:\n{exc}",
            )
            return

        # Replace the in-memory DataFrame and refresh the table model.
        self.dataframe = outcome.dataframe.fillna("")
        self.model.set_dataframe(self.dataframe)
        self.summary_label.setText(self._build_summary_text())
        self._refresh_row_details()

        message = f"Added column '{outcome.field_name}' to {self.table_name}."
        if outcome.skipped_rows > 0:
            message += (
                f"\n\nNote: {outcome.skipped_rows} row(s) had expression evaluation "
                "errors and received NULL values."
            )
        QMessageBox.information(self, "Add Calculated Field", message)
