"""
Modules réseau pour scan d'IP, ping, ARP et port scan
Implémente le workflow de scan selon workflow-scan.md
"""
import asyncio
import ipaddress
import platform
import subprocess
import socket
from typing import List, Dict, Optional, Tuple
from datetime import datetime
import re
from pathlib import Path

# Scapy pour ARP
try:
    from scapy.all import ARP, Ether, srp
    SCAPY_AVAILABLE = True
except ImportError:
    SCAPY_AVAILABLE = False


class NetworkScanner:
    """Scanner réseau principal"""

    def __init__(self, cidr: str, timeout: float = 1.0, ping_count: int = 1):
        """
        Initialise le scanner réseau

        Args:
            cidr: Réseau CIDR (ex: "192.168.1.0/24")
            timeout: Timeout pour ping et connexions (secondes)
            ping_count: Nombre de ping par IP
        """
        self.cidr = cidr
        self.timeout = timeout
        self.ping_count = max(1, int(ping_count))
        self.network = ipaddress.ip_network(cidr, strict=False)

    def generate_ip_list(self) -> List[str]:
        """
        Génère la liste complète d'IP depuis le CIDR

        Returns:
            Liste des adresses IP en string
        """
        return [str(ip) for ip in self.network.hosts()]

    async def ping(self, ip: str) -> bool:
        """
        Ping une adresse IP (async)

        Args:
            ip: Adresse IP à pinger

        Returns:
            True si l'IP répond, False sinon
        """
        # Détection de l'OS pour la commande ping
        param = '-n' if platform.system().lower() == 'windows' else '-c'
        timeout_param = '-w' if platform.system().lower() == 'windows' else '-W'

        command = [
            'ping',
            param, str(self.ping_count),
            timeout_param,
            str(int(self.timeout * 1000) if platform.system().lower() == 'windows' else str(int(self.timeout))),
            ip
        ]

        try:
            # Exécuter le ping de manière asynchrone
            process = await asyncio.create_subprocess_exec(
                *command,
                stdout=asyncio.subprocess.DEVNULL,
                stderr=asyncio.subprocess.DEVNULL
            )
            await asyncio.wait_for(process.wait(), timeout=self.timeout + 1)
            return process.returncode == 0
        except (asyncio.TimeoutError, Exception):
            return False

    async def ping_parallel(self, ip_list: List[str], max_concurrent: int = 50) -> Dict[str, bool]:
        """
        Ping multiple IPs en parallèle

        Args:
            ip_list: Liste des IPs à pinger
            max_concurrent: Nombre maximum de pings simultanés

        Returns:
            Dictionnaire {ip: online_status}
        """
        results = {}
        semaphore = asyncio.Semaphore(max_concurrent)

        async def ping_with_semaphore(ip: str):
            async with semaphore:
                results[ip] = await self.ping(ip)

        # Lancer tous les pings en parallèle avec limite
        await asyncio.gather(*[ping_with_semaphore(ip) for ip in ip_list])

        return results

    def get_arp_table(self) -> Dict[str, Tuple[str, str]]:
        """
        Récupère la table ARP du système

        Returns:
            Dictionnaire {ip: (mac, vendor)}
        """
        arp_data = {}

        if SCAPY_AVAILABLE:
            try:
                # Utiliser Scapy pour ARP scan
                answered, _ = srp(
                    Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=self.cidr),
                    timeout=2,
                    verbose=False
                )

                for sent, received in answered:
                    ip = received.psrc
                    mac = received.hwsrc
                    vendor = self._get_mac_vendor(mac)
                    arp_data[ip] = (mac, vendor)
            except Exception as e:
                print(f"Erreur ARP scan avec Scapy: {e}")
        else:
            # Fallback: parser la table ARP système
            try:
                if platform.system().lower() == 'windows':
                    output = subprocess.check_output(['arp', '-a'], text=True)
                    pattern = r'(\d+\.\d+\.\d+\.\d+)\s+([0-9a-fA-F-:]+)'
                else:
                    output = subprocess.check_output(['arp', '-n'], text=True)
                    pattern = r'(\d+\.\d+\.\d+\.\d+)\s+\w+\s+([0-9a-fA-F:]+)'

                matches = re.findall(pattern, output)
                for ip, mac in matches:
                    if ip in [str(h) for h in self.network.hosts()]:
                        vendor = self._get_mac_vendor(mac)
                        arp_data[ip] = (mac, vendor)
            except Exception as e:
                print(f"Erreur lecture table ARP: {e}")

        return arp_data

    def _get_mac_vendor(self, mac: str) -> str:
        """
        Lookup du fabricant depuis l'adresse MAC
        Simplifié pour l'instant - peut être étendu avec une vraie DB OUI

        Args:
            mac: Adresse MAC

        Returns:
            Nom du fabricant ou "Unknown"
        """
        mac_norm = re.sub(r"[^0-9A-Fa-f]", "", mac).upper()
        if not mac_norm:
            return "Unknown"

        # Lookup OUI si fichier disponible
        vendor = OuiLookup.lookup(mac_norm)
        if vendor:
            return vendor

        # Mini DB des fabricants courants (fallback)
        vendors = {
            "00:0C:29": "VMware",
            "00:50:56": "VMware",
            "08:00:27": "VirtualBox",
            "DC:A6:32": "Raspberry Pi",
            "B8:27:EB": "Raspberry Pi",
        }

        for prefix, vendor in vendors.items():
            prefix_norm = prefix.replace(":", "").upper()
            if mac_norm.startswith(prefix_norm):
                return vendor

        return "Unknown"


    async def scan_ports(self, ip: str, ports: List[int]) -> List[int]:
        """
        Scan des ports TCP sur une IP

        Args:
            ip: Adresse IP cible
            ports: Liste des ports à scanner

        Returns:
            Liste des ports ouverts
        """
        open_ports = []

        async def check_port(port: int) -> Optional[int]:
            try:
                # Tentative de connexion TCP
                reader, writer = await asyncio.wait_for(
                    asyncio.open_connection(ip, port),
                    timeout=self.timeout
                )
                writer.close()
                await writer.wait_closed()
                return port
            except:
                return None

        # Scanner tous les ports en parallèle
        results = await asyncio.gather(*[check_port(p) for p in ports])
        open_ports = [p for p in results if p is not None]

        return open_ports

    def get_hostname(self, ip: str) -> Optional[str]:
        """
        Résolution DNS inversée pour obtenir le hostname

        Args:
            ip: Adresse IP

        Returns:
            Hostname ou None
        """
        try:
            hostname, _, _ = socket.gethostbyaddr(ip)
            return hostname
        except:
            return None

    def classify_ip_status(self, is_online: bool, is_known: bool) -> str:
        """
        Classification de l'état d'une IP

        Args:
            is_online: IP en ligne
            is_known: IP connue dans la config

        Returns:
            État: "online", "offline"
        """
        return "online" if is_online else "offline"

    async def full_scan(self, known_ips: Dict[str, Dict], port_list: List[int], max_concurrent: int = 50, progress_callback=None) -> Dict[str, Dict]:
        """
        Scan complet du réseau selon workflow-scan.md

        Args:
            known_ips: Dictionnaire des IPs connues depuis config
            port_list: Liste des ports à scanner
            max_concurrent: Pings simultanés max
            progress_callback: Fonction optionnelle pour rapporter la progression

        Returns:
            Dictionnaire des résultats de scan pour chaque IP
        """
        results = {}

        # 1. Générer liste IP du CIDR
        ip_list = self.generate_ip_list()
        total_ips = len(ip_list)

        # 2. Ping parallélisé
        ping_results = await self.ping_parallel(ip_list, max_concurrent)

        # 3. ARP + MAC vendor
        arp_table = self.get_arp_table()

        # 4. Pour chaque IP
        for index, ip in enumerate(ip_list, start=1):
            is_online = ping_results.get(ip, False)
            is_known = ip in known_ips

            ip_data = {
                "ip": ip,
                "known": is_known,
                "last_status": self.classify_ip_status(is_online, is_known),
                "last_seen": datetime.now() if is_online else None,
                "mac": None,
                "vendor": None,
                "hostname": None,
                "open_ports": [],
            }

            # Ajouter infos connues
            if is_known:
                ip_data.update(known_ips[ip])

            # Infos ARP
            if ip in arp_table:
                mac, vendor = arp_table[ip]
                ip_data["mac"] = mac
                ip_data["vendor"] = vendor

            # Hostname
            if is_online:
                hostname = self.get_hostname(ip)
                if hostname:
                    ip_data["hostname"] = hostname

            # 5. Port scan (uniquement si online)
            if is_online and port_list:
                open_ports = await self.scan_ports(ip, port_list)
                ip_data["open_ports"] = open_ports

            results[ip] = ip_data

            # Rapporter la progression
            if progress_callback:
                await progress_callback(index, total_ips, ip, ip_data["last_status"], is_online)

        return results


class OuiLookup:
    """Lookup OUI basé sur un fichier local (oui.txt)"""
    _cache = {}
    _mtime = None
    _path = Path("./data/oui/oui.txt")

    @classmethod
    def _load(cls):
        if not cls._path.exists():
            cls._cache = {}
            cls._mtime = None
            return

        mtime = cls._path.stat().st_mtime
        if cls._mtime == mtime and cls._cache:
            return

        cache = {}
        try:
            with cls._path.open("r", encoding="utf-8", errors="ignore") as handle:
                for line in handle:
                    raw = line.strip()
                    if "(hex)" in raw:
                        left, right = raw.split("(hex)", 1)
                        prefix = re.sub(r"[^0-9A-Fa-f]", "", left).upper()[:6]
                        vendor = right.strip()
                        if len(prefix) == 6 and vendor:
                            cache[prefix] = vendor
        except Exception:
            cache = {}

        cls._cache = cache
        cls._mtime = mtime
        print(f"[OUI] Base chargée: {len(cls._cache)} entrées depuis {cls._path}")

    @classmethod
    def lookup(cls, mac: str) -> Optional[str]:
        if not mac:
            return None
        cls._load()
        if not cls._cache:
            return None
        prefix = re.sub(r"[^0-9A-Fa-f]", "", mac).upper()[:6]
        if len(prefix) != 6:
            return None
        return cls._cache.get(prefix)
