"""
@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, DATA_ONLY_EXTENSIONS, API_KEYS, ACTIVE_LLM, save_api_credentials
from .file_parser import (
    split_metadata_and_code,
    extract_dependencies,
    calculate_hash,
    inject_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()
    if ext in DEFAULT_EXCLUDES or ext in DATA_ONLY_EXTENSIONS or ext not in COMMENT_SYNTAX:
        return False
    return True


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()

    old_metadata, clean_code = split_metadata_and_code(original_content, ext)
    code_hash = calculate_hash(clean_code)
    today = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d")

    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.
"""


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)
    rules_file = os.path.join(prompts_dir, '01_buenas_practicas.md')
    if not os.path.exists(rules_file):
        with open(rules_file, 'w', encoding='utf-8') as f:
            f.write(BUENAS_PRACTICAS_CONTENT)
        logger.info("Archivo de buenas practicas creado en: %s", rules_file)
    else:
        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.")


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

    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 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 DIRECTAMENTE en los bloques @RAG_CONTEXT dentro de los archivos fuente (.py, .js, .html, etc.).\n"
        "PASO 4: Al finalizar de editar todo el código fuente, 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\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": os.path.basename(project_root) or "Proyecto_RAG",
        "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)


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("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()
                old_metadata, 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 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()
            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("")
    logger.info("Próximo paso: ejecuta 'rag-indexer run' para indexar tu proyecto.")


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("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: Inyecta plantillas vacías (Scaffold) para agentes locales")
    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)"
    )

    args = parser.parse_args()

    if args.command == "api":
        configure_api()
    elif args.command == "init":
        install_git_hook()
    elif args.command == "prepare":
        run_indexer(is_scaffold=True)
    elif args.command == "run":
        run_indexer(is_scaffold=False)
    elif args.command == "autosync":
        autosync()
    elif args.command == "run-sync":
        run_sync()
    else:
        parser.print_help()


if __name__ == "__main__":
    main()
