"""Tabla ARP de Windows: lectura, vigilancia de cambios y conflictos IP."""
from __future__ import annotations

import re
import subprocess
import threading
from dataclasses import dataclass, field
from typing import Callable, Dict, List, Optional, Tuple

from .models import normalize_mac, now_str


@dataclass
class ArpEntry:
    ip: str
    mac: str
    type_: str = ""  # dinámico/estático


@dataclass
class ArpChange:
    ts: str
    ip: str
    old_mac: str
    new_mac: str
    kind: str  # changed | appeared | disappeared | conflict


class ArpWatcher:
    """Vigila ARP: IP→MAC y detecta conflictos (misma IP, distinta MAC)."""

    def __init__(self, on_change: Optional[Callable[[ArpChange], None]] = None):
        self.on_change = on_change
        self._ip_to_mac: Dict[str, str] = {}
        self._mac_history: Dict[str, List[Tuple[str, str]]] = {}  # ip -> [(ts, mac)]
        self._lock = threading.Lock()
        self.changes: List[ArpChange] = []

    @property
    def table(self) -> Dict[str, str]:
        with self._lock:
            return dict(self._ip_to_mac)

    def history_for(self, ip: str) -> List[Tuple[str, str]]:
        with self._lock:
            return list(self._mac_history.get(ip, []))

    def known_mac(self, ip: str) -> str:
        with self._lock:
            return self._ip_to_mac.get(ip, "")

    @staticmethod
    def read_arp_table() -> Dict[str, ArpEntry]:
        try:
            p = subprocess.run(
                ["arp", "-a"],
                capture_output=True,
                text=True,
                timeout=10,
                creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
                encoding="utf-8",
                errors="replace",
            )
            out = p.stdout or ""
        except Exception:
            return {}
        result: Dict[str, ArpEntry] = {}
        #  192.168.1.1            00-11-22-33-44-55     dinámica
        for line in out.splitlines():
            m = re.search(
                r"(\d+\.\d+\.\d+\.\d+)\s+([0-9a-fA-F\-]{11,17})\s+(\S+)",
                line,
            )
            if not m:
                continue
            ip, mac_raw, typ = m.group(1), m.group(2), m.group(3)
            mac = normalize_mac(mac_raw)
            if mac.replace(":", "") in ("000000000000", "FFFFFFFFFFFF"):
                continue
            result[ip] = ArpEntry(ip=ip, mac=mac, type_=typ)
        return result

    def refresh(self) -> List[ArpChange]:
        current = self.read_arp_table()
        found: List[ArpChange] = []
        with self._lock:
            seen_ips = set(current.keys())
            prev_ips = set(self._ip_to_mac.keys())

            for ip, entry in current.items():
                mac = entry.mac
                old = self._ip_to_mac.get(ip)
                if old is None:
                    ch = ArpChange(ts=now_str(), ip=ip, old_mac="", new_mac=mac, kind="appeared")
                    found.append(ch)
                    self._ip_to_mac[ip] = mac
                    self._mac_history.setdefault(ip, []).append((ch.ts, mac))
                elif normalize_mac(old) != normalize_mac(mac):
                    ch = ArpChange(
                        ts=now_str(),
                        ip=ip,
                        old_mac=old,
                        new_mac=mac,
                        kind="conflict",
                    )
                    found.append(ch)
                    self._ip_to_mac[ip] = mac
                    self._mac_history.setdefault(ip, []).append((ch.ts, mac))

            # no marcar disappeared agresivo: ARP puede caducar; solo si se pide
            for ip in list(prev_ips - seen_ips):
                # mantener última MAC conocida (no borrar) para conflicto futuro
                pass

        self.changes.extend(found)
        for ch in found:
            if self.on_change:
                try:
                    self.on_change(ch)
                except Exception:
                    pass
        return found

    def record_observation(self, ip: str, mac: str) -> Optional[ArpChange]:
        """Registra MAC observada (p.ej. tras ping) y detecta conflicto."""
        if not ip or not mac:
            return None
        mac = normalize_mac(mac)
        with self._lock:
            old = self._ip_to_mac.get(ip)
            if old is None:
                self._ip_to_mac[ip] = mac
                self._mac_history.setdefault(ip, []).append((now_str(), mac))
                return None
            if normalize_mac(old) != mac:
                ch = ArpChange(
                    ts=now_str(),
                    ip=ip,
                    old_mac=old,
                    new_mac=mac,
                    kind="conflict",
                )
                self._ip_to_mac[ip] = mac
                self._mac_history.setdefault(ip, []).append((ch.ts, mac))
                self.changes.append(ch)
                if self.on_change:
                    try:
                        self.on_change(ch)
                    except Exception:
                        pass
                return ch
        return None

    def conflicts(self) -> List[ArpChange]:
        return [c for c in self.changes if c.kind == "conflict"]
