"""
Bandwidth and Throughput Analysis Module
=========================================
Analyzes bandwidth consumption, throughput rates, and traffic patterns for WiFi clients and APs.
Includes per-client, per-AP, per-application traffic analysis with protocol identification.

Features:
- Per-client throughput and bandwidth usage
- Per-AP throughput and bandwidth usage
- Application/protocol traffic identification (YouTube, Netflix, voice, video, etc.)
- Traffic direction analysis (uplink/downlink)
- Time-based throughput analysis
- Data rate distribution per client/AP
- Packet size distribution
- Protocol-specific bandwidth consumption
"""

from scapy.all import *
from scapy.layers.dot11 import *
from scapy.layers.inet import IP, TCP, UDP
from scapy.layers.inet6 import IPv6
from collections import defaultdict, Counter
import re


class BandwidthAnalyzer:
    """Analyze bandwidth and throughput patterns"""

    # Port mappings for application identification
    WELL_KNOWN_PORTS = {
        # Video Streaming
        443: 'HTTPS/Video Streaming',  # YouTube, Netflix over HTTPS
        80: 'HTTP/Video Streaming',
        1935: 'RTMP (Flash Video)',
        554: 'RTSP (Streaming)',
        8080: 'HTTP Alternate',

        # Voice/VoIP
        5060: 'SIP (VoIP)',
        5061: 'SIP-TLS (VoIP)',
        5004: 'RTP (Voice/Video)',
        5005: 'RTP (Voice/Video)',
        16384: 'RTP Dynamic',

        # Conferencing
        3478: 'STUN (WebRTC)',
        3479: 'STUN (WebRTC)',
        19302: 'TURN (WebRTC)',

        # Gaming
        3074: 'Xbox Live',
        3075: 'Xbox Live',
        3659: 'PlayStation Network',

        # File Transfer
        21: 'FTP',
        22: 'SFTP/SSH',
        445: 'SMB',

        # Email
        25: 'SMTP',
        110: 'POP3',
        143: 'IMAP',
        993: 'IMAPS',
        995: 'POP3S',

        # DNS
        53: 'DNS',

        # Other
        123: 'NTP',
        161: 'SNMP',
        162: 'SNMP Trap',
    }

    # Domain patterns for application identification
    VIDEO_DOMAINS = [
        'youtube', 'googlevideo', 'ytimg',
        'netflix', 'nflxvideo', 'nflximg',
        'hulu', 'amazon', 'primevideo',
        'disney', 'hbo', 'twitch',
        'vimeo', 'dailymotion', 'tiktok'
    ]

    SOCIAL_DOMAINS = [
        'facebook', 'instagram', 'twitter',
        'snapchat', 'whatsapp', 'telegram',
        'linkedin', 'reddit', 'pinterest'
    ]

    VOIP_DOMAINS = [
        'zoom', 'skype', 'teams', 'webex',
        'meet.google', 'discord', 'slack'
    ]

    def __init__(self, packets):
        self.packets = packets
        self.results = {
            'per_client_analysis': {},
            'per_ap_analysis': {},
            'per_application_analysis': {},
            'traffic_direction': {},
            'temporal_analysis': {},
            'protocol_distribution': {},
            'multicast_analysis': {},
            'summary': {}
        }

    def analyze(self):
        """Run complete bandwidth analysis"""
        self._analyze_per_client()
        self._analyze_per_ap()
        self._analyze_applications()
        self._analyze_traffic_direction()
        self._analyze_protocols()
        self._analyze_multicast_traffic()
        self._generate_summary()
        return self.results

    def _analyze_per_client(self):
        """Analyze bandwidth usage per WiFi client"""
        client_stats = defaultdict(lambda: {
            'total_bytes': 0,
            'total_packets': 0,
            'data_frames': 0,
            'management_frames': 0,
            'control_frames': 0,
            'tx_bytes': 0,  # Transmitted by client
            'rx_bytes': 0,  # Received by client
            'tx_packets': 0,
            'rx_packets': 0,
            'data_rates': [],
            'mcs_values': [],
            'protocols': Counter(),
            'packet_sizes': [],
            'associated_aps': set(),
            'first_seen': None,
            'last_seen': None,
            'applications': Counter()
        })

        for pkt_num, pkt in enumerate(self.packets, 1):
            if not pkt.haslayer(Dot11):
                continue

            dot11 = pkt[Dot11]
            pkt_size = len(pkt)
            timestamp = float(pkt.time) if hasattr(pkt, 'time') else 0

            # Extract addresses
            addr1 = dot11.addr1 if hasattr(dot11, 'addr1') else None
            addr2 = dot11.addr2 if hasattr(dot11, 'addr2') else None
            addr3 = dot11.addr3 if hasattr(dot11, 'addr3') else None

            # Identify client (STA) vs AP
            # In infrastructure mode:
            # - ToDS=1, FromDS=0: STA to AP (addr2=STA, addr1=BSSID)
            # - ToDS=0, FromDS=1: AP to STA (addr1=STA, addr2=BSSID)

            client_mac = None
            ap_mac = None
            direction = None

            if dot11.type == 2:  # Data frame
                # Check ToDS and FromDS flags
                to_ds = (dot11.FCfield & 0x01) != 0
                from_ds = (dot11.FCfield & 0x02) != 0

                if to_ds and not from_ds:
                    # STA to AP
                    client_mac = addr2
                    ap_mac = addr1
                    direction = 'tx'
                elif from_ds and not to_ds:
                    # AP to STA
                    client_mac = addr1
                    ap_mac = addr2
                    direction = 'rx'
                elif to_ds and from_ds:
                    # WDS (wireless distribution system) - skip for now
                    continue
                else:
                    # IBSS or other - use addr2 as source
                    client_mac = addr2
                    direction = 'tx'

            elif dot11.type == 0:  # Management frame
                # For management frames, typically addr2 is the source
                if pkt.haslayer(Dot11Beacon) or pkt.haslayer(Dot11ProbeResp):
                    # Sent by AP
                    ap_mac = addr2
                elif pkt.haslayer(Dot11ProbeReq) or pkt.haslayer(Dot11AssoReq):
                    # Sent by client
                    client_mac = addr2

            # Update client statistics
            if client_mac and client_mac != 'ff:ff:ff:ff:ff:ff':
                stats = client_stats[client_mac]
                stats['total_bytes'] += pkt_size
                stats['total_packets'] += 1

                # Update timestamps
                if stats['first_seen'] is None:
                    stats['first_seen'] = timestamp
                stats['last_seen'] = timestamp

                # Frame type counting
                if dot11.type == 2:
                    stats['data_frames'] += 1
                elif dot11.type == 0:
                    stats['management_frames'] += 1
                elif dot11.type == 1:
                    stats['control_frames'] += 1

                # Direction-specific counting
                if direction == 'tx':
                    stats['tx_bytes'] += pkt_size
                    stats['tx_packets'] += 1
                elif direction == 'rx':
                    stats['rx_bytes'] += pkt_size
                    stats['rx_packets'] += 1

                # Track associated APs
                if ap_mac:
                    stats['associated_aps'].add(ap_mac)

                # Extract data rate from RadioTap
                if pkt.haslayer(RadioTap):
                    rt = pkt[RadioTap]
                    if hasattr(rt, 'Rate') and rt.Rate is not None:
                        rate_mbps = rt.Rate * 0.5
                        stats['data_rates'].append(rate_mbps)
                    if hasattr(rt, 'MCS_index') and rt.MCS_index is not None:
                        stats['mcs_values'].append(rt.MCS_index)

                # Track packet sizes
                if pkt_size > 0:
                    stats['packet_sizes'].append(pkt_size)

                # Protocol analysis
                if pkt.haslayer(IP) or pkt.haslayer(IPv6):
                    protocol_info = self._identify_protocol(pkt)
                    if protocol_info:
                        stats['protocols'][protocol_info] += 1

                        # Application identification
                        app_info = self._identify_application(pkt)
                        if app_info:
                            stats['applications'][app_info] += pkt_size

        # Convert to serializable format and calculate metrics
        for mac, stats in client_stats.items():
            duration = stats['last_seen'] - stats['first_seen'] if stats['first_seen'] and stats['last_seen'] else 0

            self.results['per_client_analysis'][mac] = {
                'total_bytes': stats['total_bytes'],
                'total_kb': round(stats['total_bytes'] / 1024, 2),
                'total_mb': round(stats['total_bytes'] / (1024 * 1024), 2),
                'total_packets': stats['total_packets'],
                'data_frames': stats['data_frames'],
                'management_frames': stats['management_frames'],
                'control_frames': stats['control_frames'],
                'tx_bytes': stats['tx_bytes'],
                'rx_bytes': stats['rx_bytes'],
                'tx_kb': round(stats['tx_bytes'] / 1024, 2),
                'rx_kb': round(stats['rx_bytes'] / 1024, 2),
                'tx_packets': stats['tx_packets'],
                'rx_packets': stats['rx_packets'],
                'avg_packet_size': round(stats['total_bytes'] / stats['total_packets'], 2) if stats['total_packets'] > 0 else 0,
                'data_rates': {
                    'min': min(stats['data_rates']) if stats['data_rates'] else 0,
                    'max': max(stats['data_rates']) if stats['data_rates'] else 0,
                    'avg': round(sum(stats['data_rates']) / len(stats['data_rates']), 2) if stats['data_rates'] else 0,
                    'distribution': dict(Counter([int(r) for r in stats['data_rates']]))
                },
                'mcs_values': list(set(stats['mcs_values'])) if stats['mcs_values'] else [],
                'associated_aps': list(stats['associated_aps']),
                'protocols': dict(stats['protocols']),
                'applications': dict(stats['applications']),
                'duration_seconds': round(duration, 2),
                'avg_throughput_kbps': round((stats['total_bytes'] * 8) / (duration * 1024), 2) if duration > 0 else 0,
                'avg_throughput_mbps': round((stats['total_bytes'] * 8) / (duration * 1024 * 1024), 2) if duration > 0 else 0,
                'packet_size_stats': {
                    'min': min(stats['packet_sizes']) if stats['packet_sizes'] else 0,
                    'max': max(stats['packet_sizes']) if stats['packet_sizes'] else 0,
                    'avg': round(sum(stats['packet_sizes']) / len(stats['packet_sizes']), 2) if stats['packet_sizes'] else 0
                }
            }

    def _analyze_per_ap(self):
        """Analyze bandwidth usage per Access Point"""
        ap_stats = defaultdict(lambda: {
            'total_bytes': 0,
            'total_packets': 0,
            'beacon_count': 0,
            'data_frames': 0,
            'tx_bytes': 0,
            'rx_bytes': 0,
            'tx_packets': 0,
            'rx_packets': 0,
            'connected_clients': set(),
            'ssid': None,
            'channel': None,
            'first_seen': None,
            'last_seen': None,
            'data_rates': [],
            'applications': Counter()
        })

        for pkt_num, pkt in enumerate(self.packets, 1):
            if not pkt.haslayer(Dot11):
                continue

            dot11 = pkt[Dot11]
            pkt_size = len(pkt)
            timestamp = float(pkt.time) if hasattr(pkt, 'time') else 0

            ap_mac = None
            client_mac = None
            direction = None

            # Identify AP from beacons
            if pkt.haslayer(Dot11Beacon):
                ap_mac = dot11.addr2
                ap_stats[ap_mac]['beacon_count'] += 1

                # Extract SSID
                if pkt.haslayer(Dot11Elt):
                    ssid = pkt[Dot11Elt].info.decode('utf-8', errors='ignore')
                    ap_stats[ap_mac]['ssid'] = ssid

                # Extract channel
                if pkt.haslayer(RadioTap):
                    rt = pkt[RadioTap]
                    if hasattr(rt, 'Channel'):
                        ap_stats[ap_mac]['channel'] = rt.Channel

            # Data frames
            elif dot11.type == 2:
                to_ds = (dot11.FCfield & 0x01) != 0
                from_ds = (dot11.FCfield & 0x02) != 0

                if to_ds and not from_ds:
                    # STA to AP
                    ap_mac = dot11.addr1
                    client_mac = dot11.addr2
                    direction = 'rx'  # AP receiving
                elif from_ds and not to_ds:
                    # AP to STA
                    ap_mac = dot11.addr2
                    client_mac = dot11.addr1
                    direction = 'tx'  # AP transmitting

            # Update AP statistics
            if ap_mac:
                stats = ap_stats[ap_mac]
                stats['total_bytes'] += pkt_size
                stats['total_packets'] += 1

                if stats['first_seen'] is None:
                    stats['first_seen'] = timestamp
                stats['last_seen'] = timestamp

                if dot11.type == 2:
                    stats['data_frames'] += 1

                if direction == 'tx':
                    stats['tx_bytes'] += pkt_size
                    stats['tx_packets'] += 1
                elif direction == 'rx':
                    stats['rx_bytes'] += pkt_size
                    stats['rx_packets'] += 1

                if client_mac:
                    stats['connected_clients'].add(client_mac)

                # Extract data rate
                if pkt.haslayer(RadioTap):
                    rt = pkt[RadioTap]
                    if hasattr(rt, 'Rate') and rt.Rate is not None:
                        rate_mbps = rt.Rate * 0.5
                        stats['data_rates'].append(rate_mbps)

                # Application identification
                app_info = self._identify_application(pkt)
                if app_info:
                    stats['applications'][app_info] += pkt_size

        # Convert to serializable format
        for mac, stats in ap_stats.items():
            duration = stats['last_seen'] - stats['first_seen'] if stats['first_seen'] and stats['last_seen'] else 0

            self.results['per_ap_analysis'][mac] = {
                'ssid': stats['ssid'],
                'channel': stats['channel'],
                'total_bytes': stats['total_bytes'],
                'total_kb': round(stats['total_bytes'] / 1024, 2),
                'total_mb': round(stats['total_bytes'] / (1024 * 1024), 2),
                'total_packets': stats['total_packets'],
                'beacon_count': stats['beacon_count'],
                'data_frames': stats['data_frames'],
                'tx_bytes': stats['tx_bytes'],
                'rx_bytes': stats['rx_bytes'],
                'tx_kb': round(stats['tx_bytes'] / 1024, 2),
                'rx_kb': round(stats['rx_bytes'] / 1024, 2),
                'tx_packets': stats['tx_packets'],
                'rx_packets': stats['rx_packets'],
                'connected_clients_count': len(stats['connected_clients']),
                'connected_clients': list(stats['connected_clients'])[:10],  # First 10
                'avg_packet_size': round(stats['total_bytes'] / stats['total_packets'], 2) if stats['total_packets'] > 0 else 0,
                'data_rates': {
                    'min': min(stats['data_rates']) if stats['data_rates'] else 0,
                    'max': max(stats['data_rates']) if stats['data_rates'] else 0,
                    'avg': round(sum(stats['data_rates']) / len(stats['data_rates']), 2) if stats['data_rates'] else 0
                },
                'applications': dict(stats['applications']),
                'duration_seconds': round(duration, 2),
                'avg_throughput_kbps': round((stats['total_bytes'] * 8) / (duration * 1024), 2) if duration > 0 else 0,
                'avg_throughput_mbps': round((stats['total_bytes'] * 8) / (duration * 1024 * 1024), 2) if duration > 0 else 0
            }

    def _analyze_applications(self):
        """Analyze bandwidth by application type"""
        app_stats = defaultdict(lambda: {
            'total_bytes': 0,
            'packet_count': 0,
            'clients': set()
        })

        for pkt in self.packets:
            if not pkt.haslayer(Dot11):
                continue

            app_info = self._identify_application(pkt)
            if app_info:
                pkt_size = len(pkt)
                app_stats[app_info]['total_bytes'] += pkt_size
                app_stats[app_info]['packet_count'] += 1

                # Track client
                if pkt.haslayer(Dot11):
                    dot11 = pkt[Dot11]
                    to_ds = (dot11.FCfield & 0x01) != 0
                    from_ds = (dot11.FCfield & 0x02) != 0

                    if to_ds and not from_ds and hasattr(dot11, 'addr2'):
                        app_stats[app_info]['clients'].add(dot11.addr2)
                    elif from_ds and not to_ds and hasattr(dot11, 'addr1'):
                        app_stats[app_info]['clients'].add(dot11.addr1)

        # Convert to serializable format
        for app, stats in app_stats.items():
            self.results['per_application_analysis'][app] = {
                'total_bytes': stats['total_bytes'],
                'total_kb': round(stats['total_bytes'] / 1024, 2),
                'total_mb': round(stats['total_bytes'] / (1024 * 1024), 2),
                'packet_count': stats['packet_count'],
                'unique_clients': len(stats['clients']),
                'clients': list(stats['clients'])[:10]  # First 10
            }

    def _analyze_traffic_direction(self):
        """Analyze uplink vs downlink traffic"""
        direction_stats = {
            'uplink': {'bytes': 0, 'packets': 0},
            'downlink': {'bytes': 0, 'packets': 0},
            'peer_to_peer': {'bytes': 0, 'packets': 0}
        }

        for pkt in self.packets:
            if not pkt.haslayer(Dot11) or pkt[Dot11].type != 2:
                continue

            dot11 = pkt[Dot11]
            pkt_size = len(pkt)

            to_ds = (dot11.FCfield & 0x01) != 0
            from_ds = (dot11.FCfield & 0x02) != 0

            if to_ds and not from_ds:
                # Uplink (STA to AP)
                direction_stats['uplink']['bytes'] += pkt_size
                direction_stats['uplink']['packets'] += 1
            elif from_ds and not to_ds:
                # Downlink (AP to STA)
                direction_stats['downlink']['bytes'] += pkt_size
                direction_stats['downlink']['packets'] += 1
            elif to_ds and from_ds:
                # WDS or peer-to-peer
                direction_stats['peer_to_peer']['bytes'] += pkt_size
                direction_stats['peer_to_peer']['packets'] += 1

        # Calculate percentages
        total_bytes = sum(d['bytes'] for d in direction_stats.values())
        total_packets = sum(d['packets'] for d in direction_stats.values())

        for direction in direction_stats:
            stats = direction_stats[direction]
            self.results['traffic_direction'][direction] = {
                'bytes': stats['bytes'],
                'kb': round(stats['bytes'] / 1024, 2),
                'mb': round(stats['bytes'] / (1024 * 1024), 2),
                'packets': stats['packets'],
                'percentage_bytes': round((stats['bytes'] / total_bytes * 100), 2) if total_bytes > 0 else 0,
                'percentage_packets': round((stats['packets'] / total_packets * 100), 2) if total_packets > 0 else 0
            }

    def _analyze_protocols(self):
        """Analyze protocol distribution"""
        protocol_stats = defaultdict(lambda: {'bytes': 0, 'packets': 0})

        for pkt in self.packets:
            if not pkt.haslayer(Dot11):
                continue

            pkt_size = len(pkt)
            protocol = self._identify_protocol(pkt)

            if protocol:
                protocol_stats[protocol]['bytes'] += pkt_size
                protocol_stats[protocol]['packets'] += 1

        # Convert to serializable format
        for protocol, stats in protocol_stats.items():
            self.results['protocol_distribution'][protocol] = {
                'bytes': stats['bytes'],
                'kb': round(stats['bytes'] / 1024, 2),
                'mb': round(stats['bytes'] / (1024 * 1024), 2),
                'packets': stats['packets']
            }

    def _identify_protocol(self, pkt):
        """Identify network protocol"""
        if pkt.haslayer(TCP):
            return 'TCP'
        elif pkt.haslayer(UDP):
            return 'UDP'
        elif pkt.haslayer(ICMP):
            return 'ICMP'
        elif pkt.haslayer(ARP):
            return 'ARP'
        elif pkt.haslayer(DNS):
            return 'DNS'
        elif pkt.haslayer(DHCP):
            return 'DHCP'
        elif pkt.haslayer(EAPOL):
            return 'EAPOL'
        elif pkt.haslayer(IP) or pkt.haslayer(IPv6):
            return 'IP'
        else:
            return '802.11'

    def _identify_application(self, pkt):
        """Identify application type based on ports, domains, and patterns"""
        if not (pkt.haslayer(IP) or pkt.haslayer(IPv6)):
            return None

        app_type = None

        # Check TCP/UDP ports
        if pkt.haslayer(TCP):
            sport = pkt[TCP].sport
            dport = pkt[TCP].dport

            # Check well-known ports
            if dport in self.WELL_KNOWN_PORTS:
                app_type = self.WELL_KNOWN_PORTS[dport]
            elif sport in self.WELL_KNOWN_PORTS:
                app_type = self.WELL_KNOWN_PORTS[sport]

            # HTTPS (443) - try to identify specific service
            elif dport == 443 or sport == 443:
                if pkt.haslayer(Raw):
                    payload = bytes(pkt[Raw].load)
                    # Check for SNI in TLS handshake
                    domain = self._extract_sni(payload)
                    if domain:
                        if any(v in domain.lower() for v in self.VIDEO_DOMAINS):
                            return 'Video Streaming (HTTPS)'
                        elif any(v in domain.lower() for v in self.SOCIAL_DOMAINS):
                            return 'Social Media (HTTPS)'
                        elif any(v in domain.lower() for v in self.VOIP_DOMAINS):
                            return 'VoIP/Conferencing (HTTPS)'
                return 'HTTPS'

            # HTTP (80)
            elif dport == 80 or sport == 80:
                if pkt.haslayer(Raw):
                    payload = bytes(pkt[Raw].load)
                    if b'youtube' in payload.lower() or b'googlevideo' in payload.lower():
                        return 'YouTube (HTTP)'
                    elif b'netflix' in payload.lower():
                        return 'Netflix (HTTP)'
                return 'HTTP'

            # Check for streaming patterns
            elif 1024 <= dport <= 65535 or 1024 <= sport <= 65535:
                # High ports could be RTP/RTCP for streaming
                if pkt.haslayer(Raw) and len(pkt[Raw].load) > 12:
                    # RTP typically has specific patterns
                    payload = bytes(pkt[Raw].load)
                    if len(payload) > 12 and payload[0] & 0xC0 == 0x80:  # RTP version 2
                        return 'RTP (Voice/Video Stream)'

        elif pkt.haslayer(UDP):
            sport = pkt[UDP].sport
            dport = pkt[UDP].dport

            # Check well-known ports
            if dport in self.WELL_KNOWN_PORTS:
                app_type = self.WELL_KNOWN_PORTS[dport]
            elif sport in self.WELL_KNOWN_PORTS:
                app_type = self.WELL_KNOWN_PORTS[sport]

            # QUIC (used by YouTube, Google services)
            elif dport == 443 or sport == 443:
                return 'QUIC (Video/Web)'

            # RTP range
            elif (16384 <= dport <= 32767) or (16384 <= sport <= 32767):
                return 'RTP (Voice/Video Stream)'

        # Check DNS for domain patterns
        if pkt.haslayer(DNS) and pkt[DNS].qd:
            qname = pkt[DNS].qd.qname.decode('utf-8', errors='ignore').lower()
            if any(v in qname for v in self.VIDEO_DOMAINS):
                return 'Video Streaming (DNS Query)'
            elif any(v in qname for v in self.SOCIAL_DOMAINS):
                return 'Social Media (DNS Query)'
            elif any(v in qname for v in self.VOIP_DOMAINS):
                return 'VoIP/Conferencing (DNS Query)'

        return app_type

    def _extract_sni(self, payload):
        """Extract Server Name Indication from TLS handshake"""
        try:
            # Look for TLS handshake (type 0x16) and client hello (0x01)
            if len(payload) < 43:
                return None

            # Simple pattern matching for SNI
            # This is a simplified version - production code would need full TLS parsing
            if b'\x16\x03' in payload[:3]:  # TLS handshake
                # Look for SNI extension (0x0000)
                sni_start = payload.find(b'\x00\x00')
                if sni_start > 0 and sni_start + 9 < len(payload):
                    # Try to extract domain name
                    sni_len_pos = sni_start + 7
                    if sni_len_pos + 2 < len(payload):
                        sni_len = int.from_bytes(payload[sni_len_pos:sni_len_pos+2], 'big')
                        if sni_len > 0 and sni_len_pos + 2 + sni_len < len(payload):
                            domain = payload[sni_len_pos+2:sni_len_pos+2+sni_len].decode('utf-8', errors='ignore')
                            return domain
        except:
            pass

        return None

    def _analyze_multicast_traffic(self):
        """Analyze multicast/broadcast traffic patterns"""
        multicast_stats = {
            'multicast_groups': {},  # Track multicast group addresses
            'per_client_multicast': {},  # Per-client multicast statistics
            'igmp_activity': {  # IGMP group management
                'membership_reports': 0,
                'membership_queries': 0,
                'leave_group': 0,
                'joins': [],  # List of client join events
                'leaves': []  # List of client leave events
            },
            'total_multicast_bytes': 0,
            'total_multicast_packets': 0,
            'total_broadcast_bytes': 0,
            'total_broadcast_packets': 0,
            'multicast_data_rates': [],
            'streaming_groups': {}  # Likely video streaming multicast groups
        }

        # Common multicast address ranges
        # IPv4 multicast: 224.0.0.0 to 239.255.255.255
        # IPv6 multicast: ff00::/8
        # Ethernet multicast: 01:00:5e:xx:xx:xx (IPv4), 33:33:xx:xx:xx:xx (IPv6)

        for pkt_num, pkt in enumerate(self.packets, 1):
            if not pkt.haslayer(Dot11):
                continue

            dot11 = pkt[Dot11]
            pkt_size = len(pkt)
            timestamp = float(pkt.time) if hasattr(pkt, 'time') else 0

            # Get addresses
            addr1 = dot11.addr1 if hasattr(dot11, 'addr1') else None
            addr2 = dot11.addr2 if hasattr(dot11, 'addr2') else None

            # Check for multicast/broadcast at MAC level
            is_multicast_mac = False
            is_broadcast_mac = False

            if addr1:
                # Broadcast address
                if addr1.lower() == 'ff:ff:ff:ff:ff:ff':
                    is_broadcast_mac = True
                    multicast_stats['total_broadcast_bytes'] += pkt_size
                    multicast_stats['total_broadcast_packets'] += 1
                # Multicast MAC (least significant bit of first octet is 1)
                elif addr1 and int(addr1.split(':')[0], 16) & 0x01:
                    is_multicast_mac = True
                    multicast_stats['total_multicast_bytes'] += pkt_size
                    multicast_stats['total_multicast_packets'] += 1

            # Extract source client MAC for tracking
            source_mac = None
            if dot11.type == 2:  # Data frame
                to_ds = (dot11.FCfield & 0x01) != 0
                from_ds = (dot11.FCfield & 0x02) != 0

                if to_ds and not from_ds:
                    source_mac = addr2  # STA to AP
                elif from_ds and not to_ds:
                    source_mac = None  # AP to STA (we'll track receiver instead)

            # Analyze IP layer multicast
            if pkt.haslayer(IP):
                ip = pkt[IP]
                dst_ip = ip.dst
                src_ip = ip.src

                # Check if destination is multicast (224.0.0.0/4)
                if dst_ip.startswith('224.') or dst_ip.startswith('239.'):
                    multicast_group = dst_ip

                    # Initialize group if not exists
                    if multicast_group not in multicast_stats['multicast_groups']:
                        multicast_stats['multicast_groups'][multicast_group] = {
                            'total_bytes': 0,
                            'total_packets': 0,
                            'clients': set(),
                            'first_seen': timestamp,
                            'last_seen': timestamp,
                            'data_rates': [],
                            'packet_sizes': [],
                            'source_ips': set(),
                            'is_video_streaming': False
                        }

                    group_stats = multicast_stats['multicast_groups'][multicast_group]
                    group_stats['total_bytes'] += pkt_size
                    group_stats['total_packets'] += 1
                    group_stats['last_seen'] = timestamp
                    group_stats['source_ips'].add(src_ip)
                    group_stats['packet_sizes'].append(pkt_size)

                    # Track data rate
                    if pkt.haslayer(RadioTap):
                        rt = pkt[RadioTap]
                        if hasattr(rt, 'Rate') and rt.Rate is not None:
                            rate_mbps = rt.Rate * 0.5
                            group_stats['data_rates'].append(rate_mbps)
                            multicast_stats['multicast_data_rates'].append(rate_mbps)

                    # Identify likely video streaming based on:
                    # 1. High data rate packets
                    # 2. RTP protocol (UDP high ports)
                    # 3. Consistent packet sizes (typical for video)
                    if pkt.haslayer(UDP):
                        udp = pkt[UDP]
                        # RTP typically uses even ports in range 16384-32767
                        if 16384 <= udp.dport <= 32767 and udp.dport % 2 == 0:
                            group_stats['is_video_streaming'] = True
                            if multicast_group not in multicast_stats['streaming_groups']:
                                multicast_stats['streaming_groups'][multicast_group] = {
                                    'total_bytes': 0,
                                    'total_packets': 0,
                                    'clients': set(),
                                    'avg_packet_size': 0
                                }

                    # Track which client received this multicast packet
                    # In WiFi, multicast is often sent to specific STAs that joined the group
                    receiver_mac = None
                    if dot11.type == 2:
                        to_ds = (dot11.FCfield & 0x01) != 0
                        from_ds = (dot11.FCfield & 0x02) != 0
                        if from_ds and not to_ds:
                            receiver_mac = addr1  # AP to STA

                    if receiver_mac and receiver_mac != 'ff:ff:ff:ff:ff:ff':
                        group_stats['clients'].add(receiver_mac)

                        # Track per-client multicast statistics
                        if receiver_mac not in multicast_stats['per_client_multicast']:
                            multicast_stats['per_client_multicast'][receiver_mac] = {
                                'total_multicast_bytes': 0,
                                'total_multicast_packets': 0,
                                'groups_joined': set(),
                                'streaming_bytes': 0,
                                'streaming_packets': 0,
                                'first_seen': timestamp,
                                'last_seen': timestamp
                            }

                        client_mcast = multicast_stats['per_client_multicast'][receiver_mac]
                        client_mcast['total_multicast_bytes'] += pkt_size
                        client_mcast['total_multicast_packets'] += 1
                        client_mcast['groups_joined'].add(multicast_group)
                        client_mcast['last_seen'] = timestamp

                        if group_stats['is_video_streaming']:
                            client_mcast['streaming_bytes'] += pkt_size
                            client_mcast['streaming_packets'] += 1

                # IGMP protocol analysis (IP protocol 2)
                if ip.proto == 2 or pkt.haslayer(Raw):  # IGMP
                    # Try to parse IGMP message
                    if pkt.haslayer(Raw):
                        raw_data = bytes(pkt[Raw].load)
                        if len(raw_data) >= 8:
                            igmp_type = raw_data[0]

                            # IGMP v2 Membership Report (0x16)
                            if igmp_type == 0x16:
                                multicast_stats['igmp_activity']['membership_reports'] += 1
                                # Extract group address (bytes 4-7)
                                if len(raw_data) >= 8:
                                    group_addr = '.'.join(str(b) for b in raw_data[4:8])
                                    multicast_stats['igmp_activity']['joins'].append({
                                        'client': source_mac or src_ip,
                                        'group': group_addr,
                                        'timestamp': timestamp,
                                        'packet_number': pkt_num
                                    })

                            # IGMP Membership Query (0x11)
                            elif igmp_type == 0x11:
                                multicast_stats['igmp_activity']['membership_queries'] += 1

                            # IGMP Leave Group (0x17)
                            elif igmp_type == 0x17:
                                multicast_stats['igmp_activity']['leave_group'] += 1
                                if len(raw_data) >= 8:
                                    group_addr = '.'.join(str(b) for b in raw_data[4:8])
                                    multicast_stats['igmp_activity']['leaves'].append({
                                        'client': source_mac or src_ip,
                                        'group': group_addr,
                                        'timestamp': timestamp,
                                        'packet_number': pkt_num
                                    })

        # Convert sets to serializable format and calculate metrics
        for group, stats in multicast_stats['multicast_groups'].items():
            duration = stats['last_seen'] - stats['first_seen'] if stats['last_seen'] > stats['first_seen'] else 0

            multicast_stats['multicast_groups'][group] = {
                'total_bytes': stats['total_bytes'],
                'total_kb': round(stats['total_bytes'] / 1024, 2),
                'total_mb': round(stats['total_bytes'] / (1024 * 1024), 2),
                'total_packets': stats['total_packets'],
                'client_count': len(stats['clients']),
                'clients': list(stats['clients']),
                'source_count': len(stats['source_ips']),
                'source_ips': list(stats['source_ips']),
                'duration_seconds': round(duration, 2),
                'avg_throughput_kbps': round((stats['total_bytes'] * 8) / (duration * 1024), 2) if duration > 0 else 0,
                'avg_throughput_mbps': round((stats['total_bytes'] * 8) / (duration * 1024 * 1024), 2) if duration > 0 else 0,
                'avg_packet_size': round(sum(stats['packet_sizes']) / len(stats['packet_sizes']), 2) if stats['packet_sizes'] else 0,
                'data_rate_avg': round(sum(stats['data_rates']) / len(stats['data_rates']), 2) if stats['data_rates'] else 0,
                'is_video_streaming': stats['is_video_streaming'],
                'first_seen': stats['first_seen'],
                'last_seen': stats['last_seen']
            }

        # Convert per-client multicast stats
        for client, stats in multicast_stats['per_client_multicast'].items():
            duration = stats['last_seen'] - stats['first_seen'] if stats['last_seen'] > stats['first_seen'] else 0

            multicast_stats['per_client_multicast'][client] = {
                'total_multicast_bytes': stats['total_multicast_bytes'],
                'total_multicast_kb': round(stats['total_multicast_bytes'] / 1024, 2),
                'total_multicast_mb': round(stats['total_multicast_bytes'] / (1024 * 1024), 2),
                'total_multicast_packets': stats['total_multicast_packets'],
                'groups_joined_count': len(stats['groups_joined']),
                'groups_joined': list(stats['groups_joined']),
                'streaming_bytes': stats['streaming_bytes'],
                'streaming_kb': round(stats['streaming_bytes'] / 1024, 2),
                'streaming_mb': round(stats['streaming_bytes'] / (1024 * 1024), 2),
                'streaming_packets': stats['streaming_packets'],
                'duration_seconds': round(duration, 2),
                'avg_multicast_throughput_kbps': round((stats['total_multicast_bytes'] * 8) / (duration * 1024), 2) if duration > 0 else 0,
                'avg_multicast_throughput_mbps': round((stats['total_multicast_bytes'] * 8) / (duration * 1024 * 1024), 2) if duration > 0 else 0,
                'avg_streaming_throughput_mbps': round((stats['streaming_bytes'] * 8) / (duration * 1024 * 1024), 2) if duration > 0 else 0
            }

        # Calculate streaming group statistics
        for group in multicast_stats['streaming_groups']:
            if group in multicast_stats['multicast_groups']:
                group_data = multicast_stats['multicast_groups'][group]
                multicast_stats['streaming_groups'][group] = {
                    'total_bytes': group_data['total_bytes'],
                    'total_mb': group_data['total_mb'],
                    'total_packets': group_data['total_packets'],
                    'clients': group_data['clients'],
                    'client_count': group_data['client_count'],
                    'avg_throughput_mbps': group_data['avg_throughput_mbps'],
                    'avg_packet_size': group_data['avg_packet_size']
                }

        self.results['multicast_analysis'] = {
            'summary': {
                'total_multicast_groups': len(multicast_stats['multicast_groups']),
                'total_streaming_groups': len(multicast_stats['streaming_groups']),
                'total_multicast_bytes': multicast_stats['total_multicast_bytes'],
                'total_multicast_kb': round(multicast_stats['total_multicast_bytes'] / 1024, 2),
                'total_multicast_mb': round(multicast_stats['total_multicast_bytes'] / (1024 * 1024), 2),
                'total_multicast_packets': multicast_stats['total_multicast_packets'],
                'total_broadcast_bytes': multicast_stats['total_broadcast_bytes'],
                'total_broadcast_kb': round(multicast_stats['total_broadcast_bytes'] / 1024, 2),
                'total_broadcast_packets': multicast_stats['total_broadcast_packets'],
                'clients_receiving_multicast': len(multicast_stats['per_client_multicast']),
                'avg_multicast_data_rate': round(sum(multicast_stats['multicast_data_rates']) / len(multicast_stats['multicast_data_rates']), 2) if multicast_stats['multicast_data_rates'] else 0,
                'igmp_joins': len(multicast_stats['igmp_activity']['joins']),
                'igmp_leaves': len(multicast_stats['igmp_activity']['leaves']),
                'igmp_queries': multicast_stats['igmp_activity']['membership_queries']
            },
            'multicast_groups': multicast_stats['multicast_groups'],
            'streaming_groups': multicast_stats['streaming_groups'],
            'per_client_multicast': multicast_stats['per_client_multicast'],
            'igmp_activity': multicast_stats['igmp_activity']
        }

    def _generate_summary(self):
        """Generate overall summary statistics"""
        total_clients = len(self.results['per_client_analysis'])
        total_aps = len(self.results['per_ap_analysis'])

        total_data_bytes = sum(c['total_bytes'] for c in self.results['per_client_analysis'].values())
        total_packets = sum(c['total_packets'] for c in self.results['per_client_analysis'].values())

        # Top clients by bandwidth
        top_clients = sorted(
            self.results['per_client_analysis'].items(),
            key=lambda x: x[1]['total_bytes'],
            reverse=True
        )[:5]

        # Top applications by bandwidth
        top_apps = sorted(
            self.results['per_application_analysis'].items(),
            key=lambda x: x[1]['total_bytes'],
            reverse=True
        )[:5]

        self.results['summary'] = {
            'total_clients': total_clients,
            'total_aps': total_aps,
            'total_data_bytes': total_data_bytes,
            'total_data_kb': round(total_data_bytes / 1024, 2),
            'total_data_mb': round(total_data_bytes / (1024 * 1024), 2),
            'total_packets': total_packets,
            'avg_bytes_per_client': round(total_data_bytes / total_clients, 2) if total_clients > 0 else 0,
            'top_clients_by_bandwidth': [
                {
                    'mac': mac,
                    'total_mb': data['total_mb'],
                    'avg_throughput_mbps': data['avg_throughput_mbps']
                }
                for mac, data in top_clients
            ],
            'top_applications_by_bandwidth': [
                {
                    'application': app,
                    'total_mb': data['total_mb'],
                    'unique_clients': data['unique_clients']
                }
                for app, data in top_apps
            ]
        }


def detect_bandwidth_analysis(packets):
    """Main entry point for bandwidth analysis"""
    analyzer = BandwidthAnalyzer(packets)
    return analyzer.analyze()
