"""A saved Journey as a GPX 1.1 file (AV-053) for a watch, a bike computer
(Garmin Edge, Wahoo), Komoot, GPX.studio or BaseCamp.

* The whole trip: either one ``trk`` with one ``trkseg`` per stage
  (``layout="segments"``, what Garmin Connect and Komoot read as one course)
  or one ``trk`` per stage (``layout="tracks"``, what BaseCamp and
  GPX.studio show as separate days). One stage alone: one ``trk``.
* Camps (the plan's markers) and the route's points of interest as ``wpt``,
  in route order; camps carry ``<type>Nocleg</type>`` and the ``Lodging``
  symbol Garmin devices know.
* Metadata: the trip's name, the application as author and creator, a link
  back, and -- where the geometry came from OpenStreetMap through BRouter --
  the ODbL copyright. An imported route that was not matched to trails is
  the file's own geometry: no OSM copyright is claimed for it.
* Elevation only where known (never a filler value); no times (a plan is
  not a recorded walk).

The output validates against the GPX 1.1 schema (tests/fixtures/gpx/
gpx11.xsd) -- the children of each element in the schema's order.
"""

from __future__ import annotations

import xml.etree.ElementTree as ET
from dataclasses import dataclass
from typing import Sequence

GPX_NS = "http://www.topografix.com/GPX/1/1"
# AV-064: a transfer (dojazd) is marked in the application's own extension
# namespace (GPX 1.1 allows no <type> on a trkseg) and, as a whole track, by
# <type>dojazd</type>; route_files.py reads both back.
APP_NS = "https://echtro.app/gpx/1"
TRANSFER_TYPE = "dojazd"
OSM_LICENSE = "https://opendatacommons.org/licenses/odbl/"
LAYOUTS = ("segments", "tracks")


@dataclass(frozen=True)
class ExportStage:
    label: str
    start_index: int  # into the route's points, inclusive
    end_index: int  # inclusive


@dataclass(frozen=True)
class ExportWaypoint:
    latitude: float
    longitude: float
    name: str
    kind: str  # CAMP | POI
    elevation_m: float | None = None
    description: str | None = None


def _tag(name: str) -> str:
    return f"{{{GPX_NS}}}{name}"


def _sub(parent, name: str, text: str | None = None, **attributes):
    element = ET.SubElement(
        parent, _tag(name), {k: v for k, v in attributes.items() if v is not None}
    )
    if text is not None:
        element.text = text
    return element


def _coordinate(value: float) -> str:
    return f"{value:.7f}".rstrip("0").rstrip(".")


def _point(parent, name: str, latitude: float, longitude: float, elevation_m: float | None):
    element = _sub(parent, name, lat=_coordinate(latitude), lon=_coordinate(longitude))
    if elevation_m is not None:
        _sub(element, "ele", f"{elevation_m:.1f}")
    return element


def build_gpx(
    *,
    name: str,
    application: str,
    link: str | None,
    points: Sequence[tuple[float, float, float | None]],
    stages: Sequence[ExportStage],
    waypoints: Sequence[ExportWaypoint] = (),
    layout: str = "segments",
    osm_geometry: bool = True,
    description: str | None = None,
    transfer_steps: Sequence[bool] | None = None,
) -> bytes:
    """The GPX document as UTF-8 bytes. ``stages`` cover the part exported
    (all of them, or one); ``points`` are the whole route's."""
    if layout not in LAYOUTS:
        raise ValueError(f"layout must be one of {LAYOUTS}")
    if not stages:
        raise ValueError("at least one stage to export")
    ET.register_namespace("", GPX_NS)
    root = ET.Element(_tag("gpx"), {"version": "1.1", "creator": application})
    metadata = _sub(root, "metadata")
    _sub(metadata, "name", name)
    if description:
        _sub(metadata, "desc", description)
    author = _sub(metadata, "author")
    _sub(author, "name", application)
    if osm_geometry:
        copyright_ = _sub(metadata, "copyright", author="OpenStreetMap contributors")
        _sub(copyright_, "license", OSM_LICENSE)
    if link:
        link_element = _sub(metadata, "link", href=link)
        _sub(link_element, "text", f"{name} — {application}")
    used = [p for stage in stages for p in points[stage.start_index : stage.end_index + 1]]
    if used:
        _sub(
            metadata,
            "bounds",
            minlat=_coordinate(min(p[0] for p in used)),
            minlon=_coordinate(min(p[1] for p in used)),
            maxlat=_coordinate(max(p[0] for p in used)),
            maxlon=_coordinate(max(p[1] for p in used)),
        )
    for waypoint in waypoints:
        element = _point(root, "wpt", waypoint.latitude, waypoint.longitude, waypoint.elevation_m)
        _sub(element, "name", waypoint.name)
        if waypoint.description:
            _sub(element, "desc", waypoint.description)
        _sub(element, "sym", "Lodging" if waypoint.kind == "CAMP" else "Flag, Blue")
        _sub(element, "type", "Nocleg" if waypoint.kind == "CAMP" else "POI")

    steps = list(transfer_steps or [])

    def runs(stage: ExportStage) -> list[tuple[bool, int, int]]:
        """(is_transfer, first, last) stretches of one stage; a transfer
        shares its end points with the stretches around it."""
        out: list[tuple[bool, int, int]] = []
        for index in range(stage.start_index + 1, stage.end_index + 1):
            transfer = index < len(steps) and steps[index]
            if out and out[-1][0] == transfer:
                out[-1] = (transfer, out[-1][1], index)
            else:
                out.append((transfer, index - 1, index))
        return out or [(False, stage.start_index, stage.end_index)]

    def segment(parent, first: int, last: int, transfer: bool):
        trkseg = _sub(parent, "trkseg")
        for latitude, longitude, elevation in points[first : last + 1]:
            _point(trkseg, "trkpt", latitude, longitude, elevation)
        if transfer:
            extensions = _sub(trkseg, "extensions")
            ET.SubElement(extensions, f"{{{APP_NS}}}kind").text = TRANSFER_TYPE

    ET.register_namespace("echtro", APP_NS)
    if layout == "segments" or len(stages) == 1:
        track = _sub(root, "trk")
        _sub(track, "name", name if len(stages) > 1 else stages[0].label)
        if any(transfer for stage in stages for transfer, _, _ in runs(stage)):
            _sub(track, "desc", "Odcinki oznaczone jako dojazd (transport) nie są trasą marszu.")
        for stage in stages:
            for transfer, first, last in runs(stage):
                segment(track, first, last, transfer)
    else:
        number = 0
        for stage in stages:
            for transfer, first, last in runs(stage):
                number += 1
                track = _sub(root, "trk")
                _sub(track, "name", f"{stage.label} — dojazd" if transfer else stage.label)
                if transfer:
                    _sub(track, "desc", "Dojazd (transport), nie trasa marszu.")
                _sub(track, "number", str(number))
                if transfer:
                    _sub(track, "type", TRANSFER_TYPE)
                segment(track, first, last, False)
    ET.indent(root, space=" ")
    return ET.tostring(root, encoding="utf-8", xml_declaration=True) + b"\n"


def stages_from_camps(
    point_count: int, camps: Sequence[tuple[int, str]], start_label: str
) -> list[ExportStage]:
    """Stages between camps (``(route_point_index, label)`` in route order):
    D1 from the start to the first camp, ... the last to the end."""
    ends = sorted({index for index, _ in camps if 0 < index < point_count - 1})
    labels = {index: label for index, label in camps}
    bounds = [0, *ends, point_count - 1]
    stages = []
    for day, (start, end) in enumerate(zip(bounds, bounds[1:]), start=1):
        origin = start_label if start == 0 else labels.get(start, f"nocleg {day - 1}")
        target = labels.get(end, "meta") if end != point_count - 1 else "meta"
        stages.append(ExportStage(f"D{day}: {origin} → {target}", start, end))
    return stages
