import struct
import socket
import os
from collections import defaultdict


class Packet:
    def __init__(self, timestamp, src_ip, dst_ip, src_port, dst_port, protocol):
        self.timestamp = timestamp
        self.src_ip = src_ip
        self.dst_ip = dst_ip
        self.src_port = src_port
        self.dst_port = dst_port
        self.protocol = protocol  # 'TCP' or 'UDP'


def parse_pcap(file):
    packets = {'TCP': [], 'UDP': []}
    with open(file, 'rb') as f:
        # Skip the PCAP global header (24 bytes)
        f.read(24)

        while True:
            packet_header = f.read(16)
            if not packet_header:
                break

            ts_sec, ts_usec, incl_len, _ = struct.unpack('=IIII', packet_header)
            packet_data = f.read(incl_len)

            if incl_len < 54:  # Not enough data for Ethernet + IP + TCP/UDP headers
                continue

            ip_header = packet_data[14:34]  # skip Ethernet header (14 bytes)
            src_ip = socket.inet_ntoa(ip_header[12:16])
            dst_ip = socket.inet_ntoa(ip_header[16:20])
            protocol = ip_header[9]  # Protocol number

            if protocol == 6:  # TCP
                src_port = struct.unpack('!H', packet_data[34:36])[0]
                dst_port = struct.unpack('!H', packet_data[36:38])[0]
                packets['TCP'].append(Packet(ts_sec + ts_usec * 1e-6, src_ip, dst_ip, src_port, dst_port, 'TCP'))
            elif protocol == 17:  # UDP
                src_port = struct.unpack('!H', packet_data[34:36])[0]
                dst_port = struct.unpack('!H', packet_data[36:38])[0]
                packets['UDP'].append(Packet(ts_sec + ts_usec * 1e-6, src_ip, dst_ip, src_port, dst_port, 'UDP'))

    return packets


def find_clusters(packets, width, min_packets, key_func):
    clustered = defaultdict(list)  # Keep packets grouped by time/port
    results = []

    packets.sort(key=key_func)

    for packet in packets:
        packet_time_or_port = key_func(packet)

        # Initialize the cluster
        if not clustered[packet_time_or_port]:
            clustered[packet_time_or_port] = []

        # Check the last packet's timestamp
        if clustered[packet_time_or_port] and abs(
                packet.timestamp - clustered[packet_time_or_port][-1].timestamp) <= width:
            clustered[packet_time_or_port].append(packet)
        else:
            # If we have a valid cluster, add it to results
            if len(clustered[packet_time_or_port]) >= min_packets:
                src_ips = {p.src_ip for p in clustered[packet_time_or_port]}  # Unique source IPs
                results.append((packet_time_or_port, src_ips))
            # Reset for the current packet
            clustered[packet_time_or_port] = [packet]

    # Final check for any remaining clusters
    for key, cluster in clustered.items():
        if len(cluster) >= min_packets:
            src_ips = {p.src_ip for p in cluster}
            results.append((key, src_ips))

    return results


def generate_output(cluster_results, protocol, target_ip):
    output = f"Reports of {protocol}:\n"
    seen_ports = defaultdict(set)  # To track already reported ports and their source IPs

    for key, src_ips in cluster_results:
        seen_ports[key].update(src_ips)  # Aggregate source IPs per port

    for port, src_ips in seen_ports.items():
        output += f"{protocol} from {target_ip} to port {port} - Total Attempts: {len(src_ips)}\n"

    return output.strip()


def main():
    # User input for parameters
    target_ip = input("Enter the target IP address (-t): ")
    Wp = float(input("Enter the width for probing in seconds (-l): "))  # Probing width
    Np = int(input("Enter the minimum number of packets in probing (-m): "))  # Minimum packets for probing
    Ws = int(input("Enter the width for scanning in port numbers (-n): "))  # Scanning width
    Ns = int(input("Enter the minimum number of packets in scanning (-p): "))  # Minimum packets for scanning

    # Folder path where the PCAP files are stored
    folder_path = "C:\\Users\\tj471\\PycharmProjects\\Lab_Ass_4\\Lab3-pcap-2"  # Replace with your folder path

    # Get all pcap files in the specified folder
    pcap_files = [f for f in os.listdir(folder_path) if f.endswith('.pcap')]

    tcp_probing_results = []
    tcp_scanning_results = []
    udp_probing_results = []
    udp_scanning_results = []

    for pcap_file in pcap_files:
        file_path = os.path.join(folder_path, pcap_file)
        print(f"Processing {file_path}...")
        packets = parse_pcap(file_path)

        # Finding clusters for TCP
        tcp_probing_results.extend(find_clusters(
            packets['TCP'],
            Wp,
            Np,
            key_func=lambda pkt: pkt.src_port
        ))

        tcp_scanning_results.extend(find_clusters(
            packets['TCP'],
            Ws,
            Ns,
            key_func=lambda pkt: pkt.dst_port
        ))

        # Finding clusters for UDP
        udp_probing_results.extend(find_clusters(
            packets['UDP'],
            Wp,
            Np,
            key_func=lambda pkt: pkt.src_port
        ))

        udp_scanning_results.extend(find_clusters(
            packets['UDP'],
            Ws,
            Ns,
            key_func=lambda pkt: pkt.dst_port
        ))

    # Generate and print results
    print(generate_output(tcp_probing_results, 'Probing with TCP', target_ip))
    print(generate_output(tcp_scanning_results, 'Scanning with TCP', target_ip))
    print(generate_output(udp_probing_results, 'Probing with UDP', target_ip))
    print(generate_output(udp_scanning_results, 'Scanning with UDP', target_ip))

    print("Analysis complete.")


if __name__ == "__main__":
    main()