#!/usr/bin/env python3
"""Read generated files independently; require fitdecode==0.11.0."""
import csv
from datetime import datetime, timedelta
import hashlib
import json
from pathlib import Path
import sys
import xml.etree.ElementTree as ET
import fitdecode

out = Path(sys.argv[1])
fixture = json.loads((out / "fixture.json").read_text())
g = "{http://www.topografix.com/GPX/1/1}"
t = "{http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2}"
gp = "{http://www.garmin.com/xmlschemas/TrackPointExtension/v2}"
tx = "{http://www.garmin.com/xmlschemas/ActivityExtension/v2}"
gpxp = "{http://www.garmin.com/xmlschemas/PowerExtension/v1}"
fields = ["timestamp", "latitude", "longitude", "altitudeMeters", "distanceMeters",
          "heartRateBPM", "cadenceRPM", "powerWatts", "speedMetersPerSecond"]
fit_fields = ["timestamp", "position_lat", "position_long", "altitude", "distance",
              "heart_rate", "cadence", "power", "speed"]
fits, sessions = [], []
with fitdecode.FitReader(out / "synthetic-ride.fit", check_crc=fitdecode.CrcCheck.RAISE) as reader:
    for frame in reader:
        if frame.frame_type != fitdecode.FIT_FRAME_DATA:
            continue
        values = {f.name: f.value for f in frame.fields}
        if frame.name == "session":
            sessions.append(values)
        if frame.name != "record":
            continue
        row = dict(zip(fields, [values.get(f) for f in fit_fields]))
        row["timestamp"] = row["timestamp"].isoformat().replace("+00:00", "Z")
        for key in ["latitude", "longitude"]:
            if row[key] is not None:
                row[key] = row[key] * 180 / 2**31
        fits.append(row)

def number(node, path):
    value = node.findtext(path)
    return None if value is None else float(value)

def read_gpx(root):
    rows = []
    for p in root.iter(g + "trkpt"):
        rows.append(dict(zip(fields, [p.findtext(g + "time"), float(p.attrib["lat"]),
            float(p.attrib["lon"]), number(p, g + "ele"), None,
            number(p, f"{g}extensions/{gp}TrackPointExtension/{gp}hr"),
            number(p, f"{g}extensions/{gp}TrackPointExtension/{gp}cad"),
            number(p, f"{g}extensions/{gpxp}PowerInWatts"),
            number(p, f"{g}extensions/{gp}TrackPointExtension/{gp}speed")])))
    return rows

root = ET.parse(out / "synthetic-ride.gpx").getroot()
gpx = read_gpx(root)
for parent in root.iter():
    for child in list(parent):
        if child.tag == g + "extensions":
            parent.remove(child)
ET.register_namespace("", g[1:-1])
ET.ElementTree(root).write(out / "synthetic-ride-core-only.gpx", encoding="utf-8", xml_declaration=True)
core = read_gpx(root)
tcx_root = ET.parse(out / "synthetic-ride.tcx").getroot()
tcx = []
for p in tcx_root.iter(t + "Trackpoint"):
    tcx.append(dict(zip(fields, [p.findtext(t + "Time"),
        number(p, f"{t}Position/{t}LatitudeDegrees"), number(p, f"{t}Position/{t}LongitudeDegrees"),
        number(p, t + "AltitudeMeters"), number(p, t + "DistanceMeters"),
        number(p, f"{t}HeartRateBpm/{t}Value"), number(p, t + "Cadence"),
        number(p, f"{t}Extensions/{tx}TPX/{tx}Watts"), number(p, f"{t}Extensions/{tx}TPX/{tx}Speed")])))
decoded = {"FIT": fits, "GPX": gpx, "TCX": tcx, "GPX without extensions": core}
for format_name, rows in decoded.items():
    assert len(rows) == 6, (format_name, "record count")
    assert all(r["timestamp"] for r in rows)
    assert rows[2]["heartRateBPM"] is None, (format_name, "missing HR fabricated")
for format_name in ["FIT", "GPX", "TCX"]:
    rows = decoded[format_name]
    assert rows[2]["powerWatts"] == 0 and rows[3]["powerWatts"] is None
    assert rows[2]["cadenceRPM"] == 0 and rows[3]["cadenceRPM"] is None
    for row, point in zip(rows, fixture["records"]):
        expected_time = (datetime.fromisoformat(fixture["start"].replace("Z", "+00:00")) + timedelta(seconds=point["offset"])).isoformat().replace("+00:00", "Z")
        assert row["timestamp"] == expected_time, (format_name, "timestamp mismatch")
        for field in fields[1:]:
            actual, expected = row[field], point[field]
            if format_name == "GPX" and field == "distanceMeters":
                assert actual is None
                continue
            if expected is None:
                assert actual is None, (format_name, field, "fabricated value")
                continue
            tolerance = .1000001 if field == "altitudeMeters" and format_name == "FIT" else 1e-7
            assert actual is not None and abs(actual - expected) <= tolerance, (format_name, field, actual, expected)
assert all(r["powerWatts"] is None for r in core)
assert [r["longitude"] for r in core] == [r["longitude"] for r in gpx]
summary = {}
for format_name, rows in decoded.items():
    summary[format_name] = {field: sum(r[field] is not None for r in rows) for field in fields}
    summary[format_name]["altitudeMaxErrorMeters"] = max(abs(r["altitudeMeters"] - p["altitudeMeters"]) for r, p in zip(rows, fixture["records"]))
    summary[format_name]["zeroPowerSamples"] = sum(r["powerWatts"] == 0 for r in rows)
    summary[format_name]["recordCount"] = len(rows)
summary["FIT"]["session"] = {k: sessions[0].get(k) for k in ["total_timer_time", "total_distance", "total_calories"]}
lap = tcx_root.find(f".//{t}Lap")
summary["TCX"]["session"] = {"total_timer_time": number(lap, t + "TotalTimeSeconds"), "total_distance": number(lap, t + "DistanceMeters"), "total_calories": number(lap, t + "Calories")}
summary["GPX"]["session"] = None
summary["GPX without extensions"]["session"] = None
report = {"fixture": fixture["label"], "fitDecoder": f"fitdecode {fitdecode.__version__}; CRC errors raise", "xmlDecoder": "Python ElementTree; namespace-aware extraction", "observed": summary, "records": decoded}
(out / "results.json").write_text(json.dumps(report, indent=2) + "\n")
with (out / "results.csv").open("w") as f:
    writer = csv.writer(f, lineterminator="\n")
    writer.writerow(["format", "record", *fields])
    for format_name, rows in decoded.items():
        for i, row in enumerate(rows):
            writer.writerow([format_name, i, *[row[field] for field in fields]])
checksums = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in out.iterdir() if p.is_file() and p.suffix in [".fit", ".gpx", ".tcx", ".csv", ".json", ".py", ".swift", ".txt"] and p.name != "checksums.json"}
(out / "checksums.json").write_text(json.dumps(checksums, indent=2) + "\n")
print(json.dumps(summary, indent=2))
