"""
Identifica conflictos IP y quien reparte DHCP, con nombres de dispositivo.
"""
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple

from .arp_watch import ArpChange, ArpWatcher
from .dhcp_probe import dhcp_discover
from .models import Device, NetInfo, now_str
from .netinfo import suspicious_dhcp_notes
from .oui import lookup_vendor
from .topology import _is_camera, _is_nvr, refine_role


@dataclass
class Party:
    mac: str
    vendor: str
    ip_seen: str
    role: str
    hostname: str = ""
    device_type: str = ""
    confidence: str = ""


@dataclass
class ConflictCase:
    ip: str
    ts: str
    party_a: Party
    party_b: Party
    verdict: str
    certainty: str
    manual_steps: List[str] = field(default_factory=list)


@dataclass
class DhcpPicture:
    gateway: str
    ipconfig_dhcp: str
    offer_servers: List[str]
    who_commands: str
    tenda_suspect: bool
    cudy_suspect: bool
    dual: bool
    notes: List[str]
    manual_steps: List[str]
    certainty: str


@dataclass
class ConflictIntel:
    generated: str
    dhcp: DhcpPicture
    cases: List[ConflictCase]
    family_devices: List[Device]
    cctv_devices: List[Device]
    infra_devices: List[Device]
    same_mac_two_ips: List[str]
    summary: str
    honesty: str

    def as_text(self) -> str:
        lines = [
            "CONFLICTOS IP + QUIEN REPARTE DHCP",
            "=" * 56,
            f"Generado: {self.generated}",
            "",
            self.honesty,
            "",
            "QUIEN MANDA LAS IP (DHCP)",
            "-" * 40,
            f"Gateway (puerta de enlace): {self.dhcp.gateway or '-'}",
            f"Servidor DHCP segun ipconfig: {self.dhcp.ipconfig_dhcp or '-'}",
            "Servidores que respondieron DISCOVER: "
            + (", ".join(self.dhcp.offer_servers) if self.dhcp.offer_servers else "(ninguno / sondeo limitado)"),
            f"Conclusion: {self.dhcp.who_commands}",
            f"Certeza DHCP: {self.dhcp.certainty}",
        ]
        if self.dhcp.tenda_suspect:
            lines.append("ALERTA: Tenda sospechoso de DHCP (mal si no es solo AP).")
        if self.dhcp.cudy_suspect:
            lines.append("ALERTA: Cudy sospechoso de DHCP (deberia ser AP/bridge).")
        for n in self.dhcp.notes:
            lines.append(f"  * {n}")
        lines.append("QUE HACER (DHCP):")
        for s in self.dhcp.manual_steps:
            lines.append(f"  {s}")
        lines.append("")
        lines.append("CONFLICTOS IP DETECTADOS (misma IP, distinta MAC)")
        lines.append("-" * 40)
        if not self.cases:
            lines.append(
                "Ningun conflicto ARP en ESTA sesion. "
                "Si una camara desaparece a ratos, MONITORIZACION 15-30 min: "
                "el conflicto solo se ve cuando los dos equipos hablan a la vez."
            )
        for i, c in enumerate(self.cases, 1):
            lines.append(f"\n[{i}] IP EN CONFLICTO: {c.ip}  ({c.ts})  certeza={c.certainty}")
            lines.append(
                f"    Dispositivo A: MAC {c.party_a.mac}  {c.party_a.vendor or '-'}  → {c.party_a.role}"
            )
            if c.party_a.hostname:
                lines.append(f"                 hostname {c.party_a.hostname}")
            lines.append(
                f"    Dispositivo B: MAC {c.party_b.mac}  {c.party_b.vendor or '-'}  → {c.party_b.role}"
            )
            if c.party_b.hostname:
                lines.append(f"                 hostname {c.party_b.hostname}")
            lines.append(f"    Veredicto: {c.verdict}")
            lines.append("    QUE HACER:")
            for s in c.manual_steps:
                lines.append(f"      {s}")
        if self.same_mac_two_ips:
            lines.append("")
            lines.append("MISMA MAC EN DOS IP:")
            for line in self.same_mac_two_ips:
                lines.append(f"  * {line}")
        lines.append("")
        lines.append("INVENTARIO MIXTO (CCTV vs familia vs infraestructura)")
        lines.append("-" * 40)
        lines.append(f"CCTV (camaras/NVR): {len(self.cctv_devices)}")
        lines.append(f"Infra (router/AP/switch): {len(self.infra_devices)}")
        lines.append(f"Otros / familia (movil, TV, PC, IoT): {len(self.family_devices)}")
        if self.family_devices:
            lines.append("Dispositivos NO CCTV (pueden quitar IPs a camaras si DHCP libre):")
            for d in self.family_devices[:30]:
                lines.append(
                    f"  {d.ip:15}  {d.mac or '-':17}  {(d.vendor or '-'):12}  "
                    f"{d.hostname or d.device_type}"
                )
        lines.append("")
        lines.append("RESUMEN")
        lines.append("-" * 40)
        lines.append(self.summary)
        return "\n".join(lines)


def _role_for_mac(mac: str, devices: List[Device], gateway: str) -> Party:
    vendor = lookup_vendor(mac) or ""
    v = vendor.lower()
    match: Optional[Device] = None
    macn = mac.replace(":", "").upper()
    for d in devices:
        if d.mac and d.mac.replace(":", "").upper() == macn:
            match = d
            break
    hostname = match.hostname if match else ""
    dtype = match.device_type if match else ""
    ip = match.ip if match else ""
    role = "Desconocido"
    conf = "baja"
    if match:
        conf = "alta"
        r = refine_role(match, gateway)
        if _is_camera(match) or match.device_type == "Camara":
            role = "Camara CCTV"
        elif _is_nvr(match) or match.device_type == "NVR":
            role = "NVR"
        elif "Tenda" in r or "tenda" in v:
            role = "Router/AP Tenda"
        elif "Cudy" in r or "cudy" in v:
            role = "Router/AP Cudy"
        elif "Askey" in r or match.ip == gateway:
            role = "Router Movistar (Askey/gateway)"
        elif "TP-Link" in r or "tp-link" in v:
            role = "Repetidor/AP TP-Link"
        elif match.device_type in ("Router/AP", "Switch"):
            role = r or "Infraestructura red"
        else:
            role = f"Dispositivo familia / otros ({dtype or vendor or 'PC/movil/TV'})"
    else:
        if "dahua" in v or "hikvision" in v:
            role = "Camara CCTV (por fabricante MAC)"
            conf = "media"
        elif "tenda" in v:
            role = "Router/AP Tenda (por MAC)"
            conf = "media"
        elif "cudy" in v:
            role = "Router/AP Cudy (por MAC)"
            conf = "media"
        elif "askey" in v:
            role = "Router Movistar Askey (por MAC)"
            conf = "media"
        elif any(
            x in v
            for x in (
                "samsung", "apple", "xiaomi", "huawei", "oppo", "vivo",
                "oneplus", "google", "intel", "realtek", "microsoft",
                "lg", "sony", "tcl",
            )
        ):
            role = f"Dispositivo familia / otros ({vendor})"
            conf = "media"
        elif vendor:
            role = f"No CCTV probable ({vendor})"
            conf = "media"
        else:
            role = "Desconocido (MAC no esta en el escaneo actual)"
            conf = "baja"
    return Party(
        mac=mac,
        vendor=vendor,
        ip_seen=ip,
        role=role,
        hostname=hostname,
        device_type=dtype,
        confidence=conf,
    )


def _verdict_and_steps(ip: str, a: Party, b: Party, gateway: str) -> Tuple[str, List[str]]:
    roles = {a.role, b.role}
    cam = any("Camara" in r for r in roles)
    fam = any("familia" in r.lower() or "No CCTV" in r for r in roles)
    tenda = any("Tenda" in r for r in roles)
    gw = gateway or "192.168.1.1"

    if cam and fam:
        verdict = (
            f"La IP {ip} la usan una CAMARA y un dispositivo de la FAMILIA "
            "(movil/PC/TV). Eso tumba el canal en el NVR a ratos."
        )
        steps = [
            f"1. Askey http://{gw} → DHCP → Reserva: MAC de la CAMARA → IP fija (ej. {ip} si es rango CCTV).",
            "2. IPs de familia FUERA del rango camaras (ej. camaras .40-.70, familia .100-.200).",
            "3. WiFi off/on en el movil/PC familia para que coja otra IP.",
            f"4. CMD: ping {ip}  y  arp -a | findstr {ip}  → solo MAC Dahua.",
            "5. DHCP OFF en Tenda y Cudy (solo AP).",
        ]
    elif cam and tenda:
        verdict = f"Conflicto camara vs Tenda en {ip}. Tenda no debe dar DHCP ni usar IPs de camaras."
        steps = [
            "1. Web Tenda (IP del escaneo) → DHCP OFF, modo AP.",
            "2. Cable LAN-LAN al Askey, no WAN.",
            f"3. Askey: reserva MAC camara → {ip}.",
            "4. Reinicie Tenda y camara.",
        ]
    elif cam:
        verdict = (
            f"Dos equipos (al menos una camara) reclaman {ip}. "
            "Tipico: DHCP libre + IP estatica duplicada."
        )
        steps = [
            f"1. Anote ambas MAC. Abra http://{ip} si responde y vea cual es la camara.",
            f"2. Askey http://{gw} → una reserva DHCP por MAC, sin repetir IP.",
            "3. En cada Dahua: IP fija distinta O DHCP con reserva (no mezclar a ciegas).",
            f"4. arp -a | findstr {ip} varias veces: una sola MAC estable.",
        ]
    elif fam:
        verdict = f"Dos dispositivos de la casa chocan en {ip} (ensucia ARP/DHCP)."
        steps = [
            f"1. Reservas DHCP distintas en Askey http://{gw}.",
            "2. Un solo DHCP (Askey). Tenda/Cudy AP sin DHCP.",
        ]
    else:
        verdict = f"Dos MAC distintas en {ip}. Identifique A/B por fabricante y reserve IPs."
        steps = [
            f"1. arp -a | findstr {ip}",
            f"2. Askey http://{gw} → bind MAC-IP unico.",
            "3. Tenda/Cudy: DHCP desactivado.",
        ]
    return verdict, steps


def classify_inventory(
    devices: List[Device], gateway: str
) -> Tuple[List[Device], List[Device], List[Device]]:
    cctv, infra, family = [], [], []
    for d in devices:
        if _is_camera(d) or _is_nvr(d) or d.device_type in ("Camara", "NVR"):
            cctv.append(d)
        elif d.device_type in ("Router/AP", "Switch") or d.ip == gateway:
            infra.append(d)
        elif any(x in (d.vendor or "").lower() for x in ("tenda", "cudy", "askey", "tp-link")):
            infra.append(d)
        else:
            family.append(d)
    return cctv, infra, family


def _mac_on_two_ips(arp: ArpWatcher) -> List[str]:
    inv: Dict[str, List[str]] = {}
    for ip, mac in arp.table.items():
        inv.setdefault(mac.upper(), []).append(ip)
    out = []
    for mac, ips in inv.items():
        uniq = sorted(set(ips))
        if len(uniq) >= 2:
            out.append(f"{mac} → IPs {', '.join(uniq)}")
    return out


def build_dhcp_picture(
    net: Optional[NetInfo],
    devices: List[Device],
    probe: Optional[Dict] = None,
) -> DhcpPicture:
    gw = (net.gateway if net else "") or ""
    dhcp_ip = (net.dhcp_server if net else "") or ""
    notes = suspicious_dhcp_notes(net, []) if net else []
    servers: List[str] = []
    if probe and probe.get("servers"):
        servers = [s.get("server_ip") for s in probe["servers"] if s.get("server_ip")]

    tenda_ips = [d.ip for d in devices if "tenda" in (d.vendor or "").lower()]
    cudy_ips = [d.ip for d in devices if "cudy" in (d.vendor or "").lower()]

    tenda_suspect = bool(
        (dhcp_ip and dhcp_ip in tenda_ips)
        or any(s in tenda_ips for s in servers)
        or (dhcp_ip and gw and dhcp_ip != gw and tenda_ips)
    )
    cudy_suspect = bool(
        (dhcp_ip and dhcp_ip in cudy_ips) or any(s in cudy_ips for s in servers)
    )
    dual = bool(len(set(servers)) >= 2 or (dhcp_ip and gw and dhcp_ip != gw))

    if servers:
        uniq = sorted(set(servers))
        if len(uniq) == 1 and uniq[0] == gw:
            who = f"SI: manda {uniq[0]} (normalmente Askey Movistar / gateway)."
            certainty = "ALTA (OFFER DHCP de un solo servidor = gateway)"
        elif len(uniq) == 1:
            who = (
                f"El que reparte IPs es {uniq[0]}, NO el gateway {gw or '-'}. "
                "Suele ser Tenda/Cudy con DHCP ON (mal para CCTV)."
            )
            certainty = "ALTA (OFFER distinto al gateway)"
        else:
            who = (
                f"DOBLE DHCP: responden {', '.join(uniq)}. "
                "Askey DEBERIA ser el unico (192.168.1.1). Apague DHCP en Tenda/Cudy."
            )
            certainty = "ALTA (varios OFFER)"
    elif dhcp_ip and gw and dhcp_ip == gw:
        who = (
            f"Segun ipconfig, DHCP es {dhcp_ip} = gateway (lo esperado: Askey manda). "
            "DISCOVER no confirmo OFFER (puerto 68 ocupado); no se jura que Tenda no ofrezca a otros."
        )
        certainty = "MEDIA (solo ipconfig de ESTE portatil)"
    elif dhcp_ip and gw and dhcp_ip != gw:
        who = (
            f"Este PC recibio DHCP de {dhcp_ip} pero el gateway es {gw}. "
            "Casi seguro hay 2 routers dando red."
        )
        certainty = "ALTA (ipconfig incoherente)"
    else:
        who = "No se pudo determinar el servidor DHCP. Revise ipconfig /all."
        certainty = "BAJA"

    steps = [
        f"1. http://{gw or '192.168.1.1'} (Askey) → LAN/DHCP ACTIVADO. Rango familia .100-.200; camaras .40-.70 reservadas por MAC.",
        "2. Tenda: modo AP, DHCP OFF, cable LAN al Askey (no WAN).",
        "3. Cudy 3000: AP/bridge, DHCP OFF, cable al Askey.",
        "4. Repetidor TP-Link: DHCP OFF.",
        "5. CMD: ipconfig /all → Servidor DHCP debe ser 192.168.1.1.",
        "6. Si DISCOVER mostro 2 servidores: apague DHCP en el que NO sea Askey y reinicie camaras.",
    ]
    if tenda_suspect:
        steps.insert(
            1,
            "URGENTE Tenda: su IP del escaneo → DHCP off. Si Tenda da IPs, chocan familia y camaras.",
        )
    return DhcpPicture(
        gateway=gw,
        ipconfig_dhcp=dhcp_ip,
        offer_servers=servers,
        who_commands=who,
        tenda_suspect=tenda_suspect,
        cudy_suspect=cudy_suspect,
        dual=dual,
        notes=notes,
        manual_steps=steps,
        certainty=certainty,
    )


def analyze_conflicts(
    devices: List[Device],
    net: Optional[NetInfo],
    arp: ArpWatcher,
    run_dhcp_probe: bool = True,
) -> ConflictIntel:
    probe = dhcp_discover(timeout_sec=3.0) if run_dhcp_probe else {"ok": False, "servers": []}
    dhcp = build_dhcp_picture(net, devices, probe)
    gw = dhcp.gateway
    cases: List[ConflictCase] = []
    for ch in arp.conflicts():
        a = _role_for_mac(ch.old_mac, devices, gw)
        b = _role_for_mac(ch.new_mac, devices, gw)
        verdict, steps = _verdict_and_steps(ch.ip, a, b, gw)
        cases.append(
            ConflictCase(
                ip=ch.ip,
                ts=ch.ts,
                party_a=a,
                party_b=b,
                verdict=verdict,
                certainty="CONFIRMADO en esta sesion (ARP: dos MAC en la misma IP)",
                manual_steps=steps,
            )
        )

    cctv, infra, family = classify_inventory(devices, gw)
    two = _mac_on_two_ips(arp)

    parts = []
    if dhcp.dual or dhcp.tenda_suspect:
        parts.append("DHCP: indicios de mas de un servidor o Tenda/Cudy mal (DHCP ON).")
    else:
        parts.append(dhcp.who_commands)
    if cases:
        parts.append(f"{len(cases)} conflicto(s) IP CONFIRMADO(s) con identificacion de dispositivos.")
    else:
        parts.append("Sin conflicto ARP en esta ventana (monitorice si hay cortes a ratos).")
    parts.append(
        f"En la LAN hay {len(family)} dispositivo(s) de familia/otros ademas de CCTV: "
        "normal, pero con IPs fuera del rango de camaras."
    )

    honesty = (
        "CERTEZA: conflicto IP al 100% solo cuando se ven DOS MAC en la MISMA IP "
        "(escaneo o monitorizacion). Si no ha ocurrido ahora, no se afirma que no exista "
        "a otras horas (un movil puede coger la IP de una camara por la tarde). "
        "Quien reparte IPs: OFFER DHCP = certeza alta; solo ipconfig = media. "
        "192.168.1.1 suele ser Askey y DEBERIA ser el unico DHCP: hay que comprobarlo, no asumirlo."
    )

    return ConflictIntel(
        generated=now_str(),
        dhcp=dhcp,
        cases=cases,
        family_devices=family,
        cctv_devices=cctv,
        infra_devices=infra,
        same_mac_two_ips=two,
        summary=" ".join(parts),
        honesty=honesty,
    )
