#!/usr/bin/env python3
"""Capture the roster oracle's responses byte-for-byte.

    generate_roster.py [--check] [host:port]

Unlike `generate.py`, which imports the Blaze responder and calls its pure
functions, this captures over the wire. The roster response is shaped as much by
`http.server.BaseHTTPRequestHandler` as by the handler code -- HTTP/1.0 status
line, `Server:`/`Date:` injected ahead of the handler's own headers, POST
answered without a body -- and only the real socket shows all of that.

Two fields are volatile and are MASKED rather than recorded:

  Date:    changes every second
  Server:  carries the container's Python version

They are masked, not dropped, so their presence and position are still asserted.
The Server string is additionally recorded verbatim under `observed_server`, so
a drift between the container's Python and the adapter's `ORACLE_SERVER`
constant is visible rather than silent.

`--check` re-captures and compares. If the oracle is unreachable it FAILS rather
than passing: a check that cannot check must not report success.
"""
import json
import os
import re
import socket
import ssl
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "roster.json")
PATH = "/fifa17/fut/rosterupdate.xml"

DATE_RE = re.compile(rb"^Date: .+?\r\n", re.M)
SERVER_RE = re.compile(rb"^Server: (.+?)\r\n", re.M)


def fetch(host, port, method, body=None):
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE
    ctx.set_ciphers("ALL:@SECLEVEL=0")
    s = ctx.wrap_socket(socket.create_connection((host, port), timeout=8),
                        server_hostname="fixture")
    req = "%s %s HTTP/1.1\r\nHost: %s:%d\r\nAccept: */*\r\n" % (method, PATH, host, port)
    if body is not None:
        req += "Content-Length: %d\r\n" % len(body)
    req += "\r\n"
    s.sendall(req.encode() + (body or b""))
    out = b""
    while True:
        chunk = s.recv(4096)
        if not chunk:
            break
        out += chunk
    s.close()
    return out


def capture(host, port):
    result = {"path": PATH, "responses": {}}
    servers = set()
    for method, body in (("GET", None), ("HEAD", None), ("POST", b"probe=1")):
        raw = fetch(host, port, method, body)
        m = SERVER_RE.search(raw)
        if m:
            servers.add(m.group(1).decode())
        masked = DATE_RE.sub(b"Date: <MASKED>\r\n", raw)
        masked = SERVER_RE.sub(b"Server: <MASKED>\r\n", masked)
        result["responses"][method] = masked.hex()
    if len(servers) != 1:
        raise SystemExit("oracle returned inconsistent Server headers: %r" % servers)
    result["observed_server"] = servers.pop()
    return result


def main():
    check = "--check" in sys.argv
    args = [a for a in sys.argv[1:] if not a.startswith("--")]
    host, port = (args[0].split(":") if args else ("127.0.0.1", "8081"))[0], \
                 int((args[0].split(":")[1] if args and ":" in args[0] else "8081"))

    try:
        fresh = capture(host, port)
    except Exception as e:
        # Explicitly a failure. A --check that silently passes when it could not
        # reach the oracle is exactly the class of self-confirming tooling this
        # project has been bitten by repeatedly.
        raise SystemExit("cannot reach the roster oracle at %s:%d (%s). "
                         "Refusing to report success." % (host, port, e))

    if check:
        if not os.path.exists(OUT):
            raise SystemExit("no fixture at %s -- run without --check first" % OUT)
        with open(OUT) as f:
            stored = json.load(f)
        if stored.get("responses") != fresh["responses"]:
            for m in sorted(set(stored.get("responses", {})) | set(fresh["responses"])):
                a = stored.get("responses", {}).get(m)
                b = fresh["responses"].get(m)
                if a != b:
                    print("MISMATCH %s\n  stored: %s\n  live  : %s" % (m, a, b))
            raise SystemExit("roster fixtures differ from the live oracle")
        if stored.get("observed_server") != fresh["observed_server"]:
            raise SystemExit(
                "the oracle's Server header changed: %r -> %r.\n"
                "Update roster::ORACLE_SERVER and regenerate."
                % (stored.get("observed_server"), fresh["observed_server"]))
        print("roster fixtures match the live oracle (%d responses, server=%r)"
              % (len(fresh["responses"]), fresh["observed_server"]))
        return

    with open(OUT, "w") as f:
        json.dump(fresh, f, indent=2, sort_keys=True)
        f.write("\n")
    print("wrote %s (%d responses, server=%r)"
          % (OUT, len(fresh["responses"]), fresh["observed_server"]))


if __name__ == "__main__":
    main()
