#!/usr/bin/env python3
"""Probe every route the FIFA 17 FUT hub touches on the staging stack, and fail loudly
on anything that would break it.

The sold-row A/B kept stalling on preconditions the headless checks never exercised:
the identity had no squad (hub refuses to open), and any route not owned by Rust falls
through to a deliberately dead Python upstream and answers 502. Each of those cost a
full operator cycle -- launch, observe, report, diagnose -- so this front-loads the
whole surface instead of discovering the next gap one restart at a time.

Flags three distinct failure classes, because they need different fixes:
  * 502 / PYTHON_FALLBACK  -- route not implemented in Rust; the staging stack has no
    Python upstream, so it is fatal here even though production would proxy it.
  * missing_integrity      -- Rust answered 200 but the underlying state is absent
    (this is exactly what "no extension stored" was before the squad was seeded).
  * empty-but-required     -- 200 with a body the hub cannot work with, e.g. a squad
    with zero occupied slots. A 200 is not proof the client is satisfied.

    python3 scripts/sold-staging-hub-sweep.py
"""
import http.client
import json
import os
import sys

HOST, PORT = "127.0.0.1", 8299
FORBIDDEN = {8099, 8199, 18080, 8443, 42127, 42130, 42131, 4216}
HEADERS = {"X-OpenFUT-Game": "fifa17"}
LOG = "/home/alex/openfut-sold-staging/logs/utas-host.log"

# Only paths the CLIENT is actually observed to request (host log route names), with
# casing exactly as it sends them. An invented path that 502s is a bug in this list,
# not in the stack: a first pass here flagged five "fatal" routes that turned out to
# be guesses -- `/accountinfo` (the client uses `/user/accountinfo`), bare `/squad`
# (it uses `/squad/active`), and `/watchlist` (it is camelCase `watchList`). Crying
# wolf about the stack is worse than not checking, so every entry below is verified.
ROUTES = [
    "/ut/game/fifa17/user/accountinfo",
    "/ut/game/fifa17/squad/0",
    "/ut/game/fifa17/squad/active",
    "/ut/game/fifa17/club?count=200",
    "/ut/game/fifa17/club/stats/staff",
    "/ut/game/fifa17/club/stats/club",
    "/ut/game/fifa17/clientdata/store",
    "/ut/game/fifa17/purchased/items",
    "/ut/game/fifa17/tradePile",
    "/ut/game/fifa17/tradePile/counts",
    "/ut/game/fifa17/watchList",
    "/ut/game/fifa17/trade/status",
    "/ut/game/fifa17/userMassInfo",
    "/ut/game/fifa17/settings",
]


def get(path):
    assert PORT not in FORBIDDEN, "refusing to touch a production port"
    c = http.client.HTTPConnection(HOST, PORT, timeout=20)
    c.request("GET", path, headers=HEADERS)
    r = c.getresponse()
    raw = r.read()
    c.close()
    try:
        return r.status, json.loads(raw), raw
    except Exception:
        return r.status, None, raw


def squad_occupied(body):
    if not isinstance(body, dict):
        return None
    players = body.get("players")
    if not isinstance(players, list):
        return None
    return sum(1 for p in players if (p.get("itemData") or {}).get("id"))


def main():
    start = 0
    if os.path.exists(LOG):
        start = len(open(LOG, errors="replace").read().splitlines())

    fatal, warn, ok = [], [], []
    print(f"{'route':<44} {'code':>4}  finding")
    print("-" * 90)
    for path in ROUTES:
        status, body, raw = get(path)
        note = ""
        if status == 502:
            note = "FATAL: no Rust owner -> dead Python upstream"
            fatal.append((path, note))
        elif status >= 400:
            note = f"FATAL: {raw[:60]!r}"
            fatal.append((path, note))
        else:
            occ = squad_occupied(body)
            if occ is not None and "squad" in path:
                note = f"squad occupied slots = {occ}"
                if occ == 0:
                    note += "  FATAL: hub cannot open on an empty squad"
                    fatal.append((path, note))
                else:
                    ok.append(path)
            else:
                ok.append(path)
                if isinstance(body, dict):
                    n = body.get("total", body.get("totalResults"))
                    note = f"ok{'' if n is None else f', total={n}'}"
                else:
                    note = "ok"
        print(f"{path:<44} {status:>4}  {note}")

    # The host's own classification is the authority on ownership and integrity, so
    # read what it logged for the requests just made rather than inferring from bodies.
    if os.path.exists(LOG):
        new = open(LOG, errors="replace").read().splitlines()[start:]
        flagged = [l for l in new
                   if "PYTHON_FALLBACK" in l or "missing_integrity" in l
                   or "ERROR" in l]
        if flagged:
            print("\nhost-side findings for those requests:")
            for l in flagged:
                print(f"  {l[:160]}")
                if "missing_integrity" in l:
                    warn.append(l)

    print(f"\n=== {len(ok)} ok, {len(warn)} integrity warning(s), {len(fatal)} fatal ===")
    for path, note in fatal:
        print(f"  FATAL {path}: {note}")
    if fatal:
        print("\nThe hub will not work until the fatal rows are resolved.")
        return 1
    print("Every hub route the client needs is served. Safe to restart the client.")
    return 0


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