"""
@RAG_CONTEXT
{
  "archivo": "src/rag_indexer/main.py",
  "lineas": 426,
  "tokens_estimados": 4597,
  "hash_contenido": "472abdc09dc7154145d63dc20f843a25760ff90ba93acbc2109a37661873517e",
  "creacion": "2026-03-04",
  "modificacion": "2026-03-09",
  "tags": [
    "datetime",
    "COMMENT_SYNTAX",
    "time",
    ".hook_manager",
    "subprocess",
    ".logger",
    ".config",
    "stat",
    ".file_parser",
    "dotenv",
    "install_git_hook",
    "platform",
    "load_dotenv",
    "pathspec",
    "json",
    "os",
    "get_logger",
    "get_llm_descriptions",
    "argparse",
    "getpass",
    ".llm_client"
  ],
  "descripcion_corta": "Orquestador CLI con wizard `rag-indexer api`",
  "descripcion_larga": "Módulo principal del CLI. Añade configure_api(): wizard interactivo con menú de 4 proveedores, captura de API Key con getpass (no se imprime en pantalla) y llamada a save_api_credentials(). Los demás comandos son: init (git hook), prepare (scaffold), run (indexar+LLM), autosync (cron/daemon), run-sync (ciclo interno). Logger rag_indexer_main: INFO en consola, DEBUG completo en /logs/."
}
"""
import os
import sys
import stat
import json
import time
import getpass
import platform
import subprocess
import datetime
import argparse
import pathspec

try:
    import tty
    import termios
    _TTY_AVAILABLE = True
except ImportError:
    _TTY_AVAILABLE = False

from .config import (
    COMMENT_SYNTAX, DEFAULT_EXCLUDES, API_KEYS, ACTIVE_LLM,
    INDEXABLE_EXTENSIONS, INDEXABLE_FILENAMES,
    save_api_credentials, get_effective_extensions,
)
from .file_parser import (
    split_metadata_and_code,
    split_metadata_and_code_json,
    inject_metadata_json,
    extract_dependencies,
    calculate_hash,
    inject_metadata,
    read_mirror_metadata,
    write_mirror_metadata,
)
from .llm_client import get_llm_descriptions
from .hook_manager import install_git_hook
from .logger import get_logger

logger = get_logger("rag_indexer_main")


def load_gitignore(root_dir):
    patterns = [
        'node_modules/', '.git/', 'venv/', '.env',
        '__pycache__/', '*.min.js', '*.min.css',
    ]
    gitignore_path = os.path.join(root_dir, '.gitignore')
    if os.path.exists(gitignore_path):
        with open(gitignore_path, 'r', encoding='utf-8') as f:
            patterns.extend(f.read().splitlines())
    return pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, patterns)


def is_valid_file(filepath, spec, root_dir):
    rel_path = os.path.relpath(filepath, root_dir)
    if spec.match_file(rel_path):
        return False
    ext = os.path.splitext(filepath)[1].lower()
    basename = os.path.basename(filepath)
    if ext in DEFAULT_EXCLUDES:
        return False
    # JSON: indexar si tiene _rag_context legacy inline O si ya tiene espejo en .ai/rag-mirror/
    if ext == '.json':
        rel_path_fwd = os.path.relpath(filepath, root_dir).replace('\\', '/')
        if read_mirror_metadata(root_dir, rel_path_fwd) is not None:
            return True
        try:
            with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
                snippet = f.read(500)
            return '"_rag_context"' in snippet
        except Exception:
            return False
    effective_exts = get_effective_extensions(root_dir)
    if ext in effective_exts:
        return True
    if basename in INDEXABLE_FILENAMES:
        return True
    return False


def process_single_file(filepath, root_dir, is_scaffold=False):
    if not is_scaffold and not API_KEYS.get(ACTIVE_LLM, ""):
        logger.warning(
            "No se detectó API Key para %s. Entrando en modo andamiaje automático...",
            ACTIVE_LLM.upper(),
        )
        is_scaffold = True

    ext = os.path.splitext(filepath)[1].lower()
    rel_path = os.path.relpath(filepath, root_dir).replace('\\', '/')

    with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
        original_content = f.read()

    today = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d")

    # ── Rama JSON flujo (_rag_context legacy) ─────────────────────────────────
    if ext == '.json':
        # old_metadata: primero buscar en espejo, luego en campo _rag_context inline (legacy)
        old_metadata = read_mirror_metadata(root_dir, rel_path)
        if old_metadata is None:
            old_metadata, _ = split_metadata_and_code_json(original_content)
        clean_code = original_content  # hash sobre el JSON completo (sin _rag_context)
        try:
            data = json.loads(original_content)
            code_data = {k: v for k, v in data.items() if k != '_rag_context'}
            clean_code = json.dumps(code_data, ensure_ascii=False, sort_keys=True)
        except Exception:
            pass
        code_hash = calculate_hash(clean_code)
        om = old_metadata or {}
        metadata = {
            "archivo": rel_path,
            "tipo": om.get("tipo", "flujo"),
            "lineas": len(original_content.splitlines()),
            "tokens_estimados": len(clean_code) // 4,
            "hash_contenido": code_hash,
            "creacion": om.get("creacion", today),
            "modificacion": today,
            "tags": om.get("tags", []),
            "descripcion_corta": om.get("descripcion_corta", ""),
            "descripcion_larga": om.get("descripcion_larga", ""),
        }
        if not metadata["descripcion_corta"]:
            if is_scaffold:
                logger.info("Preparando andamiaje para JSON flujo: %s", rel_path)
            else:
                logger.info("Llamando a LLM para JSON flujo: %s", rel_path)
                corta, larga = get_llm_descriptions(clean_code, rel_path)
                metadata["descripcion_corta"] = corta
                metadata["descripcion_larga"] = larga
        else:
            logger.debug("JSON flujo con descripciones preservadas: %s", rel_path)
        inject_metadata_json(filepath, original_content, metadata)
        return metadata

    # ── Rama normal (todos los demás archivos indexables) ──────────────────────
    # old_metadata: primero espejo, luego bloque inline legacy
    old_metadata = read_mirror_metadata(root_dir, rel_path)
    if old_metadata is None:
        old_metadata, _ = split_metadata_and_code(original_content, ext)

    # El "código limpio" para hashing es el contenido sin bloque legacy si lo hubiera
    _, clean_code = split_metadata_and_code(original_content, ext)
    code_hash = calculate_hash(clean_code)

    metadata = {
        "archivo": rel_path,
        "lineas": len(clean_code.splitlines()),
        "tokens_estimados": len(clean_code) // 4,
        "hash_contenido": code_hash,
        "creacion": old_metadata.get("creacion", today) if old_metadata else today,
        "modificacion": today,
        "tags": extract_dependencies(clean_code, ext),
        "descripcion_corta": old_metadata.get("descripcion_corta", "") if old_metadata else "",
        "descripcion_larga": old_metadata.get("descripcion_larga", "") if old_metadata else "",
    }

    needs_update = not old_metadata or old_metadata.get("hash_contenido") != code_hash or not metadata["descripcion_corta"]

    if needs_update:
        if is_scaffold:
            logger.info("Preparando andamiaje para: %s", rel_path)
            metadata["descripcion_corta"] = ""
            metadata["descripcion_larga"] = ""
        else:
            logger.info("Llamando a LLM para contexto de: %s", rel_path)
            corta, larga = get_llm_descriptions(clean_code, rel_path)
            metadata["descripcion_corta"], metadata["descripcion_larga"] = corta, larga
    else:
        logger.debug("Sin cambios (hash idéntico): %s", rel_path)

    inject_metadata(filepath, original_content, clean_code, metadata, ext)
    return metadata


BUENAS_PRACTICAS_CONTENT = """\
# Constitución y Buenas Prácticas de Ingeniería

## 1. CONTROL DE VERSIONES (ESTRICTO)
- NUNCA termines una tarea sin hacer: `git add .` y `git commit -m "tipo(alcance): descripción"`.
- Usa Conventional Commits (`feat:`, `fix:`, `refactor:`, `chore:`).
- Haz commits atómicos y frecuentes. No acumules 50 archivos en un solo commit.

## 2. ARQUITECTURA Y CÓDIGO LIMPIO (CLEAN CODE)
- **Nombres descriptivos:** Las variables y funciones deben explicar qué hacen sin necesidad de comentarios. Evita abreviaturas oscuras (ej. usa `calcular_total_impuestos` en lugar de `calc_tot_imp`).
- **DRY (Don't Repeat Yourself):** Si copias y pegas código más de dos veces, extráelo a una función o componente reutilizable.
- **Funciones Atómicas:** Una función debe hacer UNA sola cosa y hacerla bien.
- **Tipado Estricto:** Usa Type Hints en Python (`def funcion(param: str) -> bool:`) o TypeScript en el frontend siempre que sea posible.

## 3. SEGURIDAD Y MANEJO DE ERRORES
- **Cero Secretos:** NUNCA escribas contraseñas, API Keys o tokens directamente en el código. Usa siempre variables de entorno (`os.getenv()` o `.env`).
- **Manejo de Excepciones:** No uses bloques `try/except` vacíos (silenciar errores). Captura excepciones específicas y registra el error adecuadamente.
- **Cero basura:** Elimina todos los `print()` o `console.log()` de depuración antes de dar la tarea por terminada.

## 4. PLANIFICACIÓN DE CAMBIOS GRANDES (OBLIGATORIO)
- Todo cambio complejo DEBE documentarse antes de programar en: `.ai/planes/{nombre_del_cambio}.json`.
- El JSON DEBE seguir esta estructura exacta:
  {
    "nombre_tarea": "Breve descripción",
    "estado_general": "pendiente | en_progreso | completado",
    "pasos": [
      { "id": 1, "descripcion": "Hacer X", "estado": "pendiente | completado" }
    ],
    "archivos_a_tocar": ["ruta/archivo1.py"],
    "archivos_no_contemplados": [
      { "archivo": "ruta/nuevo.py", "accion": "crear | editar | eliminar", "motivo": "Explicación lógica" }
    ]
  }

## 5. FLUJO DE TRABAJO Y ESTADO ACTIVO
- Por cada sub-cambio (paso) que completes, DEBES abrir el `.json`, cambiar su estado a "completado" y volver a leer todo el plan de acción para no perder el contexto.
- Si necesitas modificar o crear un archivo que no estaba en `archivos_a_tocar`, ESTÁ PROHIBIDO tocarlo sin antes agregarlo al arreglo `archivos_no_contemplados`.

### 🚨 REGLA OBLIGATORIA DE CIERRE Y FEEDBACK LOOP
Al finalizar cualquier tarea de programación, refactorización o actualización de código, DEBES estructurar tu respuesta final estrictamente utilizando estos dos encabezados exactos:

#### RESPUESTA PARA IA ARQUITECTO ####
[Imprime aquí el reporte técnico detallado]
- Archivos modificados: [Lista de rutas exactas de los archivos tocados]
- Nombres de variables/funciones: [Nombres exactos de las variables clave de UI/BD/Alpine que creaste o modificaste]
- Lógica/Decisiones: [Resumen técnico de 1-2 líneas sobre cómo resolviste el problema o si dejaste alguna deuda técnica]

#### MANUAL DE OPERARIO ####
[Imprime aquí la guía de pruebas para el usuario]
- Explícale al usuario en lenguaje sencillo (no técnico) qué fue lo que acabas de implementar.
- Dale instrucciones paso a paso de cómo debe interactuar en la interfaz para probar los cambios (ej. "Ve a la pestaña X, haz clic en el botón Y, observa cómo cambia Z").
- Termina la explicación preguntándole explícitamente: "¿Todo funciona en orden para que pueda marcar esta implementación como completada en la bitácora?"

CONDICIÓN DE CIERRE (QA):
Si en el siguiente turno el usuario te confirma que "Sí, todo está en orden" (o algo afirmativo), tu ÚNICA tarea en ese turno será abrir el archivo de implementaciones (el JSON en `futuras_...`) y actualizar el valor de `"estado": "en_proceso"` a `"estado": "completado"`. No escribas más código, solo actualiza la bitácora.

## 6. CONVENCIÓN DE COMMITS ENRIQUECIDA (TRAZABILIDAD)
- Formato base obligatorio: `tipo(alcance): descripción`
- Tipos válidos: `feat`, `fix`, `docs`, `chore`, `refactor`, `test`, `perf`
- **Opcional pero recomendado**: incluir un JSON body en el mensaje del commit para máxima trazabilidad.
  Esto permite a `rag-indexer trace` vincular cada cambio con la decisión técnica original:

```
git commit -m "$(cat <<'EOF'
fix(modulo): descripción del fix

{
  "resuelve": "Descripción del bug o tarea que este commit cierra",
  "antes": "Comportamiento anterior (síntoma visible del bug)",
  "deuda_tecnica": "Limitación conocida que queda pendiente (si aplica)",
  "archivos_clave": ["ruta/archivo.py:linea"]
}

Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
```

## 7. PROTOCOLO DE CONFIRMACIÓN ANTES DE EJECUTAR
Antes de modificar cualquier archivo, la IA DEBE estructurar su respuesta con las siguientes tres
banderas en este orden exacto, y ESPERAR aprobación del usuario antes de proceder:

```
##########---RESUMEN DE LA IA---##########
[Qué entendió la IA: parafrasea el problema, confirma el alcance]

##########---EXPLICACION TECNICA---##########
[Plan detallado: archivos a modificar, funciones afectadas,
 decisiones de arquitectura, efectos secundarios, pasos ordenados]

##########---EXPLICACION RESUMDA---##########
[Explicación en lenguaje sencillo para usuario no técnico:
 qué se va a resolver y cómo lo notará en la interfaz]
```

Solo si el usuario confirma ("sí", "adelante", "ok" o similar), proceder con la implementación.
"""


MENU_SESION_CONTENT = """\
# Menú de Sesión — {project_name}

## CUÁNDO MOSTRAR ESTE MENÚ

1. **Al finalizar cualquier tarea** — después del bloque `#### MANUAL DE OPERARIO ####` y la pregunta de QA.
2. **Al inicio de sesión fresca** — después de leer `rag-indexer.json` y reportar el índice.

---

## FORMATO DE RESPUESTA DEL USUARIO

El usuario responde con el número seguido de `/` y su explicación:

```
numero/explicacion detallada del cambio o problema
```

Ejemplos válidos:
- `3/El botón de guardar no reacciona al primer clic`
- `1/Agregar exportación a Excel con columnas configurables`
- `9/Commit de todo lo hecho hoy y merge a master`

---

## PROTOCOLO DE CONFIRMACIÓN (OBLIGATORIO ANTES DE EJECUTAR)

Antes de modificar cualquier archivo, estructurar respuesta con tres banderas y ESPERAR aprobación:

```
##########---RESUMEN DE LA IA---##########
[Parafrasea el problema, confirma el alcance]

##########---EXPLICACION TECNICA---##########
[Archivos a modificar, funciones afectadas, decisiones de arquitectura, efectos secundarios]

##########---EXPLICACION RESUMDA---##########
[Explicación en lenguaje sencillo: qué se va a resolver y cómo lo notará el usuario]
```

---

## MENÚ PRINCIPAL

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  {project_name} — ¿Qué hacemos ahora?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  IMPLEMENTACIONES
  1 — Nueva funcionalidad / feature
  2 — Mejora o actualización de una funcionalidad existente
  3 — Resolver un problema de interfaz de usuario (UX/UI)

  BACKEND Y DATOS
  4 — Resolver un bug de cálculo o lógica de negocio
  5 — Resolver un bug de API / base de datos / backend
  6 — Agregar o modificar estructura de base de datos (migración SQL)

  DOCUMENTACIÓN Y CONFIGURACIÓN
  7 — Actualizar o crear documentación / prompts de IA
  8 — Ajustar configuración del proyecto (puertos, variables, Docker, etc.)

  GIT Y PRODUCCIÓN
  9 — Implementación exitosa: commit + merge a rama principal
  10 — Solo hacer commit (sin merge todavía)
  11 — Revisar estado del repositorio y ramas

  MANTENIMIENTO
  12 — Refactorizar / limpiar código (sin cambio de comportamiento)
  13 — Re-indexar el RAG (actualizar rag-indexer.json)
  14 — Otro (describe libremente)

  PLANEACIÓN
  15 — Sesión de planeación / lluvia de ideas (sin escribir código)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Responde con: numero/explicacion
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```

---

## NOTAS PARA LA IA

- **Nunca ejecutes sin confirmar**: el protocolo de tres banderas es obligatorio.
- **Opciones 9 y 10**: ejecuta `git status` y lista los archivos staged para aprobación.
- **Opción 13**: leer rag-indexer.json → revisar archivos nuevos/modificados → actualizar @RAG_CONTEXT → correr indexador → reportar.
- **Opción 15 — MODO PLANEACIÓN**: solo lectura (Read/Glob/Grep). Sin escritura de código. IA participa activamente en lluvia de ideas leyendo el código real para verificar viabilidad.
"""


def setup_ai_prompts_folder(project_root):
    prompts_dir = os.path.join(project_root, '.ai', 'prompts')
    planes_dir = os.path.join(project_root, '.ai', 'planes')
    os.makedirs(prompts_dir, exist_ok=True)
    os.makedirs(planes_dir, exist_ok=True)

    # 01_buenas_practicas.md — actualizar solo si el contenido limpio (sin @RAG_CONTEXT) difiere.
    # Sobreescribir siempre destruía el bloque @RAG_CONTEXT inyectado → LLM llamado en cada run.
    rules_file = os.path.join(prompts_dir, '01_buenas_practicas.md')
    needs_write = True
    if os.path.exists(rules_file):
        with open(rules_file, 'r', encoding='utf-8') as f:
            existing = f.read()
        from .file_parser import split_metadata_and_code
        _, existing_clean = split_metadata_and_code(existing, '.md')
        needs_write = existing_clean.strip() != BUENAS_PRACTICAS_CONTENT.strip()
    if needs_write:
        with open(rules_file, 'w', encoding='utf-8') as f:
            f.write(BUENAS_PRACTICAS_CONTENT)
    logger.info("Carpetas .ai/prompts/ y .ai/planes/ listas. Reglas actualizadas.")

    # 02_menu_sesion.md — solo crear si no existe (el usuario puede personalizarlo)
    menu_file = os.path.join(prompts_dir, '02_menu_sesion.md')
    if not os.path.exists(menu_file):
        project_name = os.path.basename(project_root) or "Proyecto"
        with open(menu_file, 'w', encoding='utf-8') as f:
            f.write(MENU_SESION_CONTENT.format(project_name=project_name))
        logger.info("Menú de sesión creado en: %s", menu_file)


def run_indexer(is_scaffold=False):
    project_root = os.getcwd()
    modo = "Andamiaje (Scaffold)" if is_scaffold else "Completo (API LLM)"
    logger.info("Iniciando motor de indexación RAG en: %s | Modo: %s", project_root, modo)

    setup_ai_prompts_folder(project_root)

    spec = load_gitignore(project_root)
    master_tree = []
    total_tokens = 0
    total_files = 0

    # Leer nombre del proyecto desde .rag-config.json si existe
    _rag_config_path = os.path.join(project_root, '.rag-config.json')
    _nombre_proyecto = os.path.basename(project_root) or "Proyecto_RAG"
    if os.path.exists(_rag_config_path):
        try:
            with open(_rag_config_path, 'r', encoding='utf-8') as _f:
                _rag_cfg = json.load(_f)
            _nombre_proyecto = _rag_cfg.get('nombre_proyecto', '').strip() or _nombre_proyecto
        except Exception:
            pass

    for subdir, dirs, files in os.walk(project_root):
        dirs[:] = [
            d for d in dirs
            if not spec.match_file(
                os.path.relpath(os.path.join(subdir, d), project_root) + '/'
            )
        ]
        for file in files:
            filepath = os.path.join(subdir, file)
            if is_valid_file(filepath, spec, project_root):
                try:
                    meta = process_single_file(filepath, project_root, is_scaffold=is_scaffold)
                    master_tree.append(meta)
                    total_tokens += meta['tokens_estimados']
                    total_files += 1
                except Exception:
                    logger.exception("Error procesando archivo | filepath=%s", filepath)

    instrucciones_agente = (
        "PROTOCOLO DE INDEXACIÓN AUTÓNOMA (PARA LA IA):\n"
        "Este archivo está bloqueado a nivel de sistema (Read-Only). Tienes prohibido intentar editarlo directamente.\n"
        "Cuando el usuario te pida leer este archivo o actualizar el contexto, DEBES seguir este flujo exacto y de forma autónoma:\n\n"
        "PASO 0 (PREREQUISITO): Antes de continuar, verifica si existe el archivo `.rag-config.json` en la raíz del proyecto Y si tiene el campo `nombre_proyecto` con un valor no vacío. Si no existe o el campo está vacío, pregunta al usuario: \"¿Cómo se llama este proyecto? (Se usará como título del menú de sesión)\". Con la respuesta, crea o actualiza `.rag-config.json` con: {\"nombre_proyecto\": \"nombre indicado por el usuario\"}. Solo entonces continúa con PASO 1.\n"
        "PASO 1: Ejecuta en tu terminal el comando `rag-indexer prepare` y espera a que termine de procesar.\n"
        "PASO 2: Vuelve a leer este archivo JSON (que ahora estará actualizado) y busca los archivos que tengan las descripciones vacías.\n"
        "PASO 3: Analiza esos archivos y escribe las descripciones técnicas en sus archivos espejo ubicados en `.ai/rag-mirror/{ruta_relativa}.rag.json` — NO en los archivos fuente. Los archivos fuente deben permanecer limpios sin bloques @RAG_CONTEXT.\n"
        "PASO 4: Al finalizar de editar los archivos espejo, ejecuta obligatoriamente en tu terminal el comando `rag-indexer run` para sellar el índice.\n"
        "PASO 5: Entrégale al usuario un reporte detallado indicando cuántos archivos analizaste, qué descripciones agregaste y el resumen de salida del indexador.\n"
        "PASO 6: OBLIGATORIO — Al finalizar el protocolo, lee `.ai/prompts/02_menu_sesion.md` y muestra el bloque MENÚ PRINCIPAL completo. Usa como título el valor del campo `proyecto` de este mismo JSON (rag-indexer.json) en lugar del texto que aparece en el archivo. Espera la selección del usuario y actúa en consecuencia según las instrucciones de ese mismo archivo.\n\n"
        "IMPORTANTE: Lee obligatoriamente `.ai/prompts/01_buenas_practicas.md`. Si vas a programar código nuevo, crea el plan en `.ai/planes/`, actualiza el estado paso a paso y registra archivos no contemplados."
    )

    master_index = {
        "_INSTRUCCIONES_PARA_EL_AGENTE": instrucciones_agente,
        "proyecto": _nombre_proyecto,
        "ultima_indexacion": datetime.datetime.now(datetime.timezone.utc).isoformat() + "Z",
        "estadisticas": {
            "total_archivos": total_files,
            "total_tokens_estimados": total_tokens
        },
        "arbol": master_tree
    }

    output_path = os.path.join(project_root, 'rag-indexer.json')
    if os.path.exists(output_path):
        os.chmod(output_path, stat.S_IWRITE | stat.S_IREAD)
    with open(output_path, 'w', encoding='utf-8') as f:
        json.dump(master_index, f, indent=2, ensure_ascii=False)
    os.chmod(output_path, stat.S_IREAD)
    logger.info("Archivo %s bloqueado con exito (Read-Only).", output_path)

    logger.info("Operacion completada. Archivos: %d | Tokens: ~%d", total_files, total_tokens)

    # Tip tmux: solo en modo scaffold y si tmux disponible pero no configurado
    if is_scaffold:
        try:
            from .tmux_manager import is_tmux_available
            from dotenv import load_dotenv
            load_dotenv(os.path.join(project_root, '.env'))
            tmux_enabled = os.environ.get("RAG_TMUX_ENABLED", "false").lower() == "true"
            if is_tmux_available() and not tmux_enabled:
                logger.info(
                    "💡 Tip: tmux detectado — ejecuta 'rag-indexer tmux-init' para persistencia de sesión AI."
                )
        except Exception:
            pass


def run_sync():
    """
    Comando interno: escaneo automático con candado de API Key.
    Flujo: verificar clave → detectar cambios → llamar LLM → sellar índice.
    """
    # PASO 1 — Candado de seguridad: verificar API Key en entorno o .env
    from dotenv import load_dotenv
    load_dotenv(os.path.join(os.getcwd(), '.env'))

    api_keys_encontradas = [
        os.environ.get("RAG_API_KEY", ""),
        os.environ.get("OPENAI_API_KEY", ""),
        os.environ.get("ANTHROPIC_API_KEY", ""),
        os.environ.get("GEMINI_API_KEY", ""),
        os.environ.get("DEEPSEEK_API_KEY", ""),
    ]
    if not any(api_keys_encontradas):
        logger.warning("Auto-escaneo omitido: No se detectó API Key")
        return

    # PASO 2 — Detectar archivos modificados o con descripciones vacías (fase prepare)
    project_root = os.getcwd()
    spec = load_gitignore(project_root)
    archivos_con_cambios = []

    for subdir, dirs, files in os.walk(project_root):
        dirs[:] = [
            d for d in dirs
            if not spec.match_file(
                os.path.relpath(os.path.join(subdir, d), project_root) + '/'
            )
        ]
        for file in files:
            filepath = os.path.join(subdir, file)
            if not is_valid_file(filepath, spec, project_root):
                continue
            try:
                with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
                    content = f.read()
                ext = os.path.splitext(filepath)[1].lower()
                rel_path = os.path.relpath(filepath, project_root).replace('\\', '/')
                # Leer old_metadata desde espejo primero, luego bloque legacy
                old_metadata = read_mirror_metadata(project_root, rel_path)
                if old_metadata is None:
                    old_metadata, _ = split_metadata_and_code(content, ext)
                _, clean_code = split_metadata_and_code(content, ext)
                code_hash = calculate_hash(clean_code)
                needs_update = (
                    not old_metadata
                    or old_metadata.get("hash_contenido") != code_hash
                    or not old_metadata.get("descripcion_corta")
                )
                if needs_update:
                    archivos_con_cambios.append(filepath)
            except Exception:
                pass

    # PASO 3 — Solo si hay archivos con cambios o descripciones vacías, invocar IA
    if not archivos_con_cambios:
        logger.info("Auto-Sync: Sin cambios detectados. Ciclo completado sin trabajo.")
        return

    logger.info(
        "Auto-Sync: %d archivo(s) con cambios o descripciones vacias. Invocando IA...",
        len(archivos_con_cambios),
    )

    # PASO 4 — Sellar el índice (equivalente a `rag-indexer run`)
    run_indexer(is_scaffold=False)


def migrate_to_mirror():
    """
    Extrae bloques @RAG_CONTEXT legacy de archivos fuente y los mueve al espejo.
    Idempotente: si el espejo ya existe para un archivo, preserva sus descripciones.
    """
    project_root = os.getcwd()
    spec = load_gitignore(project_root)
    migrados = 0
    ya_limpios = 0
    errores = 0

    logger.info("Iniciando migración de bloques @RAG_CONTEXT → .ai/rag-mirror/")

    for subdir, dirs, files in os.walk(project_root):
        dirs[:] = [
            d for d in dirs
            if not spec.match_file(
                os.path.relpath(os.path.join(subdir, d), project_root) + '/'
            )
        ]
        for file in files:
            filepath = os.path.join(subdir, file)
            if not is_valid_file(filepath, spec, project_root):
                continue
            try:
                with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
                    content = f.read()
                ext = os.path.splitext(filepath)[1].lower()
                rel_path = os.path.relpath(filepath, project_root).replace('\\', '/')

                # ── JSON flujo con _rag_context inline ───────────────────────
                if ext == '.json':
                    try:
                        data = json.loads(content)
                        if '_rag_context' not in data:
                            ya_limpios += 1
                            continue
                        inline_meta = data['_rag_context']
                        # Preservar espejo existente si ya tiene descripciones
                        existing = read_mirror_metadata(project_root, rel_path)
                        if existing and existing.get('descripcion_corta'):
                            inline_meta = existing
                        write_mirror_metadata(project_root, rel_path, inline_meta)
                        # Limpiar fuente
                        clean_data = {k: v for k, v in data.items() if k != '_rag_context'}
                        with open(filepath, 'w', encoding='utf-8') as f:
                            f.write(json.dumps(clean_data, indent=2, ensure_ascii=False) + '\n')
                        logger.info("  ✓ JSON migrado: %s", rel_path)
                        migrados += 1
                    except Exception as e:
                        logger.warning("  ✗ Error en JSON %s: %s", rel_path, e)
                        errores += 1
                    continue

                # ── Archivos con COMMENT_SYNTAX inline ───────────────────────
                inline_meta, clean_code = split_metadata_and_code(content, ext)
                if inline_meta is None:
                    ya_limpios += 1
                    continue
                # Preservar espejo existente si ya tiene descripciones
                existing = read_mirror_metadata(project_root, rel_path)
                if existing and existing.get('descripcion_corta'):
                    inline_meta = existing
                write_mirror_metadata(project_root, rel_path, inline_meta)
                # Limpiar fuente
                with open(filepath, 'w', encoding='utf-8') as f:
                    f.write(clean_code)
                logger.info("  ✓ Migrado: %s", rel_path)
                migrados += 1

            except Exception as e:
                logger.warning("  ✗ Error en %s: %s", rel_path, e)
                errores += 1

    logger.info(
        "Migración completada — %d migrados | %d ya limpios | %d errores",
        migrados, ya_limpios, errores
    )
    if migrados > 0:
        logger.info("Espejo disponible en: .ai/rag-mirror/")
        logger.info("Ejecuta 'rag-indexer run' para regenerar rag-indexer.json.")


def autosync():
    """
    Activa el Agente de Sincronización Continua con detección híbrida de OS.
    - Linux: inyecta regla en crontab (0 * * * *)
    - Otros (Windows/Mac): inicia daemon Python con bucle infinito de 1 hora
    """
    sistema = platform.system()

    if sistema == 'Linux':
        # Modo Cron: registrar `rag-indexer run-sync` en el crontab del usuario
        cron_job = "0 * * * * rag-indexer run-sync >> /tmp/rag-autosync.log 2>&1\n"

        result = subprocess.run(
            ['crontab', '-l'],
            capture_output=True,
            text=True
        )
        crontab_actual = result.stdout if result.returncode == 0 else ""

        if cron_job.strip() in crontab_actual:
            logger.info("Auto-Sync (Linux Cron): La regla ya estaba registrada. Sin cambios.")
            return

        nuevo_crontab = crontab_actual + cron_job
        subprocess.run(
            ['crontab', '-'],
            input=nuevo_crontab,
            text=True,
            check=True
        )
        logger.info(
            "Auto-Sync activado (Linux Cron): rag-indexer run-sync se ejecutará cada hora."
        )
        logger.info("Log disponible en: /tmp/rag-autosync.log")
    else:
        # Modo Daemon: bucle Python bloqueante (requiere terminal abierta)
        logger.warning("Auto-Sync iniciado en modo Daemon (%s).", sistema)
        logger.info(
            "IMPORTANTE: Mantén esta terminal abierta o ejecútala en segundo plano (nohup, screen, etc.)."
        )
        logger.info("El escaneo automático se realizará cada 1 hora.")
        while True:
            run_sync()
            time.sleep(3600)


def _masked_input(prompt, visible=5):
    """
    Lee input de teclado mostrando los primeros `visible` caracteres reales
    y '*' para el resto. Soporta backspace. Fallback a getpass en entornos sin TTY.
    """
    if not _TTY_AVAILABLE or not sys.stdin.isatty():
        return getpass.getpass(prompt)

    sys.stdout.write(prompt)
    sys.stdout.flush()

    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)
    chars = []

    try:
        tty.setraw(fd)
        while True:
            ch = sys.stdin.read(1)
            if ch in ('\r', '\n'):
                sys.stdout.write('\n')
                sys.stdout.flush()
                break
            elif ch == '\x03':          # Ctrl+C
                sys.stdout.write('\n')
                sys.stdout.flush()
                raise KeyboardInterrupt
            elif ch in ('\x7f', '\x08'):  # Backspace
                if chars:
                    chars.pop()
                    sys.stdout.write('\b \b')
                    sys.stdout.flush()
            elif ch == '\x00':          # null byte: ignorar silenciosamente
                pass
            else:
                chars.append(ch)
                echo = ch if len(chars) <= visible else '*'
                sys.stdout.write(echo)
                sys.stdout.flush()
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

    return ''.join(chars)


def configure_api():
    """
    Asistente interactivo para configurar el proveedor de IA y la API Key.
    Equivalente a 'gh auth login': guía al usuario paso a paso sin tocar archivos a mano.
    La clave se captura con getpass para que no se imprima en pantalla.
    """
    PROVIDERS = {
        "1": ("openai",    "OpenAI"),
        "2": ("anthropic", "Anthropic"),
        "3": ("deepseek",  "DeepSeek"),
        "4": ("gemini",    "Google (Gemini)"),
    }

    logger.info("")
    logger.info("=== Asistente de Configuración de API ===")
    logger.info("")
    print("Selecciona tu proveedor de IA:")
    for key, (_, name) in PROVIDERS.items():
        print(f"  {key}) {name}")

    choice = input("\nNúmero de opción [1-4]: ").strip()
    if choice not in PROVIDERS:
        logger.error("Opción inválida: '%s'. Elige un número entre 1 y 4.", choice)
        return

    provider_slug, provider_name = PROVIDERS[choice]

    api_key = _masked_input(
        f"\nIntroduce tu API Key de {provider_name} (primeros 5 chars visibles): "
    )
    api_key = api_key.strip()
    if not api_key:
        logger.error("API Key vacía. Configuración cancelada.")
        return

    # Vista previa enmascarada para confirmar que se pegó correctamente
    if len(api_key) > 10:
        preview = api_key[:5] + '*' * (len(api_key) - 9) + api_key[-4:]
    else:
        preview = api_key[:5] + '*' * max(0, len(api_key) - 5)
    print(f"   Vista previa: {preview} ({len(api_key)} caracteres)")

    confirm = input("   ¿Es correcto? [S/n]: ").strip().lower()
    if confirm == 'n':
        logger.error("Configuración cancelada por el usuario.")
        return

    save_api_credentials(provider_slug, api_key)

    logger.info("")
    logger.info("✅ Configuración guardada correctamente.")
    logger.info("   Proveedor: %s", provider_name)
    logger.info("   Archivo:   .env (en la raíz del proyecto)")
    logger.info("")

    # ── Configuración opcional de tmux ───────────────
    from .tmux_manager import is_tmux_available, get_tmux_version, install_tmux_interactive, save_tmux_config, get_session_name
    print("\n--- Persistencia de sesión (tmux) ---")
    if is_tmux_available():
        print(f"  tmux detectado: {get_tmux_version()}")
        use_tmux = input("  ¿Deseas activar persistencia de sesión con tmux? [S/n]: ").strip().lower()
        if use_tmux != "n":
            ai_cmd = input("  Comando de tu AI CLI (ej. claude, cursor, aider) [claude]: ").strip() or "claude"
            session_name = get_session_name()
            save_tmux_config(enabled=True, ai_cmd=ai_cmd, session_name=session_name)
            logger.info("  tmux configurado. Ejecuta 'rag-indexer tmux-init' para inicializar la sesión.")
    else:
        print("  tmux no detectado en este sistema.")
        print("  tmux permite mantener sesiones AI activas aunque se cierre la terminal (ahorra tokens).")
        install = input("  ¿Deseas instalar tmux ahora? [S/n]: ").strip().lower()
        if install != "n":
            ok = install_tmux_interactive()
            if ok:
                ai_cmd = input("  Comando de tu AI CLI (ej. claude, cursor, aider) [claude]: ").strip() or "claude"
                session_name = get_session_name()
                save_tmux_config(enabled=True, ai_cmd=ai_cmd, session_name=session_name)
                logger.info("  tmux configurado. Ejecuta 'rag-indexer tmux-init' para inicializar la sesión.")

    logger.info("")
    logger.info("Próximo paso: ejecuta 'rag-indexer run' para indexar tu proyecto.")


def cmd_help() -> None:
    """Muestra la ayuda completa agrupada por categoría con interfaz visual."""
    try:
        from importlib.metadata import version as _pkg_version
        ver = _pkg_version("rag-context-indexer")
    except Exception:
        ver = "?"

    # ── Colores (desactivables con NO_COLOR o cuando stdout no es TTY) ──
    use_color = sys.stdout.isatty() and not os.environ.get("NO_COLOR")
    R  = "\033[0m"    if use_color else ""   # reset
    B  = "\033[1m"    if use_color else ""   # bold
    C  = "\033[36m"   if use_color else ""   # cyan
    Y  = "\033[33m"   if use_color else ""   # yellow
    G  = "\033[32m"   if use_color else ""   # green
    DM = "\033[2m"    if use_color else ""   # dim

    W = 62  # ancho de la caja

    def box(lines):
        print(f"{B}╔{'═' * W}╗{R}")
        for line in lines:
            pad = W - len(line)
            print(f"{B}║{R} {line}{' ' * (pad - 1)}{B}║{R}")
        print(f"{B}╚{'═' * W}╝{R}")

    def group(title, icon="▸"):
        print(f"\n  {Y}{B}{icon} {title}{R}")
        print(f"  {DM}{'─' * (W - 2)}{R}")

    def cmd(name, desc, internal=False):
        name_col = f"{G}{name:<20}{R}"
        desc_col = f"{DM}{desc}{R}" if internal else desc
        print(f"    {name_col}  {desc_col}")

    # ── Encabezado ──────────────────────────────────────────────────────
    box([
        f"{B}RAG Context Indexer{R}  {DM}v{ver}{R}",
        "",
        "Optimización de contexto LLM · Trazabilidad · Persistencia tmux",
    ])

    # ── Grupo 1: Configuración ───────────────────────────────────────────
    group("CONFIGURACIÓN INICIAL")
    cmd("api",     "Wizard interactivo: configura proveedor de IA y API Key")
    cmd("init",    "Instala el pre-commit hook de Git en el proyecto")
    cmd("prepare", "Crea archivos espejo .rag.json vacíos en .ai/rag-mirror/ (scaffold)")
    cmd("migrate", "Migra bloques @RAG_CONTEXT legacy de fuentes al espejo (one-shot)")

    # ── Grupo 2: Indexación ──────────────────────────────────────────────
    group("INDEXACIÓN RAG")
    cmd("run",      "Indexa el proyecto llamando al LLM (genera rag-indexer.json)")
    cmd("autosync", "Activa escaneo automático cada hora (cron en Linux, daemon en otros)")
    cmd("run-sync", "Ciclo interno de escaneo con candado de API Key", internal=True)

    # ── Grupo 3: Trazabilidad ────────────────────────────────────────────
    group("TRAZABILIDAD", icon="▸")
    cmd("trace-init",     "Crea .ai/contexto/ e instala el post-commit hook")
    cmd("trace-sync",     "Procesa sesiones nuevas desde el post-commit hook")
    cmd("trace-backfill", "Reprocesa todo el historial de sesiones (primera vez)")

    # ── Grupo 4: tmux ────────────────────────────────────────────────────
    group("PERSISTENCIA DE SESIÓN  (tmux)")
    cmd("tmux-init",        "Instala/configura tmux y crea la sesión del proyecto")
    cmd("tmux-attach",      "Conecta o reconecta a la sesión (la recrea si no existe)")
    cmd("tmux-del-session", "Elimina la sesión activa del proyecto")
    cmd("tmux-status",      "Muestra el estado de la sesión del proyecto actual")
    cmd("tmux-list",        "Lista todas las sesiones rag-indexer activas en el sistema")

    # ── Pie ──────────────────────────────────────────────────────────────
    print(f"\n  {DM}{'─' * (W - 2)}{R}")
    print(f"  {DM}Uso:{R}  {B}rag-indexer{R} {C}<comando>{R}")
    print(f"  {DM}Docs:{R} https://pypi.org/project/rag-context-indexer/\n")


def main():
    parser = argparse.ArgumentParser(
        description="RAG Context Indexer - DevTool para optimización de contexto en LLMs"
    )
    subparsers = parser.add_subparsers(dest="command", help="Comandos disponibles")
    subparsers.add_parser("help",    help="Muestra todos los comandos agrupados con descripción")
    subparsers.add_parser("api",     help="Asistente interactivo para configurar proveedor de IA y API Key")
    subparsers.add_parser("init",    help="Instala el Git Pre-commit Hook")
    subparsers.add_parser("prepare", help="Fase 1: Crea archivos espejo .rag.json vacíos en .ai/rag-mirror/ (scaffold)")
    subparsers.add_parser("migrate", help="Migra bloques @RAG_CONTEXT legacy de archivos fuente al espejo .ai/rag-mirror/")
    subparsers.add_parser("run",     help="Fase 2: Ejecuta escaneo y llama a la API (Flujo normal/Sellado)")
    subparsers.add_parser(
        "autosync",
        help="Activa sincronización continua: Cron en Linux, Daemon en otros OS"
    )
    subparsers.add_parser(
        "run-sync",
        help="[Interno] Ciclo de auto-escaneo con candado de API Key (usado por Cron/Daemon)"
    )
    subparsers.add_parser(
        "trace-init",
        help="Trazabilidad: crea .ai/contexto/ e instala post-commit hook"
    )
    subparsers.add_parser(
        "trace-sync",
        help="Trazabilidad: procesa sesiones nuevas (activado por post-commit hook)"
    )
    subparsers.add_parser(
        "trace-backfill",
        help="Trazabilidad: reprocesa todo el historial de sesiones"
    )
    subparsers.add_parser(
        "tmux-init",
        help="tmux: instala/configura tmux y crea sesión persistente del proyecto"
    )
    subparsers.add_parser(
        "tmux-attach",
        help="tmux: conecta a la sesión del proyecto (la recrea si no existe)"
    )
    subparsers.add_parser(
        "tmux-del-session",
        help="tmux: elimina la sesión activa del proyecto"
    )
    subparsers.add_parser(
        "tmux-status",
        help="tmux: muestra el estado de la sesión del proyecto actual"
    )
    subparsers.add_parser(
        "tmux-list",
        help="tmux: lista todas las sesiones rag-indexer activas en el sistema"
    )

    args = parser.parse_args()

    if args.command == "help":
        cmd_help()
    elif args.command == "api":
        configure_api()
    elif args.command == "init":
        install_git_hook()
    elif args.command == "prepare":
        run_indexer(is_scaffold=True)
    elif args.command == "migrate":
        migrate_to_mirror()
    elif args.command == "run":
        run_indexer(is_scaffold=False)
    elif args.command == "autosync":
        autosync()
    elif args.command == "run-sync":
        run_sync()
    elif args.command == "trace-init":
        from .trace_manager import cmd_init
        cmd_init()
    elif args.command == "trace-sync":
        from .trace_manager import cmd_sync
        cmd_sync()
    elif args.command == "trace-backfill":
        from .trace_manager import cmd_backfill
        cmd_backfill()
    elif args.command == "tmux-init":
        from .tmux_manager import cmd_init
        cmd_init()
    elif args.command == "tmux-attach":
        from .tmux_manager import cmd_attach
        cmd_attach()
    elif args.command == "tmux-del-session":
        from .tmux_manager import cmd_del_session
        cmd_del_session()
    elif args.command == "tmux-status":
        from .tmux_manager import cmd_status
        cmd_status()
    elif args.command == "tmux-list":
        from .tmux_manager import cmd_list
        cmd_list()
    else:
        cmd_help()


if __name__ == "__main__":
    main()
