"""A self-managing line layer of vessel trails.

For each MMSI we keep a short rolling history of its last positions. When trails
are switched on (5, 10 or 20 fixes), each vessel gets one red dotted LineString
built from its most recent N fixes, ending at its current position. Switching
trails off removes the layer; the history is kept in memory so turning it back
on redraws immediately.

The history bookkeeping (append, de-dup, cap, last-N slice) is plain Python and
unit-testable; only the rendering touches QGIS, and all QGIS calls are
best-effort so a styling/layer hiccup can never break the live feed.
"""
import time

from qgis.core import (
    QgsVectorLayer, QgsFeature, QgsGeometry, QgsPointXY, QgsProject,
)


def _trail_history(points, n):
    """Return the last n points to draw, or [] if fewer than 2 are available.
    Pure helper so the slice logic can be tested without QGIS."""
    if n <= 0 or len(points) < 2:
        return []
    return points[-n:]


class TrailLayer:
    LAYER_NAME = "AIS trails"
    MAX_HISTORY = 20                    # cap = largest selectable trail length
    REPAINT_INTERVAL = 0.7             # s; throttle canvas repaints (see _upsert)

    def __init__(self):
        """Start with no layer and empty per-MMSI history; trails are off (n=0)."""
        self.layer = None
        self._layer_id = None
        self._hist = {}                 # mmsi -> [(lon, lat), ...] oldest..newest
        self._fid = {}                  # mmsi -> feature id
        self._n = 0                     # 0 = off; else vertices to draw
        self._last_repaint = 0.0

    # ---- layer lifecycle -------------------------------------------------
    def _alive(self):
        """True if our trail layer still exists in the project (by stored id)."""
        if not self._layer_id:
            return False
        return QgsProject.instance().mapLayer(self._layer_id) is not None

    def ensure_layer(self):
        """Return the trail layer, recreating + restyling it if it was removed."""
        proj = QgsProject.instance()
        if self._layer_id:
            existing = proj.mapLayer(self._layer_id)
            if existing is not None:
                self.layer = existing
                return existing
        from .qgis_compat import make_field
        layer = QgsVectorLayer("LineString?crs=EPSG:4326", self.LAYER_NAME, "memory")
        layer.dataProvider().addAttributes([make_field("mmsi", "str")])
        layer.updateFields()
        self._style(layer)
        proj.addMapLayer(layer)
        self.layer = layer
        self._layer_id = layer.id()
        self._fid = {}                  # feature ids reset with a fresh layer
        return layer

    def _style(self, layer):
        """Render trails as thin red dotted lines (best-effort)."""
        try:
            from qgis.core import QgsLineSymbol, QgsSingleSymbolRenderer
            sym = QgsLineSymbol.createSimple({
                "line_color": "227,59,59,255",     # red
                "line_width": "0.4",
                "line_style": "dot",               # dotted
            })
            layer.setRenderer(QgsSingleSymbolRenderer(sym))
        except Exception:  # nosec B110
            pass

    # ---- configuration ---------------------------------------------------
    def set_length(self, n):
        """n in {0,5,10,20}; 0 (or less) switches trails off and drops the layer."""
        self._n = int(n) if n else 0
        if self._n <= 0:
            self._remove_layer()
            return
        self.ensure_layer()
        for mmsi in list(self._hist):
            self._upsert(mmsi)

    def is_on(self):
        """True when trails are currently enabled (length > 0)."""
        return self._n > 0

    # ---- updates ---------------------------------------------------------
    def update(self, mmsi, lon, lat):
        """Record a fix; redraw that vessel's trail when trails are on."""
        if mmsi is None or lon is None or lat is None:
            return
        mmsi = str(mmsi)
        h = self._hist.setdefault(mmsi, [])
        pt = (lon, lat)
        if not h or h[-1] != pt:            # skip repeated identical fixes
            h.append(pt)
            if len(h) > self.MAX_HISTORY:
                del h[0:len(h) - self.MAX_HISTORY]
        if self._n > 0:
            self.ensure_layer()
            self._upsert(mmsi)

    def _upsert(self, mmsi):
        """Redraw one vessel's trail feature from its last-N fixes (add or update
        the feature in place); needs 2+ vertices to draw a line."""
        pts = _trail_history(self._hist.get(mmsi, []), self._n)
        if len(pts) < 2:
            return                          # need 2+ vertices for a line
        geom = QgsGeometry.fromPolylineXY([QgsPointXY(x, y) for x, y in pts])
        pr = self.layer.dataProvider()
        if mmsi in self._fid:
            pr.changeGeometryValues({self._fid[mmsi]: geom})
        else:
            f = QgsFeature(self.layer.fields())
            f.setGeometry(geom)
            try:
                f.setAttribute("mmsi", mmsi)
            except Exception:  # nosec B110
                pass
            ok, out = pr.addFeatures([f])
            if ok and out:
                self._fid[mmsi] = out[0].id()
        # The geometry write above is cheap; the repaint is not — it re-renders the
        # whole canvas (every vessel + the basemap). Doing that per fix pinned a
        # core at high feed rates, so throttle it to REPAINT_INTERVAL like the
        # vessel layer (trails still lag the live picture by at most that interval).
        self._maybe_repaint()

    def _maybe_repaint(self):
        """Repaint the canvas at most once per the profile's repaint interval."""
        interval = self.REPAINT_INTERVAL
        try:
            from . import perf_profile
            interval = perf_profile.get("repaint_s") or interval
        except Exception:                          # noqa: BLE001  # nosec B110
            pass
        now = time.monotonic()
        if now - self._last_repaint >= interval:
            self._last_repaint = now
            if self.layer is not None:
                self.layer.triggerRepaint()

    def remove(self, mmsi):
        """Delete one vessel's trail feature + drop its history (stale/lost
        target). Best-effort."""
        mmsi = str(mmsi)
        self._hist.pop(mmsi, None)
        fid = self._fid.pop(mmsi, None)
        if fid is not None and self.layer is not None and self._alive():
            try:
                self.layer.dataProvider().deleteFeatures([fid])
                self.layer.triggerRepaint()
            except Exception:  # nosec B110
                pass

    def _remove_layer(self):
        """Drop the trail layer from the project (history stays in memory)."""
        if self._layer_id:
            try:
                QgsProject.instance().removeMapLayer(self._layer_id)
            except Exception:  # nosec B110
                pass
        self.layer = None
        self._layer_id = None
        self._fid = {}
