#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""The four coach families for the FIFA 17 offline backend.

    cardsubtypeid 5 -> cardtype 3  headcoachcards      124 rows  2000004..2000328
    cardsubtypeid 6 -> cardtype 10 gkcoachcards        121 rows  9000001..9000324
    cardsubtypeid 7 -> cardtype 5  physiocards          51 rows  4000002..4000259
    cardsubtypeid 8 -> cardtype 4  fitnesscoachcards   115 rows  3000019..3000328

All four ids come out of data/tables/, dumped READ-ONLY from the running client, so
none of this needed the live game. rowcount == rows_emitted == len(rows) on all four,
which is what makes "this id is absent from the table" a claim about a COMPLETE dump.

WHY COACHES ARE THE CHEAPEST FAMILY TO TEST
-------------------------------------------
Their merge arms are the only ones in the game that LABEL THEIR OWN FAILURE. All four
inline branches of FUN_180141660 (2,129 bytes, 214-line decompile read to its closing
`return`) do, on rowcount < 1:

    firstname = lastname = "DB Error"   rec+0xb4 = 0x32 (rating 50)   rec+0x58 = 1
    and a TABLE-UNIQUE assetid:  head 2000148  fitness 3000259  physio 4000146
                                 gkcoach 9000258

so a wrong coach id puts the words DB ERROR on the screen instead of failing silently
the way a manager does. Two independent facts make that a legitimate one-glance oracle:
no row in ANY of the four tables has value == 50, and three of those four fallback
assetids are REAL rows in their own table (3000259 is not) -- so they are excluded
from everything this module ships.

THE MISS-FILL IS NOT UNIFORM, contrary to the earlier note in
docs/plan-2026-08-04-card-families.md. Only head coach and GK coach write 0xf into the
attribute array at rec+0x98. Physio writes 0xf into a BYTE at rec+0xdd, and fitness
coach writes no 0xf at all -- it writes rec+0xde = 0x107 and rec+0xdd = 1, i.e.
fieldpos 1 / posbonus 7 / amount 1.

THE KEY IS RAW. All four staff branches pass *(u32*)(rec+0x18) unmasked into
`WHERE carddbid == ?`. Players are the ONLY family that masks with & 0xffffff. A
version byte in the top octet therefore breaks every staff lookup, silently on a
manager and loudly on a coach.

WHAT IS OURS AND WHAT IS THEIRS. The merge overwrites firstname, lastname,
assetId(+0x20), rating(+0xb4), rare(+0x58), the tier(+0x54) it derives from rating,
and the family stat block. It never writes teamid(+0x94), preferredPosition(+0x146),
nation(+0x148) or leagueId(+0x154) -- and none of the four tables even HAS a nation,
league or team column, so any value we put there would be INVENTED. We therefore send
none of them: omission leaves the memset zero, and rec+0x146/+0x98.. are read by the
generic view-model FUN_1800d7920, so sending them would hang a position label and six
attribute numbers on a coach card.
"""
import json, os

HERE = os.path.dirname(os.path.abspath(__file__))
TABLES = os.path.join(os.path.dirname(HERE), "data", "tables")

# family -> (cardsubtypeid, record+0x4c cardtype, table, miss-fill assetid)
FAMILIES = {
    "headcoach":    (5,  3,  "headcoachcards",    2000148),
    "gkcoach":      (6,  10, "gkcoachcards",      9000258),
    "physio":       (7,  5,  "physiocards",       4000146),
    "fitnesscoach": (8,  4,  "fitnesscoachcards", 3000259),
}

# The ?type= arms that ask for coaching staff. FUN_18012ec50 resolves its 29 explicit
# arms through the atom table to headcoach(3), fitnesscoach(4), physio(5), gkcoach(9)
# and staff(10) -- but NONE of those five strings has ever been seen on the wire from
# this client. Only player, manager and custom have. Hence FUT_COACHES=all, which also
# answers type=manager (the one staff request that HAS been observed, from the STAFF
# tab on 2026-08-04).
CLUB_TYPES = ("headcoach", "gkcoach", "physio", "fitnesscoach", "staff")

COACH_ID_BASE = 950000000     # clear of the save (1e8), the sweep (9e8) and the
                              # consumable overlay (9.4e8)


def _rows(table):
    with open(os.path.join(TABLES, table + ".json")) as f:
        d = json.load(f)
    assert d["rowcount"] == d["rows_emitted"] == len(d["rows"]), (
        "%s: partial dump -- every 'this id is absent' claim below would be void"
        % table)
    return d["rows"]


def _build():
    out = {}
    for fam, (sub, ct, table, missfill) in FAMILIES.items():
        rows = []
        for r in _rows(table):
            row = {
                "family":   fam,
                "subtype":  sub,
                "cardtype": ct,
                "carddbid": r["carddbid"],     # == assetid on every row, all 4 tables
                "rating":   r["value"],        # the client writes this itself
                "rare":     r["rare"],
                # the family stat, for OUR predictions only -- never sent
                "amount":   r["amount"],
            }
            if fam == "fitnesscoach":
                row["fieldpos"] = r["fieldpos"]
                row["posbonus"] = r["posbonus"]
            else:
                row["attribute"] = r["attribute"]
            rows.append(row)
        out[fam] = rows
    return out


COACHES = _build()
BY_ID = {(r["subtype"], r["carddbid"]): r for fam in COACHES for r in COACHES[fam]}

# ids that must never be shipped: they ARE the miss-fill fingerprint. 2000148, 4000146
# and 9000258 are genuine rows in their own tables, so a card carrying one of them is
# ambiguous -- a hit and a miss look identical. 3000259 is not a row at all.
MISS_FILL_IDS = {v[3] for v in FAMILIES.values()}


def tier(rating):
    """The shared tail of FUN_180141660 writes rec+0x54 for EVERY arm including the
    miss arms: 3 if rating >= 0x4b, else 2 - (rating < 0x41). Bronze/silver/gold."""
    return 3 if rating >= 75 else (2 if rating >= 65 else 1)


def coach_item(item_id, subtype, carddbid, contract=7, untradeable=True, rating=None):
    """One coach item. Eight keys, and every one of them is already proven on the wire.

      id            -> rec+0x08  our handle. NOTE FUN_180141660 opens with
                                 `if (*(longlong *)(param_1 + 8) == 0) return;` -- an
                                 item with id 0 or no id gets NO merge for ANY family:
                                 no name, no rating, not even DB Error.
      resourceId    -> rec+0x18  THE merge key, compared RAW against carddbid.
      cardsubtypeid -> rec+0x50  the ONLY family selector (FUN_1800d8330 -> rec+0x4c).
      itemType                   inert (atom 0x173 never reaches the record); "staff"
                                 is for our own readers, matching fut_staff.
      contract      -> rec+0x8c
      itemState / owners / untradeable   as on a player.

    NOT SENT, each for a reason: rating/rareflag/assetId (all overwritten by the
    merge), nation/leagueId/teamid (no such column exists in any coach table, so any
    value would be invented), preferredPosition/attributeList (they SURVIVE the merge
    and are read by the generic view-model), definitionId (not an atom).
    `rating` is exposed only so a probe can plant a sentinel.
    """
    if subtype not in {v[0] for v in FAMILIES.values()}:
        raise ValueError("cardsubtypeid %r is not a coach family (5, 6, 7 or 8)"
                         % (subtype,))
    if carddbid in MISS_FILL_IDS:
        raise ValueError("carddbid %d is a miss-fill assetid: a hit and a miss would "
                         "look identical on that card" % carddbid)
    it = {
        "id":            item_id,
        "resourceId":    carddbid,
        "cardsubtypeid": subtype,
        "itemType":      "staff",
        "contract":      contract,
        "itemState":     "free",
        "owners":        1,
        "untradeable":   untradeable,
    }
    if rating is not None:
        it["rating"] = rating
    return it


def _ambiguous(fam, r):
    """True if this row's own stat write is byte-identical to its family's miss-fill.

    Head coach and GK coach both miss-fill with *(u32*)(rec+0x98) = 0xf, i.e.
    attrs[0] = 15 -- so a genuine row with attribute 0 and amount 15 leaves the
    attribute array indistinguishable from a miss. rating and the name still tell them
    apart, but there is no reason to ship a card that needs a tie-break.
    Fitness coach's miss triple (fieldpos 1, posbonus 7, amount 1) occurs on no row of
    its table, and physio's rec+0xdd = 0xf collides only with attribute 0 amount 15."""
    if fam == "fitnesscoach":
        return (r["fieldpos"], r["posbonus"], r["amount"]) == (1, 7, 1)
    return r["attribute"] == 0 and r["amount"] == 15


def _pick(fam):
    """One real id per (tier, rare) combination that the family actually has -- so at
    most six cards, spanning bronze/silver/gold and both rare flags, with the miss-fill
    assetid and every miss-ambiguous row excluded."""
    rows = [r for r in COACHES[fam]
            if r["carddbid"] not in MISS_FILL_IDS and not _ambiguous(fam, r)]
    picked = []
    for want in ((3, 1), (3, 0), (2, 1), (2, 0), (1, 1), (1, 0)):
        for r in rows:
            if (tier(r["rating"]), r["rare"]) == want:
                picked.append(r)
                break
    return picked


# A readable STAFF tab: up to six cards per family, every one a real row.
STARTER_COACHES = {fam: [r["carddbid"] for r in _pick(fam)] for fam in FAMILIES}


def items_for_type(kind, next_id=COACH_ID_BASE):
    """The starter shelf filtered to one ?type= arm.

    `staff` (arm 10) means all four families; each family's own arm means only that
    family. Ids stay stable per family regardless of which arm asked, so the same card
    keeps the same item id across tabs."""
    all_items = starter_coaches(next_id)
    if kind == "staff":
        return all_items
    fam = FAMILIES.get(kind)
    if fam is None:
        return []
    return [i for i in all_items if i["cardsubtypeid"] == fam[0]]


def starter_coaches(next_id=COACH_ID_BASE):
    if isinstance(next_id, int):
        base = [next_id]
        alloc = lambda: (base.__setitem__(0, base[0] + 1), base[0] - 1)[1]
    else:
        alloc = next_id
    out = []
    for fam in ("headcoach", "gkcoach", "physio", "fitnesscoach"):
        sub = FAMILIES[fam][0]
        for cid in STARTER_COACHES[fam]:
            out.append(coach_item(alloc(), sub, cid))
    return out


if __name__ == "__main__":
    for fam, (sub, ct, table, miss) in sorted(FAMILIES.items()):
        rows = COACHES[fam]
        print("%-13s subtype %d  cardtype %2d  %3d rows  %d..%d  miss-fill %d"
              % (fam, sub, ct, len(rows), rows[0]["carddbid"], rows[-1]["carddbid"],
                 miss))
        for cid in STARTER_COACHES[fam]:
            r = BY_ID[(sub, cid)]
            print("    %7d  rating %2d (%s)  rare %d  %s"
                  % (cid, r["rating"], "bronze silver gold".split()[tier(r["rating"]) - 1],
                     r["rare"],
                     "fieldpos %d posbonus %d amount %d"
                     % (r["fieldpos"], r["posbonus"], r["amount"])
                     if fam == "fitnesscoach"
                     else "attrs[%d] = %d" % (r["attribute"], r["amount"])))
    print("starter_coaches(): %d item(s)" % len(starter_coaches()))
