"""Modelos y utilidades compartidas para NVR Net Diag."""
from __future__ import annotations

import json
import os
import sys
import time
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional


def app_root() -> Path:
    if getattr(sys, "frozen", False):
        return Path(sys.executable).resolve().parent
    return Path(__file__).resolve().parent.parent


def load_config() -> Dict[str, Any]:
    path = app_root() / "config.json"
    if path.exists():
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    return {}


class DeviceStatus(str, Enum):
    ONLINE = "ONLINE"
    DEGRADED = "DEGRADADO"
    OFFLINE = "OFFLINE"
    CONFLICT = "CONFLICTO IP"
    UNKNOWN = "DESCONOCIDO"


class EventLevel(str, Enum):
    INFO = "INFO"
    WARNING = "WARNING"
    ERROR = "ERROR"
    CRITICAL = "CRITICAL"


class DeviceType(str, Enum):
    CAMERA = "Camara"
    NVR = "NVR"
    ROUTER = "Router/AP"
    SWITCH = "Switch"
    PC = "PC"
    UNKNOWN = "Desconocido"


@dataclass
class Device:
    ip: str
    mac: str = ""
    vendor: str = ""
    hostname: str = ""
    device_type: str = DeviceType.UNKNOWN.value
    status: str = DeviceStatus.UNKNOWN.value
    latency_ms: Optional[float] = None
    loss_pct: float = 0.0
    last_seen: str = ""
    ports: Dict[int, bool] = field(default_factory=dict)
    notes: str = ""
    selected: bool = False
    sent: int = 0
    recv: int = 0
    rtt_min: Optional[float] = None
    rtt_max: Optional[float] = None
    rtt_avg: Optional[float] = None
    jitter: Optional[float] = None
    offline_since: Optional[float] = None
    mac_history: List[Dict[str, str]] = field(default_factory=list)

    def to_row(self) -> tuple:
        lat = f"{self.latency_ms:.0f} ms" if self.latency_ms is not None else "-"
        loss = f"{self.loss_pct:.0f}%" if self.sent else "-"
        return (
            self.ip,
            self.mac or "-",
            self.vendor or "-",
            self.hostname or "-",
            self.device_type,
            self.status,
            lat,
            loss,
            self.last_seen or "-",
        )


@dataclass
class NetInfo:
    adapter: str = ""
    ip: str = ""
    mask: str = ""
    gateway: str = ""
    cidr: str = ""
    dhcp_enabled: Optional[bool] = None
    dhcp_server: str = ""
    dns: List[str] = field(default_factory=list)
    is_admin: bool = False
    adapters: List[Dict[str, str]] = field(default_factory=list)


@dataclass
class LogEvent:
    ts: str
    level: str
    ip: str
    event: str
    details: str

    def as_line(self) -> str:
        return f"{self.ts} | {self.level} | {self.ip} | {self.event} | {self.details}"


def now_str() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")


def now_short() -> str:
    return datetime.now().strftime("%H:%M:%S")


def epoch() -> float:
    return time.time()


def normalize_mac(mac: str) -> str:
    if not mac:
        return ""
    mac = mac.replace("-", ":").replace(".", ":").upper()
    parts = [p for p in mac.split(":") if p]
    if len(parts) == 1 and len(parts[0]) == 12:
        s = parts[0]
        parts = [s[i : i + 2] for i in range(0, 12, 2)]
    if len(parts) != 6:
        return mac.upper()
    return ":".join(p.zfill(2)[-2:] for p in parts)


def mac_oui_prefix(mac: str) -> str:
    m = normalize_mac(mac).replace(":", "")
    return m[:6] if len(m) >= 6 else ""


def ip_to_int(ip: str) -> int:
    a, b, c, d = (int(x) for x in ip.split("."))
    return (a << 24) + (b << 16) + (c << 8) + d


def int_to_ip(n: int) -> str:
    return f"{(n >> 24) & 255}.{(n >> 16) & 255}.{(n >> 8) & 255}.{n & 255}"


def mask_to_cidr_bits(mask: str) -> int:
    try:
        return bin(ip_to_int(mask)).count("1")
    except Exception:
        return 24


def guess_cidr(ip: str, mask: str) -> str:
    bits = mask_to_cidr_bits(mask or "255.255.255.0")
    n = ip_to_int(ip) & ((0xFFFFFFFF << (32 - bits)) & 0xFFFFFFFF)
    return f"{int_to_ip(n)}/{bits}"


def cidr_hosts(cidr: str) -> List[str]:
    if "/" not in cidr:
        return []
    base, bits_s = cidr.split("/")
    bits = int(bits_s)
    bits = max(24, min(30, bits))
    mask = (0xFFFFFFFF << (32 - bits)) & 0xFFFFFFFF
    network = ip_to_int(base) & mask
    host_bits = 32 - bits
    count = min((1 << host_bits) - 2, 254)
    if count < 1:
        count = 1
    return [int_to_ip(network + i) for i in range(1, count + 1)]


def ensure_dirs() -> None:
    root = app_root()
    for name in ("logs", "reports", "data"):
        (root / name).mkdir(parents=True, exist_ok=True)


def classify_device(vendor: str, open_ports: Dict[int, bool], hostname: str = "") -> str:
    v = (vendor or "").lower()
    h = (hostname or "").lower()
    ports = {p for p, o in (open_ports or {}).items() if o}
    camera_vendors = ("dahua", "hikvision", "reolink", "axis", "uniview", "amcrest")
    nvr_vendors = ("pni", "xiongmai", "xm ", "hikvision", "dahua")
    router_vendors = (
        "tp-link", "tenda", "asus", "netgear", "ubiquiti", "mikrotik",
        "cisco", "askey", "huawei", "zyxel", "d-link", "belkin", "linksys", "cudy",
    )
    if "nvr" in h or "dvr" in h:
        return DeviceType.NVR.value
    # NVR PNI / Xiongmai: a menudo 37777; cámaras Dahua suelen 554+80
    if 37777 in ports and any(x.strip() in v for x in nvr_vendors if x.strip()):
        if 554 in ports and any(x in v for x in camera_vendors) and "nvr" not in h:
            return DeviceType.CAMERA.value
        return DeviceType.NVR.value
    if any(x in v for x in camera_vendors) or "ipc" in h or "cam" in h:
        return DeviceType.CAMERA.value
    if 37777 in ports and 554 in ports:
        # Ambiguo: muchos NVR y algunas IPC; priorizar cámara si vendor cámara
        return DeviceType.CAMERA.value
    if any(x in v for x in router_vendors) or "router" in h or "gateway" in h or "ap-" in h:
        return DeviceType.ROUTER.value
    if 554 in ports and (80 in ports or 8000 in ports or 8080 in ports):
        return DeviceType.CAMERA.value
    if 37777 in ports:
        return DeviceType.NVR.value
    return DeviceType.UNKNOWN.value


def status_from_stats(online: bool, loss_pct: float, latency_ms: Optional[float], conflict: bool) -> str:
    if conflict:
        return DeviceStatus.CONFLICT.value
    if not online:
        return DeviceStatus.OFFLINE.value
    if loss_pct >= 20 or (latency_ms is not None and latency_ms >= 150):
        return DeviceStatus.DEGRADED.value
    if loss_pct >= 5 or (latency_ms is not None and latency_ms >= 80):
        return DeviceStatus.DEGRADED.value
    return DeviceStatus.ONLINE.value
