"""
Diagnostico de fluidez / lag / retardo del sistema CCTV.

Cubre: red (latencia, jitter, perdida), configuracion de camara (bitrate/resolucion/FPS),
carga multi-NVR, WiFi vs cable, inventario de APs (Cudy/Tenda/Askey).

El tecnico puede estar en WiFi: la LAN es plana y debe llegar a todo; se advierte
que la latencia del portatil WiFi puede ser peor que la del NVR cableado.
"""
from __future__ import annotations

from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple

from .models import Device, NetInfo, now_str
from .onvif_rtsp import probe_video_encoders
from .pinger import ping_host
from .topology import _is_camera, _is_nvr, build_inventory, refine_role


@dataclass
class CamFluency:
    ip: str
    mac: str
    vendor: str
    link: str  # WiFi / LAN-PoE / desconocido
    status: str
    latency_ms: Optional[float]
    loss_pct: float
    jitter_ms: Optional[float]
    score: int  # 0-100 (100 = fluido)
    issues: List[str] = field(default_factory=list)
    video_summary: str = ""
    bitrate_kbps: Optional[int] = None
    resolution: str = ""
    fps: Optional[int] = None
    encoding: str = ""
    manual_steps: List[str] = field(default_factory=list)


@dataclass
class FluencyReport:
    generated: str
    summary: str
    inventory_note: str
    cams: List[CamFluency]
    nvr_count: int
    wifi_count: int
    lan_count: int
    estimated_bandwidth_mbps: float
    multi_nvr_warning: str
    findings: List[str]
    global_manual_steps: List[str]
    limitation_note: str

    def as_text(self) -> str:
        lines = [
            "DIAGNOSTICO DE FLUIDEZ / LAG / RETARDO",
            "=" * 56,
            f"Generado: {self.generated}",
            "",
            self.limitation_note,
            "",
            "RESUMEN",
            "-" * 40,
            self.summary,
            "",
            self.inventory_note,
            f"NVR detectados: {self.nvr_count}",
            f"Camaras WiFi (estimadas): {self.wifi_count}",
            f"Camaras LAN/PoE (estimadas): {self.lan_count}",
            f"Ancho de banda estimado main streams: ~{self.estimated_bandwidth_mbps:.1f} Mbps",
            "",
        ]
        if self.multi_nvr_warning:
            lines.append(self.multi_nvr_warning)
            lines.append("")

        lines.append("HALLAZGOS")
        lines.append("-" * 40)
        if not self.findings:
            lines.append("(sin hallazgos criticos de fluidez en esta pasada)")
        for f in self.findings:
            lines.append(f"* {f}")
        lines.append("")

        lines.append("POR CAMARA (peor score primero)")
        lines.append("-" * 40)
        ranked = sorted(self.cams, key=lambda c: c.score)
        for c in ranked:
            lines.append(
                f"\n[{c.score:3d}/100] {c.ip}  {c.link}  {c.status}  "
                f"lat={c.latency_ms if c.latency_ms is not None else '-'}ms  "
                f"loss={c.loss_pct:.0f}%  jit={c.jitter_ms if c.jitter_ms is not None else '-'}ms"
            )
            if c.video_summary:
                lines.append(f"         Video: {c.video_summary}")
            for iss in c.issues:
                lines.append(f"         ! {iss}")
            if c.manual_steps:
                lines.append("         QUE HACER:")
                for s in c.manual_steps:
                    lines.append(f"           {s}")

        lines.append("")
        lines.append("QUE HACER USTED — PASOS GLOBALES (sistema fluido en todos los NVR)")
        lines.append("-" * 40)
        for s in self.global_manual_steps:
            lines.append(s)
        return "\n".join(lines)


def classify_link(d: Device) -> str:
    role = refine_role(d)
    if "WiFi" in role:
        return "WiFi"
    if "LAN" in role or "PoE" in role:
        return "LAN/PoE"
    # Heuristica: latencia >= 8 ms o vendor WiFi tipico
    if d.latency_ms is not None and d.latency_ms >= 8:
        return "WiFi"
    if d.latency_ms is not None and d.latency_ms <= 3:
        return "LAN/PoE"
    return "desconocido"


def _burst_ping_stats(ip: str, count: int = 12) -> Tuple[Optional[float], float, Optional[float], bool]:
    """Devuelve avg_ms, loss_pct, jitter_ms, online."""
    rtts: List[float] = []
    sent = recv = 0
    for _ in range(count):
        pr = ping_host(ip, timeout_ms=700, count=1)
        sent += pr.sent
        recv += pr.recv
        if pr.rtt_ms is not None:
            rtts.append(pr.rtt_ms)
    loss = 100.0 * (sent - recv) / sent if sent else 100.0
    avg = sum(rtts) / len(rtts) if rtts else None
    jitter = None
    if len(rtts) >= 2:
        deltas = [abs(rtts[i] - rtts[i - 1]) for i in range(1, len(rtts))]
        jitter = sum(deltas) / len(deltas)
    return avg, loss, jitter, recv > 0


def _score_camera(
    avg: Optional[float],
    loss: float,
    jitter: Optional[float],
    link: str,
    video_flags: List[str],
    online: bool,
) -> Tuple[int, List[str]]:
    issues: List[str] = []
    score = 100
    if not online:
        return 0, ["OFFLINE: no responde ping (red/WiFi/alimentacion)"]

    if loss >= 20:
        score -= 40
        issues.append(f"Perdida de paquetes alta ({loss:.0f}%) → cortes/lag")
    elif loss >= 5:
        score -= 20
        issues.append(f"Perdida de paquetes ({loss:.0f}%) → microcortes")

    if avg is not None:
        if link == "WiFi":
            if avg >= 80:
                score -= 25
                issues.append(f"Latencia WiFi alta ({avg:.0f} ms) → retardo en NVR")
            elif avg >= 40:
                score -= 12
                issues.append(f"Latencia WiFi elevada ({avg:.0f} ms)")
        else:
            if avg >= 30:
                score -= 20
                issues.append(f"Latencia cable alta ({avg:.0f} ms) — inusual en PoE")
            elif avg >= 15:
                score -= 8
                issues.append(f"Latencia cable algo alta ({avg:.0f} ms)")

    if jitter is not None:
        if jitter >= 25:
            score -= 20
            issues.append(f"Jitter alto ({jitter:.0f} ms) → imagen a tirones")
        elif jitter >= 12:
            score -= 10
            issues.append(f"Jitter notable ({jitter:.0f} ms)")

    for fl in video_flags:
        if fl.startswith("BITRATE_MUY_ALTO"):
            score -= 25
            issues.append(f"Config camara: {fl} (causa tipica de lag WiFi)")
        elif fl.startswith("BITRATE_ALTO"):
            score -= 15
            issues.append(f"Config camara: {fl}")
        elif fl.startswith("RESOLUCION_ALTA"):
            score -= 12
            issues.append(f"Config camara: {fl}")
        elif fl.startswith("FPS_ALTO"):
            score -= 10
            issues.append(f"Config camara: {fl}")
        elif fl.startswith("IFRAME"):
            score -= 10
            issues.append(f"Config camara: {fl}")
        elif fl.startswith("CODEC"):
            score -= 20
            issues.append(f"Config camara: {fl}")
        else:
            score -= 5
            issues.append(f"Config: {fl}")

    return max(0, min(100, score)), issues


def run_fluency_analysis(
    devices: List[Device],
    net: Optional[NetInfo],
    probe_video: bool = True,
    ping_count: int = 12,
    max_video_probes: int = 16,
) -> FluencyReport:
    inv = build_inventory(devices, net)
    cams_dev = inv["Camaras"]
    nvrs = inv["NVR"]
    routers = inv["Routers/AP"] + inv["Gateway"]

    # Detectar Cudy / Tenda / Askey en inventario
    ap_names = []
    for d in devices:
        v = (d.vendor or "").lower()
        h = (d.hostname or "").lower()
        if "cudy" in v or "cudy" in h:
            ap_names.append(f"Cudy {d.ip}")
        elif "tenda" in v:
            ap_names.append(f"Tenda {d.ip}")
        elif "askey" in v or d.ip == (net.gateway if net else ""):
            ap_names.append(f"Gateway/Askey {d.ip}")
        elif "tp-link" in v:
            ap_names.append(f"TP-Link/repetidor {d.ip}")

    inventory_note = (
        "Infraestructura detectada: "
        + (", ".join(ap_names) if ap_names else "sin AP etiquetado Cudy/Tenda; revise OUI/hostname")
        + f". Total hosts escaneados: {len(devices)}."
    )

    # Orden: primero WiFi (mas problematicas), limitar probes video
    cams_sorted = sorted(
        cams_dev,
        key=lambda d: (0 if classify_link(d) == "WiFi" else 1, d.ip),
    )

    results: List[CamFluency] = []
    total_bitrate = 0
    wifi_n = lan_n = 0

    def process_one(d: Device, do_video: bool) -> CamFluency:
        link = classify_link(d)
        avg, loss, jitter, online = _burst_ping_stats(d.ip, count=ping_count)
        video_flags: List[str] = []
        video_hints: List[str] = []
        video_summary = ""
        br = res = enc = None
        fps = None
        if do_video and online:
            vid = probe_video_encoders(d.ip)
            if vid.get("ok") and vid.get("profiles"):
                video_flags = list(vid.get("lag_flags") or [])
                video_hints = list(vid.get("manual_hints") or [])
                # main = mayor bitrate/resolucion
                profiles = vid["profiles"]

                def score(p):
                    return (p.get("bitrate_kbps") or 0) * 10 + (p.get("width") or 0) * (p.get("height") or 0)

                main = max(profiles, key=score)
                br = main.get("bitrate_kbps")
                fps = main.get("fps")
                enc = main.get("encoding") or ""
                w, h = main.get("width"), main.get("height")
                res = f"{w}x{h}" if w and h else ""
                video_summary = (
                    f"{enc or '?'} {res or '?'} @ {fps or '?'}fps, "
                    f"{br if br is not None else '?'} kbps"
                    + (f" | {len(profiles)} perfiles" if len(profiles) > 1 else "")
                )
            elif vid.get("error"):
                video_summary = f"ONVIF video no leido ({vid.get('error')[:60]})"
            else:
                video_summary = "ONVIF video: sin perfiles (auth o deshabilitado)"

        score, issues = _score_camera(avg, loss, jitter, link, video_flags, online)
        manual: List[str] = []
        if not online:
            manual = [
                f"1. ping {d.ip}",
                "2. Alimentacion 12V / PoE y LED",
                "3. Si WiFi: SSID Tenda (NO _EXT), modo Manual, gateway 192.168.1.1",
                "4. arp -a | findstr " + d.ip,
            ]
        else:
            if link == "WiFi" and (loss >= 5 or (avg and avg >= 40) or (jitter and jitter >= 12)):
                manual.extend(
                    [
                        "1. Camara solo en SSID del Tenda (CCTV), canal 2.4 GHz fijo 1/6/11, 20 MHz",
                        "2. Acerque camara al Tenda; evite repetidor MOVISTAR_EXT",
                        "3. En NVR: vista en vivo = sub-stream (no main)",
                    ]
                )
            for h in video_hints[:4]:
                manual.append(f"CONFIG: {h}")
            if not manual and score >= 80:
                manual.append("Sin accion urgente en esta camara; monitorizar si el NVR sigue con lag.")
            elif score < 80 and not any(x.startswith("CONFIG") for x in manual):
                manual.append(
                    f"1. Abrir http://{d.ip} → Video → CBR main 1024–1536 kbps, 15 fps, I-frame=FPS"
                )
                manual.append("2. En NVR: live = sub-stream; grabacion = main")

        return CamFluency(
            ip=d.ip,
            mac=d.mac,
            vendor=d.vendor,
            link=link,
            status=d.status if online else "OFFLINE",
            latency_ms=avg if avg is not None else d.latency_ms,
            loss_pct=loss,
            jitter_ms=jitter,
            score=score,
            issues=issues,
            video_summary=video_summary,
            bitrate_kbps=br,
            resolution=res or "",
            fps=fps,
            encoding=enc or "",
            manual_steps=manual,
        )

    # Ping en paralelo; video limitado
    video_ips = {d.ip for d in cams_sorted[:max_video_probes]} if probe_video else set()

    with ThreadPoolExecutor(max_workers=8) as ex:
        futs = {
            ex.submit(process_one, d, d.ip in video_ips and probe_video): d
            for d in cams_sorted
        }
        for fut in as_completed(futs):
            try:
                results.append(fut.result())
            except Exception as e:
                d = futs[fut]
                results.append(
                    CamFluency(
                        ip=d.ip,
                        mac=d.mac,
                        vendor=d.vendor,
                        link=classify_link(d),
                        status=d.status,
                        latency_ms=d.latency_ms,
                        loss_pct=d.loss_pct,
                        jitter_ms=None,
                        score=50,
                        issues=[f"Error midiendo: {e}"],
                    )
                )

    for c in results:
        if c.link == "WiFi":
            wifi_n += 1
        elif c.link == "LAN/PoE":
            lan_n += 1
        if c.bitrate_kbps:
            total_bitrate += c.bitrate_kbps

    # Si faltan bitrates, estimar WiFi 2048 + LAN 3072 por defecto solo para aviso
    missing_br = sum(1 for c in results if c.bitrate_kbps is None and c.score > 0)
    est_extra = missing_br * 2048
    total_kbps = total_bitrate + est_extra
    mbps = total_kbps / 1000.0
    n_nvr = max(1, len(nvrs))
    # Si todos los NVR piden main stream a la vez
    worst_case_mbps = mbps * n_nvr

    findings: List[str] = []
    bad = [c for c in results if c.score < 60]
    wifi_bad = [c for c in results if c.link == "WiFi" and c.score < 70]
    cfg_bad = [c for c in results if any("Config" in i or "BITRATE" in i or "RESOLUCION" in i for i in c.issues)]

    if wifi_bad:
        findings.append(
            f"{len(wifi_bad)} camara(s) WiFi con fluidez mala/media — "
            "coincide con su sintoma (WiFi = mas conflictos)."
        )
    if cfg_bad:
        findings.append(
            f"{len(cfg_bad)} camara(s) con configuracion de video agresiva "
            "(bitrate/resolucion/FPS) — causa tipica de lag aunque el ping sea OK."
        )
    if bad:
        findings.append(f"{len(bad)} camara(s) con score < 60 (prioridad de arreglo).")

    multi = ""
    if n_nvr >= 2:
        multi = (
            f"CARGA MULTI-NVR: {n_nvr} NVR x ~{mbps:.1f} Mbps de mains ≈ "
            f"{worst_case_mbps:.0f} Mbps si TODOS piden main a la vez. "
            "Eso explica: cable 'bien' en un NVR principal y lag/cortes en otros. "
            "ACCION: en CADA NVR use sub-stream para vista en vivo; main solo grabacion."
        )
        findings.append(multi)

    if wifi_n >= 8:
        findings.append(
            f"{wifi_n} camaras WiFi en Tenda: saturan 2.4 GHz si bitrate alto. "
            "Canal fijo 1/6/11, 20 MHz, sin _EXT, CBR bajo."
        )

    # Gateway / Cudy note
    if any("cudy" in (d.vendor or "").lower() for d in devices):
        findings.append(
            "Cudy detectado: debe estar en modo AP/bridge (sin segundo DHCP) "
            "y cableado al Askey; si hace NAT propio, algunos NVR veran subconjuntos distintos."
        )

    offline = [c for c in results if c.score == 0]
    if offline:
        findings.append(
            f"{len(offline)} camara(s) OFFLINE ahora (como las 2 que el NVR principal "
            "no detecto esta tarde): ver checklist ping/WiFi/IP de cada una."
        )

    avg_score = int(sum(c.score for c in results) / len(results)) if results else 0
    summary = (
        f"Fluidez media del parque: {avg_score}/100. "
        f"{len(results)} camaras medidas ({wifi_n} WiFi, {lan_n} LAN). "
        f"{len(bad)} criticas. "
        + ("Problema principal probable: WiFi + bitrate + multi-NVR." if wifi_bad or n_nvr >= 2 else "Revise hallazgos.")
    )

    global_steps = [
        "1. Puede diagnosticar por WiFi del tecnico si la LAN es plana; para medir como el NVR, preferible cable al mismo switch.",
        "2. En TODOS los NVR PNI: vista en vivo = sub-stream (subtype=1); grabacion = main. Reduce lag y carga.",
        "3. Camaras WiFi Dahua (http://IP): Video → Main CBR 1024–1536 kbps, 1280x720 o 1920x1080, 10–15 fps, I-frame = FPS, H.264/H.265.",
        "4. Sub-stream: 512–768 kbps, D1/720p, 10–15 fps.",
        "5. SSID unico CCTV en Tenda; NO repetidor MOVISTAR_EXT; aislamiento de clientes OFF.",
        "6. Tenda + Cudy: modo AP, DHCP OFF; solo Askey (192.168.1.1) reparte IP. Reserva DHCP por MAC.",
        "7. Askey/Tenda 2.4 GHz: canal fijo 1, 6 u 11; ancho 20 MHz.",
        "8. Cable PoE: si un NVR ve mal y otro bien, no es la camara — es carga/sesion ONVIF de ese NVR (reiniciar canal o usar sub).",
        "9. Tras cambios: boton Fluidez/Lag otra vez y compare scores WiFi vs cable.",
        "10. Monitoree 15–30 min mientras falla un NVR (INICIAR MONITORIZACION).",
    ]

    limitation = (
        "LIMITACION: el portatil en WiFi mide la red desde su enlace; el NVR cableado puede ir mejor. "
        "La configuracion de video se lee por ONVIF si la camara lo permite sin auth o con auth temporal. "
        "Para bajar caudal: boton «Caudal 12-16 (aplicar)» (simula y luego escribe Encode por CGI/ONVIF)."
    )

    return FluencyReport(
        generated=now_str(),
        summary=summary,
        inventory_note=inventory_note,
        cams=results,
        nvr_count=n_nvr,
        wifi_count=wifi_n,
        lan_count=lan_n,
        estimated_bandwidth_mbps=mbps,
        multi_nvr_warning=multi,
        findings=findings,
        global_manual_steps=global_steps,
        limitation_note=limitation,
    )
