# SPDX-License-Identifier: GPL-3.0-or-later
"""History tab — browse the recorded voyage archive.

A table of stored voyages (across all vessels) with a name/MMSI search, an
optional time window and an optional "current map view" area filter. Click a
voyage to draw its track; draw every track of a vessel; export the shown rows to
CSV; and clear the history. Reads through HistoryReader (GeoPackage / PostGIS) and
draws via the shared 'AIS history' layer group.
"""
import csv
import time

from qgis.PyQt.QtCore import Qt, QDateTime
from qgis.PyQt.QtWidgets import (
    QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QCheckBox, QPushButton,
    QTableWidget, QTableWidgetItem, QDateTimeEdit, QAbstractItemView, QFileDialog,
    QHeaderView, QMessageBox)

_COLS = ["Vessel", "MMSI", "Start (UTC)", "End (UTC)", "Duration", "Reason",
         "Destination"]


def _utc(ts):
    return time.strftime("%Y-%m-%d %H:%M", time.gmtime(ts)) if ts else ""


def _dur(a, b):
    if not a or not b:
        return "…"
    m = int((b - a) / 60)
    return "%dh %02dm" % (m // 60, m % 60) if m >= 60 else "%dm" % m


# distinct colours cycled for drawn tracks (esp. 'show all tracks for vessel')
_TRACK_COLORS = ["255,0,200,255", "0,150,255,255", "0,190,90,255",
                 "255,140,0,255", "180,0,255,255", "220,40,40,255",
                 "0,190,190,255", "150,110,0,255"]


class _SortItem(QTableWidgetItem):
    """Table cell that displays text but sorts by a supplied numeric key, so
    MMSI / Start / End / Duration sort by value, not lexically."""
    def __init__(self, text, key):
        super().__init__(text)
        self._key = key

    def __lt__(self, other):
        try:
            return self._key < other._key
        except Exception:                          # noqa: BLE001
            return super().__lt__(other)


class HistoryTab(QWidget):
    def __init__(self, stream, iface, parent=None):
        """stream = StreamWidget (history config + recorder + delete flow);
        iface = QGIS interface (canvas extent for the area filter + drawing)."""
        super().__init__(parent)
        self.stream = stream
        self.iface = iface
        self._rows = []                 # voyage dicts, row-aligned
        self._track_i = 0               # colour cycle for drawn tracks
        lay = QVBoxLayout(self)

        # header: backend + counts + refresh
        top = QHBoxLayout()
        self.header = QLabel("")
        self.header.setWordWrap(True)
        top.addWidget(self.header, 1)
        self.btn_refresh = QPushButton("Refresh")
        self.btn_refresh.clicked.connect(self.refresh)
        top.addWidget(self.btn_refresh)
        lay.addLayout(top)

        # filter row 1: name/MMSI search
        f1 = QHBoxLayout()
        f1.addWidget(QLabel("Search:"))
        self.search = QLineEdit()
        self.search.setPlaceholderText("vessel name or MMSI")
        self.search.setClearButtonEnabled(True)
        self.search.returnPressed.connect(self.refresh)
        f1.addWidget(self.search, 1)
        lay.addLayout(f1)

        # filter row 2: time window
        f2 = QHBoxLayout()
        self.time_chk = QCheckBox("Time window:")
        self.time_chk.toggled.connect(self._toggle_time)
        f2.addWidget(self.time_chk)
        self.t_from = QDateTimeEdit()
        self.t_to = QDateTimeEdit()
        for w in (self.t_from, self.t_to):
            w.setDisplayFormat("yyyy-MM-dd HH:mm")
            w.setTimeSpec(Qt.UTC)
            w.setEnabled(False)
        f2.addWidget(self.t_from)
        f2.addWidget(QLabel("to"))
        f2.addWidget(self.t_to)
        f2.addWidget(QLabel("  last:"))
        for label, hours in (("hour", 1), ("day", 24), ("week", 168)):
            b = QPushButton(label)
            b.setMaximumWidth(52)
            b.clicked.connect(lambda _=False, h=hours: self._preset(h))
            f2.addWidget(b)
        f2.addStretch(1)
        lay.addLayout(f2)

        # filter row 3: area + apply
        f3 = QHBoxLayout()
        self.area_chk = QCheckBox("Only voyages crossing the current map view")
        f3.addWidget(self.area_chk)
        f3.addStretch(1)
        self.btn_apply = QPushButton("Apply filters")
        self.btn_apply.clicked.connect(self.refresh)
        f3.addWidget(self.btn_apply)
        lay.addLayout(f3)

        # the table
        self.table = QTableWidget(0, len(_COLS))
        self.table.setHorizontalHeaderLabels(_COLS)
        self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.table.setSelectionMode(QAbstractItemView.SingleSelection)
        self.table.setSortingEnabled(True)
        try:
            self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        except Exception:  # nosec B110
            pass
        self.table.itemDoubleClicked.connect(lambda *_: self._show_track())
        lay.addWidget(self.table, 1)

        # action buttons
        btns = QHBoxLayout()
        self.btn_track = QPushButton("Show track")
        self.btn_track.clicked.connect(self._show_track)
        self.btn_all = QPushButton("Show all tracks for vessel")
        self.btn_all.clicked.connect(self._show_all_tracks)
        self.btn_clear = QPushButton("Clear tracks")
        self.btn_clear.clicked.connect(self._clear_tracks)
        self.btn_csv = QPushButton("Export CSV…")
        self.btn_csv.clicked.connect(self._export_csv)
        self.btn_delete = QPushButton("Delete history…")
        self.btn_delete.clicked.connect(self._delete)
        for b in (self.btn_track, self.btn_all, self.btn_clear, self.btn_csv):
            btns.addWidget(b)
        btns.addStretch(1)
        btns.addWidget(self.btn_delete)
        lay.addLayout(btns)

    # ---- data ----------------------------------------------------------- #
    def _reader(self):
        from .history_reader import HistoryReader
        cfg = getattr(self.stream, "_history_cfg", None)
        return HistoryReader(cfg) if cfg is not None else None

    def _toggle_time(self, on):
        self.t_from.setEnabled(on)
        self.t_to.setEnabled(on)

    def showEvent(self, ev):            # refresh whenever the tab is shown
        super().showEvent(ev)
        self.refresh()

    def refresh(self):
        rd = self._reader()
        if rd is None:
            self.header.setText("Voyage history is not configured.")
            self.table.setRowCount(0)
            return
        ok, why = rd.available()
        if not ok:
            self.header.setText("No history to browse — %s. Turn on "
                                "'Record voyage history' on the AIS stream tab."
                                % why)
            self.table.setRowCount(0)
            self._rows = []
            return
        try:
            st = rd.history_stats()
            self._seed_time_defaults(st)
            limit = 20000
            self._rows = rd.list_all_voyages(limit=limit, **self._filters())
            filtered = self._is_filtered()
            shown = "showing %d" % len(self._rows)
            if len(self._rows) >= limit:
                shown += " (capped — narrow the filters)"
            elif filtered:
                shown += " of %d" % st["voyages"]
            self.header.setText(
                "Stored: %d voyages · %d vessels · %d fixes   (%s – %s UTC)   —   %s"
                % (st["voyages"], st["vessels"], st["fixes"],
                   _utc(st["t_min"]), _utc(st["t_max"]), shown))
        except Exception as exc:                    # noqa: BLE001
            self.header.setText("Could not read history: %s" % exc)
            self._rows = []
        self._fill_table()

    def _is_filtered(self):
        return bool(self.search.text().strip() or self.time_chk.isChecked()
                    or self.area_chk.isChecked())

    def _seed_time_defaults(self, st):
        """First time, default the time-window pickers to the data span."""
        if getattr(self, "_time_seeded", False) or not st.get("t_min"):
            return
        self.t_from.setDateTime(QDateTime.fromSecsSinceEpoch(int(st["t_min"]), Qt.UTC))
        self.t_to.setDateTime(QDateTime.fromSecsSinceEpoch(int(st["t_max"]) + 60, Qt.UTC))
        self._time_seeded = True

    def _filters(self):
        f = {"search": self.search.text().strip() or None}
        if self.time_chk.isChecked():
            f["t_from"] = self.t_from.dateTime().toSecsSinceEpoch()
            f["t_to"] = self.t_to.dateTime().toSecsSinceEpoch()
        if self.area_chk.isChecked():
            f["bbox"] = self._canvas_bbox_4326()
        return f

    def _canvas_bbox_4326(self):
        """Current map extent as (minx, miny, maxx, maxy) in EPSG:4326."""
        try:
            from qgis.core import (QgsCoordinateTransform, QgsCoordinateReferenceSystem,
                                   QgsProject)
            canvas = self.iface.mapCanvas()
            ext = canvas.extent()
            src = canvas.mapSettings().destinationCrs()
            dst = QgsCoordinateReferenceSystem("EPSG:4326")
            if src != dst:
                tr = QgsCoordinateTransform(src, dst, QgsProject.instance())
                ext = tr.transformBoundingBox(ext)
            return (ext.xMinimum(), ext.yMinimum(), ext.xMaximum(), ext.yMaximum())
        except Exception:                           # noqa: BLE001
            return None

    def _fill_table(self):
        self.table.setSortingEnabled(False)
        self.table.setRowCount(len(self._rows))
        for r, v in enumerate(self._rows):
            st, et = v.get("start_t"), v.get("end_t")
            try:
                mmsi_key = int(v.get("mmsi"))
            except (TypeError, ValueError):
                mmsi_key = -1
            dur_key = (et - st) if (st and et) else -1
            # (text, numeric-sort-key or None for plain string cells)
            cells = [(v.get("name") or "—", None),
                     (str(v.get("mmsi") or ""), mmsi_key),
                     (_utc(st), st or 0),
                     (_utc(et) or "(open)", et or 0),
                     (_dur(st, et), dur_key),
                     (v.get("end_reason") or "", None),
                     (v.get("destination") or "—", None)]
            for c, (text, key) in enumerate(cells):
                it = _SortItem(text, key) if key is not None else QTableWidgetItem(text)
                if c == 0:
                    it.setData(Qt.UserRole, v.get("voyage_id"))
                    it.setData(Qt.UserRole + 1, v.get("mmsi"))
                self.table.setItem(r, c, it)
        self.table.setSortingEnabled(True)
        self.table.sortItems(2, Qt.DescendingOrder)   # newest first by default

    # ---- actions -------------------------------------------------------- #
    def _selected(self):
        r = self.table.currentRow()
        if r < 0:
            return None
        it = self.table.item(r, 0)
        return it if it is not None else None

    def _next_color(self):
        color = _TRACK_COLORS[self._track_i % len(_TRACK_COLORS)]
        self._track_i += 1
        return color

    def _draw(self, coords, label, color=None):
        from .history_reader import add_track_layer
        if color is None:
            color = self._next_color()
        lyr = add_track_layer(label, coords, color)
        if lyr is None:
            return None
        try:
            from qgis.core import QgsCoordinateTransform, QgsProject
            canvas = self.iface.mapCanvas()
            tr = QgsCoordinateTransform(lyr.crs(), QgsProject.instance().crs(),
                                        QgsProject.instance())
            ext = tr.transformBoundingBox(lyr.extent())
            ext.scale(1.2)
            canvas.setExtent(ext)
            canvas.refresh()
        except Exception:                           # noqa: BLE001  # nosec B110
            pass
        return lyr

    def _show_track(self):
        it = self._selected()
        if it is None:
            return
        vid = it.data(Qt.UserRole)
        rd = self._reader()
        coords = rd.voyage_track(vid) if rd else []
        if coords and len(coords) >= 2:
            tail = str(vid).split(":")[-1]
            self._draw(coords, "Voyage %s (%s)" % (it.text(), tail))
        else:
            QMessageBox.information(self, "Show track",
                                    "That voyage has no stored track.")

    def _show_all_tracks(self):
        it = self._selected()
        if it is None:
            return
        mmsi = it.data(Qt.UserRole + 1)
        rd = self._reader()
        tracks = rd.vessel_tracks(mmsi) if rd else []
        if not tracks:
            QMessageBox.information(self, "Show all tracks",
                                    "No stored tracks for MMSI %s." % mmsi)
            return
        n = 0
        for vid, coords in tracks:
            if self._draw(coords, "Voyage %s (%s)" % (it.text(),
                                                      str(vid).split(":")[-1])):
                n += 1
        self.header.setText("Drew %d track(s) for %s." % (n, it.text()))

    def _preset(self, hours):
        """Time-window preset button: last N hours up to now, then refresh."""
        now = QDateTime.currentDateTimeUtc()
        self.time_chk.setChecked(True)
        self.t_from.setDateTime(now.addSecs(-int(hours) * 3600))
        self.t_to.setDateTime(now.addSecs(120))
        self.refresh()

    def _clear_tracks(self):
        from .history_reader import clear_track_layers
        n = clear_track_layers()
        self._track_i = 0
        try:
            self.iface.mapCanvas().refresh()
        except Exception:                           # noqa: BLE001  # nosec B110
            pass
        self.header.setText("Cleared %d drawn track(s)." % n)

    def _export_csv(self):
        if not self._rows:
            QMessageBox.information(self, "Export", "No voyages to export.")
            return
        path, _ = QFileDialog.getSaveFileName(self, "Export voyages",
                                              "ais_voyages.csv", "CSV (*.csv)")
        if not path:
            return
        try:
            with open(path, "w", newline="", encoding="utf-8") as fh:
                w = csv.writer(fh)
                w.writerow(["voyage_id", "mmsi", "name", "start_utc", "end_utc",
                            "end_reason", "destination"])
                for v in self._rows:
                    w.writerow([v.get("voyage_id"), v.get("mmsi"), v.get("name"),
                                _utc(v.get("start_t")), _utc(v.get("end_t")),
                                v.get("end_reason"), v.get("destination")])
        except Exception as exc:                    # noqa: BLE001
            QMessageBox.warning(self, "Export", "Could not write CSV:\n%s" % exc)

    def _delete(self):
        # the stream owns the recorder, so it runs the confirmed stop/clear/resume
        try:
            self.stream._delete_history()
        except Exception:                           # noqa: BLE001  # nosec B110
            pass
        self._time_seeded = False
        self.refresh()
