# ---------------------------------------------------------------------------
# OpenFUT / FIFA 17 UTAS -- forged squad ladder (clean-room). Imported by
# utas_server.py. Derived from CardsDLL_Win64_retail.dll (PE base 0x180000000).
#
# The prior full 11-player squad HUNG FIFA. Workflow wf_0bc80ab3 (5 agents,
# adversarially verified) proved: the deserializer PARSES our JSON fine; the
# freeze is POST-PARSE, at the per-item finalize resolve 0x180141176 (call
# singleton 0x18011a830 -> [r9+0xa08]) that fires for EVERY parsed item object
# (manager, players[].itemData, actives[]). Whether that resolve BLOCKS offline
# on an unresolvable item is UNVERIFIED -> we bisect it empirically with a ladder.
#
# The ladder (select via env FUT_SQUAD_STEP, default "s1"). Each step is one
# small change so a single FIFA relaunch isolates one variable:
#   s1    zero-resolve: players are bare {index,kitNumber}, no itemData, manager=[]
#         -> item deser NEVER entered, resolve fires 0 times. Tests envelope+HTTP
#         framing only. Reaches hub => hang IS item-resolve. Freezes => framing.
#   s1b   players[0] gets an EMPTY itemData {id:0,dream:false} (still no real asset)
#         -> resolve fires once on id 0. Freezes => id-0 resolve itself blocks
#         offline. Reaches hub => id-0 is fine, the asset value is what matters.
#   s2v0  players[0] = ONE real item, resourceId==assetId (version byte 0x00);
#         club serves the same item. Renders => version 0 is correct.
#   s2v1  same but version byte 0x01 (resourceId = 0x01<<24|assetId).
#   s3v0  full XI with the winning version byte (default 0x00); club in lockstep.
#   s3v1  full XI, version 0x01.
# resourceId decompose 0x180166ca0 CONFIRMED: assetId = resourceId & 0xffffff,
# high byte = version. Version byte value is the open question s2v0/s2v1 answer.
# ---------------------------------------------------------------------------
import os, sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from fut_account import ACCOUNT      # single source of truth for identity

# Back-compat snapshot; prefer ACCOUNT.persona_id in new code.
PERSONA_ID = ACCOUNT.persona_id
ITEM_ID_BASE = 100000000

# Real FIFA17 assetIds read earlier from the live InGameDB. assetId 41 (Iniesta)
# was flagged by the verifier as possibly having NO InGameDB definition -> it is
# DROPPED from the XI until a live test confirms a replacement. (asset, rating, pos, kit)
REAL_XI = [
    (20801,  94, "LW",  7),   # Ronaldo   -- STEP-2 uses this one
    (158023, 93, "RW", 10),   # Messi
    (200389, 87, "GK",  1),   # Oblak
    (183907, 90, "CB",  5),   # Boateng
    (155862, 89, "CB",  4),   # Ramos
    (197445, 87, "LB",  2),   # Alaba
    (189332, 86, "LB",  3),   # Alba
    (182521, 88, "CM",  8),   # Kroos
    (183277, 88, "LM", 11),   # Hazard
    (176580, 92, "ST",  9),   # Suarez
    # (41,   88, "CM", ..) DROPPED: verifier says no InGameDB def -> stall risk
]


def player_item(asset, rating, pos, version=0x00, nation=38, team=243, league=53,
                attrs=(90, 93, 82, 91, 33, 80)):
    """FULL item -- in case the card system needs more than the minimal set to
    PLACE + render a real player (the minimal item rendered generic + rating 0).
    Defaults are Ronaldo (Portugal 38 / Real Madrid 243 / La Liga 53)."""
    rid = (version << 24) | asset
    return {
        "id":                ITEM_ID_BASE + (asset & 0xffffff) + 1,   # unique, != 0
        "resourceId":        rid,
        "assetId":           asset,
        "cardassetid":       asset,
        # definitionId is INERT in FIFA 17: it is not in the atom table at all, so
        # the client's key hash never matches and it routes straight to the value-SKIP
        # handler 0x180135ff0 -- same class as the itemDbVersion/checkServerDbVersion
        # keys proven phantom in blaze_responder. Kept (harmless, and other FIFA
        # versions do use it) but it is NOT read here; the live key is resourceId.
        "definitionId":      rid,
        "cardsubtypeid":     0,          # 0..3 => PLAYER
        "itemType":          "player",
        "rareflag":          1,
        "rating":            rating,
        "preferredPosition": pos,        # STRING enum "GK"/"CB"/...
        "nation":            nation,
        "teamid":            team,
        "leagueId":          league,
        "playStyle":         250,
        "attributeList":     [{"index": i, "value": v} for i, v in enumerate(attrs)],
        "itemState":         "free",
        "owners":            1,
        "untradeable":       True,
        "contract":          7,
        "fitness":           99,
        "loans":             0,
        "discardValue":      0,
        "statsList":         [],
        "lifetimeStats":     [],
    }


def _base_squad():
    """Envelope shared by every step: valid formation/custom/kicktakers, but
    players are bare {index,kitNumber} (index is the direct hash bucket key,
    MUST be unique 0..22) and manager empty -> zero item-deser calls by default."""
    return {
        "id":         0,
        "personaId":  ACCOUNT.persona_id,  # must equal logged-in persona (0x18014659c)
        "squadName":  ACCOUNT.squad_name,
        "formation":  "f442",
        "squadType":  "REGULAR_SQUAD",
        "chemistry":  100,
        "starRating": 5,
        "captain":    0,
        "changed":    0,
        "manager":    [],
        "actives":    [],
        "custom":     "[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
                      "50,50,0,50,40,65,0,65,50,50,1]",
        "players":    [{"index": i, "kitNumber": 0} for i in range(23)],
        "kicktakers": [{"index": i, "id": 0, "dream": False} for i in range(5)],
    }


def _put_item(squad, index, itemdata, kit=0):
    squad["players"][index] = {"index": index, "itemData": itemdata, "kitNumber": kit}


def make_squad(step="s1"):
    """Return (squad, club) for a ladder step. club is {"itemData":[...]}."""
    step = step.lower()
    squad = _base_squad()
    club_items = []

    if step == "s0":
        # ABSOLUTE minimal squad: no custom (only field re-parsed by a sub-reader,
        # 0x1801c8270 -- prime suspect for the reader EOF-spin), empty players &
        # kicktakers arrays. Reaches hub => envelope OK, spin is custom/players/
        # kicktakers -> add back one at a time. Freezes => any squad object spins.
        squad.pop("custom", None)
        squad["players"] = []
        squad["kicktakers"] = []

    elif step == "s1":
        pass                                             # bare players, empty club

    elif step == "s1b":
        _put_item(squad, 0, {"id": 0, "dream": False})   # one empty item, no asset

    elif step in ("s2v0", "s2v1"):
        version = 0x00 if step.endswith("v0") else 0x01
        asset, rating, pos, kit = REAL_XI[0]             # Ronaldo
        it = player_item(asset, rating, pos, version)
        _put_item(squad, 0, it, kit)
        squad["captain"] = it["id"]
        club_items = [it]

    elif step in ("s3v0", "s3v1"):
        version = 0x00 if step.endswith("v0") else 0x01
        for slot, (asset, rating, pos, kit) in enumerate(REAL_XI):
            it = player_item(asset, rating, pos, version)
            _put_item(squad, slot, it, kit)
            club_items.append(it)
        squad["captain"] = club_items[0]["id"]

    else:
        raise ValueError("unknown FUT_SQUAD_STEP=%r (s1|s1b|s2v0|s2v1|s3v0|s3v1)" % step)

    return squad, {"itemData": club_items}


def squad_rating(squad):
    """Squad rating = mean of the rated players actually placed (0 when the squad
    is empty, e.g. the s1 zero-resolve ladder step)."""
    ratings = [(pl.get("itemData") or {}).get("rating", 0)
               for pl in squad.get("players", [])]
    ratings = [r for r in ratings if isinstance(r, int) and r > 0]
    return sum(ratings) // len(ratings) if ratings else 0


def squad_summary(squad):
    """One FutSquadList element (deser 0x180141fc0, verified 2026-08-03).

    Exactly six atoms are recognised; everything else is SKIP'd:
      rating    0x274 int    (scalar getter 0x1801c79d0)
      chemistry 0x81  int    (scalar getter 0x1801c79d0)
      formation 0x12b STRING (string getter 0x1801c7aa0 -> enum conv 0x180166590)
      id        0x15c int
      squadName 0x2d3 STRING
      squadType 0x2d6 STRING (string getter 0x1801c7aa0 -> enum conv 0x1801668e0)

    NOTE: formation/squadType are STRINGS here, exactly as in the full-squad parser
    0x18013d1f0 -- they go through the same 0x1801c7aa0 + converter pair. (The
    rebuild plan's "<int>" for those two was wrong; feeding ints to a string getter
    is the classic type-mismatch freeze at 0x1801c7f1a.)
    """
    return {
        "rating":    squad_rating(squad),
        "chemistry": int(squad.get("chemistry", 0)),
        "formation": squad.get("formation", "f442"),
        "id":        int(squad.get("id", 0)),
        "squadName": squad.get("squadName", ACCOUNT.squad_name),
        "squadType": squad.get("squadType", "REGULAR_SQUAD"),
    }


# Selected at import time from the environment (default s1 = the zero-resolve test).
STEP = os.environ.get("FUT_SQUAD_STEP", "s1")
SQUAD, CLUB = make_squad(STEP)
# Back-compat exports for utas_server.
USER_LIST = {"user": []}
