"""User-drawn custom routes.

For users outside the bundled (Dutch) fairway network, this gives them a way to
define their own routes: an editable QGIS LineString layer where each feature is
one named route. The route's drawn direction (first vertex -> last vertex) sets
inbound/outbound (drawn direction = INBOUND). Custom routes are injected into the
live FairwayNetwork with NEGATIVE routeids and take binding PRIORITY over native
routes (see fairway_network._CUSTOM_PRIORITY), so a vessel within tolerance of
both binds to the custom route.

Storage: custom_routes.gpkg in the profile dir (user data — persists across
sessions, not shipped with the plugin).
"""
import math
import os

CUSTOM_LAYER_NAME = "Custom routes"

# Drawn direction (start -> end) counts as INBOUND. The flow formula in
# map_matcher reads outbound = (course_sense > 0) == route_seaward, and +km points
# toward the line's end, so route_seaward = False makes the drawn direction inbound.
_DRAWN_DIRECTION_IS_INBOUND = True


def custom_gpkg_path():
    """Path to the user's custom-routes store in the profile dir."""
    from .guardian_store import default_base_dir
    return os.path.join(default_base_dir(), "custom_routes.gpkg")


def _create_custom_gpkg(path):
    """Create an empty custom_routes.gpkg (LineString layer 'routes', field name)."""
    from osgeo import ogr, osr
    os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
    ds = ogr.GetDriverByName("GPKG").CreateDataSource(path)
    srs = osr.SpatialReference()
    srs.ImportFromEPSG(4326)
    lyr = ds.CreateLayer("routes", srs, ogr.wkbLineString)
    lyr.CreateField(ogr.FieldDefn("name", ogr.OFTString))
    ds = None


def ensure_custom_routes_layer():
    """Create (if needed) and add the editable 'Custom routes' LineString layer,
    file-backed by custom_routes.gpkg in the profile dir so routes persist across
    sessions. Idempotent; returns the layer or None on failure."""
    from qgis.core import (QgsProject, QgsVectorLayer, QgsLineSymbol,
                           QgsPalLayerSettings, QgsVectorLayerSimpleLabeling,
                           QgsTextFormat, QgsTextBufferSettings)
    from qgis.PyQt.QtGui import QColor
    proj = QgsProject.instance()
    for l in proj.mapLayersByName(CUSTOM_LAYER_NAME):
        return l
    path = custom_gpkg_path()
    try:
        if not os.path.exists(path):
            _create_custom_gpkg(path)
    except Exception:                          # noqa: BLE001
        return None
    lyr = QgsVectorLayer(path + "|layername=routes", CUSTOM_LAYER_NAME, "ogr")
    if not lyr.isValid():
        return None
    try:                                       # blue line + name label
        sym = QgsLineSymbol.createSimple({"color": "0,90,200,220", "width": "0.6"})
        lyr.renderer().setSymbol(sym)
        st = QgsPalLayerSettings()
        st.fieldName = "name"
        try:
            st.placement = getattr(QgsPalLayerSettings, "Line",
                                   getattr(QgsPalLayerSettings.Placement, "Line", 2))
        except Exception:  # nosec B110
            pass
        fmt = QgsTextFormat()
        fmt.setSize(8)
        buf = QgsTextBufferSettings()
        buf.setEnabled(True)
        buf.setSize(1.0)
        buf.setColor(QColor("white"))
        fmt.setBuffer(buf)
        st.setFormat(fmt)
        lyr.setLabeling(QgsVectorLayerSimpleLabeling(st))
        lyr.setLabelsEnabled(True)
    except Exception:                          # noqa: BLE001  # nosec B110
        pass
    proj.addMapLayer(lyr)
    return lyr


def read_custom_routes(layer):
    """[{fid, name, coords}] from the custom-routes layer (EPSG:4326).

    coords is a list of (lon, lat). MultiLineString features use their first part.
    Reads live, so edits in the current QGIS session are reflected on rebuild."""
    out = []
    if layer is None:
        return out
    try:
        has_name = "name" in [f.name() for f in layer.fields()]
        for f in layer.getFeatures():
            g = f.geometry()
            if g is None or g.isEmpty():
                continue
            if g.isMultipart():
                parts = g.asMultiPolyline()
                pts = parts[0] if parts else []
            else:
                pts = g.asPolyline()
            if len(pts) < 2:
                continue
            out.append({"fid": int(f.id()),
                        "name": (f["name"] if has_name else "") or "",
                        "coords": [(p.x(), p.y()) for p in pts]})
    except Exception:                          # noqa: BLE001
        return out
    return out


def set_custom_routes(net, customs):
    """(Re)inject the user's custom routes into ``net`` for live binding.

    Clears any previously-injected custom data and rebuilds ``net.custom_index``
    plus the negative-routeid entries in the net's meta (names, drawn-direction
    inbound/outbound flag, end nodes for the route-km range). Cheap — a handful of
    routes — so it's safe to call whenever the layer changes.
    """
    seaward = not _DRAWN_DIRECTION_IS_INBOUND   # False => drawn direction = inbound
    rn = net.meta.setdefault("route_names", {})
    rs = net.meta.setdefault("route_seaward", {})
    re_ = net.meta.setdefault("route_ends", {})
    nx_ = net.meta.setdefault("node_xy", {})
    for d in (rn, rs, re_):
        for k in [k for k in d if isinstance(k, int) and k < 0]:
            del d[k]
    for k in [k for k in nx_ if isinstance(k, tuple) and k and k[0] == "c"]:
        del nx_[k]
    net.custom_index = {}
    cell = net.cell
    for c in customs:
        coords = c["coords"]
        if len(coords) < 2:
            continue
        rid = -(int(c["fid"]) + 1)             # stable, unique, negative
        name = c.get("name") or ("custom %d" % (-rid))
        m = [net.to_m(lo, la) for lo, la in coords]
        cum = [0.0]
        for i in range(len(m) - 1):
            cum.append(cum[-1] + math.hypot(m[i + 1][0] - m[i][0],
                                            m[i + 1][1] - m[i][1]))
        total = cum[-1]
        if total <= 0:
            continue
        kme = total / 1000.0                   # km = drawn length
        for i in range(len(m) - 1):
            ax, ay = m[i]; bx, by = m[i + 1]
            rec = (ax, ay, bx, by, int(c["fid"]), rid, name, 0.0, kme, total,
                   cum[i], coords[i][0], coords[i][1],
                   coords[i + 1][0], coords[i + 1][1])
            c0 = (int(ax // cell), int(ay // cell))
            c1 = (int(bx // cell), int(by // cell))
            for cx in range(min(c0[0], c1[0]), max(c0[0], c1[0]) + 1):
                for cy in range(min(c0[1], c1[1]), max(c0[1], c1[1]) + 1):
                    net.custom_index.setdefault((cx, cy), []).append(rec)
        rn[rid] = name
        rs[rid] = seaward
        re_[rid] = [(0.0, ("c", rid, 0)), (kme, ("c", rid, 1))]
        nx_[("c", rid, 0)] = (coords[0][0], coords[0][1])
        nx_[("c", rid, 1)] = (coords[-1][0], coords[-1][1])
