#!/usr/bin/env python3
"""Send the routes a real FIFA session used to BOTH backends and diff the replies.

    ./route-ab.py [python-host:port] [rust-host:port]

WHY

The gate 5-6 live session exercised 14 RPCs the recorded fixtures never covered
-- Stats, Clubs, OSDKSettings, SponsoredEvents, Messaging::fetchMessages,
Util::getTelemetryServer, the transport PING -- and every one took the
empty-reply fallback. That Python does the same was an INFERENCE from reading
its dispatch table. This turns it into evidence, using the routes FIFA actually
sent.

None of these RPCs read their request body (they are unhandled on both sides),
so sending them with an empty payload exercises the same path the client did.

Read-only: opens a client connection to each backend and sends requests. All
routes here are unimplemented on both sides, so nothing mutates.
"""
import socket, struct, sys

# (component, command, msg_type) observed in the live FIFA session.
MESSAGE, PING = 0, 4
ROUTES = [
    (0x0009, 0x0005, MESSAGE, "Util::getTelemetryServer"),
    (0x0009, 0x000C, MESSAGE, "Util::userSettingsLoadAll"),
    (0x0009, 0x001C, MESSAGE, "Util::setClientState"),
    (0x7802, 0x0008, MESSAGE, "UserSessions::cmd:0x0008"),
    (0x000F, 0x0002, MESSAGE, "Messaging::fetchMessages"),
    (0x000B, 0x0640, MESSAGE, "Clubs::cmd:0x0640"),
    (0x000B, 0x0A28, MESSAGE, "Clubs::cmd:0x0a28"),
    (0x08C9, 0x0001, MESSAGE, "OSDKSettings::cmd:0x0001"),
    (0x08C9, 0x0002, MESSAGE, "OSDKSettings::cmd:0x0002"),
    (0x0007, 0x0003, MESSAGE, "Stats::cmd:0x0003"),
    (0x0007, 0x000F, MESSAGE, "Stats::cmd:0x000f"),
    (0x0007, 0x0014, MESSAGE, "Stats::cmd:0x0014"),
    (0x081C, 0x0003, MESSAGE, "SponsoredEvents::cmd:0x0003"),
    (0x0000, 0x0000, PING,    "transport PING"),
]

def frame(comp, cmd, num, mtype, payload=b""):
    h = bytearray(16)
    struct.pack_into(">I", h, 0, len(payload))
    struct.pack_into(">H", h, 4, 0)
    struct.pack_into(">H", h, 6, comp)
    struct.pack_into(">H", h, 8, cmd)
    h[10], h[11], h[12] = (num >> 16) & 0xFF, (num >> 8) & 0xFF, num & 0xFF
    h[13] = (mtype & 7) << 5
    return bytes(h) + payload

def ask(host, port, routes):
    s = socket.create_connection((host, port), timeout=10)
    s.settimeout(10)
    out, buf = [], b""
    for i, (comp, cmd, mtype, name) in enumerate(routes):
        s.sendall(frame(comp, cmd, i + 1, mtype))
        while len(buf) < 16:
            d = s.recv(65536)
            if not d:
                out.append((name, "NO REPLY")); s.close(); return out
            buf += d
        total = 16 + struct.unpack_from(">H", buf, 4)[0] + struct.unpack_from(">I", buf, 0)[0]
        while len(buf) < total:
            buf += s.recv(65536)
        out.append((name, buf[:total].hex()))
        buf = buf[total:]
    s.close()
    return out

def endpoint(arg, default_port):
    if ":" in arg:
        h, p = arg.rsplit(":", 1)
        return h, int(p)
    return arg, default_port

args = sys.argv[1:]
PY_HOST, PY_PORT = endpoint(args[0], 42130) if len(args) > 0 else ("127.0.0.1", 42130)
RS_HOST, RS_PORT = endpoint(args[1], 42230) if len(args) > 1 else ("127.0.0.1", 42230)

print("python %s:%d   rust %s:%d" % (PY_HOST, PY_PORT, RS_HOST, RS_PORT))
py = ask(PY_HOST, PY_PORT, ROUTES)
rs = ask(RS_HOST, RS_PORT, ROUTES)

bad = 0
for (n1, a), (n2, b) in zip(py, rs):
    same = (a == b)
    if not same:
        bad += 1
    print("  %-32s %s" % (n1, "identical" if same else "DIFFERS\n      py=%s\n      rs=%s" % (a, b)))
print("\n%d/%d identical" % (len(ROUTES) - bad, len(ROUTES)))
sys.exit(1 if bad else 0)
