#!/usr/bin/env python3
"""
editor_sipi_dev.py - Editor para desarrolladores de SiPi.

Pensado para gente que quiere ir mas alla de escribir un programa .sipi
suelto: crear paquetes/modulos reutilizables, declarar dependencias de
un proyecto, y (la parte de "tester") automatizar pruebas de estres
sobre el propio editor de SiPi para encontrarle bugs antes que un
usuario final.

No reinventa nada que SiPi ya tenga resuelto de verdad -- se apoya en
el sistema de paquetes/modulos/dependencias que YA EXISTE en
'sipi.py' (instalar_modulo, sipi_paquetes.json como manifiesto de
dependencias, el registro de paquetes de GitHub). Esto es una interfaz
grafica real sobre esas piezas, no un sistema paralelo inventado.

Tres pestanas:
  1. Paquetes y dependencias -- ver/editar 'sipi_paquetes.json',
     instalar modulos desde una URL o desde GitHub, ver que hay
     instalado.
  2. Crear comando / paquete -- arma un archivo .sipi nuevo con
     'funcion's (que es como SiPi define comandos reutilizables),
     usando una plantilla real y funcional, listo para publicarse
     como modulo.
  3. Tester -- corre pruebas de estres automatizadas: tocar un boton
     N veces, tipear codigo random valido/invalido, y correr un lote
     de archivos .sipi de ejemplo repetidas veces buscando
     excepciones no manejadas o crashes, con un log de resultados.

Uso:
    python editor_sipi_dev.py
"""
import os
import sys
import re
import json
import time
import subprocess
import threading
import tempfile

try:
    import tkinter as tk
    from tkinter import filedialog, messagebox, ttk
    try:
        import customtkinter as ctk
        ctk.set_appearance_mode("dark")
        ctk.set_default_color_theme("blue")
        _CTK_DISPONIBLE = True
    except ImportError:
        ctk = None
        _CTK_DISPONIBLE = False
except ImportError:
    print("[SiPi Dev] Este editor necesita tkinter (igual que el editor normal).")
    print(" - Linux (Debian/Ubuntu): sudo apt install python3-tk")
    print(" - Windows: reinstala Python marcando 'tcl/tk and IDLE'.")
    sys.exit(1)

try:
    from editor_sipi import _es_exe_empaquetado, _bandera_reentrada_motor, _intentar_correr_como_motor_interno
except ImportError:
    def _es_exe_empaquetado():
        return getattr(sys, "frozen", False)

    def _bandera_reentrada_motor():
        return "--sipi-motor-interno"

    def _intentar_correr_como_motor_interno():
        return False


COLORES = {
    "fondo": "#1e1e2e", "texto": "#e0e0f0", "barra": "#181825",
    "acento": "#7ec4ff", "ok": "#40a02b", "error": "#d20f39", "aviso": "#df8e1d",
    "boton": "#313244",
}

PLANTILLA_COMANDO = '''# Paquete SiPi generado con el Editor para Desarrolladores
# Nombre: {nombre}
# Cada 'funcion' que definas aca es un COMANDO reutilizable: cualquier
# programa que haga  importar "{nombre}"  puede llamarlo por su nombre,
# igual que a los comandos que ya trae SiPi (decir, repetir, etc.).
# Esto es codigo SiPi real -- se corre con el mismo motor de siempre,
# no hace falta tocar el interprete en Python para agregar un comando
# nuevo.

funcion {nombre_funcion}({parametros})
    # TODO: escribi aca la logica real de tu comando.
    decir "Comando '{nombre_funcion}' ejecutado."
    devolver verdadero
fin

# Ejemplo de uso (dejalo comentado si este archivo es solo el paquete):
# {nombre_funcion}({ejemplo_args})
'''


def _leer_json_seguro(ruta):
    if os.path.exists(ruta):
        try:
            with open(ruta, "r", encoding="utf-8") as f:
                return json.load(f)
        except (json.JSONDecodeError, OSError):
            return None
    return None


class EditorDev:
    def __init__(self, root):
        self.root = root
        self.root.title("SiPi - Editor para Desarrolladores")
        self.root.geometry("1180x760")
        self.root.configure(bg=COLORES["fondo"])
        self.aqui = os.path.dirname(os.path.abspath(__file__))
        self.carpeta_proyecto = os.getcwd()
        self._detener_tester = threading.Event()

        self._aplicar_icono()
        self._construir_interfaz()

    # ------------------------------------------------------------------
    def _aplicar_icono(self):
        try:
            import iconos_datos
            img = tk.PhotoImage(data=iconos_datos.ICONOS_EDITOR_B64["sipi_logo"])
            self.root.iconphoto(True, img)
            self._icono_ref = img
        except Exception:
            pass

    def _construir_interfaz(self):
        barra_superior = tk.Frame(self.root, bg=COLORES["barra"], height=44)
        barra_superior.pack(fill=tk.X, side=tk.TOP)
        tk.Label(barra_superior, text="🛠  SiPi - Editor para Desarrolladores",
                 bg=COLORES["barra"], fg=COLORES["texto"], font=("Segoe UI", 13, "bold")
                 ).pack(side=tk.LEFT, padx=14, pady=8)
        tk.Button(barra_superior, text="📁 Carpeta del proyecto...", command=self._elegir_carpeta_proyecto,
                  bg=COLORES["boton"], fg=COLORES["texto"], bd=0, padx=10
                  ).pack(side=tk.LEFT, padx=6, pady=6)
        self.etiqueta_proyecto = tk.Label(barra_superior, text=self.carpeta_proyecto,
                                           bg=COLORES["barra"], fg="#a6adc8", font=("Consolas", 9))
        self.etiqueta_proyecto.pack(side=tk.LEFT, padx=6)

        notebook_estilo = ttk.Style()
        try:
            notebook_estilo.theme_use("clam")
        except tk.TclError:
            pass
        notebook_estilo.configure("TNotebook", background=COLORES["fondo"], borderwidth=0)
        notebook_estilo.configure("TNotebook.Tab", background=COLORES["barra"], foreground=COLORES["texto"],
                                   padding=[14, 8])
        notebook_estilo.map("TNotebook.Tab", background=[("selected", COLORES["boton"])])

        self.pestanas = ttk.Notebook(self.root)
        self.pestanas.pack(fill=tk.BOTH, expand=True)

        self.tab_paquetes = tk.Frame(self.pestanas, bg=COLORES["fondo"])
        self.tab_comandos = tk.Frame(self.pestanas, bg=COLORES["fondo"])
        self.tab_explorar = tk.Frame(self.pestanas, bg=COLORES["fondo"])
        self.tab_tester = tk.Frame(self.pestanas, bg=COLORES["fondo"])
        self.pestanas.add(self.tab_paquetes, text="📦 Paquetes y dependencias")
        self.pestanas.add(self.tab_comandos, text="⚙️ Crear comando / paquete")
        self.pestanas.add(self.tab_explorar, text="🌐 Explorar librerías")
        self.pestanas.add(self.tab_tester, text="🧪 Tester")

        self._construir_tab_paquetes()
        self._construir_tab_comandos()
        self._construir_tab_explorar()
        self._construir_tab_tester()

    def _modulo_registro(self):
        """Importa 'sipi_registro' de forma perezosa (recien cuando hace
        falta) para no romper el arranque del editor en instalaciones
        viejas que no tengan ese archivo todavia."""
        if getattr(self, "_registro_mod", None) is not None:
            return self._registro_mod
        if self.aqui not in sys.path:
            sys.path.insert(0, self.aqui)
        import sipi_registro
        self._registro_mod = sipi_registro
        return sipi_registro

    def _elegir_carpeta_proyecto(self):
        carpeta = filedialog.askdirectory(initialdir=self.carpeta_proyecto)
        if carpeta:
            self.carpeta_proyecto = carpeta
            self.etiqueta_proyecto.config(text=carpeta)
            self._cargar_manifiesto()
            self._refrescar_lista_instalados()

    # ================= PESTANA 1: PAQUETES Y DEPENDENCIAS =================
    def _construir_tab_paquetes(self):
        contenedor = self.tab_paquetes
        izquierda = tk.Frame(contenedor, bg=COLORES["fondo"])
        izquierda.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10)
        derecha = tk.Frame(contenedor, bg=COLORES["fondo"], width=360)
        derecha.pack(side=tk.RIGHT, fill=tk.Y, padx=10, pady=10)

        tk.Label(izquierda, text="sipi_paquetes.json (dependencias del proyecto)",
                 bg=COLORES["fondo"], fg=COLORES["texto"], font=("Segoe UI", 11, "bold")).pack(anchor="w")
        tk.Label(izquierda, text="Igual que un package.json/requirements.txt: declara aca los modulos\n"
                                  "que tu proyecto necesita, y 'instalar_dependencias()' (o el boton de\n"
                                  "abajo) los baja todos de una.",
                 bg=COLORES["fondo"], fg="#a6adc8", font=("Segoe UI", 9), justify="left").pack(anchor="w", pady=(0, 8))

        self.texto_manifiesto = tk.Text(izquierda, bg="#11111b", fg="#a6e3a1", insertbackground="#a6e3a1",
                                         font=("Consolas", 11), height=16, wrap="none", bd=0)
        self.texto_manifiesto.pack(fill=tk.BOTH, expand=True)

        botones_manifiesto = tk.Frame(izquierda, bg=COLORES["fondo"])
        botones_manifiesto.pack(fill=tk.X, pady=8)
        tk.Button(botones_manifiesto, text="💾 Guardar sipi_paquetes.json", command=self._guardar_manifiesto,
                  bg=COLORES["ok"], fg="white", bd=0, padx=10, pady=5).pack(side=tk.LEFT, padx=(0, 6))
        tk.Button(botones_manifiesto, text="⬇ Instalar todas las dependencias", command=self._instalar_todas_dependencias,
                  bg=COLORES["acento"], fg="#11111b", bd=0, padx=10, pady=5).pack(side=tk.LEFT, padx=6)
        tk.Button(botones_manifiesto, text="+ Agregar modulo", command=self._agregar_modulo_manifiesto,
                  bg=COLORES["boton"], fg=COLORES["texto"], bd=0, padx=10, pady=5).pack(side=tk.LEFT, padx=6)

        tk.Label(derecha, text="Instalar un modulo suelto", bg=COLORES["fondo"], fg=COLORES["texto"],
                 font=("Segoe UI", 11, "bold")).pack(anchor="w")
        tk.Label(derecha, text="Nombre, URL directa a un .sipi, o usuario/repo de GitHub:",
                 bg=COLORES["fondo"], fg="#a6adc8", font=("Segoe UI", 9)).pack(anchor="w", pady=(6, 2))
        self.entrada_instalar = tk.Entry(derecha, bg="#11111b", fg=COLORES["texto"], insertbackground=COLORES["texto"], bd=0)
        self.entrada_instalar.pack(fill=tk.X, ipady=5)
        tk.Button(derecha, text="⬇ Instalar", command=self._instalar_modulo_suelto,
                  bg=COLORES["acento"], fg="#11111b", bd=0, padx=10, pady=5).pack(anchor="w", pady=8)

        tk.Label(derecha, text="Modulos ya instalados (modulos_instalados/)", bg=COLORES["fondo"],
                 fg=COLORES["texto"], font=("Segoe UI", 11, "bold")).pack(anchor="w", pady=(16, 4))
        self.lista_instalados = tk.Listbox(derecha, bg="#11111b", fg=COLORES["texto"], bd=0, height=14,
                                            selectbackground=COLORES["acento"])
        self.lista_instalados.pack(fill=tk.BOTH, expand=True)
        tk.Button(derecha, text="⟳ Refrescar", command=self._refrescar_lista_instalados,
                  bg=COLORES["boton"], fg=COLORES["texto"], bd=0, padx=10, pady=4).pack(anchor="w", pady=6)

        self.estado_paquetes = tk.Label(contenedor, text="Listo.", bg=COLORES["barra"], fg="#a6adc8",
                                         anchor="w", font=("Segoe UI", 9))
        self.estado_paquetes.pack(side=tk.BOTTOM, fill=tk.X)

        self._cargar_manifiesto()
        self._refrescar_lista_instalados()

    def _ruta_manifiesto(self):
        return os.path.join(self.carpeta_proyecto, "sipi_paquetes.json")

    def _cargar_manifiesto(self):
        datos = _leer_json_seguro(self._ruta_manifiesto())
        if datos is None:
            datos = {"modulos": {}}
        self.texto_manifiesto.delete("1.0", tk.END)
        self.texto_manifiesto.insert("1.0", json.dumps(datos, indent=2, ensure_ascii=False))

    def _guardar_manifiesto(self):
        contenido = self.texto_manifiesto.get("1.0", "end-1c")
        try:
            datos = json.loads(contenido)
        except json.JSONDecodeError as e:
            messagebox.showerror("SiPi Dev", f"El JSON no es valido:\n{e}")
            return
        try:
            with open(self._ruta_manifiesto(), "w", encoding="utf-8") as f:
                json.dump(datos, f, indent=2, ensure_ascii=False)
        except OSError as e:
            messagebox.showerror("SiPi Dev", f"No se pudo guardar: {e}")
            return
        self.estado_paquetes.config(text=f"Guardado: {self._ruta_manifiesto()}")

    def _agregar_modulo_manifiesto(self):
        ventana = tk.Toplevel(self.root)
        ventana.title("Agregar modulo")
        ventana.configure(bg=COLORES["fondo"])
        ventana.geometry("420x160")
        tk.Label(ventana, text="Nombre de la dependencia:", bg=COLORES["fondo"], fg=COLORES["texto"]).pack(anchor="w", padx=10, pady=(10, 0))
        e_nombre = tk.Entry(ventana, bg="#11111b", fg=COLORES["texto"], insertbackground=COLORES["texto"])
        e_nombre.pack(fill=tk.X, padx=10, pady=4)
        tk.Label(ventana, text="URL o nombre en el registro:", bg=COLORES["fondo"], fg=COLORES["texto"]).pack(anchor="w", padx=10)
        e_origen = tk.Entry(ventana, bg="#11111b", fg=COLORES["texto"], insertbackground=COLORES["texto"])
        e_origen.pack(fill=tk.X, padx=10, pady=4)

        def _confirmar():
            nombre = e_nombre.get().strip()
            origen = e_origen.get().strip()
            if not nombre or not origen:
                return
            contenido = self.texto_manifiesto.get("1.0", "end-1c")
            try:
                datos = json.loads(contenido) if contenido.strip() else {"modulos": {}}
            except json.JSONDecodeError:
                datos = {"modulos": {}}
            datos.setdefault("modulos", {})[nombre] = origen
            self.texto_manifiesto.delete("1.0", tk.END)
            self.texto_manifiesto.insert("1.0", json.dumps(datos, indent=2, ensure_ascii=False))
            ventana.destroy()

        tk.Button(ventana, text="Agregar", command=_confirmar, bg=COLORES["ok"], fg="white", bd=0,
                  padx=10, pady=5).pack(pady=10)

    def _correr_sipi_en_hilo(self, argv_extra, on_linea, on_fin):
        """Corre el motor de SiPi (sipi.py o sipi_protegido.py, el que
        exista) como subprocess real -- reusa el motor de verdad en vez
        de reimplementar instalacion de paquetes a mano -- y va
        mandando cada linea de salida a 'on_linea' a medida que llega,
        sin bloquear la interfaz."""
        motor = os.path.join(self.aqui, "sipi.py")
        if not os.path.exists(motor):
            motor = os.path.join(self.aqui, "sipi_protegido.py")
        if not os.path.exists(motor):
            on_linea("[SiPi Dev] No se encontro el motor de SiPi (sipi.py) al lado de este editor.\n")
            on_fin(1)
            return

        if _es_exe_empaquetado():
            comando = [sys.executable, _bandera_reentrada_motor()] + argv_extra
        else:
            comando = [sys.executable, motor] + argv_extra

        def _tarea():
            try:
                proceso = subprocess.Popen(
                    comando, cwd=self.carpeta_proyecto, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                    text=True, encoding="utf-8", errors="replace",
                    env={**os.environ, "PYTHONIOENCODING": "utf-8"},
                )
                for linea in proceso.stdout:
                    on_linea(linea)
                proceso.wait()
                on_fin(proceso.returncode)
            except Exception as e:
                on_linea(f"[SiPi Dev] ERROR: {e}\n")
                on_fin(1)

        threading.Thread(target=_tarea, daemon=True).start()

    def _instalar_todas_dependencias(self):
        self._guardar_manifiesto()
        if not os.path.exists(self._ruta_manifiesto()):
            messagebox.showwarning("SiPi Dev", "Guarda primero sipi_paquetes.json.")
            return
        self.estado_paquetes.config(text="Instalando dependencias, mira la consola del sistema...")
        # El motor de SiPi ya sabe leer sipi_paquetes.json e instalar todo
        # via '_instalar_dependencias' -- lo disparamos corriendo un
        # programa .sipi de una linea que llama a ese comando real.
        ruta_temp = os.path.join(tempfile.gettempdir(), "_sipi_dev_instalar_deps.sipi")
        with open(ruta_temp, "w", encoding="utf-8") as f:
            f.write("instalar_dependencias()\n")

        def on_linea(linea):
            self.root.after(0, lambda: self.estado_paquetes.config(text=linea.strip()[:120]))

        def on_fin(codigo):
            texto = "Dependencias instaladas." if codigo == 0 else f"Termino con codigo {codigo} (revisa la consola)."
            self.root.after(0, lambda: (self.estado_paquetes.config(text=texto), self._refrescar_lista_instalados()))

        self._correr_sipi_en_hilo([ruta_temp], on_linea, on_fin)

    def _instalar_modulo_suelto(self):
        origen = self.entrada_instalar.get().strip()
        if not origen:
            return
        self.estado_paquetes.config(text=f"Instalando '{origen}'...")
        origen_escapado = origen.replace('"', '\\"')
        ruta_temp = os.path.join(tempfile.gettempdir(), "_sipi_dev_instalar_modulo.sipi")
        with open(ruta_temp, "w", encoding="utf-8") as f:
            f.write(f'instalar_modulo "{origen_escapado}"\n')

        def on_linea(linea):
            self.root.after(0, lambda: self.estado_paquetes.config(text=linea.strip()[:120]))

        def on_fin(codigo):
            self.root.after(0, self._refrescar_lista_instalados)

        self._correr_sipi_en_hilo([ruta_temp], on_linea, on_fin)

    def _refrescar_lista_instalados(self):
        self.lista_instalados.delete(0, tk.END)
        carpeta = os.path.join(self.carpeta_proyecto, "modulos_instalados")
        registro = _leer_json_seguro(os.path.join(carpeta, "_registro.json")) or {}
        if not os.path.isdir(carpeta):
            self.lista_instalados.insert(tk.END, "(sin modulos instalados todavia)")
            return
        archivos = sorted(f for f in os.listdir(carpeta) if f.endswith(".sipi"))
        if not archivos:
            self.lista_instalados.insert(tk.END, "(sin modulos instalados todavia)")
            return
        for archivo in archivos:
            nombre = archivo[:-5]
            info = registro.get(nombre, {})
            fecha = info.get("instalado", "")[:10]
            self.lista_instalados.insert(tk.END, f"{nombre}  ({fecha})" if fecha else nombre)

    # ================= PESTANA 2: CREAR COMANDO / PAQUETE =================
    def _construir_tab_comandos(self):
        contenedor = self.tab_comandos
        arriba = tk.Frame(contenedor, bg=COLORES["fondo"])
        arriba.pack(fill=tk.X, padx=10, pady=10)

        tk.Label(arriba, text="Nombre del paquete/archivo:", bg=COLORES["fondo"], fg=COLORES["texto"]).grid(row=0, column=0, sticky="w")
        self.entrada_nombre_paquete = tk.Entry(arriba, bg="#11111b", fg=COLORES["texto"], insertbackground=COLORES["texto"], width=24)
        self.entrada_nombre_paquete.grid(row=0, column=1, padx=6)
        self.entrada_nombre_paquete.insert(0, "mi_paquete")

        tk.Label(arriba, text="Nombre del comando (funcion):", bg=COLORES["fondo"], fg=COLORES["texto"]).grid(row=0, column=2, sticky="w", padx=(16, 0))
        self.entrada_nombre_comando = tk.Entry(arriba, bg="#11111b", fg=COLORES["texto"], insertbackground=COLORES["texto"], width=24)
        self.entrada_nombre_comando.grid(row=0, column=3, padx=6)
        self.entrada_nombre_comando.insert(0, "mi_comando")

        tk.Label(arriba, text="Parametros (separados por coma):", bg=COLORES["fondo"], fg=COLORES["texto"]).grid(row=1, column=0, sticky="w", pady=(8, 0))
        self.entrada_parametros = tk.Entry(arriba, bg="#11111b", fg=COLORES["texto"], insertbackground=COLORES["texto"], width=24)
        self.entrada_parametros.grid(row=1, column=1, padx=6, pady=(8, 0))
        self.entrada_parametros.insert(0, "nombre")

        tk.Button(arriba, text="🪄 Generar plantilla", command=self._generar_plantilla_comando,
                  bg=COLORES["acento"], fg="#11111b", bd=0, padx=10, pady=5).grid(row=1, column=3, pady=(8, 0), sticky="e")

        self.texto_comando = tk.Text(contenedor, bg="#11111b", fg="#a6e3a1", insertbackground="#a6e3a1",
                                      font=("Consolas", 12), wrap="none", bd=0)
        self.texto_comando.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 6))
        self._generar_plantilla_comando()

        botones = tk.Frame(contenedor, bg=COLORES["fondo"])
        botones.pack(fill=tk.X, padx=10, pady=(0, 10))
        tk.Button(botones, text="💾 Guardar como .sipi", command=self._guardar_comando,
                  bg=COLORES["ok"], fg="white", bd=0, padx=10, pady=6).pack(side=tk.LEFT, padx=(0, 6))
        tk.Button(botones, text="▶ Probar este comando ahora", command=self._probar_comando,
                  bg=COLORES["acento"], fg="#11111b", bd=0, padx=10, pady=6).pack(side=tk.LEFT, padx=6)
        tk.Button(botones, text="🚀 Publicar en el registro", command=self._publicar_paquete_actual,
                  bg=COLORES["aviso"], fg="#11111b", bd=0, padx=10, pady=6).pack(side=tk.LEFT, padx=6)

        self.salida_comando = tk.Text(contenedor, bg="#11111b", fg=COLORES["texto"], bd=0, height=8, font=("Consolas", 10))
        self.salida_comando.pack(fill=tk.X, padx=10, pady=(0, 10))

    def _generar_plantilla_comando(self):
        nombre = (self.entrada_nombre_paquete.get().strip() or "mi_paquete")
        nombre_funcion = (self.entrada_nombre_comando.get().strip() or "mi_comando")
        parametros = self.entrada_parametros.get().strip()
        ejemplo_args = ", ".join(f'"{p.strip()}"' for p in parametros.split(",") if p.strip()) or '"valor"'
        contenido = PLANTILLA_COMANDO.format(
            nombre=nombre, nombre_funcion=nombre_funcion, parametros=parametros, ejemplo_args=ejemplo_args,
        )
        self.texto_comando.delete("1.0", tk.END)
        self.texto_comando.insert("1.0", contenido)

    def _guardar_comando(self):
        nombre = (self.entrada_nombre_paquete.get().strip() or "mi_paquete")
        carpeta = os.path.join(self.carpeta_proyecto, "modulos_instalados")
        os.makedirs(carpeta, exist_ok=True)
        ruta = os.path.join(carpeta, f"{nombre}.sipi")
        with open(ruta, "w", encoding="utf-8") as f:
            f.write(self.texto_comando.get("1.0", "end-1c"))
        messagebox.showinfo("SiPi Dev", f"Guardado en:\n{ruta}\n\nOtros programas lo usan con:\n  importar \"modulos_instalados/{nombre}.sipi\"")
        self._refrescar_lista_instalados()

    def _probar_comando(self):
        codigo = self.texto_comando.get("1.0", "end-1c")
        # Descomenta la linea de ejemplo (si esta comentada) para que la
        # prueba realmente llame al comando y no solo lo defina.
        codigo_prueba = re.sub(r"^#\s*(\w+\([^\n]*\))\s*$", r"\1", codigo, flags=re.MULTILINE)
        ruta_temp = os.path.join(tempfile.gettempdir(), "_sipi_dev_probar_comando.sipi")
        with open(ruta_temp, "w", encoding="utf-8") as f:
            f.write(codigo_prueba)
        self.salida_comando.delete("1.0", tk.END)
        self.salida_comando.insert(tk.END, "Ejecutando...\n")

        def on_linea(linea):
            self.root.after(0, lambda: self.salida_comando.insert(tk.END, linea))

        def on_fin(codigo_salida):
            estado = "OK" if codigo_salida == 0 else f"salio con codigo {codigo_salida}"
            self.root.after(0, lambda: self.salida_comando.insert(tk.END, f"\n--- Terminado ({estado}) ---\n"))

        self._correr_sipi_en_hilo([ruta_temp], on_linea, on_fin)

    def _publicar_paquete_actual(self):
        """Arma un 'sipi.json' real a partir de lo que hay en esta pestaña
        y llama a 'sipi_registro.publicar()' -- el mismo empaquetado +
        publicacion (via git, si hay un clon local con permisos) que ya
        existe como funcion probada en sipi_registro.py. Esto es lo que
        hace que 'crear una libreria' termine de verdad en 'subida al
        registro', en vez de quedar solo como un .sipi local suelto."""
        try:
            registro = self._modulo_registro()
        except ImportError as e:
            messagebox.showerror("SiPi Dev", f"No se encontro 'sipi_registro.py' junto a este editor:\n{e}")
            return

        nombre = (self.entrada_nombre_paquete.get().strip() or "mi_paquete")
        codigo = self.texto_comando.get("1.0", "end-1c")
        if not codigo.strip():
            messagebox.showwarning("SiPi Dev", "No hay codigo para publicar todavia.")
            return

        ventana = tk.Toplevel(self.root)
        ventana.title(f"Publicar '{nombre}'")
        ventana.configure(bg=COLORES["fondo"])
        ventana.geometry("460x300")

        def campo(etiqueta, valor_inicial=""):
            tk.Label(ventana, text=etiqueta, bg=COLORES["fondo"], fg=COLORES["texto"]).pack(anchor="w", padx=12, pady=(10, 0))
            e = tk.Entry(ventana, bg="#11111b", fg=COLORES["texto"], insertbackground=COLORES["texto"])
            e.pack(fill=tk.X, padx=12)
            e.insert(0, valor_inicial)
            return e

        e_version = campo("Version (semver, ej. 1.0.0):", "1.0.0")
        e_descripcion = campo("Descripcion corta:", f"Paquete {nombre} generado con SiPi Dev")
        tk.Label(ventana, text="Clon local del registro (opcional -- si lo dejas vacio,\n"
                                "se arma el .zip y las instrucciones para publicarlo a mano):",
                 bg=COLORES["fondo"], fg="#a6adc8", font=("Segoe UI", 8), justify="left").pack(anchor="w", padx=12, pady=(10, 0))
        marco_clon = tk.Frame(ventana, bg=COLORES["fondo"])
        marco_clon.pack(fill=tk.X, padx=12)
        e_clon = tk.Entry(marco_clon, bg="#11111b", fg=COLORES["texto"], insertbackground=COLORES["texto"])
        e_clon.pack(side=tk.LEFT, fill=tk.X, expand=True)

        def elegir_clon():
            carpeta = filedialog.askdirectory(initialdir=self.carpeta_proyecto)
            if carpeta:
                e_clon.delete(0, tk.END)
                e_clon.insert(0, carpeta)

        tk.Button(marco_clon, text="...", command=elegir_clon, bg=COLORES["boton"], fg=COLORES["texto"], bd=0).pack(side=tk.LEFT, padx=(4, 0))

        etiqueta_resultado = tk.Label(ventana, text="", bg=COLORES["fondo"], fg="#a6adc8",
                                       font=("Segoe UI", 9), justify="left", wraplength=430)
        etiqueta_resultado.pack(anchor="w", padx=12, pady=(10, 0))

        def confirmar():
            version = e_version.get().strip() or "1.0.0"
            descripcion = e_descripcion.get().strip()
            clon = e_clon.get().strip() or None

            carpeta_tmp = tempfile.mkdtemp(prefix="sipi_publicar_")
            punto_entrada = f"{nombre}.sipi"
            with open(os.path.join(carpeta_tmp, punto_entrada), "w", encoding="utf-8") as f:
                f.write(codigo)
            manifiesto = {
                "nombre": nombre, "version": version, "descripcion": descripcion,
                "punto_entrada": punto_entrada, "dependencias": {},
            }
            with open(os.path.join(carpeta_tmp, "sipi.json"), "w", encoding="utf-8") as f:
                json.dump(manifiesto, f, indent=2, ensure_ascii=False)

            try:
                ruta_zip, resultado = registro.publicar(carpeta_tmp, clon_local_registro=clon)
            except registro.ErrorRegistro as e:
                messagebox.showerror("SiPi Dev", f"No se pudo publicar:\n{e}")
                return

            if clon:
                etiqueta_resultado.config(
                    text=f"Publicado y subido (o commiteado localmente si el push fallo).\nZip: {ruta_zip}")
            else:
                etiqueta_resultado.config(
                    text=f"Paquete armado en:\n{ruta_zip}\n\n"
                         f"Falta: subir ese .zip a una URL publica (un Release de GitHub sirve gratis) "
                         f"y agregar la entrada al index.json de tu registro -- los datos exactos "
                         f"quedaron impresos en la consola de este editor.")

        tk.Button(ventana, text="📦 Empaquetar y publicar", command=confirmar,
                  bg=COLORES["ok"], fg="white", bd=0, padx=10, pady=6).pack(pady=14)

    # ================= PESTANA 3: EXPLORAR LIBRERIAS (registro central) =================
    def _construir_tab_explorar(self):
        """Antes, la unica forma de 'descubrir' paquetes era una busqueda
        cruda contra la API de GitHub (buscar_paquete), sin index ni forma
        de instalar en un clic. Esta pestana usa 'ClienteRegistro' de
        sipi_registro.py -- ya implementado y probado -- para listar/buscar
        contra un index.json real (el oficial, o el que el usuario
        configure) y para instalar con un boton, sin pasar por la consola."""
        contenedor = self.tab_explorar
        arriba = tk.Frame(contenedor, bg=COLORES["fondo"])
        arriba.pack(fill=tk.X, padx=10, pady=10)

        tk.Label(arriba, text="URL del registro (index.json):", bg=COLORES["fondo"],
                 fg=COLORES["texto"]).grid(row=0, column=0, sticky="w")
        self.entrada_url_registro = tk.Entry(arriba, bg="#11111b", fg=COLORES["texto"],
                                              insertbackground=COLORES["texto"], width=60)
        self.entrada_url_registro.grid(row=0, column=1, padx=6, sticky="we")
        try:
            registro = self._modulo_registro()
            self.entrada_url_registro.insert(0, os.environ.get("SIPI_REGISTRO_URL", registro.REGISTRO_URL_POR_DEFECTO))
        except ImportError:
            pass
        arriba.grid_columnconfigure(1, weight=1)

        tk.Label(arriba, text="Buscar:", bg=COLORES["fondo"], fg=COLORES["texto"]).grid(row=1, column=0, sticky="w", pady=(8, 0))
        marco_buscar = tk.Frame(arriba, bg=COLORES["fondo"])
        marco_buscar.grid(row=1, column=1, sticky="we", pady=(8, 0))
        self.entrada_buscar_registro = tk.Entry(marco_buscar, bg="#11111b", fg=COLORES["texto"],
                                                  insertbackground=COLORES["texto"])
        self.entrada_buscar_registro.pack(side=tk.LEFT, fill=tk.X, expand=True)
        self.entrada_buscar_registro.bind("<Return>", lambda e: self._buscar_en_registro())
        tk.Button(marco_buscar, text="🔍 Buscar", command=self._buscar_en_registro,
                  bg=COLORES["acento"], fg="#11111b", bd=0, padx=10).pack(side=tk.LEFT, padx=(6, 0))
        tk.Button(marco_buscar, text="📋 Ver todos", command=lambda: self._buscar_en_registro(""),
                  bg=COLORES["boton"], fg=COLORES["texto"], bd=0, padx=10).pack(side=tk.LEFT, padx=(6, 0))

        columnas = ("nombre", "version", "descripcion")
        self.tabla_registro = ttk.Treeview(contenedor, columns=columnas, show="headings", height=16)
        for col, ancho in zip(columnas, (180, 90, 500)):
            self.tabla_registro.heading(col, text=col.capitalize())
            self.tabla_registro.column(col, width=ancho, anchor="w")
        self.tabla_registro.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 6))

        botones_explorar = tk.Frame(contenedor, bg=COLORES["fondo"])
        botones_explorar.pack(fill=tk.X, padx=10, pady=(0, 10))
        tk.Button(botones_explorar, text="⬇ Instalar seleccionado", command=self._instalar_desde_registro,
                  bg=COLORES["ok"], fg="white", bd=0, padx=10, pady=6).pack(side=tk.LEFT)

        self.estado_registro = tk.Label(contenedor, text="Escribi un termino y toca Buscar, o Ver todos.",
                                         bg=COLORES["barra"], fg="#a6adc8", anchor="w", font=("Segoe UI", 9))
        self.estado_registro.pack(side=tk.BOTTOM, fill=tk.X)

    def _cliente_registro_actual(self):
        registro = self._modulo_registro()
        url = self.entrada_url_registro.get().strip()
        if url:
            os.environ["SIPI_REGISTRO_URL"] = url
        return registro.ClienteRegistro(url_indice=url or None, carpeta_proyecto=self.carpeta_proyecto)

    def _buscar_en_registro(self, texto=None):
        texto = self.entrada_buscar_registro.get().strip() if texto is None else texto
        self.estado_registro.config(text="Consultando el registro...")

        def trabajo():
            try:
                cliente = self._cliente_registro_actual()
                resultados = cliente.buscar(texto)
            except Exception as e:
                self.root.after(0, lambda: self.estado_registro.config(text=f"Error: {e}"))
                return

            def volcar():
                self.tabla_registro.delete(*self.tabla_registro.get_children())
                for nombre, descripcion, version in resultados:
                    self.tabla_registro.insert("", tk.END, values=(nombre, version, descripcion))
                self.estado_registro.config(text=f"{len(resultados)} paquete(s) encontrados.")

            self.root.after(0, volcar)

        threading.Thread(target=trabajo, daemon=True).start()

    def _instalar_desde_registro(self):
        seleccion = self.tabla_registro.selection()
        if not seleccion:
            messagebox.showinfo("SiPi Dev", "Elegi un paquete de la lista primero.")
            return
        nombre = self.tabla_registro.item(seleccion[0], "values")[0]
        self.estado_registro.config(text=f"Instalando {nombre}...")

        def trabajo():
            try:
                cliente = self._cliente_registro_actual()
                version = cliente.instalar(nombre)
                mensaje = f"{nombre}@{version} instalado en paquetes_instalados/."
            except Exception as e:
                mensaje = f"Error instalando {nombre}: {e}"
            self.root.after(0, lambda: self.estado_registro.config(text=mensaje))

        threading.Thread(target=trabajo, daemon=True).start()

    # ================= PESTANA 4: TESTER =================
    def _construir_tab_tester(self):
        contenedor = self.tab_tester
        arriba = tk.Frame(contenedor, bg=COLORES["fondo"])
        arriba.pack(fill=tk.X, padx=10, pady=10)

        tk.Label(arriba, text="🧪 Pruebas de estres automatizadas", bg=COLORES["fondo"], fg=COLORES["texto"],
                 font=("Segoe UI", 12, "bold")).pack(anchor="w")
        tk.Label(arriba, text="Corre un archivo .sipi (o un lote de ejemplos) muchas veces seguidas para\n"
                              "encontrar errores intermitentes -- lo mismo que un tester manual haria\n"
                              "tocando 'Ejecutar' 30 veces, pero automatico y con un log de resultados.",
                 bg=COLORES["fondo"], fg="#a6adc8", font=("Segoe UI", 9), justify="left").pack(anchor="w", pady=(2, 10))

        fila1 = tk.Frame(contenedor, bg=COLORES["fondo"])
        fila1.pack(fill=tk.X, padx=10)
        tk.Label(fila1, text="Archivo .sipi a probar:", bg=COLORES["fondo"], fg=COLORES["texto"]).pack(side=tk.LEFT)
        self.entrada_archivo_tester = tk.Entry(fila1, bg="#11111b", fg=COLORES["texto"], insertbackground=COLORES["texto"])
        self.entrada_archivo_tester.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=6, ipady=3)
        tk.Button(fila1, text="Elegir...", command=self._elegir_archivo_tester,
                  bg=COLORES["boton"], fg=COLORES["texto"], bd=0, padx=8).pack(side=tk.LEFT)

        fila2 = tk.Frame(contenedor, bg=COLORES["fondo"])
        fila2.pack(fill=tk.X, padx=10, pady=8)
        tk.Label(fila2, text="Repeticiones:", bg=COLORES["fondo"], fg=COLORES["texto"]).pack(side=tk.LEFT)
        self.entrada_repeticiones = tk.Entry(fila2, bg="#11111b", fg=COLORES["texto"], insertbackground=COLORES["texto"], width=6)
        self.entrada_repeticiones.pack(side=tk.LEFT, padx=6)
        self.entrada_repeticiones.insert(0, "30")

        self.var_correr_ejemplos = tk.BooleanVar(value=False)
        tk.Checkbutton(fila2, text="Tambien correr todos los ejemplos de examples/ una vez cada uno",
                        variable=self.var_correr_ejemplos, bg=COLORES["fondo"], fg=COLORES["texto"],
                        selectcolor=COLORES["boton"], activebackground=COLORES["fondo"],
                        activeforeground=COLORES["texto"]).pack(side=tk.LEFT, padx=16)

        fila3 = tk.Frame(contenedor, bg=COLORES["fondo"])
        fila3.pack(fill=tk.X, padx=10)
        self.boton_iniciar_tester = tk.Button(fila3, text="▶ Iniciar pruebas (tocar 'Ejecutar' N veces)",
                                               command=self._iniciar_tester, bg=COLORES["ok"], fg="white",
                                               bd=0, padx=10, pady=6)
        self.boton_iniciar_tester.pack(side=tk.LEFT)
        tk.Button(fila3, text="■ Detener", command=self._detener_tester_ahora,
                  bg=COLORES["error"], fg="white", bd=0, padx=10, pady=6).pack(side=tk.LEFT, padx=8)

        self.barra_progreso = ttk.Progressbar(contenedor, mode="determinate")
        self.barra_progreso.pack(fill=tk.X, padx=10, pady=8)

        self.resumen_tester = tk.Label(contenedor, text="Listo para empezar.", bg=COLORES["fondo"],
                                        fg=COLORES["texto"], anchor="w", font=("Segoe UI", 10, "bold"))
        self.resumen_tester.pack(fill=tk.X, padx=10)

        self.log_tester = tk.Text(contenedor, bg="#11111b", fg=COLORES["texto"], bd=0, font=("Consolas", 9))
        self.log_tester.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        self.log_tester.tag_config("ok", foreground=COLORES["ok"])
        self.log_tester.tag_config("error", foreground=COLORES["error"])
        self.log_tester.tag_config("aviso", foreground=COLORES["aviso"])

    def _elegir_archivo_tester(self):
        ruta = filedialog.askopenfilename(initialdir=self.carpeta_proyecto,
                                           filetypes=[("Archivos SiPi", "*.sipi")])
        if ruta:
            self.entrada_archivo_tester.delete(0, tk.END)
            self.entrada_archivo_tester.insert(0, ruta)

    def _detener_tester_ahora(self):
        self._detener_tester.set()

    def _log(self, texto, etiqueta=None):
        def _hacer():
            self.log_tester.insert(tk.END, texto + "\n", etiqueta)
            self.log_tester.see(tk.END)
        self.root.after(0, _hacer)

    def _iniciar_tester(self):
        ruta = self.entrada_archivo_tester.get().strip()
        try:
            repeticiones = max(1, int(self.entrada_repeticiones.get().strip()))
        except ValueError:
            messagebox.showwarning("SiPi Dev", "Las repeticiones tienen que ser un numero.")
            return
        if not ruta or not os.path.exists(ruta):
            messagebox.showwarning("SiPi Dev", "Elegi un archivo .sipi valido primero.")
            return

        motor = os.path.join(self.aqui, "sipi.py")
        if not os.path.exists(motor):
            motor = os.path.join(self.aqui, "sipi_protegido.py")
        if not os.path.exists(motor):
            messagebox.showerror("SiPi Dev", "No se encontro el motor de SiPi (sipi.py) al lado de este editor.")
            return

        self._detener_tester.clear()
        self.log_tester.delete("1.0", tk.END)
        self.boton_iniciar_tester.config(state=tk.DISABLED)

        tareas_extra = []
        if self.var_correr_ejemplos.get():
            carpeta_ejemplos = os.path.join(os.path.dirname(self.aqui), "examples")
            if os.path.isdir(carpeta_ejemplos):
                for raiz, _, archivos in os.walk(carpeta_ejemplos):
                    for archivo in archivos:
                        if archivo.endswith(".sipi"):
                            tareas_extra.append(os.path.join(raiz, archivo))

        total = repeticiones + len(tareas_extra)
        self.barra_progreso.config(maximum=total, value=0)

        def _tarea():
            resultados = {"ok": 0, "error": 0, "timeout": 0}
            errores_unicos = {}

            def _correr_una_vez(ruta_archivo, etiqueta_intento):
                if self._detener_tester.is_set():
                    return None
                if _es_exe_empaquetado():
                    comando = [sys.executable, _bandera_reentrada_motor(), ruta_archivo]
                else:
                    comando = [sys.executable, motor, ruta_archivo]
                inicio = time.time()
                try:
                    resultado = subprocess.run(
                        comando, capture_output=True, text=True, timeout=15,
                        encoding="utf-8", errors="replace",
                        env={**os.environ, "PYTHONIOENCODING": "utf-8"},
                    )
                    duracion = time.time() - inicio
                    salida_error = (resultado.stderr or "").strip()
                    if resultado.returncode == 0 and not salida_error:
                        resultados["ok"] += 1
                        self._log(f"[OK]  {etiqueta_intento}  ({duracion:.2f}s)", "ok")
                    else:
                        resultados["error"] += 1
                        primera_linea_error = (salida_error.splitlines() or ["(sin detalle)"])[-1][:160]
                        errores_unicos[primera_linea_error] = errores_unicos.get(primera_linea_error, 0) + 1
                        self._log(f"[ERROR] {etiqueta_intento}  ({duracion:.2f}s) -> {primera_linea_error}", "error")
                except subprocess.TimeoutExpired:
                    resultados["timeout"] += 1
                    self._log(f"[TIMEOUT] {etiqueta_intento} (>15s, probablemente espera input o quedo colgado)", "aviso")

            hecho = 0
            for i in range(1, repeticiones + 1):
                if self._detener_tester.is_set():
                    self._log("--- Detenido por el usuario ---", "aviso")
                    break
                _correr_una_vez(ruta, f"Repeticion {i}/{repeticiones} de {os.path.basename(ruta)}")
                hecho += 1
                self.root.after(0, lambda h=hecho: self.barra_progreso.config(value=h))

            for ruta_ejemplo in tareas_extra:
                if self._detener_tester.is_set():
                    break
                _correr_una_vez(ruta_ejemplo, f"Ejemplo: {os.path.relpath(ruta_ejemplo, os.path.dirname(self.aqui))}")
                hecho += 1
                self.root.after(0, lambda h=hecho: self.barra_progreso.config(value=h))

            def _finalizar():
                self.boton_iniciar_tester.config(state=tk.NORMAL)
                total_corridas = resultados["ok"] + resultados["error"] + resultados["timeout"]
                self.resumen_tester.config(
                    text=(f"Terminado: {total_corridas} corrida(s) -- "
                          f"✅ {resultados['ok']} ok, ❌ {resultados['error']} con error, "
                          f"⏱ {resultados['timeout']} timeout"),
                    fg=COLORES["ok"] if resultados["error"] == 0 and resultados["timeout"] == 0 else COLORES["error"],
                )
                if errores_unicos:
                    self._log("\n--- Resumen de errores distintos encontrados ---", "aviso")
                    for msg, cantidad in sorted(errores_unicos.items(), key=lambda x: -x[1]):
                        self._log(f"  x{cantidad}  {msg}", "error")
            self.root.after(0, _finalizar)

        threading.Thread(target=_tarea, daemon=True).start()


def main():
    if _CTK_DISPONIBLE:
        root = ctk.CTk()
    else:
        root = tk.Tk()
    EditorDev(root)
    root.mainloop()


if __name__ == "__main__":
    if not _intentar_correr_como_motor_interno():
        main()
