# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "pyroparse==0.11.0",
#   "garmin-fit-sdk==21.217.0",
# ]
# ///
"""
Message- and field-level comparison of the watch's original FIT file and the Connect API file,
for "One activity, four FIT files".

    uv run static/files/garmin-fit/fit_fields.py

Every field in the original file is classified two ways:
  - documented: does Garmin's public FIT profile (FIT SDK 21.217, the profile version
    in the API file's header) define this message and field?
  - kept: is the field present in the API file?
For kept fields, the value in both files is compared as well.

pyroparse does the parsing. The FIT SDK is only used for its profile, as a lookup table.
"""

from collections import Counter, defaultdict
from pathlib import Path

import pyroparse
from garmin_fit_sdk.profile import Profile

HERE = Path(__file__).parent
ORIGINAL = HERE / "garmin_device_usb.fit"
API = HERE / "garmin_connect_api.fit"

MESSAGES = Profile["messages"]
MESSAGE_NUMBERS = {message["name"]: number for number, message in MESSAGES.items()}


def message_number(kind) -> int:
    # pyroparse names the messages it knows and returns a number for the ones it doesn't
    return int(kind) if str(kind).isdigit() else MESSAGE_NUMBERS[str(kind)]


def documented(number: int, field: dict) -> bool | str:
    if field["developer_data_index"] is not None:
        return "developer"
    return number in MESSAGES and field["number"] in MESSAGES[number]["fields"]


def by_message(path: Path) -> dict[int, list[dict]]:
    grouped = defaultdict(list)
    for message in pyroparse.all_messages(path):
        grouped[message_number(message["kind"])].append(message)
    return grouped


def table(columns: list[str], rows: list[list]) -> None:
    print("| " + " | ".join(columns) + " |")
    print("|" + "---|" * len(columns))
    for row in rows:
        print("| " + " | ".join(str(value) for value in row) + " |")
    print()


original, api = by_message(ORIGINAL), by_message(API)


print("## Messages\n")
rows = []
for number in sorted(original, key=lambda n: (n not in MESSAGES, n)):
    name = MESSAGES[number]["name"] if number in MESSAGES else "(undocumented)"
    rows.append([number, name, len(original[number]), len(api.get(number, []))])
table(["mesg_num", "name", "original", "API"], rows)
assert set(api) <= set(original), "API file has messages the original doesn't"


outcomes = Counter()
value_differences = Counter()
dropped_fields = Counter()
for number, messages in original.items():
    api_messages = api.get(number, [])
    assert len(api_messages) in (0, len(messages)), f"message {number}: counts differ"
    for i, message in enumerate(messages):
        api_fields = {}
        if api_messages:
            api_fields = {(f["number"], f["developer_data_index"]): f for f in api_messages[i]["fields"]}
        for field in message["fields"]:
            key = (field["number"], field["developer_data_index"])
            is_documented = documented(number, field)
            kept = key in api_fields
            outcomes[(is_documented, kept)] += 1
            if kept and api_fields[key]["value"] != field["value"]:
                value_differences[(number, field["name"])] += 1
            if not kept and number in MESSAGES:
                dropped_fields[(MESSAGES[number]["name"], field["number"])] += 1
        original_keys = {(f["number"], f["developer_data_index"]) for f in message["fields"]}
        assert set(api_fields) <= original_keys, f"message {number}: API file adds fields"


print("## Field values\n")
labels = {True: "documented", False: "undocumented", "developer": "developer (Connect IQ)"}
table(
    ["field is", "kept in API", "dropped from API"],
    [[labels[d], outcomes[(d, True)], outcomes[(d, False)]] for d in (True, False, "developer")],
)
print(f"Kept field values that differ between the two files: {sum(value_differences.values())}")
in_documented_messages = sum(dropped_fields.values())
print(f"Dropped values in undocumented messages: {outcomes[(False, False)] - in_documented_messages}")
print(f"Dropped values in documented messages:   {in_documented_messages}\n")


print("## Undocumented fields dropped from documented messages\n")
per_message = defaultdict(dict)
for (message, number), count in dropped_fields.items():
    per_message[message][number] = count
table(
    ["message", "undocumented fields", "values dropped", "field numbers"],
    [
        [message, len(fields), sum(fields.values()), ", ".join(map(str, sorted(fields)))]
        for message, fields in sorted(per_message.items(), key=lambda item: -sum(item[1].values()))
    ],
)
