"""Read-only provenance view for a task (docs/provenance/00, Phase 2).

Shows, for the selected task, the latest run's manifest — model snapshot
(name/hash/version), engine version, parameters, field mappings, inputs, run
time and user — plus a run-history table and a "stale" banner when the current
model file or task configuration no longer matches what was last run.
"""

from __future__ import annotations

import hashlib
import json
import os
from typing import Any, List, Optional

from ..qt_compat import (
    QAbstractItemView,
    QDialog,
    QFileDialog,
    QFormLayout,
    QGroupBox,
    QHBoxLayout,
    QHeaderView,
    QLabel,
    QMessageBox,
    QPushButton,
    QScrollArea,
    QTableWidget,
    QTableWidgetItem,
    QTextEdit,
    QVBoxLayout,
    QWidget,
    Qt,
)
from ..translator import tr


def _norm(value: Any) -> str:
    try:
        return json.dumps(value, sort_keys=True, default=str)
    except Exception:
        return str(value)


def _sha256_of_file(path: str) -> str:
    try:
        with open(path, "rb") as handle:
            return hashlib.sha256(handle.read()).hexdigest()
    except OSError:
        return ""


def _short(value: str, length: int = 12) -> str:
    value = value or ""
    return value[:length] + "…" if len(value) > length else value


def _fmt_duration(ms: int) -> str:
    try:
        ms = int(ms or 0)
    except Exception:
        return ""
    if ms < 1000:
        return f"{ms} ms"
    seconds = ms / 1000.0
    if seconds < 60:
        return f"{seconds:.1f} s"
    minutes, secs = divmod(int(seconds), 60)
    return f"{minutes}m {secs}s"


class ProvenanceDialog(QDialog):
    """Provenance / lineage view for one task's latest run."""

    def __init__(self, metadata_manager, workflow_step, parent=None, compare_callback=None):
        super().__init__(parent)
        self.manager = metadata_manager
        self.task = workflow_step
        self._compare_callback = compare_callback
        self.setWindowTitle(tr("provenance.dialog_title", name=getattr(workflow_step, "name", "")))
        self.resize(720, 640)
        self._build()

    # ------------------------------------------------------------------
    def _build(self) -> None:
        outer = QVBoxLayout()
        self.setLayout(outer)

        header = QLabel(f"<b>Provenance — {self.task.name}</b>")
        header.setTextFormat(Qt.TextFormat.RichText)
        outer.addWidget(header)

        task_id = self.task.id
        task_meta = self.manager.get_task(task_id)
        step_runs = self.manager.get_step_runs_for_task(task_id)
        latest = step_runs[0] if step_runs else None

        if latest is None:
            info = QLabel(
                "No runs have been recorded for this task yet.\n\n"
                "Run the task to capture its model snapshot, parameters and engine version."
            )
            info.setWordWrap(True)
            outer.addWidget(info)
            outer.addStretch()
            self._add_close_button(outer)
            return

        run = self.manager.get_workflow_run(latest.run_id)
        snapshot_id = latest.model_snapshot_id or (task_meta.model_snapshot_id if task_meta else "")
        snapshot = self.manager.get_model_snapshot(snapshot_id) if snapshot_id else None

        # Scrollable body
        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        body = QWidget()
        body_layout = QVBoxLayout()
        body.setLayout(body_layout)
        scroll.setWidget(body)
        outer.addWidget(scroll, 1)

        # Stale banner
        for message in self._stale_messages(latest, snapshot, task_meta):
            banner = QLabel("⚠ " + message)
            banner.setWordWrap(True)
            banner.setStyleSheet(
                "color: #92400e; background-color: #fef3c7; padding: 6px; border-radius: 4px;"
            )
            body_layout.addWidget(banner)

        body_layout.addWidget(self._latest_run_group(latest, run))
        body_layout.addWidget(self._model_group(snapshot))
        body_layout.addWidget(self._config_group(latest))
        body_layout.addWidget(self._history_group(step_runs))
        body_layout.addStretch()

        self._add_close_button(outer)

    # ------------------------------------------------------------------
    def _stale_messages(self, latest, snapshot, task_meta) -> List[str]:
        messages: List[str] = []
        params = self.task.parameters or {}
        model_path = params.get("model_file") or (task_meta.model_path if task_meta else "")

        if snapshot and model_path and os.path.isfile(model_path):
            current_sha = _sha256_of_file(model_path)
            if current_sha and current_sha != snapshot.content_sha256:
                messages.append("The model file has changed since this task was last run.")

        current_mappings = params.get("field_mappings", []) or []
        if _norm(current_mappings) != _norm(latest.field_mappings):
            messages.append("Field mappings have changed since this task was last run.")

        current_params = {k: v for k, v in params.items() if k != "field_mappings"}
        if _norm(current_params) != _norm(latest.parameters):
            messages.append("Task parameters have changed since this task was last run.")

        return messages

    def _latest_run_group(self, latest, run) -> QGroupBox:
        group = QGroupBox("Latest run")
        form = QFormLayout()
        group.setLayout(form)
        when = (run.start_time if run and run.start_time else "") or latest.start_time or (
            run.created_at if run else ""
        )
        self._add_row(form, "When", when)
        self._add_row(form, "Status", latest.status or (run.status if run else ""))
        self._add_row(form, "Rows processed", str(latest.rows_processed or 0))
        self._add_row(form, "Duration", _fmt_duration(latest.duration_ms))
        self._add_row(form, "Engine", latest.engine_type)
        self._add_row(form, "Engine version", latest.engine_version or "(not reported)")
        if run:
            self._add_row(form, "Run kind", run.run_kind)
            self._add_row(form, "User", run.created_by or "(unknown)")
            self._add_row(form, "EMDS version", run.emds_version or "")
            gis = " ".join(p for p in (run.gis_environment, run.gis_version) if p)
            self._add_row(form, "GIS", gis)
        if latest.error_message:
            self._add_row(form, "Error", latest.error_message)
        return group

    def _model_group(self, snapshot) -> QGroupBox:
        group = QGroupBox("Model snapshot")
        form = QFormLayout()
        group.setLayout(form)
        if snapshot is None:
            self._add_row(
                form, "", "No model snapshot recorded (engine without a model file, "
                          "or model captured before provenance was enabled)."
            )
            return group
        registry_id = getattr(snapshot, "registry_id", "") or ""
        registry_version = getattr(snapshot, "registry_version", "") or ""
        if registry_id:
            verified = QLabel("✓ " + tr("provenance.verified_registry_model"))
            verified.setStyleSheet(
                "color: #065f46; background-color: #d1fae5; padding: 4px 8px; border-radius: 4px;"
            )
            form.addRow(QLabel(""), verified)
        self._add_row(form, "Name", snapshot.display_name or "")
        self._add_row(form, "Kind", snapshot.model_kind or "")
        self._add_row(form, "Source path", snapshot.source_path or "")
        self._add_row(form, "Format", snapshot.source_format or "")
        self._add_row(form, "Content SHA-256", snapshot.content_sha256 or "")
        self._add_row(form, "Schema version", snapshot.schema_version or "")
        self._add_row(form, "Authoring tool", snapshot.authoring_tool or "")
        self._add_row(form, "Engine version", snapshot.engine_version or "")
        if registry_id:
            self._add_row(form, tr("provenance.registry_model_id"), registry_id)
            self._add_row(form, tr("provenance.registry_version"), registry_version or "(current)")
        self._add_row(form, "Captured at", snapshot.created_at or "")
        return group

    def _config_group(self, latest) -> QGroupBox:
        group = QGroupBox("Run configuration (as executed)")
        layout = QVBoxLayout()
        group.setLayout(layout)
        payload = {
            "parameters": latest.parameters,
            "field_mappings": latest.field_mappings,
            "input_refs": latest.input_refs,
        }
        viewer = QTextEdit()
        viewer.setReadOnly(True)
        try:
            viewer.setPlainText(json.dumps(payload, indent=2, default=str))
        except Exception:
            viewer.setPlainText(str(payload))
        viewer.setMinimumHeight(160)
        layout.addWidget(viewer)
        return group

    def _history_group(self, step_runs) -> QGroupBox:
        group = QGroupBox(f"Run history ({len(step_runs)})")
        layout = QVBoxLayout()
        group.setLayout(layout)
        table = QTableWidget()
        headers = ["When", "Status", "Engine ver.", "Model hash", "Rows", "Duration"]
        table.setColumnCount(len(headers))
        table.setHorizontalHeaderLabels(headers)
        table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        table.setRowCount(len(step_runs))
        for row_idx, step in enumerate(step_runs):
            snapshot = (
                self.manager.get_model_snapshot(step.model_snapshot_id)
                if step.model_snapshot_id
                else None
            )
            model_hash = _short(snapshot.content_sha256) if snapshot else ""
            values = [
                step.start_time or step.created_at or "",
                step.status or "",
                step.engine_version or "",
                model_hash,
                str(step.rows_processed or 0),
                _fmt_duration(step.duration_ms),
            ]
            for col_idx, value in enumerate(values):
                table.setItem(row_idx, col_idx, QTableWidgetItem(str(value)))
        header = table.horizontalHeader()
        header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
        for col in range(1, len(headers)):
            header.setSectionResizeMode(col, QHeaderView.ResizeMode.ResizeToContents)
        table.setMinimumHeight(160)
        layout.addWidget(table)
        return group

    # ------------------------------------------------------------------
    @staticmethod
    def _add_row(form: QFormLayout, label: str, value: str) -> None:
        value_label = QLabel(str(value if value is not None else ""))
        value_label.setWordWrap(True)
        value_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
        form.addRow(QLabel(f"<b>{label}</b>" if label else ""), value_label)

    def _add_close_button(self, outer: QVBoxLayout) -> None:
        row = QHBoxLayout()
        if self._compare_callback is not None:
            compare_button = QPushButton(tr("provenance.btn_compare_versions_inline"))
            compare_button.setToolTip(tr("provenance.tip_compare_versions_inline"))
            compare_button.clicked.connect(self._on_compare_clicked)
            row.addWidget(compare_button)
        export_button = QPushButton(tr("provenance.btn_export_report"))
        export_button.setToolTip(tr("provenance.tip_export_report"))
        export_button.clicked.connect(self._on_export_report)
        row.addWidget(export_button)
        row.addStretch()
        close_button = QPushButton(tr("provenance.btn_close"))
        close_button.clicked.connect(self.close)
        row.addWidget(close_button)
        outer.addLayout(row)

    def _on_export_report(self) -> None:
        try:
            from ..provenance_report import build_run_report_html
            html_text = build_run_report_html(self.manager, self.task.id)
        except Exception as exc:
            QMessageBox.warning(self, tr("provenance.mbox_export_report"),
                                tr("provenance.report_build_failed", error=exc))
            return
        default_name = f"provenance_{getattr(self.task, 'name', 'task')}.html".replace(" ", "_")
        path, _ = QFileDialog.getSaveFileName(self, tr("provenance.report_save_title"), default_name, "HTML (*.html)")
        if not path:
            return
        try:
            with open(path, "w", encoding="utf-8") as handle:
                handle.write(html_text)
        except OSError as exc:
            QMessageBox.warning(self, tr("provenance.mbox_export_report"),
                                tr("provenance.report_write_failed", error=exc))
            return
        QMessageBox.information(self, tr("provenance.mbox_export_report"),
                                tr("provenance.report_saved", path=path))

    def _on_compare_clicked(self) -> None:
        if self._compare_callback is None:
            return
        try:
            self._compare_callback()
        except Exception:
            pass
