r"""
Per-day course fit for the days whose marks were re-laid (May 23/24) — V2 stored only the May-22
"digital 8" course, so for the other days we keep that KNOWN figure-8 (its waypoint order + start/finish
structure are what the scoring engine needs) and just move it onto where the fleet actually sailed:

  1. day_marks_from_tracks — the 4 buoys ≈ the densest rounding clusters, split 2 west (windward gate) +
     2 east (leeward / start-finish), from that day's race-lap tracks.
  2. best_transform — the similarity (rotate + translate + scale) that best maps the template's 4 marks
     onto those 4, trying all pairings and keeping the lowest-residual one.
  3. fit_course — apply that transform to the WHOLE template course (6 waypoints + start/finish lines).

Pure (no network); `fit_course` takes/returns the feed `course` block in lat/lon.
"""
from __future__ import annotations

import math
import sys
from pathlib import Path

_HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(_HERE))
from fit_marks import cluster, to_latlon, turn_apexes  # noqa: E402
from segment_gsys import to_xy  # noqa: E402


def umeyama(src: list[tuple[float, float]], dst: list[tuple[float, float]]):
    """Best-fit 2-D similarity transform (rotation + uniform scale + translation) mapping `src`→`dst`.
    Returns (transform_fn, sum_squared_residual)."""
    n = len(src)
    mus = (sum(p[0] for p in src) / n, sum(p[1] for p in src) / n)
    mud = (sum(p[0] for p in dst) / n, sum(p[1] for p in dst) / n)
    s = [(p[0] - mus[0], p[1] - mus[1]) for p in src]
    d = [(p[0] - mud[0], p[1] - mud[1]) for p in dst]
    sxy = sum(s[i][0] * d[i][1] for i in range(n))
    syx = sum(s[i][1] * d[i][0] for i in range(n))
    sxx = sum(s[i][0] * d[i][0] for i in range(n))
    syy = sum(s[i][1] * d[i][1] for i in range(n))
    th = math.atan2(sxy - syx, sxx + syy)
    c, sn = math.cos(th), math.sin(th)
    var = sum(p[0] ** 2 + p[1] ** 2 for p in s) or 1.0
    num = sum(d[i][0] * (c * s[i][0] - sn * s[i][1]) + d[i][1] * (sn * s[i][0] + c * s[i][1])
              for i in range(n))
    scale = num / var

    def tf(p):
        x, y = p[0] - mus[0], p[1] - mus[1]
        return (scale * (c * x - sn * y) + mud[0], scale * (sn * x + c * y) + mud[1])

    return tf, sum(math.dist(tf(src[i]), dst[i]) ** 2 for i in range(n))


# Both mark lists are ordered [windward A, windward B, leeward A, leeward B]. We only try pairings that
# keep windward→windward and leeward→leeward (swapping only the port/starboard within each gate) — a free
# 24-permutation match would happily FLIP windward↔leeward, reversing the figure-8 so the engine can't
# track it (and the derived wind points the wrong way).
_ORIENT_PERMS = [(0, 1, 2, 3), (1, 0, 2, 3), (0, 1, 3, 2), (1, 0, 3, 2)]


def best_transform(template_marks, day_marks):
    """The lowest-residual similarity transform of `template_marks` → `day_marks`, over the 4
    orientation-preserving gate pairings (windward stays windward, leeward stays leeward)."""
    return min((umeyama(template_marks, [day_marks[p[i]] for i in range(4)])
                for p in _ORIENT_PERMS), key=lambda x: x[1])[0]


def day_marks_from_tracks(tracks_xy, *, radius_m: float = 22.0):
    """The 4 buoy positions (XY) for a day: the densest rounding clusters split into the 2 westernmost
    (windward gate) + 2 easternmost (leeward / start-finish). None if the fleet's roundings don't form
    a usable 4-mark figure-8."""
    cl = cluster([a for xy in tracks_xy for a in turn_apexes(xy)], radius_m=radius_m)
    if len(cl) < 4:
        return None
    medx = sorted(c[0][0] for c in cl)[len(cl) // 2]
    west = [c for c in cl if c[0][0] < medx][:2]
    east = [c for c in cl if c[0][0] >= medx][:2]
    if len(west) < 2 or len(east) < 2:
        return None
    return [west[0][0], west[1][0], east[0][0], east[1][0]]


def fit_course(template_course: dict, tracks_latlon, ref: tuple[float, float]) -> dict | None:
    """Move the template figure-8 onto this day's tracks: fit the similarity transform from the template
    marks to the day's rounding clusters, then apply it to every waypoint + the start/finish lines.
    Returns a feed `course` block (lat/lon), or None if the day's marks couldn't be found."""
    tracks_xy = [[to_xy(la, lo, ref) for la, lo in t] for t in tracks_latlon if len(t) > 30]
    day = day_marks_from_tracks(tracks_xy)
    if day is None:
        return None
    tmarks = [to_xy(w["lat"], w["lon"], ref) for w in template_course["waypoints"][:4]]
    tf = best_transform(tmarks, day)

    def move(ll):
        x, y = tf(to_xy(ll["lat"], ll["lon"], ref))
        la, lo = to_latlon(x, y, ref)
        return {"lat": round(la, 7), "lon": round(lo, 7)}

    wps = [{"name": w.get("name") or "", "type": w.get("type") or "rounding",
            "rounding": w.get("rounding"), **move(w)} for w in template_course["waypoints"]]

    def line(ln):
        return {"port": move(ln["port"]), "starboard": move(ln["starboard"])}
    pts = [(w["lat"], w["lon"]) for w in wps]
    center = {"lat": sum(p[0] for p in pts) / len(pts), "lon": sum(p[1] for p in pts) / len(pts)}
    return {"name": template_course.get("name") or "digital 8", "center": center,
            "startLine": line(template_course["startLine"]),
            "finishLine": line(template_course["finishLine"]), "waypoints": wps}
