#!/usr/bin/env python3
"""Offline check: every /settings flag we ship is one the client actually switches on.

A flag name is not validated by anything at runtime. The client hashes the string
we send and switches on the result, so a typo, a renamed field or a flag that
simply has no arm in the switch is INERT and looks exactly like "the fix did not
work". This asserts each shipped name against two independent sources:

  1. docs/fut_atoms.tsv  -- the recovered atom table (the name must hash to an id)
  2. the switch arms recovered from 0x18013c6d0 (the id must have an arm)

Source 2 is the one that matters: enableSquadBuildingSetsFeature is a perfectly
real atom with NO arm, so source 1 alone would have passed it.

Run before shipping any settings change. No server needed.
"""
import os
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)

# The 42 atoms with an arm in FUN_18013c6d0, recovered 2026-08-05 by
# tools/ghidra_queries/q_settings_flags.py + q_settings_types.py (full decompile,
# both halves of the switch, coverage asserted by char count).
SWITCH_ARMS = {
    0x18: "allowGracePeriodForSquadBuildingSets",
    0x19: "allowUntradeableForSquadBuildingSets",
    0x6D: "cardPackStoreEnabled", 0x6E: "cardPackStoreEnabled_JP",
    0x80: "checkServerDbVersion", 0x86: "clientKeepAliveResetTimeoutSec",
    0x8C: "clubCreateThreshold", 0x98: "coinEnabled", 0x99: "coinEnabled_JP",
    0xA3: "constrainGracePeriod", 0xBB: "couchPlayEnabled",
    0xF9: "enableDraftMode", 0xFA: "enableOfflineDraftMode",
    0xFB: "enableLiveMessaging", 0xFC: "enableLoyaltyBonusForConceptPlayers",
    0xFD: "enableObjectives", 0xFE: "enableObjectivesAsManagerTasks",
    0xFF: "enableSinglePlayerDraftMode", 0x118: "extendGameSessionTimerSec",
    0x11F: "fifaPointsEnabled", 0x120: "fifaPointsEnabled_JP",
    0x133: "friendlySeasonsEnabled", 0x13D: "getOperationTimeoutSec",
    0x16C: "itemDbVersion", 0x1C0: "maximumTradePileSize",
    0x1CD: "mtxEnabled", 0x1CE: "mtxEnabled_JP",
    0x1DE: "numEndMatchRetriesAllowed", 0x20E: "packOpeningAnimationEnabled",
    0x242: "pointsPackStoreEnabled", 0x257: "processingStateEnabled",
    0x28A: "returningUserRewardsScreenEnabled",
    0x2D0: "squadBuildingSetsGracePeriodMinutes",
    0x2F1: "storeEnabled", 0x2F2: "storeEnabled_JP",
    0x2F3: "storyModeRewardEnabled",
    0x2F5: "championsScheduleViewPeriodInMinutes",
    0x30F: "enableFloatPointSquadRating",
    0x310: "enableLegacyYearInfoInItemResourceId",
    0x320: "tokenRedemptionEnabled", 0x32D: "tournamentQuitEnabled",
    0x336: "tradingEnabled",
}

# Arms that do NOT simply store a value. Shipping these has side effects.
SPECIAL = {
    "enableObjectives": "shared arm can only CLEAR the field; 1 is a no-op, 0 disables",
    "enableObjectivesAsManagerTasks": "same shared arm as enableObjectives",
    "clientKeepAliveResetTimeoutSec": "reprograms a client timer with value*1000",
    "getOperationTimeoutSec": "reprograms a client timer with value*1000",
    "checkServerDbVersion": "makes the client go read a server_db_version config",
}


def main():
    fails = []

    atoms = {}
    with open(os.path.join(HERE, "..", "docs", "fut_atoms.tsv")) as fh:
        for line in fh:
            p = line.rstrip("\n").split("\t")
            if len(p) >= 3:
                try:
                    atoms[p[2]] = int(p[1], 16)
                except ValueError:
                    pass

    # Cross-check the recovered table against the atom table both ways.
    by_name = {v: k for k, v in SWITCH_ARMS.items()}
    for name, aid in by_name.items():
        if name not in atoms:
            fails.append("switch arm %s (%#x) is not in fut_atoms.tsv" % (name, aid))
        elif atoms[name] != aid:
            fails.append("%s: switch says %#x, atom table says %#x"
                         % (name, aid, atoms[name]))

    import utas_server as u

    body = u.SETTINGS
    if not isinstance(body, dict) or list(body) != ["configs"]:
        fails.append("body must be exactly {'configs': [...]}, got %r" % (body,))
        return report(fails)
    rows = body["configs"]
    if not isinstance(rows, list):
        fails.append("configs must be a LIST (a scalar here desyncs the parser)")
        return report(fails)

    seen = set()
    for r in rows:
        if not isinstance(r, dict) or set(r) != {"type", "value"}:
            fails.append("row must be exactly {type, value}: %r" % (r,))
            continue
        t, v = r["type"], r["value"]
        # value: any scalar is safe (getter 0x1801c79d0 coerces int/float/bool/str),
        # but the applier tests `== 1`, so a bool True would work and a string "1"
        # would work -- ints keep it unambiguous. An object or array FREEZES.
        if isinstance(v, (dict, list)):
            fails.append("%s: value is %s -- an object/array here FREEZES the client"
                         % (t, type(v).__name__))
        if not isinstance(t, str):
            fails.append("type must be a string, got %r" % (t,))
            continue
        if t in seen:
            fails.append("%s sent twice; last one wins, so this is at best confusing" % t)
        seen.add(t)
        if t not in by_name:
            hint = " (it IS an atom, but has no arm in the switch)" if t in atoms else ""
            fails.append("%s has no arm in 0x18013c6d0 -- INERT%s" % (t, hint))
        elif t in SPECIAL:
            print("  NOTE  %-34s %s" % (t, SPECIAL[t]))

    gates = {"friendlySeasonsEnabled", "enableDraftMode", "tournamentQuitEnabled"}
    # Read the mode off the server module, never re-declare the default here: a
    # checker with its own copy of a default tests the copy, not the server.
    mode = u._SETTINGS_MODE
    if mode == "gates":
        for g in sorted(gates - seen):
            fails.append("mode 'gates' but %s is missing" % g)
        for t in sorted(seen & gates):
            row = next(r for r in rows if r["type"] == t)
            if row["value"] != 1:
                fails.append("%s = %r; the applier tests `== 1`, nothing else opens "
                             "the gate" % (t, row["value"]))

    print("  mode=%s, %d rows, %d distinct flags, all with a live switch arm"
          % (mode, len(rows), len(seen)))
    return report(fails)


def report(fails):
    if fails:
        print("\nFAIL (%d)" % len(fails))
        for f in fails:
            print("  - %s" % f)
        return 1
    print("PASS")
    return 0


if __name__ == "__main__":
    sys.exit(main())
