"""Vulnerability mixin for IPv6 RA/ND-related issues.

Evaluates Router Advertisement patterns and flags insecure configurations.
"""
from __future__ import annotations

import csv
from collections import defaultdict

from ptnetinspector.utils.ip_utils import (
    check_ipv6_addresses_generated_from_prefix,
    has_additional_data,
    is_global_unicast_ipv6,
    is_valid_ipv6,
)


class RAMixin:
    def store_ra_vulnerability(self) -> None:
        role_dict = self.read_role_dict(self.role_file)
        if not role_dict or not getattr(self.ipver, "ipv6", False):
            return

        if not self.time_all_file or not has_additional_data(self.time_all_file):
            return

        # Align RA mixin with catalog and ICMP logic: support a and a+
        mode = self._filter_modes("a,a+")
        if not mode:
            return

        existing_rows = self._load_existing_rows()
        new_rows = []

        ra_sent = False
        ra_first_idx = -1

        device_fakeradns = defaultdict(bool)
        device_fakera = defaultdict(bool)

        # New logic for ICMPv6 out-of-range handling in RA path
        outrange_ips: set[str] = set()
        response_macs_outrange: set[str] = set()
        outrange_seen = False

        # Prefix set used to evaluate whether src is out of local ranges
        ra_prefixes = self._load_ra_prefixes()
        all_prefixes = set()
        if self.network:
            all_prefixes.add(f"{self.network}/{self.prefix_len}")
        all_prefixes.update(ra_prefixes)

        try:
            with open(self.time_all_file, "r", newline="") as csvfile:
                reader = csv.DictReader(csvfile)
                current_index = 0

                for row in reader:
                    packet = row.get("packet", "")
                    mac = row.get("MAC", "").strip()

                    if not ra_sent and mac == self.smac and "ICMPv6 Neighbor Discovery - Router Advertisement" in packet:
                        ra_sent = True
                        ra_first_idx = current_index

                    if ra_sent and current_index > ra_first_idx:
                        if self.dns:
                            for dns_ip in self.dns:
                                if is_valid_ipv6(dns_ip) and f"dst = {dns_ip}" in packet:
                                    device_fakeradns[mac] = True
                                    device_fakera[mac] = True

                        # Collect out-of-range source IPs and track reactions to them
                        src_ip = self._extract_address(packet, "src")
                        if "ICMPv6" in packet and src_ip and is_valid_ipv6(src_ip):
                            if self._check_icmp6_outrange(packet, all_prefixes):
                                outrange_ips.add(src_ip)
                                outrange_seen = True
                                device_fakera[mac] = True

                        if outrange_ips:
                            dst_ip = self._extract_address(packet, "dst")
                            if dst_ip and dst_ip in outrange_ips:
                                response_macs_outrange.add(mac)

                        if self.network and src_ip:
                            if check_ipv6_addresses_generated_from_prefix(src_ip, f"{self.network}/{self.prefix_len}"):
                                device_fakera[mac] = True

                    current_index += 1

        except (FileNotFoundError, KeyError):
            return

        vuln_specs = [
            (
                "FAKERADNSDEV",
                "PTV-NET-MITM-6-FAKERADNSDEV",
                "Device connects to a fraudulent DNS server through a spoofed RA message",
                device_fakeradns,
                True,
            ),
            (
                "FAKERADEV",
                "PTV-NET-NET-MISCONF-6-FAKERADEV",
                "Device does not ignore the illegitimate RA packets",
                device_fakera,
                False,
            ),
            (
                "ICMP6-OUTRANGEDEV",
                "PTV-NET-IDENT-ICMP-6-OUTRANGEDEV",
                "Device responds to valid or invalid ICMPv6 packets from sources outside the scanned local network range",
                None,
                False,
            ),
        ]

        for name, code, desc, device_dict, dns_required in vuln_specs:
            for device_number, mac in role_dict.items():
                if not ra_sent:
                    label = "2"
                elif dns_required and self.dns is None:
                    label = "2"
                elif name == "ICMP6-OUTRANGEDEV":
                    if not outrange_seen:
                        label = "2"
                    else:
                        # Mark device as reacting if it sent any packet to an out-of-range IP
                        label = "1" if mac in response_macs_outrange else "0"
                else:
                    label = "1" if device_dict[mac] else "0"

                row = (str(device_number), mac, mode, "6", code, desc, label)
                self._add_vuln_if_new(row, existing_rows, new_rows)

        # RAGUARD should be vulnerable if any device has a device vuln with Test=6-FAKERA
        raguard_vuln = any(device_fakera.values()) or any(device_fakeradns.values())

        net_vulns = [
            (
                "PTV-NET-MITM-6-FAKERADNS",
                "Network allows a client to be forced to use a fraudulent DNS server through a spoofed RA message",
                any(device_fakeradns.values()),
                True,
            ),
            (
                "PTV-NET-MISCONF-6-RAGUARD",
                "Network does not have RA guard configured",
                raguard_vuln,
                False,
            ),
            (
                "PTV-NET-NET-MISCONF-6-FAKERA",
                "Network allows the delivery of illegitimate RA packets",
                any(device_fakera.values()),
                False,
            ),
            (
                "PTV-NET-IDENT-ICMP-6-OUTRANGE",
                "Network allows valid or invalid ICMPv6 packets to be delivered from sources outside the scanned local network range",
                None,
                False,
            ),
        ]

        for code, desc, detected, dns_required in net_vulns:
            if not ra_sent:
                label = "2"
            elif dns_required and self.dns is None:
                label = "2"
            elif code == "PTV-NET-IDENT-ICMP-6-OUTRANGE":
                if not outrange_seen:
                    label = "2"
                else:
                    label = "1" if response_macs_outrange else "0"
            else:
                label = "1" if detected else "0"

            row = ("Network", "", mode, "6", code, desc, label)
            self._add_vuln_if_new(row, existing_rows, new_rows)

        self._write_vulnerabilities(new_rows)
