"""Informe focalizado: vista y checklist desde un NVR concreto."""
from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Set

from .models import Device, NetInfo, app_root, ensure_dirs, now_str
from .topology import (
    DeviceViewpoint,
    _is_camera,
    _is_nvr,
    analyze_viewpoint,
    build_inventory,
    refine_role,
    same_l3_subnet,
)


@dataclass
class CameraChecklistItem:
    ip: str
    mac: str
    vendor: str
    role: str
    status: str
    in_discovery: bool
    reachable: bool
    reason: str
    manual_steps: List[str] = field(default_factory=list)


@dataclass
class NvrFocusReport:
    nvr_ip: str
    nvr_mac: str
    nvr_vendor: str
    generated: str
    net_gateway: str
    viewpoint: DeviceViewpoint
    checklist: List[CameraChecklistItem]
    global_manual_steps: List[str]
    summary: str

    def as_text(self) -> str:
        lines = [
            f"INFORME DESDE NVR {self.nvr_ip}",
            "=" * 56,
            f"Generado: {self.generated}",
            f"NVR: {self.nvr_ip}  MAC {self.nvr_mac or '-'}  {self.nvr_vendor or '-'}",
            f"Gateway red: {self.net_gateway or '-'}",
            "",
            "RESUMEN",
            "-" * 40,
            self.summary,
            "",
            "NUMEROS (desde la misma LAN que este NVR)",
            "-" * 40,
            f"  Camaras misma subred /24:     {len(self.viewpoint.same_subnet_cameras)}",
            f"  Alcanzables ping/puertos:      {len(self.viewpoint.reachable_unicast)}",
            f"  Visibles en Buscar (discovery): {len(self.viewpoint.discoverable_onvif)}",
            f"  Estimacion Buscar en NVR:      {self.viewpoint.estimated_visible}",
            "",
        ]
        if self.viewpoint.capacity_hint:
            lines.append(self.viewpoint.capacity_hint)
            lines.append("")

        # Checklist: add manually
        add_manual = [c for c in self.checklist if c.reachable and not c.in_discovery]
        offline = [c for c in self.checklist if not c.reachable]
        ok_disc = [c for c in self.checklist if c.in_discovery]

        lines.append("CHECKLIST — ANADIR MANUALMENTE EN ESTE NVR (no salen en Buscar)")
        lines.append("-" * 40)
        if add_manual:
            lines.append(f"Total: {len(add_manual)} camara(s). En NVR PNI IP816:")
            lines.append("  Menu HDMI -> Configuracion -> Canal IP / Digital -> ONVIF")
            lines.append("  -> Anadir manual (NO solo Buscar) -> IP, puerto 80, usuario admin")
            lines.append("")
            for i, c in enumerate(add_manual, 1):
                lines.append(f"  [{i}] IP {c.ip}  MAC {c.mac or '-'}  {c.vendor or '-'}  ({c.role})")
                lines.append(f"       Motivo: {c.reason}")
                for step in c.manual_steps[:6]:
                    lines.append(f"       {step}")
                lines.append("")
        else:
            lines.append("  (ninguna: todas las alcanzables aparecen en discovery o no hay brecha)")
        lines.append("")

        lines.append("CHECKLIST — CAMARAS OFFLINE (arreglar red antes del NVR)")
        lines.append("-" * 40)
        if offline:
            for i, c in enumerate(offline, 1):
                lines.append(f"  [{i}] IP {c.ip}  MAC {c.mac or '-'}  estado={c.status}")
                lines.append(f"       Motivo: {c.reason}")
                for step in c.manual_steps[:5]:
                    lines.append(f"       {step}")
                lines.append("")
        else:
            lines.append("  (ninguna offline en inventario de esta subred)")
        lines.append("")

        lines.append("CAMARAS QUE SI DEBERIAN SALIR EN BUSCAR")
        lines.append("-" * 40)
        if ok_disc:
            lines.append(", ".join(c.ip for c in ok_disc))
        else:
            lines.append("(ninguna en discovery en esta pasada)")
        lines.append("")

        lines.append("QUE HACER USTED — PASOS MANUALES GLOBALES")
        lines.append("-" * 40)
        for step in self.global_manual_steps:
            lines.append(step)
        lines.append("")

        lines.append("COMANDOS WINDOWS (verificar desde PC en el mismo switch que este NVR)")
        lines.append("-" * 40)
        lines.append("  ping " + self.nvr_ip + "          -> NVR responde")
        if add_manual:
            sample = add_manual[0].ip
            lines.append(f"  ping {sample}          -> camara que falta en Buscar")
            lines.append(f"  arp -a | findstr {sample}")
        lines.append("  ipconfig /all | findstr /i \"DHCP Gateway\"")
        return "\n".join(lines)


def _reason_and_steps_for_camera(
    cam: Device,
    in_discovery: bool,
    reachable: bool,
    nvr_ip: str,
    net: Optional[NetInfo],
) -> tuple[str, List[str]]:
    gw = (net.gateway if net else "") or "192.168.1.1"
    role = refine_role(cam, gw)
    steps: List[str] = []

    if not reachable or cam.status == "OFFLINE":
        reason = "No responde ping/puertos desde el PC (misma prueba que haria el NVR por unicast)."
        steps = [
            f"1. CMD: ping {cam.ip}  (si falla, el NVR tambien fallara)",
            f"2. Comprobar alimentacion 12V y LED de la camara",
            f"3. Si es WiFi: en http://{cam.ip} -> Red -> WiFi -> SSID Tenda_2724C0, modo Manual, NO _EXT",
            f"4. Gateway en camara: {gw}; mascara 255.255.255.0",
            f"5. arp -a | findstr {cam.ip}  -> debe mostrar MAC Dahua",
            "6. Cuando ping OK, anadir en NVR (pasos abajo)",
        ]
        return reason, steps

    if reachable and not in_discovery:
        if "WiFi" in role or (cam.latency_ms is not None and cam.latency_ms >= 8):
            reason = (
                "Ping OK pero NO responde ONVIF multicast (Buscar). "
                "Tipico en camara WiFi o AP que filtra multicast."
            )
            steps = [
                f"1. NVR {nvr_ip}: Canal IP -> eliminar canal si existe -> Anadir manual",
                f"2. IP: {cam.ip}  Puerto: 80  Protocolo: ONVIF  Usuario: admin (clave Dahua)",
                "3. En camara http://" + cam.ip + " -> Red -> WiFi -> Manual -> Tenda_2724C0 (sin _EXT)",
                f"4. Gateway camara: {gw}",
                "5. En Tenda/Askey: desactivar aislamiento de clientes WiFi",
                "6. Tras guardar en NVR, probar sub-stream si hay lag",
            ]
        else:
            reason = (
                "Ping OK pero no anuncia ONVIF por multicast. "
                "Anadir por IP fija en el NVR; revisar switch/AP multicast."
            )
            steps = [
                f"1. NVR {nvr_ip}: Canal IP -> Anadir manual -> {cam.ip} puerto 80 ONVIF",
                "2. Usuario/contraseña configurados en la camara Dahua",
                f"3. CMD: ping {cam.ip}  (confirmar mientras anade en NVR)",
                "4. Si otras camaras PoE si salen en Buscar: comparar segmento/switch",
                "5. Reiniciar canal en NVR si dice red inalcanzable con ping OK",
            ]
        return reason, steps

    if in_discovery:
        reason = "Deberia aparecer en Buscar ONVIF del NVR."
        steps = [
            f"1. En NVR {nvr_ip}: Canal IP -> Buscar -> debe listar {cam.ip}",
            "2. Si no aparece en pantalla pero aqui si: reiniciar NVR y volver a Buscar",
            "3. Credenciales correctas (admin + clave camara, no app movil)",
        ]
        return reason, steps

    return "Estado mixto; verificar ping y discovery.", [
        f"ping {cam.ip}",
        "Re-ejecutar Topologia / Vista NVR en la app",
    ]


def _global_manual_steps(
    vp: DeviceViewpoint,
    nvr_ip: str,
    net: Optional[NetInfo],
    add_manual_count: int,
    offline_count: int,
) -> List[str]:
    gw = (net.gateway if net else "") or "192.168.1.1"
    steps: List[str] = []

    steps.append(
        "1. Portatil por CABLE al mismo switch que el NVR " + nvr_ip + " (no WiFi del tecnico)."
    )
    steps.append(
        "2. Compare este informe con la pantalla Buscar del NVR: "
        f"deberian coincidir ~{vp.estimated_visible} camaras en discovery."
    )

    if add_manual_count > 0:
        steps.append(
            f"3. Anadir MANUALMENTE {add_manual_count} IP(s) listadas arriba "
            "(NVR PNI: Canal IP -> Anadir -> ONVIF -> IP + puerto 80)."
        )
        steps.append(
            "4. Red WiFi: todas las camaras al SSID Tenda_2724C0; NO MOVISTAR-WIFI6-6C08_EXT."
        )
        steps.append(
            "5. Tenda en modo AP, DHCP OFF; un solo DHCP en Askey " + gw + "."
        )
        steps.append(
            "6. Askey WiFi 2.4 GHz: canal fijo 1/6/11, ancho 20 MHz."
        )

    if offline_count > 0:
        steps.append(
            f"7. {offline_count} camara(s) OFFLINE: arreglar red/alimentacion/WiFi ANTES de anadir en NVR."
        )
        steps.append("8. Monitorizar 15 min con la app mientras el NVR falla.")

    if not add_manual_count and not offline_count and vp.estimated_visible >= len(vp.same_subnet_cameras):
        steps.append(
            "3. Desde red, este NVR deberia ver todas las camaras en Buscar. "
            "Si en pantalla faltan: credenciales ONVIF, canales ya llenos (max 16), o reiniciar NVR."
        )

    steps.append(
        "9. Tras cambios: ESCANEAR -> Topologia -> volver a generar este informe para el mismo NVR."
    )
    return steps


def build_nvr_focus_report(
    nvr: Device,
    devices: List[Device],
    net: Optional[NetInfo],
    discovery_ips: Set[str],
) -> NvrFocusReport:
    inventory = build_inventory(devices, net)
    cameras = inventory["Camaras"]
    vp = analyze_viewpoint(nvr, cameras, discovery_ips, net)

    checklist: List[CameraChecklistItem] = []
    for cam in cameras:
        if not same_l3_subnet(nvr.ip, cam.ip):
            continue
        in_disc = cam.ip in discovery_ips
        reachable = cam.ip in vp.reachable_unicast
        reason, manual = _reason_and_steps_for_camera(cam, in_disc, reachable, nvr.ip, net)
        checklist.append(
            CameraChecklistItem(
                ip=cam.ip,
                mac=cam.mac,
                vendor=cam.vendor,
                role=refine_role(cam, net.gateway if net else ""),
                status=cam.status,
                in_discovery=in_disc,
                reachable=reachable,
                reason=reason,
                manual_steps=manual,
            )
        )

    checklist.sort(key=lambda c: tuple(int(p) for p in c.ip.split(".")))

    add_manual = [c for c in checklist if c.reachable and not c.in_discovery]
    offline = [c for c in checklist if not c.reachable]

    summary_parts = [
        f"NVR {nvr.ip}: {len(vp.discoverable_onvif)} camaras en Buscar (discovery), "
        f"{len(vp.reachable_unicast)} alcanzables por ping/puertos, "
        f"{len(vp.same_subnet_cameras)} en la misma subred.",
    ]
    if add_manual:
        summary_parts.append(
            f"Debe ANADIR MANUALMENTE {len(add_manual)} IP(s): "
            + ", ".join(c.ip for c in add_manual[:12])
            + ("..." if len(add_manual) > 12 else "")
        )
    if offline:
        summary_parts.append(
            f"{len(offline)} camara(s) OFFLINE: arreglar red antes del NVR."
        )
    if not add_manual and not offline:
        summary_parts.append("Sin brecha discovery/unicast grave en esta pasada.")

    global_steps = _global_manual_steps(vp, nvr.ip, net, len(add_manual), len(offline))

    return NvrFocusReport(
        nvr_ip=nvr.ip,
        nvr_mac=nvr.mac,
        nvr_vendor=nvr.vendor,
        generated=now_str(),
        net_gateway=net.gateway if net else "",
        viewpoint=vp,
        checklist=checklist,
        global_manual_steps=global_steps,
        summary=" ".join(summary_parts),
    )


def find_nvr_device(devices: List[Device], nvr_ip: str) -> Optional[Device]:
    for d in devices:
        if d.ip == nvr_ip:
            return d
    return None


def is_nvr_device(d: Device) -> bool:
    return _is_nvr(d) or d.device_type == "NVR"


def generate_nvr_html_report(report: NvrFocusReport, net: Optional[NetInfo]) -> Path:
    import html

    ensure_dirs()
    stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    path = app_root() / "reports" / f"informe_nvr_{report.nvr_ip.replace('.', '_')}_{stamp}.html"

    def esc(s) -> str:
        return html.escape(str(s if s is not None else ""))

    add_manual = [c for c in report.checklist if c.reachable and not c.in_discovery]
    offline = [c for c in report.checklist if not c.reachable]

    rows_add = []
    for i, c in enumerate(add_manual, 1):
        steps = "<br>".join(esc(s) for s in c.manual_steps)
        rows_add.append(
            f"<tr><td>{i}</td><td><b>{esc(c.ip)}</b></td><td>{esc(c.mac)}</td>"
            f"<td>{esc(c.vendor)}</td><td>{esc(c.reason)}</td><td>{steps}</td></tr>"
        )

    rows_off = []
    for i, c in enumerate(offline, 1):
        steps = "<br>".join(esc(s) for s in c.manual_steps)
        rows_off.append(
            f"<tr><td>{i}</td><td><b>{esc(c.ip)}</b></td><td>{esc(c.status)}</td>"
            f"<td>{esc(c.reason)}</td><td>{steps}</td></tr>"
        )

    global_html = "<ol>" + "".join(f"<li>{esc(s)}</li>" for s in report.global_manual_steps) + "</ol>"

    css = """
    body{font-family:Segoe UI,Arial,sans-serif;margin:24px;color:#1a1a1a;background:#f7f7f7}
    h1{color:#0b3d5c} h2{color:#0b5a7a;border-bottom:1px solid #ccc;padding-bottom:4px}
    table{border-collapse:collapse;width:100%;background:#fff;margin:12px 0}
    th,td{border:1px solid #ddd;padding:8px;font-size:13px;text-align:left;vertical-align:top}
    th{background:#0b5a7a;color:#fff}
    .box{background:#fff;padding:16px;border-radius:8px;margin:12px 0}
  """

    doc = f"""<!DOCTYPE html>
<html lang="es"><head><meta charset="utf-8">
<title>Informe NVR {esc(report.nvr_ip)}</title>
<style>{css}</style></head><body>
<h1>Informe desde NVR {esc(report.nvr_ip)}</h1>
<p>Generado: {esc(report.generated)} · NVR Net Diag</p>
<div class="box"><b>Resumen:</b> {esc(report.summary)}</div>
<div class="box">
<b>Numeros:</b><br>
Camaras misma /24: {len(report.viewpoint.same_subnet_cameras)}<br>
Alcanzables ping/puertos: {len(report.viewpoint.reachable_unicast)}<br>
Buscar (WS-Discovery): {len(report.viewpoint.discoverable_onvif)}<br>
Estimacion Buscar NVR: {report.viewpoint.estimated_visible}
</div>
<h2>Anadir manualmente en este NVR ({len(add_manual)})</h2>
<table>
<tr><th>#</th><th>IP</th><th>MAC</th><th>Fabricante</th><th>Motivo</th><th>Pasos manuales</th></tr>
{''.join(rows_add) if rows_add else '<tr><td colspan="6">Sin IPs pendientes de anadir manual</td></tr>'}
</table>
<h2>Camaras OFFLINE — arreglar red primero ({len(offline)})</h2>
<table>
<tr><th>#</th><th>IP</th><th>Estado</th><th>Motivo</th><th>Pasos manuales</th></tr>
{''.join(rows_off) if rows_off else '<tr><td colspan="5">Sin OFFLINE en esta subred</td></tr>'}
</table>
<h2>Que hacer usted (global)</h2>
<div class="box">{global_html}</div>
<p style="color:#666;font-size:12px">Informe local. Compare con Buscar en pantalla del NVR {esc(report.nvr_ip)}.</p>
</body></html>"""

    path.write_text(doc, encoding="utf-8")
    return path
