#!/usr/bin/env python3
"""Passive FOCAS/2 wire capture for protocol-version scoping.

Sends ONLY the initiate PDU the OtOpcUa WireFocasClient sends (a read-only handshake;
NO data/write PDUs) and dumps the raw response so the real wire framing — notably the
PDU version field — can be inspected. Used to diagnose the 30i-B PDU-v3 gap; see
docs/plans/2026-06-25-focas-pdu-v3-30i-b-support.md.

Usage:  python3 scripts/focas/capture-initiate.py <host> [port]
"""
import socket, sys

MAGIC = bytes([0xA0, 0xA0, 0xA0, 0xA0])


def build_pdu(version, type_, direction, body):
    h = bytearray(MAGIC)
    h += version.to_bytes(2, "big")
    h += bytes([type_, direction])
    h += len(body).to_bytes(2, "big")
    return bytes(h) + body


def hexdump(b):
    return " ".join(f"{x:02x}" for x in b)


def parse_header(b):
    if len(b) < 10:
        return f"(short read, {len(b)} bytes)"
    return (f"magic={hexdump(b[0:4])} version={int.from_bytes(b[4:6],'big')} "
            f"type=0x{b[6]:02x} dir=0x{b[7]:02x} bodyLen={int.from_bytes(b[8:10],'big')}")


def try_initiate(host, port, version, socket_index):
    print(f"\n=== initiate header version={version}, socketIndex={socket_index} ===")
    pdu = build_pdu(version, 0x01, 0x01, socket_index.to_bytes(2, "big"))
    print(f"  -> {len(pdu)} bytes: {hexdump(pdu)}")
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(6)
    try:
        s.connect((host, port))
        s.sendall(pdu)
        resp = b""
        try:
            while len(resp) < 512:
                chunk = s.recv(512 - len(resp))
                if not chunk:
                    break
                resp += chunk
                if len(resp) >= 10 and len(resp) >= 10 + int.from_bytes(resp[8:10], "big"):
                    break
        except socket.timeout:
            pass
        print(f"  <- {len(resp)} bytes: {hexdump(resp)}")
        print(f"  <- header: {parse_header(resp)}")
        if len(resp) > 10:
            print(f"  <- body:   {hexdump(resp[10:])}")
    except Exception as e:
        print(f"  !! {type(e).__name__}: {e}")
    finally:
        s.close()


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(__doc__)
        sys.exit(2)
    host = sys.argv[1]
    port = int(sys.argv[2]) if len(sys.argv) > 2 else 8193
    try_initiate(host, port, 1, 1)   # what OtOpcUa sends today (header version=1)
    try_initiate(host, port, 3, 1)   # probe whether the CNC negotiates on our advertised version
