"""
@RAG_CONTEXT
{
  "archivo": "src/rag_indexer/trace_manager.py",
  "lineas": 0,
  "tokens_estimados": 0,
  "hash_contenido": "",
  "creacion": "2026-03-16",
  "modificacion": "2026-03-16",
  "tags": [],
  "descripcion_corta": "",
  "descripcion_larga": ""
}
"""
#!/usr/bin/env python3
"""
trace_manager.py — Sistema de Contexto con Trazabilidad
Parsea sesiones .jsonl de Claude Code y genera contexto_activo.json
con vinculación commit ↔ líneas exactas ↔ decisión técnica.

Uso via CLI:
  rag-indexer trace-init      # crea .ai/contexto/ + instala post-commit hook
  rag-indexer trace-sync      # procesa sesiones nuevas (activado por hook)
  rag-indexer trace-backfill  # reprocesa todo el historial
"""

import json
import os
import re
import subprocess
import sys
from pathlib import Path


# ── Auto-detección del directorio de sesiones ─────────────────────────────────

def auto_detect_sessions_dir(repo_root: Path) -> Path | None:
    """
    Detecta el directorio de sesiones .jsonl de la IA activa.

    Orden de precedencia:
      1. RAG_TRACE_SESSIONS_DIR en .env (override manual)
      2. Claude Code: ~/.claude/projects/{slug}/
         donde slug = str(cwd).replace('/', '-')

    Retorna Path si se encuentra, None si no.
    """
    try:
        from dotenv import load_dotenv
        load_dotenv(repo_root / ".env")
    except ImportError:
        pass

    # Override manual
    override = os.environ.get("RAG_TRACE_SESSIONS_DIR", "").strip()
    if override:
        p = Path(override)
        if p.exists():
            return p
        print(f"[trace] RAG_TRACE_SESSIONS_DIR no encontrado: {override}")
        return None

    # Auto-detección Claude Code
    # El slug se genera reemplazando cada / por - en el path absoluto
    # Ej: /root/mi-proyecto → -root-mi-proyecto
    slug = str(repo_root).replace("/", "-")
    claude_dir = Path.home() / ".claude" / "projects" / slug
    if claude_dir.exists() and any(claude_dir.glob("*.jsonl")):
        return claude_dir

    print(
        f"[trace] No se detectó directorio de sesiones para: {repo_root}\n"
        f"        Buscado en: {claude_dir}\n"
        f"        Establece RAG_TRACE_SESSIONS_DIR=/ruta/manual en .env para override."
    )
    return None


# ── Helpers JSON ───────────────────────────────────────────────────────────────

def _load_json(path: Path, default):
    if path.exists():
        try:
            return json.loads(path.read_text())
        except Exception:
            return default
    return default


def _save_json(path: Path, data):
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2))


# ── Extractor de líneas modificadas por commit ────────────────────────────────

def _get_lineas_modificadas(repo_root: Path, commit_hash: str, archivo: str) -> list:
    """Extrae números de línea tocados por un commit en un archivo via git diff."""
    try:
        out = subprocess.check_output(
            ["git", "diff", f"{commit_hash}^", commit_hash, "--unified=0", "--", archivo],
            cwd=repo_root, stderr=subprocess.DEVNULL, text=True
        )
        lineas = []
        for match in re.finditer(r"@@ [^@]* \+(\d+)(?:,(\d+))? @@", out):
            start = int(match.group(1))
            count = int(match.group(2)) if match.group(2) is not None else 1
            if count > 0:
                lineas.extend(range(start, start + min(count, 10)))
        return sorted(set(lineas))
    except Exception:
        return []


def _get_archivos_de_commit(repo_root: Path, commit_hash: str) -> list:
    """Lista archivos tocados por un commit."""
    try:
        out = subprocess.check_output(
            ["git", "diff", "--name-only", f"{commit_hash}^", commit_hash],
            cwd=repo_root, stderr=subprocess.DEVNULL, text=True
        )
        return [f.strip() for f in out.strip().split("\n") if f.strip()]
    except Exception:
        return []


def _get_commit_hash_by_message(repo_root: Path, first_line: str) -> str | None:
    """Encuentra el hash real de un commit buscando por primera línea del mensaje."""
    try:
        out = subprocess.check_output(
            ["git", "log", "--pretty=format:%H %s", "--all", "-200"],
            cwd=repo_root, stderr=subprocess.DEVNULL, text=True
        )
        for log_line in out.strip().split("\n"):
            parts = log_line.split(" ", 1)
            if len(parts) == 2 and first_line.strip() in parts[1]:
                return parts[0][:8]
    except Exception:
        pass
    return None


# ── Parser de commit desde comando Bash ───────────────────────────────────────

def _parse_commit_cmd(cmd: str) -> dict | None:
    """Extrae mensaje y campos JSON del body de un git commit."""
    if "git commit" not in cmd:
        return None

    msg_match = re.search(r'git commit -m "\$\(cat <<\'EOF\'\n(.*?)\nEOF', cmd, re.DOTALL)
    if not msg_match:
        msg_match = re.search(r'git commit -m ["\']([^"\']+)["\']', cmd)
        raw_msg = msg_match.group(1) if msg_match else ""
        return {"mensaje_raw": raw_msg, "json_body": {}} if raw_msg else None

    raw_msg = msg_match.group(1).strip()
    primera_linea = raw_msg.split("\n")[0].strip()

    json_body = {}
    json_match = re.search(r'\{[^}]+\}', raw_msg, re.DOTALL)
    if json_match:
        try:
            json_body = json.loads(json_match.group(0))
        except Exception:
            pass

    return {"mensaje_raw": primera_linea, "json_body": json_body}


# ── Parser de sesión .jsonl ────────────────────────────────────────────────────

def parse_session(repo_root: Path, jsonl_path: Path) -> list:
    """
    Parsea una sesión .jsonl de Claude Code.
    Retorna lista de interacciones {turno, fecha, opcion_menu, usuario_input,
    resumen_tecnico, commits, deuda_tecnica}.
    """
    entries = []
    try:
        lines = jsonl_path.read_text(errors="replace").splitlines()
    except Exception:
        return []

    messages = []
    for raw in lines:
        try:
            obj = json.loads(raw)
        except Exception:
            continue
        if obj.get("type") in ("user", "assistant"):
            messages.append(obj)

    i = 0
    while i < len(messages):
        obj = messages[i]

        if obj.get("type") != "user":
            i += 1
            continue

        content = obj.get("message", {}).get("content", "")
        if not isinstance(content, str) or not content.strip():
            i += 1
            continue

        usuario_input = content.strip()
        timestamp = obj.get("timestamp", "")

        opcion_match = re.match(r"^(\d{1,2})\s*/\s*", usuario_input)
        opcion_menu = opcion_match.group(1) if opcion_match else "libre"

        resumen_tecnico = ""
        commits_data = []
        j = i + 1
        lookahead = min(i + 300, len(messages))

        while j < lookahead:
            aobj = messages[j]
            if aobj.get("type") == "user":
                # Solo romper en mensajes reales del humano, no en tool_results (arrays)
                aobj_content = aobj.get("message", {}).get("content", "")
                if isinstance(aobj_content, str) and aobj_content.strip():
                    break

            if aobj.get("type") == "assistant":
                for block in aobj.get("message", {}).get("content", []):
                    if not isinstance(block, dict):
                        continue

                    # Extraer sección RESPUESTA PARA IA ARQUITECTO
                    if block.get("type") == "text" and not resumen_tecnico:
                        txt = block.get("text", "")
                        if "RESPUESTA PARA IA ARQUITECTO" in txt:
                            start = txt.find("#### RESPUESTA PARA IA ARQUITECTO ####")
                            end = txt.find("#### MANUAL DE OPERARIO ####")
                            if start != -1 and end != -1:
                                section = txt[start + len("#### RESPUESTA PARA IA ARQUITECTO ####"):end]
                                logica_match = re.search(
                                    r"(?:Lógica|Lógica/Decisiones|Decisiones)[:\s]+(.+?)(?:\n- |\Z)",
                                    section, re.DOTALL
                                )
                                if logica_match:
                                    resumen_tecnico = logica_match.group(1).strip()[:400]
                                else:
                                    resumen_tecnico = section.strip()[:400]

                    # Extraer commits desde tool_use Bash
                    if block.get("type") == "tool_use" and block.get("name") == "Bash":
                        cmd = block.get("input", {}).get("command", "")
                        parsed = _parse_commit_cmd(cmd)
                        if parsed and parsed["mensaje_raw"]:
                            jb = parsed["json_body"]
                            commit_hash = _get_commit_hash_by_message(repo_root, parsed["mensaje_raw"])

                            archivos_commit = []
                            if commit_hash:
                                for af in _get_archivos_de_commit(repo_root, commit_hash):
                                    lineas = _get_lineas_modificadas(repo_root, commit_hash, af)
                                    archivos_commit.append({
                                        "archivo": af,
                                        "lineas_modificadas": lineas[:20]
                                    })

                            commits_data.append({
                                "hash": commit_hash or "desconocido",
                                "mensaje": parsed["mensaje_raw"],
                                "resuelve": jb.get("resuelve", ""),
                                "antes": jb.get("antes", ""),
                                "archivos": archivos_commit
                            })
            j += 1

        if usuario_input and len(usuario_input) > 3:
            entries.append({
                "turno": None,
                "fecha": timestamp,
                "opcion_menu": opcion_menu,
                "usuario_input": usuario_input[:300],
                "resumen_tecnico": resumen_tecnico,
                "commits": commits_data,
                "deuda_tecnica": jb.get("deuda_tecnica") if commits_data else None,
            })

        i = j if j > i else i + 1

    return entries


# ── Rotación de bloques ────────────────────────────────────────────────────────

def _next_archivo_index(archivo_dir: Path) -> int:
    indices = []
    for p in archivo_dir.glob("contexto_*.json"):
        m = re.match(r"contexto_(\d+)\.json", p.name)
        if m:
            indices.append(int(m.group(1)))
    return max(indices, default=0) + 1


def _build_resumen_bloque(entries: list, bloque_idx: int) -> dict:
    if not entries:
        return {}
    fechas = [e["fecha"] for e in entries if e.get("fecha")]
    commits_all, archivos_counter, opciones = [], {}, []
    for e in entries:
        opciones.append(e.get("opcion_menu", "libre"))
        for c in e.get("commits", []):
            if c.get("mensaje"):
                commits_all.append(c["mensaje"])
            for a in c.get("archivos", []):
                af = a.get("archivo", "")
                archivos_counter[af] = archivos_counter.get(af, 0) + 1
    return {
        "bloque": bloque_idx,
        "periodo": f"{fechas[0][:10] if fechas else '?'} — {fechas[-1][:10] if fechas else '?'}",
        "total_interacciones": len(entries),
        "opciones_usadas": list(set(opciones)),
        "commits": commits_all[:30],
        "archivos_mas_tocados": sorted(archivos_counter, key=lambda x: -archivos_counter[x])[:5],
    }


def _rotar_si_necesario(activo: list, archivo_dir: Path, resumen_path: Path, limite: int) -> list:
    if len(activo) < limite:
        return activo

    bloque_idx = _next_archivo_index(archivo_dir)
    resumen_bloque = _build_resumen_bloque(activo, bloque_idx)

    archivo_path = archivo_dir / f"contexto_{bloque_idx:03d}.json"
    _save_json(archivo_path, {"bloque": bloque_idx, "resumen": resumen_bloque, "interacciones": activo})

    resumen_global = _load_json(resumen_path, [])
    resumen_global.append(resumen_bloque)
    _save_json(resumen_path, resumen_global)

    print(f"  [rotate] Bloque {bloque_idx:03d} archivado ({len(activo)} entradas) → {archivo_path.name}")
    return []


def _ultimo_turno_global(archivo_dir: Path, activo_path: Path) -> int:
    ultimo = 0
    for p in sorted(archivo_dir.glob("contexto_*.json")):
        for e in _load_json(p, {}).get("interacciones", []):
            if isinstance(e.get("turno"), int):
                ultimo = max(ultimo, e["turno"])
    for e in _load_json(activo_path, []):
        if isinstance(e.get("turno"), int):
            ultimo = max(ultimo, e["turno"])
    return ultimo


# ── Comandos públicos ──────────────────────────────────────────────────────────

def cmd_init(repo_root: Path | None = None):
    """Crea estructura .ai/contexto/ e instala el post-commit hook."""
    from .hook_manager import install_post_commit_hook

    root = repo_root or Path(os.getcwd())
    contexto_dir = root / ".ai" / "contexto"
    archivo_dir = contexto_dir / "archivo"
    procesados_path = contexto_dir / "_procesados.json"

    contexto_dir.mkdir(parents=True, exist_ok=True)
    archivo_dir.mkdir(exist_ok=True)

    if not procesados_path.exists():
        _save_json(procesados_path, [])

    install_post_commit_hook()

    sessions_dir = auto_detect_sessions_dir(root)
    print(f"[trace] Estructura .ai/contexto/ creada en: {root}")
    if sessions_dir:
        print(f"[trace] Sesiones detectadas en: {sessions_dir}")
        print(f"[trace] Ejecuta 'rag-indexer trace-backfill' para procesar el historial.")
    else:
        print(f"[trace] Sin sesiones detectadas. Establece RAG_TRACE_SESSIONS_DIR en .env.")


def cmd_sync(repo_root: Path | None = None, limite: int = 25):
    """Procesa sesiones nuevas (modo normal). Activado por post-commit hook."""
    _run(backfill=False, repo_root=repo_root, limite=limite)


def cmd_backfill(repo_root: Path | None = None, limite: int = 25):
    """Reprocesa todo el historial de sesiones."""
    _run(backfill=True, repo_root=repo_root, limite=limite)


def _run(backfill: bool, repo_root: Path | None, limite: int):
    root = repo_root or Path(os.getcwd())
    contexto_dir = root / ".ai" / "contexto"
    archivo_dir = contexto_dir / "archivo"
    activo_path = contexto_dir / "contexto_activo.json"
    resumen_path = contexto_dir / "contexto_resumen.json"
    procesados_path = contexto_dir / "_procesados.json"

    if not contexto_dir.exists():
        print("[trace] Directorio .ai/contexto/ no encontrado. Ejecuta 'rag-indexer trace-init' primero.")
        sys.exit(0)

    sessions_dir = auto_detect_sessions_dir(root)
    if not sessions_dir:
        sys.exit(0)

    procesados = _load_json(procesados_path, [])
    activo = _load_json(activo_path, [])
    turno_counter = _ultimo_turno_global(archivo_dir, activo_path)

    all_sessions = sorted(
        [p for p in sessions_dir.glob("*.jsonl") if not p.name.startswith("agent-")],
        key=lambda p: p.stat().st_mtime
    )

    if backfill:
        sesiones_a_procesar = all_sessions
        print(f"[trace] Modo backfill: {len(sesiones_a_procesar)} sesiones")
    else:
        sesiones_a_procesar = [s for s in all_sessions if s.name not in procesados]
        if not sesiones_a_procesar:
            print("[trace] Sin sesiones nuevas.")
            sys.exit(0)
        print(f"[trace] {len(sesiones_a_procesar)} sesión(es) nueva(s)")

    nuevas_entradas = 0
    for session_path in sesiones_a_procesar:
        print(f"  → Parseando {session_path.name} ({session_path.stat().st_size // 1024}KB)...")
        entries = parse_session(root, session_path)
        print(f"     {len(entries)} interacciones extraídas")

        for entry in entries:
            turno_counter += 1
            entry["turno"] = turno_counter
            activo.append(entry)
            nuevas_entradas += 1
            activo = _rotar_si_necesario(activo, archivo_dir, resumen_path, limite)

        if session_path.name not in procesados:
            procesados.append(session_path.name)

    _save_json(activo_path, activo)
    _save_json(procesados_path, procesados)
    print(f"[trace] ✓ {nuevas_entradas} entradas nuevas. Activo: {len(activo)}/{limite}")
