#!/usr/bin/env python3
"""
conny — CLI para gestionar instancias de Conny v5.0

Uso:
    conny                  → menú principal
    conny init             → configurar por primera vez
    conny new              → crear nuevo cliente
    conny list             → ver todas las instancias
    conny chat [nombre]    → hablar en lenguaje natural
    conny status [nombre]  → estado detallado
    conny logs [nombre]    → logs en tiempo real
    conny restart [nombre] → reiniciar
    conny stop [nombre]    → detener
    conny omni             → abrir Conny Omni (el ojo de Santiago)
    conny nova             → gestionar Nova governance
    conny update           → actualizar archivos desde GitHub

Instalación:
    sudo python3 conny --install     → pone 'conny' en /usr/local/bin
"""

import sys
import os
import json
import time
import random
import subprocess
import threading
import shutil
import platform
import re
import argparse
from pathlib import Path
from datetime import datetime

try:
    import readline
    readline.set_history_length(200)
    _READLINE = True
except ImportError:
    _READLINE = False

try:
    import httpx as _httpx
    _HTTPX = True
except ImportError:
    import urllib.request
    _HTTPX = False

CONNY_VERSION = "9.6.0"
CONNY_BUILD   = "2026.03"

# ══════════════════════════════════════════════════════════════════════════════
# DEFAULTS
# ══════════════════════════════════════════════════════════════════════════════

CONNY_DIR      = os.getenv("CONNY_DIR",      "/home/ubuntu/conny")
INSTANCES_DIR    = os.getenv("INSTANCES_DIR",    "/home/ubuntu/conny-instances")
NOVA_DIR         = os.getenv("NOVA_DIR",         "/home/ubuntu/nova-os")
OMNI_PORT        = int(os.getenv("OMNI_PORT",    "9001"))
NOVA_PORT        = int(os.getenv("NOVA_PORT",    "9002"))
BASE_PORT        = 8001
MAX_PORT         = 8099

# ══════════════════════════════════════════════════════════════════════════════
# COLOR SYSTEM — Purple palette (like Nova but violet)
# ══════════════════════════════════════════════════════════════════════════════

def _e(code):
    if not sys.stdout.isatty() and not os.getenv("FORCE_COLOR"):
        return ""
    return f"\033[{code}m"

class C:
    R     = _e("0")       # reset
    BOLD  = _e("1")
    DIM   = _e("2")
    ITALIC= _e("3")

    # Purple palette (Conny signature color)
    P1  = _e("38;5;183")  # Lavender — brightest
    P2  = _e("38;5;141")  # Soft purple
    P3  = _e("38;5;135")  # Medium purple
    P4  = _e("38;5;99")   # Deep purple
    P5  = _e("38;5;93")   # Dark purple — darkest

    # Text
    W   = _e("38;5;15")   # Pure white
    G0  = _e("38;5;252")  # Near white — primary text
    G1  = _e("38;5;248")  # Light gray
    G2  = _e("38;5;244")  # Mid gray
    G3  = _e("38;5;240")  # Dark gray

    # Semantic
    GRN = _e("38;5;114")  # Green
    RED = _e("38;5;203")  # Red
    YLW = _e("38;5;221")  # Yellow
    CYN = _e("38;5;117")  # Cyan

def q(color, text, bold=False):
    if bold:
        return f"{C.BOLD}{color}{text}{C.R}"
    return f"{color}{text}{C.R}"


# ══════════════════════════════════════════════════════════════════════════════
# CONNY ASCII LOGO (purple gradient)
# ══════════════════════════════════════════════════════════════════════════════

_M_BLOCK = [
    "  ███╗   ███╗███████╗██╗     ██╗███████╗███████╗ █████╗ ",
    "  ████╗ ████║██╔════╝██║     ██║██╔════╝██╔════╝██╔══██╗",
    "  ██╔████╔██║█████╗  ██║     ██║███████╗███████╗███████║",
    "  ██║╚██╔╝██║██╔══╝  ██║     ██║╚════██║╚════██║██╔══██║",
    "  ██║ ╚═╝ ██║███████╗███████╗██║███████║███████║██║  ██║",
    "  ╚═╝     ╚═╝╚══════╝╚══════╝╚═╝╚══════╝╚══════╝╚═╝  ╚═╝",
]

_TAGLINES = [
    "Recepcionista virtual para clínicas estéticas.",
    "La IA que convierte conversaciones en citas.",
    "Responde, agenda y aprende. Sola.",
    "Tu clínica, disponible 24/7.",
    "WhatsApp, Telegram, sin diferencia.",
    "El equipo que nunca descansa.",
]

_M_COLORS = [
    _e("38;5;183"),  # Lavender
    _e("38;5;177"),  # Light purple
    _e("38;5;141"),  # Soft purple
    _e("38;5;135"),  # Medium purple
    _e("38;5;99"),   # Deep purple
    _e("38;5;93"),   # Dark purple
]


def print_logo(compact=False, animated=False):
    print()
    if compact:
        print("  " + q(C.P1, "✦", bold=True) + "  " + q(C.W, "conny", bold=True) +
              "  " + q(C.G2, f"v{CONNY_VERSION}"))
        print()
        return

    for i, line in enumerate(_M_BLOCK):
        col = _M_COLORS[i]
        sys.stdout.write(C.BOLD + col + line + C.R)
        sys.stdout.write("  " + col + "✦" + C.R if i == 2 else "")
        print()
        if animated:
            time.sleep(0.04)

    print()
    tl = random.choice(_TAGLINES)
    if animated:
        ghost_write(tl, color=C.P2, delay=0.015)
    else:
        print("  " + q(C.W, tl))
    print("  " + q(C.P1, "✦") + " " + q(C.G2, f"Conny v{CONNY_VERSION} · {CONNY_BUILD}"))
    print("  " + q(C.G3, "─" * 58))
    print()


# ══════════════════════════════════════════════════════════════════════════════
# ANIMATION PRIMITIVES
# ══════════════════════════════════════════════════════════════════════════════

def ghost_write(text, color=None, delay=0.018, prefix="  "):
    c = color or C.P2
    sys.stdout.write(prefix + C.BOLD + c)
    for ch in text:
        sys.stdout.write(ch)
        sys.stdout.flush()
        if ch in ".!?":    time.sleep(delay * 7)
        elif ch in ",:;":  time.sleep(delay * 3)
        elif ch == " ":    time.sleep(delay * 1.2)
        else:              time.sleep(delay + random.uniform(-0.004, 0.008))
    sys.stdout.write(C.R)
    print()


_SPINNER_FRAMES = ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"]
_CONNY_SPIN   = ["✦","✧","✦","✧"]

class Spinner:
    def __init__(self, msg, style="dots"):
        self.msg    = msg
        self.frames = _CONNY_SPIN if style == "conny" else _SPINNER_FRAMES
        self._stop  = threading.Event()
        self._t     = None

    def __enter__(self):
        self.start(); return self

    def __exit__(self, *a):
        self.finish()

    def start(self):
        def _run():
            i = 0
            while not self._stop.is_set():
                f = q(C.P2, self.frames[i % len(self.frames)], bold=True)
                sys.stdout.write(f"\r  {f}  {q(C.G1, self.msg)}")
                sys.stdout.flush()
                time.sleep(0.1)
                i += 1
        self._t = threading.Thread(target=_run, daemon=True)
        self._t.start()

    def update(self, msg):
        self.msg = msg

    def finish(self, msg=None, ok=True):
        self._stop.set()
        if self._t:
            self._t.join(timeout=0.5)
        icon = q(C.GRN, "✓") if ok else q(C.RED, "✗")
        sys.stdout.write(f"\r  {icon}  {q(C.W, msg or self.msg)}\n")
        sys.stdout.flush()


# ══════════════════════════════════════════════════════════════════════════════
# UI PRIMITIVES
# ══════════════════════════════════════════════════════════════════════════════

def ok(msg):    print(f"  {q(C.GRN,'✓')}  {q(C.W, msg)}")
def fail(msg):  print(f"  {q(C.RED,'✗')}  {q(C.W, msg)}")
def warn(msg):  print(f"  {q(C.YLW,'!')}  {q(C.G1, msg)}")
def info(msg):  print(f"  {q(C.P2,'·')}  {q(C.G1, msg)}")
def dim(msg):   print(f"       {q(C.G3, msg)}")
def nl(n=1):    print("\n" * (n-1))

def hr():
    print("  " + q(C.G3, "─" * 58))

def section(title, sub=""):
    print()
    print("  " + q(C.P1, "✦", bold=True) + "  " + q(C.W, title, bold=True))
    if sub:
        print("  " + q(C.G2, "   " + sub))
    print()

def kv(key, val, color=None):
    c = color or C.P2
    k = q(C.G2, f"{key:<22}")
    v = q(c, str(val))
    print(f"  {k}  {v}")

def prompt(label, default="", secret=False):
    d = f" [{default}]" if default else ""
    sys.stdout.write(f"\n  {q(C.P2,'?')}  {q(C.W, label)}{q(C.G3, d)}  ")
    sys.stdout.flush()
    try:
        import getpass
        val = getpass.getpass("") if secret else input("")
    except (EOFError, KeyboardInterrupt):
        print(); return default
    return val.strip() or default

def confirm(label, default=True):
    d = "S/n" if default else "s/N"
    sys.stdout.write(f"\n  {q(C.P2,'?')}  {q(C.W, label)} {q(C.G3, f'[{d}]')}  ")
    sys.stdout.flush()
    try:
        v = input("").strip().lower()
    except (EOFError, KeyboardInterrupt):
        print(); return default
    if not v: return default
    return v in ("s", "si", "sí", "yes", "y")

def select(options, descriptions=None, title=""):
    if title:
        print(f"\n  {q(C.G2, title)}")
    for i, opt in enumerate(options):
        num = q(C.P2, f"{i+1}.", bold=True)
        desc = f"  {q(C.G3, descriptions[i])}" if descriptions and i < len(descriptions) else ""
        print(f"    {num}  {q(C.W, opt)}{desc}")
    while True:
        sys.stdout.write(f"\n  {q(C.P2,'→')}  ")
        sys.stdout.flush()
        try:
            v = input("").strip()
        except (EOFError, KeyboardInterrupt):
            print(); return 0
        if v.isdigit() and 1 <= int(v) <= len(options):
            return int(v) - 1
        warn("Escribe el número de la opción")


# ══════════════════════════════════════════════════════════════════════════════
# HELPERS — instancias, puertos, env
# ══════════════════════════════════════════════════════════════════════════════

def load_env(path):
    env = {}
    try:
        with open(path) as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith('#') and '=' in line:
                    k, _, v = line.partition('=')
                    env[k.strip()] = v.strip().strip('"').strip("'")
    except Exception:
        pass
    return env

def update_env(path, key, val):
    try:
        content = Path(path).read_text() if Path(path).exists() else ""
        if re.search(rf"^{key}=", content, re.MULTILINE):
            content = re.sub(rf"^{key}=.*$", f"{key}={val}", content, flags=re.MULTILINE)
        else:
            content += f"\n{key}={val}"
        Path(path).write_text(content)
    except Exception as e:
        warn(f"No pude actualizar .env: {e}")

def slug(name):
    name = name.lower().strip()
    for a, b in [("á","a"),("é","e"),("í","i"),("ó","o"),("ú","u"),("ñ","n"),("ü","u")]:
        name = name.replace(a, b)
    return re.sub(r'[^a-z0-9]+', '-', name).strip('-')

def next_free_port():
    import socket
    for p in range(BASE_PORT + 1, MAX_PORT + 1):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            try:
                s.bind(("0.0.0.0", p))
                return p
            except OSError:
                continue
    return None

def get_instances():
    instances = []
    base_env = load_env(f"{CONNY_DIR}/.env")
    if base_env:
        instances.append({
            "name": "base", "label": "instancia base",
            "port": int(base_env.get("PORT", BASE_PORT)),
            "dir": CONNY_DIR, "env": base_env, "is_base": True,
        })
    if os.path.isdir(INSTANCES_DIR):
        for name in sorted(os.listdir(INSTANCES_DIR)):
            d = f"{INSTANCES_DIR}/{name}"
            ep = f"{d}/.env"
            if not os.path.isdir(d) or not os.path.exists(ep):
                continue
            env = load_env(ep)
            instances.append({
                "name": name, "label": name.replace("-", " ").title(),
                "port": int(env.get("PORT", 8002)),
                "dir": d, "env": env, "is_base": False,
            })
    return instances

def health(port, timeout=3):
    url = f"http://localhost:{port}/health"
    try:
        if _HTTPX:
            import httpx
            r = httpx.get(url, timeout=timeout)
            return r.json() if r.status_code == 200 else {}
        else:
            req = urllib.request.urlopen(url, timeout=timeout)
            return json.loads(req.read())
    except Exception:
        return {}

def pm2(cmd, name=""):
    args = ["pm2", cmd]
    if name: args.append(name)
    r = subprocess.run(args, capture_output=True, text=True)
    return r.returncode == 0

def pm2_exists(name):
    r = subprocess.run(["pm2", "describe", name], capture_output=True, text=True)
    return r.returncode == 0

def public_ip():
    try:
        if _HTTPX:
            import httpx
            return httpx.get("https://api.ipify.org", timeout=5).text.strip()
        else:
            return urllib.request.urlopen("https://api.ipify.org", timeout=5).read().decode().strip()
    except Exception:
        return ""

def get_status_icon(h):
    if not h:       return q(C.RED, "●"), "offline"
    status = h.get("status", "")
    if status == "online" or h.get("reachable"):  return q(C.GRN, "●"), "online"
    return q(C.RED, "●"), "offline"


# ══════════════════════════════════════════════════════════════════════════════
# COMMANDS
# ══════════════════════════════════════════════════════════════════════════════

def cmd_init(args):
    """Configuración inicial de Conny en este servidor."""
    print_logo(animated=True)
    section("Bienvenido a Conny", "Vamos a configurar tu servidor.")

    ghost_write("Conny es la recepcionista virtual para tus clínicas estéticas.", C.G1)
    ghost_write("Responde WhatsApp y Telegram, agenda citas, aprende de tu negocio.", C.G1)
    print()

    # Verificar dependencias
    section("Verificando dependencias")

    deps = [("python3", "Python 3"), ("pm2", "PM2"), ("curl", "curl")]
    all_ok = True
    for cmd_name, label in deps:
        if shutil.which(cmd_name):
            ok(f"{label} encontrado")
        else:
            fail(f"{label} NO encontrado")
            all_ok = False

    if not all_ok:
        print()
        warn("Faltan dependencias. Instala PM2 con: npm install -g pm2")
        return

    # Verificar archivos
    section("Verificando archivos de Conny")
    required = ["conny.py", "search.py", "knowledge_base.py"]
    for f in required:
        path = f"{CONNY_DIR}/{f}"
        if os.path.exists(path):
            ok(f"{f}")
        else:
            warn(f"{f} no encontrado en {CONNY_DIR}")

    # Verificar .env
    env_path = f"{CONNY_DIR}/.env"
    if not os.path.exists(env_path):
        fail(f".env no encontrado en {CONNY_DIR}")
        info("Crea el .env con: cp .env.example .env  y rellena las claves")
        return

    env = load_env(env_path)
    print()

    # Verificar claves críticas
    section("Verificando configuración")
    checks = [
        ("TELEGRAM_TOKEN", "Telegram bot"),
        ("GROQ_API_KEY", "Groq LLM"),
        ("BASE_URL", "URL pública"),
        ("MASTER_API_KEY", "Master API key"),
    ]
    for key, label in checks:
        if env.get(key):
            ok(f"{label}")
        else:
            warn(f"{label} no configurado ({key})")

    # IP pública
    print()
    with Spinner("Detectando IP pública del servidor...", "conny") as sp:
        ip = public_ip()
        sp.finish(f"IP pública: {ip}" if ip else "No pude detectar IP pública")

    # Arrancar Conny si no está corriendo
    h = health(int(env.get("PORT", BASE_PORT)))
    if h:
        ok(f"Conny ya está corriendo en puerto {env.get('PORT', BASE_PORT)}")
    else:
        print()
        if confirm("Arrancar Conny ahora?"):
            start_sh = f"{CONNY_DIR}/start.sh"
            if os.path.exists(start_sh):
                with Spinner("Arrancando Conny...", "conny") as sp:
                    subprocess.run(["bash", start_sh], cwd=CONNY_DIR,
                                   capture_output=True)
                    time.sleep(3)
                    h = health(int(env.get("PORT", BASE_PORT)))
                    sp.finish("Conny online" if h else "Arrancando (puede tardar unos segundos)")
            else:
                warn("start.sh no encontrado. Arranca manualmente: pm2 start conny.py --interpreter python3")

    print()
    section("Todo listo")
    ghost_write("Usa 'conny new' para crear tu primer cliente.", C.P2)
    ghost_write("Usa 'conny list' para ver las instancias activas.", C.P2)
    ghost_write("Usa 'conny chat' para hablarme en lenguaje natural.", C.P2)
    print()


def cmd_list(args):
    """Ver todas las instancias de Conny."""
    print_logo(compact=True)
    section("Instancias activas")

    instances = get_instances()
    if not instances:
        warn("No hay instancias. Crea una con: conny new")
        return

    # Health de todas en paralelo
    import concurrent.futures
    with concurrent.futures.ThreadPoolExecutor(max_workers=10) as ex:
        healths = list(ex.map(lambda i: health(i["port"]), instances))

    for inst, h in zip(instances, healths):
        icon, status = get_status_icon(h)
        port      = inst["port"]
        platform  = h.get("platform", inst["env"].get("PLATFORM", "telegram"))
        wa        = h.get("whatsapp", {})
        plat_str  = f"WA {wa.get('phone','')}" if wa.get("connected") else platform
        clinic    = h.get("clinic", inst["label"])
        nova_on   = inst["env"].get("NOVA_ENABLED", "false") == "true"
        nova_str  = q(C.P3, " ✦Nova") if nova_on else ""
        mem       = h.get("memory_items", "")
        mem_str   = q(C.G3, f"  {mem} mem") if mem else ""

        print(f"  {icon}  {q(C.W, clinic, bold=True)}{nova_str}")
        print(f"       {q(C.G2, inst['name'])}  ·  :{port}  ·  {q(C.G1, plat_str)}{mem_str}")
        print()


def cmd_new(args):
    """Crear nueva instancia para un cliente."""
    print_logo(compact=True)
    section("Nuevo cliente", "Vamos a crear una instancia nueva.")

    # Nombre
    name_raw = getattr(args, 'name', '') or prompt("Nombre del cliente (ej: Clínica Bella)")
    if not name_raw:
        fail("Necesitas un nombre"); return
    name = slug(name_raw)

    inst_dir = f"{INSTANCES_DIR}/{name}"
    if os.path.isdir(inst_dir):
        fail(f"Ya existe una instancia llamada '{name}'")
        info(f"Usa 'conny status {name}' para verla")
        return

    # Token Telegram
    print()
    info("Obtén el token en @BotFather → /newbot en Telegram")
    tg_token = prompt("Token de Telegram")
    if not tg_token:
        fail("El token de Telegram es obligatorio"); return

    # Puerto
    port = next_free_port()
    if not port:
        fail("No hay puertos disponibles (8002-8099)"); return

    # URL base
    ip = public_ip()
    default_url = f"http://{ip}:{port}" if ip else f"http://servidor:{port}"
    base_env = load_env(f"{CONNY_DIR}/.env")
    base_url_template = base_env.get("BASE_URL", "")
    if base_url_template:
        default_url = base_url_template

    base_url = prompt(f"URL pública para este cliente", default=default_url)

    # Master key
    import secrets as _s
    master_key = _s.token_hex(12)

    print()

    # Crear directorio y archivos
    with Spinner(f"Creando instancia '{name}'...", "conny") as sp:
        os.makedirs(inst_dir, exist_ok=True)
        os.makedirs(f"{inst_dir}/logs", exist_ok=True)

        # Copiar archivos base
        files = ["conny.py", "search.py", "knowledge_base.py", "requirements.txt"]
        for f in files:
            src = f"{CONNY_DIR}/{f}"
            if os.path.exists(src):
                shutil.copy(src, f"{inst_dir}/{f}")

        # Copiar nova_bridge si existe
        bridge = f"{CONNY_DIR}/nova_bridge.py"
        if os.path.exists(bridge):
            shutil.copy(bridge, f"{inst_dir}/nova_bridge.py")

        # Leer claves compartidas del .env base
        shared_keys = [
            "GROQ_API_KEY","GEMINI_API_KEY","GEMINI_API_KEY_2","GEMINI_API_KEY_3",
            "OPENROUTER_API_KEY","OPENAI_API_KEY",
            "SERP_API_KEY","BRAVE_API_KEY","APIFY_API_KEY",
            "META_APP_ID","META_APP_SECRET",
        ]
        shared = {k: base_env.get(k, "") for k in shared_keys}

        # Escribir .env limpio
        import secrets as _s2
        webhook_secret = _s2.token_hex(8)
        env_lines = [
            f"# Conny — Instancia: {name}",
            f"# Creada: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
            "",
            f"PLATFORM=telegram",
            f"TELEGRAM_TOKEN={tg_token}",
            "",
            "# LLM — heredadas del servidor base",
            f"GROQ_API_KEY={shared['GROQ_API_KEY']}",
            f"GEMINI_API_KEY={shared['GEMINI_API_KEY']}",
            f"GEMINI_API_KEY_2={shared['GEMINI_API_KEY_2']}",
            f"GEMINI_API_KEY_3={shared['GEMINI_API_KEY_3']}",
            f"OPENROUTER_API_KEY={shared['OPENROUTER_API_KEY']}",
            f"OPENAI_API_KEY={shared['OPENAI_API_KEY']}",
            "",
            "# Búsqueda",
            f"SERP_API_KEY={shared['SERP_API_KEY']}",
            f"BRAVE_API_KEY={shared['BRAVE_API_KEY']}",
            f"APIFY_API_KEY={shared['APIFY_API_KEY']}",
            "",
            "# Servidor",
            f"PORT={port}",
            f"HOST=0.0.0.0",
            f"BASE_URL={base_url}",
            f"WEBHOOK_SECRET=conny_{name}_{webhook_secret}",
            "",
            "# Base de datos — aislada por cliente",
            f"DB_PATH={inst_dir}/conny.db",
            f"VECTOR_DB_PATH={inst_dir}/vectors.db",
            "",
            "# Seguridad",
            f"MASTER_API_KEY={master_key}",
            "",
            "# Timing",
            "BUFFER_WAIT_MIN=35",
            "BUFFER_WAIT_MAX=55",
            "BUBBLE_PAUSE_MIN=1.0",
            "BUBBLE_PAUSE_MAX=3.0",
            "",
            "# WhatsApp (configurar con /whatsapp en Telegram)",
            "WA_PHONE_ID=",
            "WA_ACCESS_TOKEN=",
            "WA_VERIFY_TOKEN=",
            f"META_APP_ID={shared['META_APP_ID']}",
            f"META_APP_SECRET={shared['META_APP_SECRET']}",
            "",
            "# Nova",
            f"NOVA_URL=http://localhost:{NOVA_PORT}",
            "NOVA_API_KEY=",
            "NOVA_TOKEN=",
            "NOVA_ENABLED=false",
            "",
            "# Omni",
            f"OMNI_URL=http://localhost:{OMNI_PORT}",
            f"OMNI_KEY={base_env.get('OMNI_KEY', '')}",
        ]
        Path(f"{inst_dir}/.env").write_text("\n".join(env_lines))

        # Metadata
        Path(f"{inst_dir}/instance.json").write_text(json.dumps({
            "name": name, "label": name_raw, "port": port,
            "base_url": base_url, "master_key": master_key,
            "created_at": datetime.utcnow().isoformat() + "Z",
        }, indent=2))

        sp.finish(f"Instancia '{name}' creada")

    # Lanzar con PM2
    pm2_name = f"conny-{name}"
    with Spinner(f"Arrancando {pm2_name}...", "conny") as sp:
        pm2("delete", pm2_name)  # limpiar si existía
        r = subprocess.run([
            "pm2", "start", f"{inst_dir}/conny.py",
            "--name", pm2_name,
            "--interpreter", "python3",
            "--restart-delay", "3000",
            "--max-restarts", "10",
            "--log", f"{inst_dir}/logs/conny.log",
            "--error", f"{inst_dir}/logs/error.log",
        ], capture_output=True, text=True)
        subprocess.run(["pm2", "save"], capture_output=True)
        time.sleep(4)
        h = health(port)
        sp.finish("Online" if h else "Arrancando (puede tardar unos segundos)", ok=bool(h))

    # ¿Integrar Nova?
    nova_h = health(NOVA_PORT)
    if nova_h:
        print()
        if confirm("Nova está activo. ¿Quieres que gobierne esta instancia?"):
            _connect_nova_to_instance(inst_dir, name, master_key)

    # Resumen
    print()
    section(f"'{name_raw}' lista")
    kv("Instancia",   name)
    kv("Puerto",      str(port))
    kv("URL",         base_url)
    kv("Master Key",  master_key)
    kv("PM2",         pm2_name)
    print()
    info(f"Siguiente paso: abre el bot en Telegram y envía el token de activación")
    info(f"Para generar un token: conny chat → 'dame el token para {name_raw}'")
    print()


def _connect_nova_to_instance(inst_dir, name, master_key):
    """Conectar una instancia específica con Nova."""
    with Spinner("Creando agente Nova...", "conny") as sp:
        base_env = load_env(f"{CONNY_DIR}/.env")
        nova_api_key = base_env.get("NOVA_API_KEY", "")
        try:
            if _HTTPX:
                import httpx
                r = httpx.post(
                    f"http://localhost:{NOVA_PORT}/tokens",
                    json={
                        "agent_name": f"Conny - {name}",
                        "description": "Recepcionista virtual clínica estética",
                        "can_do": ["send appointment confirmations",
                                   "answer questions about clinic services",
                                   "book free consultation appointments"],
                        "cannot_do": ["provide medical diagnosis",
                                      "share patient personal information",
                                      "guarantee treatment results"],
                        "authorized_by": "Santiago"
                    },
                    headers={"Authorization": f"Bearer {nova_api_key}",
                             "X-API-Key": nova_api_key},
                    timeout=10
                )
                if r.status_code == 200:
                    token_id = r.json().get("token_id", "")
                    if token_id:
                        update_env(f"{inst_dir}/.env", "NOVA_TOKEN", token_id)
                        update_env(f"{inst_dir}/.env", "NOVA_ENABLED", "true")
                        sp.finish(f"Agente creado: {token_id[:20]}...")
                        subprocess.run(["pm2", "restart", f"conny-{name}"], capture_output=True)
                        return
        except Exception as e:
            pass
        sp.finish("No pude crear el agente — configura Nova manualmente con 'conny nova'", ok=False)


def cmd_status(args):
    """Estado detallado de una instancia."""
    name = getattr(args, 'name', '') or ''
    print_logo(compact=True)

    instances = get_instances()
    if name:
        instances = [i for i in instances if name.lower() in i["name"].lower()]
    if not instances:
        fail(f"No encontré instancia '{name}'"); return

    for inst in instances:
        h = health(inst["port"])
        icon, status = get_status_icon(h)
        section(f"{inst.get('label', inst['name'])} {icon}")

        kv("Estado",     status.upper())
        kv("Puerto",     str(inst["port"]))
        kv("Directorio", inst["dir"])
        kv("PM2",        f"conny-{inst['name']}" if not inst.get("is_base") else "conny")

        if h:
            kv("Clínica",    h.get("clinic", "—"))
            kv("Plataforma", h.get("platform", "—"))
            wa = h.get("whatsapp", {})
            if wa.get("connected"):
                kv("WhatsApp",   wa.get("phone", "conectado"))
            kv("Memoria",    f"{h.get('memory_items', 0)} items")
            kv("Buffers",    str(h.get("active_buffers", 0)))
            kv("Nova",       "activo" if inst["env"].get("NOVA_ENABLED") == "true" else "inactivo")

        print()


def cmd_logs(args):
    """Logs en tiempo real de una instancia."""
    name = getattr(args, 'name', '') or ''
    print_logo(compact=True)

    if not name:
        instances = get_instances()
        opts = [f"{i['name']}  :{i['port']}" for i in instances]
        if not opts:
            fail("No hay instancias"); return
        idx = select(opts, title="¿De cuál instancia quieres ver los logs?")
        name = instances[idx]["name"]

    pm2_name = "conny" if name == "base" else f"conny-{name}"
    section(f"Logs de {name}", "Ctrl+C para salir")
    os.execvp("pm2", ["pm2", "logs", pm2_name, "--lines", "50"])


def cmd_restart(args):
    """Reiniciar una o todas las instancias."""
    name = getattr(args, 'name', '') or ''
    print_logo(compact=True)

    if not name or name == "all":
        if confirm("Reiniciar TODAS las instancias?"):
            with Spinner("Reiniciando todo...", "conny") as sp:
                pm2("restart", "all")
                sp.finish("Todas las instancias reiniciadas")
        return

    pm2_name = "conny" if name == "base" else f"conny-{name}"
    with Spinner(f"Reiniciando {name}...", "conny") as sp:
        if pm2("restart", pm2_name):
            sp.finish(f"{name} reiniciado")
        else:
            sp.finish(f"No encontré {pm2_name} en PM2", ok=False)


def cmd_stop(args):
    """Detener una instancia."""
    name = getattr(args, 'name', '') or ''
    if not name:
        fail("Especifica el nombre: conny stop clinica-bella"); return
    pm2_name = "conny" if name == "base" else f"conny-{name}"
    if pm2("stop", pm2_name):
        ok(f"{name} detenida")
    else:
        fail(f"No encontré {pm2_name}")


def cmd_delete(args):
    """Eliminar una instancia completamente."""
    name = getattr(args, 'name', '') or ''
    if not name:
        fail("Especifica el nombre: conny delete clinica-bella"); return

    inst_dir = f"{INSTANCES_DIR}/{name}"
    if not os.path.isdir(inst_dir):
        fail(f"No existe instancia '{name}'"); return

    warn(f"Esto eliminará '{name}' y TODOS sus datos (conversaciones, DB, config)")
    typed = prompt(f"Escribe '{name}' para confirmar")
    if typed != name:
        info("Cancelado"); return

    pm2_name = f"conny-{name}"
    pm2("delete", pm2_name)
    subprocess.run(["pm2", "save"], capture_output=True)
    shutil.rmtree(inst_dir, ignore_errors=True)
    ok(f"Instancia '{name}' eliminada permanentemente")


def cmd_chat(args):
    """Abrir chat en lenguaje natural con una instancia."""
    name = getattr(args, 'name', '') or ''
    chat_script = f"{CONNY_DIR}/conny-chat.py"
    if not os.path.exists(chat_script):
        fail(f"conny-chat.py no encontrado en {CONNY_DIR}")
        info("Asegúrate de subir conny-chat.py al servidor"); return
    cmd = [sys.executable, chat_script]
    if name: cmd.append(name)
    os.execvp(sys.executable, cmd)


def cmd_omni(args):
    """Abrir Conny Omni — el ojo de Santiago."""
    omni_script = f"{CONNY_DIR}/conny-omni.py"
    if not os.path.exists(omni_script):
        fail("conny-omni.py no encontrado"); return

    sub = getattr(args, 'subcommand', '') or 'chat'
    os.execvp(sys.executable, [sys.executable, omni_script, sub])


def cmd_nova(args):
    """Gestión del motor de gobernanza Nova."""
    print_logo(compact=True)
    nova_py = f"{NOVA_DIR}/nova.py" if os.path.isdir(NOVA_DIR) else shutil.which("nova")

    sub = getattr(args, 'subcommand', '') or ''

    if sub == "status" or not sub:
        h = health(NOVA_PORT)
        if h:
            section("Nova activo", f"puerto {NOVA_PORT}")
            kv("Versión",  h.get("version", "—"))
            kv("Estado",   "online")
        else:
            section("Nova", f"no responde en :{NOVA_PORT}")
            info("Para arrancarlo: cd /home/ubuntu/nova-os && docker-compose up -d")
        return

    if not nova_py or not os.path.exists(nova_py):
        fail(f"nova.py no encontrado en {NOVA_DIR}")
        info("Clona nova-os: git clone https://github.com/sxrubyo/nova-os.git /home/ubuntu/nova-os")
        return

    # Delegar al CLI de Nova
    os.execvp(sys.executable, [sys.executable, nova_py] + sys.argv[2:])


def cmd_update(args):
    """Actualizar conny desde GitHub."""
    print_logo(compact=True)
    section("Actualizar Conny")

    repo = prompt("URL del repositorio GitHub", default="https://github.com/sxrubyo/conny.git")
    if not repo:
        fail("Necesitas la URL del repo"); return

    with Spinner("Descargando actualización...", "conny") as sp:
        r = subprocess.run(
            ["git", "pull", "origin", "main"],
            cwd=CONNY_DIR, capture_output=True, text=True
        )
        if r.returncode == 0:
            sp.finish("Archivos actualizados")
        else:
            sp.finish(f"Error: {r.stderr[:80]}", ok=False)
            return

    if confirm("Reiniciar todas las instancias para aplicar cambios?"):
        with Spinner("Reiniciando...", "conny") as sp:
            pm2("restart", "all")
            sp.finish("Todas las instancias reiniciadas con la nueva versión")


def cmd_install(args):
    """Instalar 'conny' como comando del sistema."""
    target = "/usr/local/bin/conny"
    this   = os.path.abspath(__file__)

    try:
        if os.path.exists(target):
            os.remove(target)
        os.symlink(this, target)
        os.chmod(this, 0o755)
        ok(f"'conny' instalado en {target}")
        info("Ahora puedes usar 'conny' desde cualquier directorio")
        print()
        print("  " + q(C.P2, "Prueba:"))
        dim("  conny list")
        dim("  conny new")
        dim("  conny chat")
        print()
    except PermissionError:
        fail("Necesitas sudo: sudo python3 conny --install")
    except Exception as e:
        fail(f"Error: {e}")


def cmd_help(args):
    """Mostrar ayuda."""
    print_logo()
    print("  " + q(C.W, "Comandos disponibles:", bold=True))
    print()

    commands = [
        ("conny init",             "Configurar por primera vez"),
        ("conny new",              "Crear nueva instancia para un cliente"),
        ("conny new clinica-bella","Crear con nombre directo"),
        ("conny list",             "Ver todas las instancias y su estado"),
        ("conny status [nombre]",  "Estado detallado de una instancia"),
        ("conny logs [nombre]",    "Logs en tiempo real"),
        ("conny restart [nombre]", "Reiniciar instancia (o 'all' para todas)"),
        ("conny stop [nombre]",    "Detener instancia"),
        ("conny delete [nombre]",  "Eliminar instancia y sus datos"),
        ("conny chat",             "Hablar en lenguaje natural (instancia base)"),
        ("conny chat [nombre]",    "Hablar con una instancia específica"),
        ("conny omni",             "Abrir Conny Omni — monitoreo de clientes"),
        ("conny omni status",      "Estado rápido de todas las instancias"),
        ("conny nova",             "Gestionar motor de gobernanza Nova"),
        ("conny nova status",      "Ver si Nova está activo"),
        ("conny update",           "Actualizar desde GitHub"),
    ]
    for cmd_str, desc in commands:
        print(f"    {q(C.P2, cmd_str, bold=True)}")
        print(f"    {q(C.G2, desc)}")
        print()

    print("  " + q(C.G3, "─" * 58))
    print()
    info("conny chat acepta lenguaje natural — dile lo que necesitas")
    print()


# ══════════════════════════════════════════════════════════════════════════════
# NATURAL LANGUAGE FALLBACK
# ══════════════════════════════════════════════════════════════════════════════

def nl_route(text):
    """
    Detecta intención del texto libre y redirige al comando correcto.
    'nuevo cliente' → cmd_new
    'ver clientes'  → cmd_list
    etc.
    """
    t = text.lower().strip()

    if any(w in t for w in ["nuevo", "new", "crear", "create", "cliente", "client"]):
        class A: name = ""
        cmd_new(A())
        return True

    if any(w in t for w in ["listar", "list", "ver", "show", "instancias"]):
        class A: pass
        cmd_list(A())
        return True

    if any(w in t for w in ["chat", "hablar", "lenguaje"]):
        parts = t.split()
        class A: name = parts[-1] if len(parts) > 1 else ""
        cmd_chat(A())
        return True

    if any(w in t for w in ["estado", "status", "health"]):
        parts = t.split()
        class A: name = parts[-1] if len(parts) > 1 else ""
        cmd_status(A())
        return True

    if any(w in t for w in ["log", "logs"]):
        parts = t.split()
        class A: name = parts[-1] if len(parts) > 1 else ""
        cmd_logs(A())
        return True

    if any(w in t for w in ["reiniciar", "restart"]):
        parts = t.split()
        class A: name = parts[-1] if len(parts) > 1 else ""
        cmd_restart(A())
        return True

    if any(w in t for w in ["omni", "monitoreo", "monitor"]):
        class A: subcommand = "chat"
        cmd_omni(A())
        return True

    if any(w in t for w in ["nova", "gobernanza"]):
        class A: subcommand = "status"
        cmd_nova(A())
        return True

    return False


# ══════════════════════════════════════════════════════════════════════════════
# MAIN — router de comandos
# ══════════════════════════════════════════════════════════════════════════════

def main():
    parser = argparse.ArgumentParser(prog="conny", add_help=False)
    parser.add_argument("command",    nargs="?", default="")
    parser.add_argument("subcommand", nargs="?", default="")
    parser.add_argument("name",       nargs="?", default="")
    parser.add_argument("--install",  action="store_true")
    parser.add_argument("--version",  "-V", action="store_true")
    parser.add_argument("--help",     "-h", action="store_true")
    args = parser.parse_args()

    if args.version:
        print(f"conny {CONNY_VERSION} ({CONNY_BUILD})")
        return

    if args.install:
        cmd_install(args); return

    # Combinar subcommand en name si es necesario
    if args.subcommand and not args.name:
        args.name = args.subcommand

    cmd = args.command.lower().strip()

    routes = {
        "init":     cmd_init,
        "new":      cmd_new,
        "nuevo":    cmd_new,
        "list":     cmd_list,
        "listar":   cmd_list,
        "ls":       cmd_list,
        "status":   cmd_status,
        "estado":   cmd_status,
        "logs":     cmd_logs,
        "log":      cmd_logs,
        "restart":  cmd_restart,
        "reiniciar":cmd_restart,
        "stop":     cmd_stop,
        "detener":  cmd_stop,
        "delete":   cmd_delete,
        "eliminar": cmd_delete,
        "rm":       cmd_delete,
        "chat":     cmd_chat,
        "omni":     cmd_omni,
        "nova":     cmd_nova,
        "update":   cmd_update,
        "help":     cmd_help,
        "--help":   cmd_help,
        "":         cmd_help,
    }

    if args.help or cmd in ("help", "--help", ""):
        cmd_help(args)
        return

    if cmd in routes:
        routes[cmd](args)
    else:
        # Intentar lenguaje natural con el comando completo
        full_text = " ".join(filter(None, [cmd, args.subcommand, args.name]))
        if not nl_route(full_text):
            fail(f"Comando desconocido: '{cmd}'")
            info("Usa 'conny help' para ver los comandos disponibles")
            info("O prueba en lenguaje natural: conny 'nuevo cliente clinica bella'")


if __name__ == "__main__":
    main()
