"""Step080: a small IPv4 SYN observation exercise, fixed to local loopback."""
import argparse
import json
import secrets
import sys

from scapy.all import IP, TCP, ICMP, conf, sr1

TARGET = "127.0.0.1"


def configure_linux():
    if not sys.platform.startswith("linux"):
        raise RuntimeError("Run live exercises in a Linux lab; offline packet construction works elsewhere.")
    from scapy.supersocket import L3RawSocket
    conf.use_pcap = False
    conf.L3socket = L3RawSocket


def classify(answer, sent):
    """Inspect the OUTER response, never a TCP header quoted inside ICMP."""
    if answer is None:
        return {"state": "NO_RESPONSE", "detail": "No matching reply before timeout; cause undetermined"}
    if not isinstance(answer, IP):
        return {"state": "OTHER", "detail": "Not an IPv4 response"}
    if isinstance(answer.payload, ICMP):
        return {"state": "ICMP", "detail": f"type={answer[ICMP].type} code={answer[ICMP].code}; not a TCP verdict"}
    if not isinstance(answer.payload, TCP):
        return {"state": "OTHER", "detail": "Not a direct TCP response"}
    tcp = answer[TCP]
    if (answer.src, answer.dst, tcp.sport, tcp.dport) != (
        sent.dst, sent.src, sent[TCP].dport, sent[TCP].sport
    ) or not (int(tcp.flags) & 0x10) or tcp.ack != ((sent[TCP].seq + 1) & 0xffffffff):
        return {"state": "UNMATCHED", "detail": "Addresses, ports or SYN acknowledgment do not match"}
    # Ignore ECN flags here, but reject conflicting SYN/RST/FIN combinations.
    control = int(tcp.flags) & 0x17
    state = "OPEN" if control == 0x12 else "CLOSED" if control == 0x14 else "OTHER"
    return {"state": state, "detail": f"TCP flags={tcp.flags}"}


def observe(port, timeout=1.0):
    configure_linux()
    sent = IP(src=TARGET, dst=TARGET) / TCP(
        sport=secrets.randbelow(20000) + 40000,
        dport=port, flags="S", seq=secrets.randbits(32)
    )
    answer = sr1(sent, timeout=timeout, verbose=0)
    return {"port": port, **classify(answer, sent)}


def parse_ports(value):
    try:
        ports = sorted(set(int(p) for p in value.split(",")))
    except ValueError as exc:
        raise argparse.ArgumentTypeError("Use comma-separated integer ports") from exc
    if not ports or len(ports) > 32 or not all(1 <= p <= 65535 for p in ports):
        raise argparse.ArgumentTypeError("Choose 1 to 32 distinct ports, each 1..65535")
    return ports


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--ports", type=parse_ports, default=parse_ports("21,22,80,443,9999"))
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()
    try:
        result = {"target": TARGET, "observations": [observe(port) for port in args.ports]}
    except (PermissionError, OSError, RuntimeError) as exc:
        print(f"Cannot run packet exercise: {exc}. Use the lab venv Python with raw-socket permission.", file=sys.stderr)
        return 1
    if args.json:
        print(json.dumps(result))
    else:
        print(f"Target: {TARGET}")
        for row in result["observations"]:
            print(f"{row['port']:5} {row['state']:12} {row['detail']}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
