"""Pruebas básicas ONVIF/RTSP sin grabar vídeo ni persistir credenciales."""
from __future__ import annotations

import re
import socket
from typing import Dict, List, Optional, Tuple
from xml.etree import ElementTree as ET

from .ports import check_port


ONVIF_NS = {
    "s": "http://www.w3.org/2003/05/soap-envelope",
    "tds": "http://www.onvif.org/ver10/device/wsdl",
    "tt": "http://www.onvif.org/ver10/schema",
}


def probe_rtsp(ip: str, port: int = 554, timeout: float = 2.0, path: str = "/") -> Dict:
    """Comprueba si hay servicio RTSP (OPTIONS/DESCRIBE sin auth fuerte)."""
    result = {
        "ok": False,
        "port_open": False,
        "banner": "",
        "status_line": "",
        "needs_auth": False,
        "error": "",
    }
    p, opened, _ = check_port(ip, port, timeout)
    result["port_open"] = opened
    if not opened:
        result["error"] = f"Puerto {port} cerrado"
        return result

    try:
        req = (
            f"OPTIONS rtsp://{ip}:{port}{path} RTSP/1.0\r\n"
            f"CSeq: 1\r\n"
            f"User-Agent: NVR-Net-Diag/1.0\r\n"
            f"\r\n"
        )
        with socket.create_connection((ip, port), timeout=timeout) as s:
            s.settimeout(timeout)
            s.sendall(req.encode("ascii", errors="ignore"))
            data = s.recv(1024)
        text = data.decode("utf-8", errors="replace")
        result["banner"] = text[:300]
        first = text.splitlines()[0] if text else ""
        result["status_line"] = first
        if "RTSP/1.0" in first or "RTSP/1.1" in first:
            result["ok"] = True
            if "401" in first or "Unauthorized" in text:
                result["needs_auth"] = True
        elif text:
            result["ok"] = True  # algo respondió en 554
    except Exception as e:
        result["error"] = str(e)
    return result


def _soap_get_device_information(user: str = "", password: str = "") -> str:
    # Sin WS-Security por defecto; muchos dispositivos responden GetDeviceInformation sin auth en LAN
    return """<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
  <s:Body>
    <tds:GetDeviceInformation/>
  </s:Body>
</s:Envelope>"""


def probe_onvif(
    ip: str,
    ports: Optional[List[int]] = None,
    timeout: float = 2.5,
    user: str = "",
    password: str = "",
) -> Dict:
    """Intenta GetDeviceInformation en rutas ONVIF habituales."""
    import urllib.error
    import urllib.request

    ports = ports or [80, 8000, 8080, 8899]
    paths = [
        "/onvif/device_service",
        "/onvif/device",
        "/onvif/Device",
        "/Device",
        "/onvif/",
    ]
    result = {
        "ok": False,
        "endpoint": "",
        "manufacturer": "",
        "model": "",
        "firmware": "",
        "serial": "",
        "error": "",
        "port_tried": [],
    }
    body = _soap_get_device_information(user, password).encode("utf-8")
    headers = {
        "Content-Type": "application/soap+xml; charset=utf-8",
        "SOAPAction": '"http://www.onvif.org/ver10/device/wsdl/GetDeviceInformation"',
    }

    for port in ports:
        p, opened, _ = check_port(ip, port, min(timeout, 1.5))
        result["port_tried"].append({"port": port, "open": opened})
        if not opened:
            continue
        for path in paths:
            url = f"http://{ip}:{port}{path}"
            try:
                req = urllib.request.Request(url, data=body, headers=headers, method="POST")
                if user:
                    # Basic auth opcional (temporal, no se guarda)
                    import base64

                    token = base64.b64encode(f"{user}:{password}".encode()).decode()
                    req.add_header("Authorization", f"Basic {token}")
                with urllib.request.urlopen(req, timeout=timeout) as resp:
                    data = resp.read()
                text = data.decode("utf-8", errors="replace")
                if "GetDeviceInformationResponse" in text or "Manufacturer" in text:
                    result["ok"] = True
                    result["endpoint"] = url
                    result.update(_parse_device_info(text))
                    return result
            except Exception as e:
                result["error"] = str(e)
                continue
    if not result["ok"] and not result["error"]:
        result["error"] = "No se obtuvo respuesta ONVIF"
    return result


def _parse_device_info(xml_text: str) -> Dict[str, str]:
    out = {"manufacturer": "", "model": "", "firmware": "", "serial": ""}
    try:
        # quitar namespaces para parseo simple
        cleaned = re.sub(r'xmlns(:\w+)?="[^"]+"', "", xml_text)
        cleaned = re.sub(r"(</?)(\w+):", r"\1", cleaned)
        root = ET.fromstring(cleaned)
        for tag, key in (
            ("Manufacturer", "manufacturer"),
            ("Model", "model"),
            ("FirmwareVersion", "firmware"),
            ("SerialNumber", "serial"),
        ):
            el = root.find(f".//{tag}")
            if el is not None and el.text:
                out[key] = el.text.strip()
    except Exception:
        # regex fallback
        for tag, key in (
            ("Manufacturer", "manufacturer"),
            ("Model", "model"),
            ("FirmwareVersion", "firmware"),
            ("SerialNumber", "serial"),
        ):
            m = re.search(rf"<{tag}[^>]*>([^<]+)</{tag}>", xml_text)
            if m:
                out[key] = m.group(1).strip()
    return out


def camera_service_probe(ip: str, user: str = "", password: str = "") -> Dict:
    rtsp = probe_rtsp(ip)
    onvif = probe_onvif(ip, user=user, password=password)
    video = probe_video_encoders(ip, user=user, password=password)
    return {"rtsp": rtsp, "onvif": onvif, "video": video}


def _soap_post(
    url: str,
    body: bytes,
    soap_action: str,
    timeout: float,
    user: str = "",
    password: str = "",
) -> str:
    import urllib.request

    headers = {
        "Content-Type": "application/soap+xml; charset=utf-8",
        "SOAPAction": f'"{soap_action}"',
    }
    req = urllib.request.Request(url, data=body, headers=headers, method="POST")
    if user:
        import base64

        token = base64.b64encode(f"{user}:{password}".encode()).decode()
        req.add_header("Authorization", f"Basic {token}")
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return resp.read().decode("utf-8", errors="replace")


def _clean_xml(xml_text: str) -> str:
    cleaned = re.sub(r'xmlns(:\w+)?="[^"]+"', "", xml_text)
    cleaned = re.sub(r"(</?)(\w+):", r"\1", cleaned)
    return cleaned


def _parse_int(text: Optional[str]) -> Optional[int]:
    if text is None:
        return None
    try:
        return int(float(str(text).strip()))
    except Exception:
        return None


def _extract_encoder_from_node(node: ET.Element) -> Dict:
    """Extrae campos de un VideoEncoderConfiguration / Profile."""
    enc: Dict = {
        "name": "",
        "token": "",
        "encoding": "",
        "width": None,
        "height": None,
        "fps": None,
        "bitrate_kbps": None,
        "gov_length": None,
        "quality": None,
    }
    name_el = node.find(".//Name")
    if name_el is not None and name_el.text:
        enc["name"] = name_el.text.strip()
    token = node.attrib.get("token") or ""
    if not token:
        tok_el = node.find(".//token")
        if tok_el is not None and tok_el.text:
            token = tok_el.text.strip()
    enc["token"] = token
    enc_el = node.find(".//Encoding")
    if enc_el is not None and enc_el.text:
        enc["encoding"] = enc_el.text.strip()
    w = node.find(".//Width")
    h = node.find(".//Height")
    enc["width"] = _parse_int(w.text if w is not None else None)
    enc["height"] = _parse_int(h.text if h is not None else None)
    fr = node.find(".//FrameRateLimit")
    if fr is None:
        fr = node.find(".//FrameRate")
    enc["fps"] = _parse_int(fr.text if fr is not None else None)
    br = node.find(".//BitrateLimit")
    if br is None:
        br = node.find(".//Bitrate")
    enc["bitrate_kbps"] = _parse_int(br.text if br is not None else None)
    gov = node.find(".//GovLength")
    if gov is None:
        gov = node.find(".//Govlength")
    enc["gov_length"] = _parse_int(gov.text if gov is not None else None)
    q = node.find(".//Quality")
    enc["quality"] = _parse_int(q.text if q is not None else None)
    return enc


def probe_video_encoders(
    ip: str,
    ports: Optional[List[int]] = None,
    timeout: float = 3.0,
    user: str = "",
    password: str = "",
) -> Dict:
    """
    Lee perfiles/encoders ONVIF (resolución, FPS, bitrate, I-frame).
    Útil para detectar lag por configuración de cámara (no solo red).
    """
    ports = ports or [80, 8000, 8080, 8899]
    media_paths = [
        "/onvif/media_service",
        "/onvif/Media",
        "/onvif/media",
        "/Media",
        "/onvif/device_service",
    ]
    result: Dict = {
        "ok": False,
        "endpoint": "",
        "profiles": [],
        "error": "",
        "lag_flags": [],
        "manual_hints": [],
    }

    get_profiles = b"""<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
  <s:Body><trt:GetProfiles/></s:Body>
</s:Envelope>"""

    get_configs = b"""<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
            xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
  <s:Body><trt:GetVideoEncoderConfigurations/></s:Body>
</s:Envelope>"""

    text = ""
    endpoint = ""
    for port in ports:
        p, opened, _ = check_port(ip, port, min(timeout, 1.2))
        if not opened:
            continue
        for path in media_paths:
            url = f"http://{ip}:{port}{path}"
            for body, action in (
                (get_profiles, "http://www.onvif.org/ver10/media/wsdl/GetProfiles"),
                (get_configs, "http://www.onvif.org/ver10/media/wsdl/GetVideoEncoderConfigurations"),
            ):
                try:
                    text = _soap_post(url, body, action, timeout, user, password)
                    if "VideoEncoder" in text or "Profile" in text or "Width" in text:
                        endpoint = url
                        break
                except Exception as e:
                    result["error"] = str(e)
                    continue
            if endpoint:
                break
        if endpoint:
            break

    if not endpoint or not text:
        if not result["error"]:
            result["error"] = "No se pudo leer GetProfiles/VideoEncoder (auth o sin ONVIF Media)"
        return result

    result["endpoint"] = endpoint
    try:
        cleaned = _clean_xml(text)
        root = ET.fromstring(cleaned)
        profiles = []
        # Profiles con VideoEncoderConfiguration embebida
        for prof in root.findall(".//Profiles") + root.findall(".//Profile"):
            enc_node = prof.find(".//VideoEncoderConfiguration")
            if enc_node is None:
                continue
            enc = _extract_encoder_from_node(enc_node)
            pname = prof.find("Name")
            if pname is not None and pname.text:
                enc["profile_name"] = pname.text.strip()
            enc["profile_token"] = prof.attrib.get("token", "")
            profiles.append(enc)
        # Lista plana de VideoEncoderConfiguration
        if not profiles:
            for node in root.findall(".//Configurations") + root.findall(".//VideoEncoderConfiguration"):
                if "VideoEncoder" not in (node.tag or "") and node.find(".//Encoding") is None:
                    continue
                enc = _extract_encoder_from_node(node)
                if enc.get("width") or enc.get("bitrate_kbps") or enc.get("encoding"):
                    profiles.append(enc)
        # Regex fallback
        if not profiles:
            for m in re.finditer(
                r"<Width>(\d+)</Width>.*?<Height>(\d+)</Height>.*?(?:<FrameRateLimit>(\d+)</FrameRateLimit>)?.*?(?:<BitrateLimit>(\d+)</BitrateLimit>)?",
                text,
                re.I | re.S,
            ):
                profiles.append(
                    {
                        "name": "",
                        "encoding": "",
                        "width": int(m.group(1)),
                        "height": int(m.group(2)),
                        "fps": int(m.group(3)) if m.group(3) else None,
                        "bitrate_kbps": int(m.group(4)) if m.group(4) else None,
                        "gov_length": None,
                    }
                )
        result["profiles"] = profiles
        result["ok"] = bool(profiles)
        result["lag_flags"], result["manual_hints"] = evaluate_encoder_lag(profiles)
    except Exception as e:
        result["error"] = f"Parse video: {e}"
    return result


def evaluate_encoder_lag(profiles: List[Dict]) -> Tuple[List[str], List[str]]:
    """Reglas de configuración que provocan lag/retardo en NVR (sobre todo WiFi)."""
    flags: List[str] = []
    hints: List[str] = []
    if not profiles:
        return flags, hints

    # Ordenar por bitrate/resolución: el mayor suele ser main stream
    def score(p: Dict) -> int:
        w = p.get("width") or 0
        h = p.get("height") or 0
        br = p.get("bitrate_kbps") or 0
        return br * 10 + w * h

    main = max(profiles, key=score)
    w = main.get("width") or 0
    h = main.get("height") or 0
    br = main.get("bitrate_kbps")
    fps = main.get("fps")
    gov = main.get("gov_length")
    enc = (main.get("encoding") or "").upper()
    pixels = w * h

    if br is not None and br >= 4096:
        flags.append(f"BITRATE_MUY_ALTO:{br}kbps")
        hints.append(
            f"Main stream a {br} kbps es excesivo para WiFi/CCTV. "
            "En cámara Dahua: Configurar → Video → Codificación → Main: CBR 1024–2048 kbps."
        )
    elif br is not None and br >= 2500:
        flags.append(f"BITRATE_ALTO:{br}kbps")
        hints.append(
            f"Main stream {br} kbps: baje a ~1536 kbps (WiFi) o use sub-stream en vista en vivo del NVR."
        )

    if pixels >= 2560 * 1440:  # ~2K/5MP
        flags.append(f"RESOLUCION_ALTA:{w}x{h}")
        hints.append(
            f"Resolución {w}x{h}: en WiFi use 1280x720 o 1920x1080 en main; "
            "en NVR muestre sub-stream (D1/720p) para live y main solo para grabación."
        )
    elif pixels >= 1920 * 1080 and (br is None or br >= 2048):
        flags.append(f"FULLHD_BITRATE:{w}x{h}")
        hints.append(
            "1080p con bitrate alto satura WiFi si varios NVR piden el main a la vez. "
            "Sub-stream para live en cada NVR."
        )

    if fps is not None and fps >= 25:
        flags.append(f"FPS_ALTO:{fps}")
        hints.append(f"FPS {fps}: baje a 10–15 fps en main (CCTV). Menos carga y menos lag.")

    if gov is not None and fps and gov < max(1, fps):
        flags.append(f"IFRAME_DEMASIADO_FRECUENTE:gov={gov}")
        hints.append(
            f"I-frame (GovLength)={gov} con FPS={fps}: ponga I-frame = FPS (ej. 15 si fps=15). "
            "I-frames muy frecuentes suben bitrate y provocan microcortes."
        )

    if enc in ("JPEG", "MJPEG"):
        flags.append("CODEC_MJPEG")
        hints.append("Codec MJPEG: cambie a H.264 o H.265 (mucho menos ancho de banda).")

    if len(profiles) == 1:
        hints.append(
            "Solo se leyó 1 perfil: active/configure sub-stream en la cámara Dahua "
            "(subtype=1) y en el NVR use sub para visualización."
        )

    return flags, hints

