#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Manager (and, later, coach) cards for the FIFA 17 offline backend.

HAND-OVER MODULE. Nothing here is imported by utas_server / fut_store / fut_cards
yet -- those three have a single writer. This file is the data plus the exact item
shape that the integrator should wire in. See the spec at the bottom.

WHY A MANAGER ITEM LOOKS NOTHING LIKE A PLAYER ITEM
---------------------------------------------------
Both go through the SAME deserializer, `FUN_18013fe00` (1234 instructions, fully
enumerated -- record base is RBP+0x160, the record is memset to zero at
0x180140020 and spans rec+0x00..0x157). At the tail it calls the merge dispatcher
`FUN_180141660`, which switches on rec+0x4c (the cardtype that `FUN_1800d8330`
derives from `cardsubtypeid` alone) and, for cardtype 2, calls `FUN_1801356c0`:

    SELECT firstname,lastname,assetid,value,talkrating,negotiation,rare
    FROM managercards WHERE carddbid == *(u32*)(rec+0x18)      <-- RAW, no & 0xffffff

    on rowcount > 0:  rec+0xb8 firstname(16)   rec+0xc8 lastname(21)
                      rec+0x20 assetid          rec+0xb4 rating   ( = `value` )
                      rec+0xe2 talkrating       rec+0xe3 negotiation
                      rec+0x58 (rare == 1)
    on rowcount < 1:  NOTHING. There is no else-branch. A wrong manager id is silent.

So every field above is the CLIENT's to fill and ours to leave out. What the merge
does NOT write is ours alone, and the parser reserves two slots specifically for a
manager (verified at instruction level, 0x180140e1c..0x180140e37):

    MOV [RBP+0x1ac],EAX      ; rec+0x4c = cardtype
    DEC EAX / JZ  ...e32     ; cardtype == 1  -> player
    DEC EAX / JNZ ...e3e     ; cardtype != 2  -> the JSON `nation` is DISCARDED
    MOVZX EAX,word [RSP+0x38]
    MOV  word [RBP+0x23e],AX ; rec+0xde  <-- MANAGER nation
    ...
    MOV  word [RBP+0x2a8],AX ; rec+0x148 <-- PLAYER nation

`leagueId` (atom 0x18a) is stored unconditionally as a u16 at rec+0xe0
(0x180140739). For a player the merge immediately overwrites rec+0xdd.. with the
knownAs string, so it only survives on a card whose merge does not write there --
i.e. a manager. rec+0xde nation, rec+0xe0 league, rec+0xe2 talkrating,
rec+0xe3 negotiation is one contiguous manager block, and it is exactly the
league+nation pair that manager chemistry is built on.

CONCLUSION, and it inverts the player rule: for a PLAYER we send zero and let the
client fill in; for a MANAGER the client fills in name/rating/rare/assetid and
WE are the only source of nation and league.
"""
import json, os

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


def _table(name):
    with open(os.path.join(TABLES, name + ".json")) as f:
        return json.load(f)["rows"]


def _fix(s):
    """db_dump wrote UTF-8 bytes through a latin-1 decode; undo it."""
    try:
        return s.encode("latin-1").decode("utf-8")
    except Exception:
        return s


def _build():
    """managercards x manager x leagueteamlinks -> one row per manager card.

    `managercards` (417 rows, carddbid 1000001..1001552, assetid == carddbid for
    all 417) has firstname/lastname as 32-bit string-pool offsets whose pool was
    never located, so it gives us no names. The `manager` table (747 rows,
    managerid 1..1631) has INLINE names, and the client itself joins the two:
    `FUN_1801bb060` does

        SELECT firstname,surname,headid,suittypeid,skintonecode,... FROM manager
        WHERE managerid == (*(u32*)(rec+0x18) & 0xffffff) - 1000000

    to build the in-match manager. carddbid - 1000000 == managerid, confirmed by
    the names it produces: 1000089 Wenger 86, 1000509 Luis Enrique 88, 1000417
    Guardiola 87, 1000113 Zidane 88. 297 of the 417 join; the 120 that do not are
    the 1001432+ tail, all rated 57, with no career appearance row.

    Names are for OUR logs only. We never put a name on the wire -- the client
    writes its own into rec+0xb8/+0xc8 from managercards.
    """
    # `manager` has 747 rows but only 746 distinct managerids: managerid 107 appears
    # twice, once as Leonid Slutskiy (teamid 315) and once as an EMPTY-name row
    # (teamid 1357). A plain dict comprehension keeps the last, which silently gave
    # carddbid 1000107 a blank name and the wrong club. Prefer the row that has a name.
    mgr = {}
    for r in _table("manager"):
        prev = mgr.get(r["managerid"])
        if prev is None or (not (prev["firstname"] or prev["surname"])
                            and (r["firstname"] or r["surname"])):
            mgr[r["managerid"]] = r
    league_of = {}
    for r in _table("leagueteamlinks"):
        league_of.setdefault(r["teamid"], r["leagueid"])
    out = []
    for r in _table("managercards"):
        m = mgr.get(r["carddbid"] - 1000000)
        team = m["teamid"] if m else 0
        out.append({
            "carddbid":  r["carddbid"],       # the merge key; also the artwork key
            "rating":    r["value"],          # the client writes this itself
            "rare":      r["rare"],           # the client writes this itself
            "nation":    r["nation"],         # OURS: rec+0xde
            "leagueId":  league_of.get(team, 0),   # OURS: rec+0xe0
            "teamid":    team,                # OURS: rec+0x94
            "name":      _fix(m["firstname"] + " " + m["surname"]) if m else "",
        })
    return out


MANAGERS = _build()
BY_ID = {m["carddbid"]: m for m in MANAGERS}

# Ten real carddbids spread across the whole 57..88 rating band, every one of them
# joined to a `manager` row (so the portrait resolves) and to a league (so the
# league logo and the league half of chemistry resolve).
STARTER_MANAGERS = [
    1000509,   # Luis Enrique          88  Spain(45)        LaLiga(53)        Barcelona
    1000183,   # Antonio Conte         87  Italy(27)        Premier(13)       Chelsea
    1000089,   # Arsene Wenger         86  France(18)       Premier(13)       Arsenal
    1000414,   # Juergen Klopp         84  Germany(21)      Premier(13)       Liverpool
    1000096,   # Ronald Koeman         82  Netherlands(34)  Premier(13)       Everton
    1000052,   # Alan Pardew           80  England(14)      Premier(13)       Crystal Palace
    1000034,   # Eusebio Di Francesco  78  Italy(27)        Serie A(31)       Sassuolo
    1000090,   # Peter Maes            75  Belgium(7)       Pro League(4)     Genk
    1000019,   # Dariusz Wdowczyk      70  Poland(37)       Ekstraklasa(66)   Wisla Krakow
    1000505,   # Martin Canning        65  Scotland(42)     Scottish Prem(50) Hamilton
]

MANAGER_SUBTYPE = 4          # cardsubtypeid 4 -> FUN_1800d8330 -> cardtype 2

# The four staff families that DO write a loud miss-fill, for use as positive
# controls: firstname/lastname "DB Error", rating 0x32, rare 1, and a table-unique
# assetid. subtype -> (cardtype, table, a deliberately INVALID carddbid).
STAFF_CONTROLS = {
    5: (3,  "headcoachcards",     2999999),
    6: (10, "gkcoachcards",       9999999),
    7: (5,  "physiocards",        4999999),
    8: (4,  "fitnesscoachcards",  3999999),
}


def manager_item(item_id, carddbid, rating=None, contract=7, untradeable=True):
    """The manager item JSON. Every key is justified; nothing else is sent.

    id            -> rec+0x08   our item handle, needed for move/quick-sell
    resourceId    -> rec+0x18   THE merge key, read RAW as a u32. It must equal
                                carddbid exactly: no version byte, because
                                FUN_1801356c0 does not mask. The same field, masked
                                to &0xffffff, is what the view-model (FUN_1800d7920,
                                p2[1]) and FUN_1801bb060 use as the artwork key.
    cardsubtypeid -> rec+0x50   4. This alone selects the managercards merge.
    itemType      -> DISCARDED  atom 0x173 is parsed into a stack std::string at
                                RBP+0xc8 (0x180140262..0x180140279) and freed at the
                                end of the function. It is not written into the
                                record: all 1234 instructions of FUN_18013fe00 were
                                enumerated and every store and every LEA into
                                RBP+0x160..0x2b7 accounted for; none comes from that
                                string. "staff" is therefore inert on the wire, and
                                is used only so OUR OWN readers can see at a glance
                                that this is not a footballer. Nothing may depend on
                                it -- see the spec note about the three filters in
                                utas_server that currently key on itemType.
    nation        -> rec+0xde   MANAGER-ONLY SLOT. Not touched by the merge, so this
                                is the only source. Nation half of chemistry + flag.
    leagueId      -> rec+0xe0   Same: ours alone. League half of chemistry + logo.
    teamid        -> rec+0x94   read by the view-model (p2[2]). The manager's real
                                club, from the `manager` table.
    contract      -> rec+0x8c   staff cards consume contracts like players do.
    itemState     -> rec+0x5c   "free" == not listed; same meaning as for a player.
    owners        -> rec+0x48
    untradeable   -> rec+0x49

    DELIBERATELY ABSENT, each because it provably does nothing on a manager card:
      assetId / cardassetid   rec+0x20 is overwritten by the merge with the DB
                              assetid (which equals carddbid for all 417 rows).
      definitionId            not one of the 52 atoms this parser handles -> routed
                              to the value-SKIP handler FUN_180135ff0.
      rating                  rec+0xb4 is overwritten by the merge with `value`.
                              Pass rating=<sentinel> ONLY for the live probe below.
      rareflag                rec+0x58 is overwritten with (rare == 1). This is also
                              why fut_store._item()'s hardcoded "rareflag": 1 -- the
                              trap that silently converts a Player Fitness consumable
                              into a Squad Fitness one -- cannot hurt a manager.
      preferredPosition       rec+0x146 survives the merge and is read by the
                              view-model, so sending it would hang a position label
                              on a manager.
      attributeList           rec+0x98.. likewise survives and is likewise read.
      playStyle / fitness     player-only.
    """
    it = {
        "id":            item_id,
        "resourceId":    carddbid,
        "cardsubtypeid": MANAGER_SUBTYPE,
        "itemType":      "staff",
        "nation":        BY_ID[carddbid]["nation"],
        "leagueId":      BY_ID[carddbid]["leagueId"],
        "teamid":        BY_ID[carddbid]["teamid"],
        "contract":      contract,
        "itemState":     "free",
        "owners":        1,
        "untradeable":   untradeable,
    }
    if rating is not None:
        it["rating"] = rating
    return it


def staff_control_item(item_id, subtype, carddbid=None):
    """A self-labelling positive control. The four coach arms of FUN_180141660 all
    write, on rowcount < 1: firstname = lastname = "DB Error", rec+0xb4 = 0x32,
    rec+0x58 = 1, and a table-unique assetid. A deliberately invalid id therefore
    puts the words DB ERROR on screen, which proves the whole chain ran."""
    ct, table, bad = STAFF_CONTROLS[subtype]
    return {
        "id":            item_id,
        "resourceId":    carddbid if carddbid is not None else bad,
        "cardsubtypeid": subtype,
        "itemType":      "staff",
        "itemState":     "free",
        "owners":        1,
        "untradeable":   True,
    }


OVERLAY_ID_BASE = 960000000      # the club overlay's ids: clear of the save (1e8), the
                                 # sweep (9e8), the consumable shelf (9.4e8), the coach
                                 # shelf (9.5e8) and the probe below (9.1e8)
PROBE_ID_BASE = 910000000        # distinct from the save (1e8) and the sweep (9e8)
PROBE_SENTINEL_RATING = 7        # nowhere near the 57..88 band `value` can produce


def probe_items(carddbid=1000509):
    """THE live test. Four items, one club fetch, every outcome interpretable.

      A  real manager, sentinel rating 7
      B  the same manager with a version byte in the top octet of resourceId
      C  a headcoach id that cannot exist  -> must render "DB Error"
      D  a manager id that cannot exist    -> the silent-miss reference
    """
    n = PROBE_ID_BASE
    m = BY_ID[carddbid]
    a = manager_item(n + 1, carddbid, rating=PROBE_SENTINEL_RATING)
    b = manager_item(n + 2, carddbid, rating=PROBE_SENTINEL_RATING)
    b["resourceId"] = (1 << 24) | carddbid
    c = staff_control_item(n + 3, 5)
    d = manager_item(n + 4, carddbid, rating=PROBE_SENTINEL_RATING)
    d["resourceId"] = 1009999
    d["nation"] = m["nation"]
    d["leagueId"] = m["leagueId"]
    d["teamid"] = m["teamid"]
    return [a, b, c, d]


if __name__ == "__main__":
    import sys
    if "--probe" in sys.argv:
        print(json.dumps({"itemData": probe_items()}, indent=1))
    elif "--starter" in sys.argv:
        print(json.dumps([manager_item(100900000 + i, c)
                          for i, c in enumerate(STARTER_MANAGERS)], indent=1))
    else:
        print("%d manager cards, carddbid %d..%d, rating %d..%d, %d rare, "
              "%d with a `manager` row"
              % (len(MANAGERS), MANAGERS[0]["carddbid"], MANAGERS[-1]["carddbid"],
                 min(m["rating"] for m in MANAGERS),
                 max(m["rating"] for m in MANAGERS),
                 sum(m["rare"] for m in MANAGERS),
                 sum(1 for m in MANAGERS if m["name"])))
        for c in STARTER_MANAGERS:
            m = BY_ID[c]
            print("  %d  %-24s rating %2d rare %d nation %3d league %3d team %6d"
                  % (c, m["name"], m["rating"], m["rare"], m["nation"],
                     m["leagueId"], m["teamid"]))
