"""
Reusable matplotlib-backed histogram widget for EMDS result analysis.
"""

from __future__ import annotations

from typing import List, Optional, Sequence, Tuple

import numpy as np
import pandas as pd
from .netweaver_scheme import is_no_data
from .theme import muted_text_color
from .qt_compat import (
    QAbstractItemView,
    QCheckBox,
    QFrame,
    QGroupBox,
    QHBoxLayout,
    QLabel,
    QListWidget,
    QListWidgetItem,
    QPushButton,
    QSizePolicy,
    QSpinBox,
    QTextEdit,
    QVBoxLayout,
    QWidget,
    Qt,
)

try:
    from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
    from matplotlib.figure import Figure
    _MATPLOTLIB_AVAILABLE = True
    _MATPLOTLIB_IMPORT_ERROR = ""
except Exception as exc:  # pragma: no cover - fallback only used when matplotlib is missing
    FigureCanvas = None
    Figure = None
    _MATPLOTLIB_AVAILABLE = False
    _MATPLOTLIB_IMPORT_ERROR = str(exc)


def get_numeric_columns(dataframe: Optional[pd.DataFrame]) -> List[str]:
    """Return columns that contain at least one numeric value."""
    if dataframe is None or dataframe.empty:
        return []

    numeric_columns: List[str] = []
    for column in dataframe.columns:
        series = pd.to_numeric(dataframe[column], errors="coerce")
        if series.notna().any():
            numeric_columns.append(str(column))
    return numeric_columns


def drop_no_data(series: pd.Series) -> pd.Series:
    """Numeric view of *series* with NaN and no-data markers removed.

    Result tables written before defect D5 still hold the raw -9999.888777 and
    double.MaxValue sentinels. Left in, a single one of them sets the histogram
    bin edges (every bar collapses into one bin) and drags the mean far outside
    the real range. Engine scores live in [-1, 1] (CDP/LPA/Portfolio in [0, 1])
    and never approach those magnitudes, so this is safe for every result grid.
    """
    numeric = pd.to_numeric(series, errors="coerce").dropna()
    if numeric.empty:
        return numeric
    return numeric[~numeric.map(is_no_data)]


class HistogramWidget(QWidget):
    """Interactive histogram plotting surface for result columns."""

    def __init__(
        self,
        dataframe: Optional[pd.DataFrame],
        initial_columns: Optional[Sequence[str]] = None,
        title: str = "Histogram",
        subtitle: str = "",
        parent=None,
    ):
        super().__init__(parent)
        self.dataframe = dataframe.copy() if dataframe is not None else pd.DataFrame()
        self.title_text = title
        self.subtitle_text = subtitle
        self.numeric_columns = get_numeric_columns(self.dataframe)
        self._matplotlib_available = _MATPLOTLIB_AVAILABLE
        self._import_error = _MATPLOTLIB_IMPORT_ERROR

        self._init_ui()
        self._populate_columns(initial_columns or [])
        self._refresh_plot()

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

        header = QLabel(self.title_text)
        header.setWordWrap(True)
        header.setStyleSheet("font-weight: bold;")
        layout.addWidget(header)

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

        controls_row = QHBoxLayout()
        layout.addLayout(controls_row)

        column_group = QGroupBox("Numeric Columns")
        column_layout = QVBoxLayout()
        column_group.setLayout(column_layout)

        self.column_list = QListWidget()
        self.column_list.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
        self.column_list.itemSelectionChanged.connect(self._refresh_plot)
        column_layout.addWidget(self.column_list, 1)

        column_button_row = QHBoxLayout()
        self.btn_select_all = QPushButton("Select All")
        self.btn_select_all.clicked.connect(self._select_all_columns)
        column_button_row.addWidget(self.btn_select_all)

        self.btn_select_none = QPushButton("Select None")
        self.btn_select_none.clicked.connect(self._select_no_columns)
        column_button_row.addWidget(self.btn_select_none)

        column_layout.addLayout(column_button_row)
        controls_row.addWidget(column_group, 1)

        options_group = QGroupBox("Plot Options")
        options_layout = QVBoxLayout()
        options_group.setLayout(options_layout)

        bin_row = QHBoxLayout()
        bin_row.addWidget(QLabel("Bins"))
        self.bin_spin = QSpinBox()
        self.bin_spin.setRange(1, 200)
        self.bin_spin.setValue(10)
        self.bin_spin.valueChanged.connect(self._refresh_plot)
        bin_row.addWidget(self.bin_spin)
        bin_row.addStretch()
        options_layout.addLayout(bin_row)

        self.normalize_check = QCheckBox("Normalize")
        self.normalize_check.stateChanged.connect(self._refresh_plot)
        options_layout.addWidget(self.normalize_check)

        self.cumulative_check = QCheckBox("Cumulative")
        self.cumulative_check.stateChanged.connect(self._refresh_plot)
        options_layout.addWidget(self.cumulative_check)

        self.legend_check = QCheckBox("Show Legend")
        self.legend_check.setChecked(True)
        self.legend_check.stateChanged.connect(self._refresh_plot)
        options_layout.addWidget(self.legend_check)

        options_layout.addStretch()
        controls_row.addWidget(options_group, 0)

        self.summary_box = QTextEdit()
        self.summary_box.setReadOnly(True)
        self.summary_box.setMinimumHeight(130)
        layout.addWidget(self.summary_box)

        self.plot_host = QWidget()
        self.plot_layout = QVBoxLayout()
        self.plot_host.setLayout(self.plot_layout)
        layout.addWidget(self.plot_host, 1)

        if self._matplotlib_available:
            self.figure = Figure(figsize=(7, 4.5), tight_layout=True)
            self.canvas = FigureCanvas(self.figure)
            self.axes = self.figure.add_subplot(111)
            self.plot_layout.addWidget(self.canvas)
        else:  # pragma: no cover - only used when matplotlib is unavailable
            self.figure = None
            self.canvas = None
            self.axes = None
            self.plot_layout.addWidget(
                QLabel(
                    "Matplotlib is not available in this environment.\n"
                    f"{self._import_error}"
                )
            )

    def _populate_columns(self, initial_columns: Sequence[str]) -> None:
        """Populate the selectable histogram columns."""
        self.column_list.clear()
        for column_name in self.numeric_columns:
            item = QListWidgetItem(column_name)
            self.column_list.addItem(item)

        selected_names = [name for name in initial_columns if name in self.numeric_columns]
        if not selected_names and self.numeric_columns:
            selected_names = [self.numeric_columns[0]]

        for row_index in range(self.column_list.count()):
            item = self.column_list.item(row_index)
            item.setSelected(item.text() in selected_names)

    def _selected_columns(self) -> List[str]:
        """Return the currently selected columns."""
        return [item.text() for item in self.column_list.selectedItems()]

    def _select_all_columns(self) -> None:
        """Select every numeric column."""
        self.column_list.selectAll()

    def _select_no_columns(self) -> None:
        """Clear the current histogram column selection."""
        self.column_list.clearSelection()

    def set_dataframe(self, dataframe: Optional[pd.DataFrame], initial_columns: Optional[Sequence[str]] = None) -> None:
        """Replace the backing DataFrame and refresh the widget."""
        self.dataframe = dataframe.copy() if dataframe is not None else pd.DataFrame()
        self.numeric_columns = get_numeric_columns(self.dataframe)
        self._populate_columns(initial_columns or [])
        self._refresh_plot()

    def _selected_series(self) -> List[Tuple[str, np.ndarray]]:
        """Resolve selected numeric series from the DataFrame."""
        selected_columns = self._selected_columns()
        series_list: List[Tuple[str, np.ndarray]] = []

        for column_name in selected_columns:
            if column_name not in self.dataframe.columns:
                continue

            numeric_series = drop_no_data(self.dataframe[column_name])
            if numeric_series.empty:
                continue

            series_list.append((column_name, numeric_series.to_numpy(dtype=float)))

        return series_list

    def _refresh_plot(self) -> None:
        """Redraw the histogram."""
        if not self._matplotlib_available:
            self.summary_box.setPlainText(
                "Matplotlib is not available, so the histogram cannot be rendered."
            )
            return

        series_list = self._selected_series()
        if not series_list:
            self.axes.clear()
            self.axes.text(
                0.5,
                0.5,
                "Select one or more numeric columns to plot.",
                ha="center",
                va="center",
                transform=self.axes.transAxes,
            )
            self.axes.set_axis_off()
            self.canvas.draw_idle()
            self.summary_box.setPlainText(self._build_summary_text([]))
            return

        self.axes.clear()
        self.axes.set_axis_on()

        combined_values = np.concatenate([values for _, values in series_list])
        bin_count = self.bin_spin.value()
        bin_edges = np.histogram_bin_edges(combined_values, bins=bin_count)

        density = self.normalize_check.isChecked()
        cumulative = self.cumulative_check.isChecked()
        colors = [
            "#4e79a7",
            "#f28e2b",
            "#e15759",
            "#76b7b2",
            "#59a14f",
            "#edc948",
            "#b07aa1",
            "#ff9da7",
            "#9c755f",
            "#bab0ab",
        ]

        for index, (column_name, values) in enumerate(series_list):
            color = colors[index % len(colors)] if colors else None
            self.axes.hist(
                values,
                bins=bin_edges,
                alpha=0.6,
                label=column_name,
                density=density,
                cumulative=cumulative,
                color=color,
                edgecolor="white",
                linewidth=0.5,
            )

        self.axes.set_title(self.title_text)
        self.axes.set_xlabel("Value")
        self.axes.set_ylabel("Density" if density else "Frequency")
        self.axes.grid(True, alpha=0.25)
        if self.legend_check.isChecked() and len(series_list) > 1:
            self.axes.legend()

        self.canvas.draw_idle()
        self.summary_box.setPlainText(self._build_summary_text(series_list))

    def _build_summary_text(self, series_list: Sequence[Tuple[str, np.ndarray]]) -> str:
        """Build a compact statistics summary for the current selection."""
        lines = [
            f"Rows in table: {len(self.dataframe.index)}",
            f"Numeric columns detected: {len(self.numeric_columns)}",
            f"Selected columns: {', '.join(name for name, _ in series_list) if series_list else 'None'}",
            f"Bins: {self.bin_spin.value()}",
            f"Normalize: {'Yes' if self.normalize_check.isChecked() else 'No'}",
            f"Cumulative: {'Yes' if self.cumulative_check.isChecked() else 'No'}",
        ]

        if not series_list:
            lines.append("")
            lines.append("No numeric series selected.")
            return "\n".join(lines)

        lines.append("")
        for column_name, values in series_list:
            series = pd.Series(values)
            lines.extend(
                [
                    f"{column_name}:",
                    f"  Count: {len(series)}",
                    f"  Mean: {self._format_number(series.mean())}",
                    f"  Median: {self._format_number(series.median())}",
                    f"  Std Dev: {self._format_number(series.std(ddof=1))}",
                    f"  Min: {self._format_number(series.min())}",
                    f"  Max: {self._format_number(series.max())}",
                ]
            )

        return "\n".join(lines)

    @staticmethod
    def _format_number(value) -> str:
        """Format a floating-point summary value."""
        if pd.isna(value):
            return "n/a"
        try:
            return f"{float(value):.4f}"
        except Exception:
            return str(value)
