#!/usr/bin/env python3
"""Seed the staging seller a valid starting XI so the FIFA hub will open.

The sold-row A/B only ever needed the tradePile wire, so the staging identity was
created with owned items but NO squad. Every headless check passed because none of
them asks for a squad -- but the real client refuses to enter the FUT hub with an
empty one ("squad update error"), because a squad is a hub precondition, not a
Transfer-List detail.

This seeds it through the REAL route -- `PUT /ut/game/fifa17/squad/0`, the same
request the client itself sends -- so the squad is written by
parse_squad_put/build_squad_write exactly as a genuine save would be. Writing
Core/DB rows by hand would risk a shape the live path would never produce, which is
the sort of divergence that invalidates an experiment.

Wire facts (openfut-adapter-fifa17/src/fut/squad.rs): the players array is a fixed 23
slots; 0..=10 are the pitch, 11..=22 bench/reserves, derived from the INDEX alone and
never from the formation token. Empty slots are `itemData.id == 0`. The formation
token is carried verbatim and never interpreted server-side. A successful save
answers `{"id": 0}` and does NOT echo the squad.

    python3 scripts/sold-staging-seed-squad.py            # seed, then verify
    python3 scripts/sold-staging-seed-squad.py --show     # read-only
"""
import http.client
import json
import sys

HOST, PORT = "127.0.0.1", 8299
FORBIDDEN = {8099, 8199, 18080, 8443, 42127, 42130, 42131, 4216}
HEADERS = {"X-OpenFUT-Game": "fifa17", "Content-Type": "application/json"}
SLOTS = 23
STARTERS = 11
# 4-3-3, laid out in the conventional FIFA slot order so the pitch reads correctly:
# GK, RB, CB, CB, LB, CM, CM, CM, RW, ST, LW.
LINEUP = ["GK", "RB", "CB", "CB", "LB", "CM", "CM", "CM", "RW", "ST", "LW"]
FORMATION = "f433"
SQUAD_TYPE = "REGULAR_SQUAD"
CHEMISTRY = 50
# Production's squad-manager reference, mirrored verbatim; it resolves to nothing in
# production either (absent from its own /club/staff listing), which is exactly why
# copying it is safe.
PRODUCTION_MANAGER_REF = 100000427
# Production's opaque 33-int tactics array. Never parsed server-side; reused because
# only the shape (a JSON-encoded int array, not null) matters to the client.
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]")


def call(method, path, body=None):
    assert PORT not in FORBIDDEN, "refusing to touch a production port"
    c = http.client.HTTPConnection(HOST, PORT, timeout=20)
    payload = json.dumps(body) if body is not None else None
    c.request(method, path, body=payload, headers=HEADERS)
    r = c.getresponse()
    raw = r.read()
    c.close()
    try:
        return r.status, json.loads(raw)
    except Exception:
        return r.status, raw.decode(errors="replace")


def club_players():
    status, body = call("GET", "/ut/game/fifa17/club?count=200")
    assert status == 200, f"/club -> {status}"
    items = body.get("itemData", body.get("items", []))
    return [i for i in items if i.get("itemType") == "player"]


def pick_xi(players):
    """Choose one owned player per lineup position, best rating first, without
    reusing an instance. A position with no candidate is reported rather than
    quietly left empty -- an incomplete XI is why the hub errored in the first
    place, so silently reproducing it would defeat the point."""
    remaining = sorted(players, key=lambda p: -int(p.get("rating") or 0))
    chosen, missing = [], []
    for want in LINEUP:
        hit = next((p for p in remaining if p.get("preferredPosition") == want), None)
        if hit is None:
            hit = next((p for p in remaining), None)  # any spare body, flagged below
            if hit is not None:
                missing.append(f"{want}->{hit.get('preferredPosition')}")
        if hit is None:
            missing.append(f"{want}->NONE")
            chosen.append(None)
            continue
        remaining.remove(hit)
        chosen.append(hit)
    return chosen, missing


def build_put(chosen):
    players = []
    for idx in range(SLOTS):
        occupant = chosen[idx] if idx < STARTERS and chosen[idx] else None
        players.append({
            "index": idx,
            "itemData": {"id": int(occupant["id"]) if occupant else 0, "dream": False},
            "kitNumber": idx + 1 if occupant else 0,
        })
    captain = next((int(p["id"]) for p in chosen if p), 0)
    ratings = [int(p.get("rating") or 0) for p in chosen if p]
    mean_rating = sum(ratings) // len(ratings) if ratings else 0
    return {
        "id": 0,
        "squadName": "Staging XI",
        "formation": FORMATION,
        "captain": captain,
        "players": players,
        # A first attempt sent only name/formation/captain/players and the client
        # STILL refused the hub, while the host logged squad-active 200 ok -- the
        # route was fine and the body was not. Diffing against production's
        # known-good squad showed staging returning null for exactly these five,
        # because the PUT never carried them and the extension stored nothing.
        # Types matter here: the client wants scalars, not null.
        "squadType": SQUAD_TYPE,
        "chemistry": CHEMISTRY,
        "rating": mean_rating,
        "starRating": mean_rating,
        # Opaque 33-int tactics array, carried verbatim and never interpreted
        # server-side. Reused from production because only its SHAPE matters.
        "custom": CUSTOM,
        # Production carries five, all pointing at one player. Mirrored so the
        # array is populated rather than empty.
        "kicktakers": [{"index": i, "id": captain, "dream": False} for i in range(5)],
        # Mirrors production's manager reference verbatim, including the fact that
        # the id resolves to nothing.
        #
        # This looked unfixable at first: production points at instance 100000427
        # while the staging club holds 11 players and zero staff, so there was
        # apparently no manager to reference. Then checking production properly
        # showed 100000427 is absent from its OWN club listing too --
        # /club/staff returns 1975 items spanning ids 100000001..100004826 and
        # 100000427 is not one of them. Production's manager reference is dangling
        # and the client accepts that squad regardless, which proves the client does
        # not validate the manager id against the club. Only a populated array
        # matters, so replicating the known-good state exactly is both faithful and
        # sufficient -- and it beats pointing the manager slot at a player.
        "manager": [{"id": PRODUCTION_MANAGER_REF, "dream": False}],
    }


def show():
    status, body = call("GET", "/ut/game/fifa17/squad/0")
    filled = [p for p in body.get("players", []) if p.get("itemData", {}).get("id")]
    print(f"  /squad/0 -> {status}  id={body.get('id')} "
          f"players={len(body.get('players', []))} occupied={len(filled)}")
    for p in filled:
        d = p.get("itemData", {})
        print(f"    index={p.get('index')} id={d.get('id')} res={d.get('resourceId')} "
              f"pos={d.get('preferredPosition')} rating={d.get('rating')}")
    return len(filled)


def main():
    print("=== staging squad, before ===")
    before = show()
    if "--show" in sys.argv:
        return 0
    players = club_players()
    print(f"=== owned players available: {len(players)} ===")
    chosen, missing = pick_xi(players)
    if missing:
        # Loud, because an out-of-position XI still opens the hub but is not what a
        # real save would look like.
        print(f"  NOTE: substitutions made for {len(missing)} slot(s): {missing}")
    if any(c is None for c in chosen):
        raise SystemExit("not enough owned players for a starting XI; refusing to "
                         "write a partial squad")
    put = build_put(chosen)
    print(f"=== PUT /squad/0  formation={put['formation']} captain={put['captain']} "
          f"occupied={sum(1 for p in put['players'] if p['itemData']['id'])} ===")
    status, body = call("PUT", "/ut/game/fifa17/squad/0", put)
    print(f"  -> {status} {json.dumps(body)}")
    if status != 200 or body != {"id": 0}:
        raise SystemExit(f"squad save did not acknowledge as {{'id':0}}: {status} {body}")
    print("=== staging squad, after ===")
    after = show()
    if after != STARTERS:
        raise SystemExit(f"VERIFY FAILED: expected {STARTERS} occupied slots, got {after}")
    print(f"  OK: {before} -> {after} occupied slots; the hub precondition is satisfied")
    return 0


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