"""Descubre servicios locales expuestos como subdominios ``*.localhost``.

Fase de descubrimiento del sistema ``local_hub`` (reverse proxy Caddy +
dashboard Glance). Lee el manifiesto YAML de servicios, normaliza la metadata
de cada uno, opcionalmente añade los servicios del registry con puerto, y
comprueba si cada servicio está vivo (puerto TCP aceptando conexiones).

La función es robusta: nunca lanza por un servicio caído (lo marca ``up=False``)
ni por un fallo de ``fn doctor`` (captura y continúa solo con el manifiesto).
"""

import json
import os
import socket
import subprocess
import sys

import yaml

# Alias para derivar el subdominio de un servicio del registry a partir de su
# nombre. Para los nombres no listados se usa el primer token antes de "_".
_SUBDOMAIN_ALIAS = {
    "dag_engine": "dag",
    "registry_api": "registry",
    "sqlite_api": "sqlite",
    "osint_db": "osint",
    "services_api": "services",
    "web_proxy": "proxy",
}


def _is_port_up(port: int) -> bool:
    """Devuelve True si 127.0.0.1:<port> acepta conexiones TCP.

    Comprobación pura de puerto (no HTTP): algunos servicios no responden 200
    en ``/`` pero sí aceptan la conexión. Timeout corto para no bloquear.
    """
    try:
        with socket.create_connection(("127.0.0.1", int(port)), timeout=0.5):
            return True
    except (OSError, ValueError, TypeError):
        return False


def _normalize_manifest_service(svc: dict) -> dict:
    """Normaliza un servicio del manifiesto a todas las claves esperadas."""
    name = str(svc.get("name", "")).strip()
    subdomain = str(svc.get("subdomain", "") or name).strip()
    try:
        port = int(svc.get("port", 0) or 0)
    except (ValueError, TypeError):
        port = 0
    return {
        "name": name,
        "subdomain": subdomain,
        "port": port,
        "health_path": str(svc.get("health_path") or "/"),
        "title": str(svc.get("title") or name),
        "icon": str(svc.get("icon") or ""),
        "category": str(svc.get("category") or "Otros"),
        "rewrite_host": bool(svc.get("rewrite_host", False)),
        "up": _is_port_up(port) if port > 0 else False,
    }


def _is_registry_root(path: str) -> bool:
    """Una raíz válida del registry tiene registry.db Y el paquete cmd/fn.

    El doble marcador evita falsos positivos como un registry.db vacío y
    espurio dentro de python/ (la regla db_locations exige que registry.db
    solo viva en la raíz, pero protegemos por si acaso).
    """
    return os.path.isfile(os.path.join(path, "registry.db")) and os.path.isdir(
        os.path.join(path, "cmd", "fn")
    )


def _find_registry_root() -> str:
    """Localiza la raíz del registry (FN_REGISTRY_ROOT o subiendo hasta la raíz real)."""
    root = os.environ.get("FN_REGISTRY_ROOT")
    if root and _is_registry_root(root):
        return root
    cur = os.path.abspath(os.path.dirname(__file__))
    while True:
        if _is_registry_root(cur):
            return cur
        parent = os.path.dirname(cur)
        if parent == cur:
            break
        cur = parent
    return os.getcwd()


def _derive_subdomain(name: str) -> str:
    """Deriva un subdominio del nombre de un servicio del registry."""
    name = (name or "").strip()
    if name in _SUBDOMAIN_ALIAS:
        return _SUBDOMAIN_ALIAS[name]
    return name.split("_", 1)[0] if name else name


def _fetch_registry_services(root: str) -> list[dict]:
    """Obtiene los servicios del registry con puerto via ``fn doctor``.

    Intenta ``fn doctor services-spec --json`` y cae a ``services --json``.
    Devuelve lista vacía si ambos fallan (la función sigue con el manifiesto).
    """
    fn_bin = os.path.join(root, "fn")
    cmd_base = [fn_bin] if os.path.isfile(fn_bin) else ["fn"]
    for sub in ("services-spec", "services"):
        try:
            proc = subprocess.run(
                cmd_base + ["doctor", sub, "--json"],
                cwd=root,
                capture_output=True,
                text=True,
                timeout=20,
            )
        except (OSError, subprocess.SubprocessError):
            continue
        if proc.returncode != 0 or not proc.stdout.strip():
            continue
        try:
            data = json.loads(proc.stdout)
        except (json.JSONDecodeError, ValueError):
            continue
        if isinstance(data, dict):
            for key in ("services", "items", "results"):
                if isinstance(data.get(key), list):
                    data = data[key]
                    break
        if isinstance(data, list):
            return data
    return []


def discover_local_services(manifest_path: str, include_registry: bool = True) -> list[dict]:
    """Descubre y normaliza los servicios locales del manifiesto local_hub.

    Args:
        manifest_path: ruta al manifiesto YAML (``apps/local_hub/local_services.yaml``).
        include_registry: si True añade los servicios del registry con puerto>0 que
            no estén ya en el manifiesto (dedup por port y por subdomain).

    Returns:
        Lista de dicts normalizados, cada uno con las claves: name, subdomain, port,
        health_path, title, icon, category, rewrite_host, up. La clave
        ``rewrite_host`` (bool) es passthrough del manifiesto (default ``False``;
        siempre ``False`` para los servicios añadidos desde el registry) y la
        consume ``render_caddyfile`` para reescribir el header ``Host`` del
        upstream en servicios que lo validan (ej. Jupyter).
    """
    try:
        with open(manifest_path, "r", encoding="utf-8") as fh:
            manifest = yaml.safe_load(fh) or {}
    except (OSError, yaml.YAMLError) as exc:
        raise RuntimeError(f"discover_local_services: cannot read manifest {manifest_path}: {exc}") from exc

    raw_services = manifest.get("services") or []
    result: list[dict] = []
    seen_ports: set[int] = set()
    seen_subdomains: set[str] = set()

    for svc in raw_services:
        if not isinstance(svc, dict):
            continue
        norm = _normalize_manifest_service(svc)
        result.append(norm)
        if norm["port"] > 0:
            seen_ports.add(norm["port"])
        if norm["subdomain"]:
            seen_subdomains.add(norm["subdomain"])

    if include_registry:
        try:
            root = _find_registry_root()
            for svc in _fetch_registry_services(root):
                if not isinstance(svc, dict):
                    continue
                try:
                    port = int(svc.get("port", 0) or 0)
                except (ValueError, TypeError):
                    port = 0
                if port <= 0 or port in seen_ports:
                    continue
                name = str(svc.get("name", "")).strip()
                subdomain = _derive_subdomain(name)
                if subdomain in seen_subdomains:
                    continue
                result.append({
                    "name": name,
                    "subdomain": subdomain,
                    "port": port,
                    "health_path": str(svc.get("health_endpoint") or "/"),
                    "title": name,
                    "icon": "",
                    "category": "Registry",
                    "rewrite_host": False,
                    "up": _is_port_up(port),
                })
                seen_ports.add(port)
                seen_subdomains.add(subdomain)
        except Exception:
            # fn doctor opcional: si algo falla, seguimos solo con el manifiesto.
            pass

    return result


if __name__ == "__main__":
    path = sys.argv[1] if len(sys.argv) > 1 else "apps/local_hub/local_services.yaml"
    services = discover_local_services(path)
    print(json.dumps(services, indent=2, ensure_ascii=False))
