#!/usr/bin/env python3
"""
knowledge_graph.py — Visualización de un grafo de conocimiento con NetworkX

Construye un grafo de conocimiento con las herramientas del stack del podcast,
muestra consultas sobre el grafo, y genera una imagen PNG del mismo.

Propósito para el episodio 828:
  Demostrar visualmente qué es un "grafo de conocimiento" — la base de GraphRAG.
  Se ejecuta después de la comparativa RAG vs GraphRAG para mostrar un grafo real.

Uso:
  python knowledge_graph.py              # Modo interactivo (consulta + PNG)
  python knowledge_graph.py --no-png     # Solo consultas, sin guardar imagen
  python knowledge_graph.py --png-only   # Solo generar PNG

Requisitos:
  pip install networkx matplotlib
"""

import argparse
import sys
from typing import Dict, List, Tuple

try:
    import networkx as nx
except ImportError:
    print("❌ Falta networkx. Instala con: pip install networkx matplotlib")
    sys.exit(1)

try:
    from networkx.drawing.nx_pydot import graphviz_layout
except ImportError:
    print("❌ Falta pydot. Instala con: pip install pydot")
    sys.exit(1)

try:
    import matplotlib.pyplot as plt
except ImportError:
    print("❌ Falta matplotlib. Instala con: pip install matplotlib")
    sys.exit(1)


# ─── Datos del grafo ─────────────────────────────────────────────────────────

# Nodos: (id, nombre, tipo, descripcion)
ENTIDADES: List[Tuple[int, str, str, str]] = [
    (1, "Traefik", "software", "Reverse proxy para servicios Docker"),
    (2, "Docker", "software", "Plataforma de contenedores"),
    (3, "Podman", "software", "Alternativa a Docker sin daemon"),
    (4, "Ollama", "software", "Ejecución local de modelos LLM"),
    (5, "Open WebUI", "software", "Interfaz web para LLMs locales"),
    (6, "LightRAG", "software", "GraphRAG ligero con actualización incremental"),
    (7, "sqlite-vec", "libreria", "Extensión vectorial para SQLite"),
    (8, "Whisper", "software", "Speech-to-text local"),
    (9, "Piper TTS", "software", "Text-to-speech local"),
    (10, "LLaVA", "modelo", "Modelo de visión local"),
    (11, "MCP", "protocolo", "Model Context Protocol"),
    (12, "OpenCode", "software", "CLI/TUI para desarrollo asistido con IA"),
    (13, "Hermes Agent", "software", "Agente autónomo con skills y MCP"),
    (14, "Anacleto", "software", "Motor de orquestación de agentes en Rust"),
    (15, "CrewAI", "libreria", "Framework Python para equipos de agentes"),
    (16, "systemd", "software", "Init system y gestor de servicios"),
    (17, "Quadlets", "software", "Contenedores gestionados por systemd"),
    (18, "NetworkX", "libreria", "Biblioteca Python para análisis de grafos"),
]

# Aristas: (origen_id, destino_id, relacion)
RELACIONES: List[Tuple[int, int, str]] = [
    # Traefik
    (1, 2, "gestiona tráfico de"),
    (1, 3, "también funciona con"),
    (1, 17, "se despliega con"),
    # Docker
    (2, 3, "es alternativa de"),
    (2, 17, "gestionado por"),
    # Ollama
    (4, 5, "tiene interfaz en"),
    (4, 11, "expone API compatible"),
    # Open WebUI
    (5, 11, "soporta"),
    (5, 6, "integra RAG con"),
    # LightRAG
    (6, 7, "puede usar"),
    (6, 18, "construye grafos con"),
    # Agentes
    (12, 11, "implementa"),
    (13, 11, "implementa"),
    (14, 11, "implementa"),
    (12, 4, "usa modelos de"),
    (13, 4, "usa modelos de"),
    (14, 4, "usa modelos de"),
    (15, 12, "comparable con"),
    # Multimedia
    (8, 4, "se integra con"),
    (9, 4, "se integra con"),
    (10, 4, "se ejecuta vía"),
    # Infraestructura
    (16, 17, "gestiona"),
    (17, 2, "gestiona"),
    (17, 3, "gestiona"),
]

# Colores por tipo de entidad (para el gráfico)
COLORES_TIPO = {
    "software": "#4A90D9",  # azul
    "libreria": "#50C878",  # verde
    "modelo": "#E8A838",  # naranja
    "protocolo": "#9B59B6",  # púrpura
}


# ─── Construcción del grafo ──────────────────────────────────────────────────


def construir_grafo() -> nx.DiGraph:
    """Construye el grafo dirigido con entidades y relaciones."""
    G = nx.DiGraph()

    # Añadir nodos con atributos
    for eid, nombre, tipo, desc in ENTIDADES:
        G.add_node(nombre, tipo=tipo, descripcion=desc, id=eid)

    # Añadir aristas con atributos
    for origen_id, destino_id, relacion in RELACIONES:
        # Buscar nombres por ID
        origen_nombre = next(n for eid, n, _, _ in ENTIDADES if eid == origen_id)
        destino_nombre = next(n for eid, n, _, _ in ENTIDADES if eid == destino_id)
        G.add_edge(origen_nombre, destino_nombre, relacion=relacion)

    return G


# ─── Consultas sobre el grafo ────────────────────────────────────────────────


def consultar_relaciones(G: nx.DiGraph, entidad: str) -> None:
    """
    Consulta todas las relaciones de una entidad: qué sale de ella y qué llega.
    Esta es la consulta equivalente a la del SQL en demo-graphrag.sh.
    """
    print(f"\n{'=' * 60}")
    print(f"  CONSULTA: ¿Qué está relacionado con «{entidad}»?")
    print(f"{'=' * 60}\n")

    if entidad not in G:
        print(f"  ⚠ La entidad «{entidad}» no existe en el grafo.\n")
        return

    # Relaciones salientes (entidad → destino)
    salientes = list(G.out_edges(entidad, data=True))
    if salientes:
        print(f"  ▶ {entidad} se relaciona CON:")
        for _, destino, data in salientes:
            print(f"     • {data['relacion']} → {destino}")
    else:
        print(f"  ▶ {entidad} no tiene relaciones salientes.")

    # Relaciones entrantes (origen → entidad)
    entrantes = list(G.in_edges(entidad, data=True))
    if entrantes:
        print(f"\n  ▶ {entidad} es relacionado POR:")
        for origen, _, data in entrantes:
            print(f"     • {origen} → {data['relacion']}")
    else:
        print(f"\n  ▶ {entidad} no tiene relaciones entrantes.")

    print()


def consultar_camino(G: nx.DiGraph, origen: str, destino: str) -> None:
    """
    Encuentra el camino más corto entre dos entidades en el grafo.
    Demuestra que el grafo permite navegación semántica.
    """
    print(f"\n{'=' * 60}")
    print(f"  CONSULTA: Camino de «{origen}» a «{destino}»")
    print(f"{'=' * 60}\n")

    if origen not in G:
        print(f"  ⚠ La entidad «{origen}» no existe.\n")
        return
    if destino not in G:
        print(f"  ⚠ La entidad «{destino}» no existe.\n")
        return

    try:
        camino = nx.shortest_path(G, origen, destino)
        print(f"  Ruta encontrada ({len(camino) - 1} saltos):")
        for i in range(len(camino) - 1):
            a, b = camino[i], camino[i + 1]
            relacion = G[a][b]["relacion"]
            print(f"     {a} ──[{relacion}]──→ {b}")
        print()
    except nx.NetworkXNoPath:
        print(f"  ⚠ No hay camino entre «{origen}» y «{destino}».\n")


def consultar_vecinos_comunes(G: nx.DiGraph, entidad_a: str, entidad_b: str) -> None:
    """
    Encuentra vecinos comunes entre dos entidades.
    Útil para descubrir conexiones indirectas.
    """
    print(f"\n{'=' * 60}")
    print(f"  CONSULTA: Vecinos comunes de «{entidad_a}» y «{entidad_b}»")
    print(f"{'=' * 60}\n")

    if entidad_a not in G or entidad_b not in G:
        print("  ⚠ Una de las entidades no existe.\n")
        return

    # Vecinos: predecesores + sucesores
    vecinos_a = set(G.predecessors(entidad_a)) | set(G.successors(entidad_a))
    vecinos_b = set(G.predecessors(entidad_b)) | set(G.successors(entidad_b))
    comunes = vecinos_a & vecinos_b

    if comunes:
        print(f"  Entidades conectadas tanto con «{entidad_a}» como con «{entidad_b}»:")
        for v in sorted(comunes):
            print(f"     • {v}")
    else:
        print(f"  No hay vecinos comunes entre «{entidad_a}» y «{entidad_b}».")
    print()


def mostrar_estadisticas(G: nx.DiGraph) -> None:
    """Muestra estadísticas básicas del grafo."""
    print(f"\n{'=' * 60}")
    print(f"  ESTADÍSTICAS DEL GRAFO DE CONOCIMIENTO")
    print(f"{'=' * 60}\n")
    print(f"  • Nodos (entidades):     {G.number_of_nodes()}")
    print(f"  • Aristas (relaciones):  {G.number_of_edges()}")
    print(f"  • Densidad:              {nx.density(G):.4f}")
    print(f"  • ¿Es conexo?:           {'Sí' if nx.is_weakly_connected(G) else 'No'}")
    print(
        f"  • Diámetro:              {nx.diameter(G.to_undirected()) if nx.is_weakly_connected(G) else 'N/A'}"
    )
    print()


# ─── Visualización ───────────────────────────────────────────────────────────


def generar_png(G: nx.DiGraph, archivo: str = "grafo_conocimiento.png") -> str:
    """
    Genera una imagen PNG del grafo usando matplotlib.
    Los nodos se colorean por tipo y las aristas se etiquetan con la relación.
    """
    print(f"  Generando visualización: {archivo} ...", end=" ")

    plt.figure(figsize=(16, 12))

    # Layout: usar Graphviz si está disponible, sino spring_layout
    try:
        pos = graphviz_layout(G, prog="dot")
    except Exception:
        pos = nx.spring_layout(G, k=1.5, seed=42, iterations=50)

    # Dibujar nodos por tipo (cada tipo con su color)
    for tipo in COLORES_TIPO:
        nodos_tipo = [n for n, attr in G.nodes(data=True) if attr["tipo"] == tipo]
        nx.draw_networkx_nodes(
            G,
            pos,
            nodelist=nodos_tipo,
            node_color=COLORES_TIPO[tipo],
            node_size=2500,
            node_shape="o",
            edgecolors="white",
            linewidths=1.5,
            alpha=0.95,
        )

    # Dibujar aristas con flechas
    nx.draw_networkx_edges(
        G,
        pos,
        edge_color="#888888",
        arrows=True,
        arrowsize=20,
        arrowstyle="->",
        width=1.5,
        alpha=0.7,
        connectionstyle="arc3,rad=0.1",
    )

    # Etiquetas de los nodos
    nx.draw_networkx_labels(
        G,
        pos,
        font_size=11,
        font_weight="bold",
        font_family="sans-serif",
    )

    # Etiquetas de las aristas (relaciones)
    edge_labels = {(a, b): data["relacion"] for a, b, data in G.edges(data=True)}
    nx.draw_networkx_edge_labels(
        G,
        pos,
        edge_labels=edge_labels,
        font_size=8,
        font_family="sans-serif",
        alpha=0.8,
        label_pos=0.5,
    )

    # Leyenda
    legend_elements = []
    for tipo, color in COLORES_TIPO.items():
        legend_elements.append(
            plt.scatter(
                [], [], c=color, s=150, label=tipo, edgecolors="white", linewidths=1
            )
        )
    plt.legend(
        handles=legend_elements,
        title="Tipo de entidad",
        loc="upper right",
        fontsize=10,
        title_fontsize=12,
    )

    plt.title(
        "Grafo de conocimiento — Stack de herramientas del podcast",
        fontsize=16,
        fontweight="bold",
        pad=20,
    )
    plt.axis("off")
    plt.tight_layout()
    plt.savefig(archivo, dpi=150, bbox_inches="tight", facecolor="#FAFAFA")
    plt.close()

    print("✅")
    return archivo


# ─── Main ────────────────────────────────────────────────────────────────────


def main():
    parser = argparse.ArgumentParser(
        description="Demo de grafo de conocimiento con NetworkX",
    )
    parser.add_argument(
        "--no-png",
        action="store_true",
        help="No generar PNG, solo mostrar consultas",
    )
    parser.add_argument(
        "--png-only",
        action="store_true",
        help="Solo generar PNG sin consultas",
    )
    args = parser.parse_args()

    print()
    print("╔══════════════════════════════════════════════════════════╗")
    print("║  GRAFO DE CONOCIMIENTO — Demo para el episodio 828     ║")
    print("║  Construido con NetworkX + matplotlib                  ║")
    print("╚══════════════════════════════════════════════════════════╝")

    # Construir el grafo
    print("\n  Construyendo grafo...")
    G = construir_grafo()
    print(
        f"  ✓ Grafo creado: {G.number_of_nodes()} nodos, {G.number_of_edges()} aristas"
    )

    if not args.png_only:
        # Mostrar estadísticas
        mostrar_estadisticas(G)

        # Consulta principal: ¿Qué está relacionado con Traefik?
        consultar_relaciones(G, "Traefik")

        # Consulta: camino de OpenCode a Quadlets
        consultar_camino(G, "OpenCode", "Quadlets")

        # Consulta: vecinos comunes de Ollama y Open WebUI
        consultar_vecinos_comunes(G, "Ollama", "Open WebUI")

    if not args.no_png:
        archivo = generar_png(G)
        print(f"\n  📁 Imagen guardada: {archivo}")
        print(f"     Ábrela para ver el grafo visualmente.")

    print("\n  ✅ Demo completada.")
    print()


if __name__ == "__main__":
    main()
