#!/usr/bin/env python3
"""
Indexe les fichiers Markdown de la doc Funk dans Qdrant.
Usage: rag-ingest [docs_dir]
       docs_dir : répertoire contenant les .md (défaut: /srv/data/rag/docs)
"""
import os
import sys
import json
import time
import hashlib
import re
import urllib.request
import urllib.error

QDRANT_URL  = os.environ.get("QDRANT_URL",  "http://127.0.0.1:6333")
# Embedder DÉDIÉ nomic-embed-text (gpu-01 :1238, dim 768) — pas le modèle de chat :1234.
# nomic est un vrai modèle d'embedding → similarités bien plus discriminantes que qwen3-8b.
# Doit rester aligné avec rag-query (même modèle/dim) et l'instance llama-embed (rôle llama_server).
EMBED_URL   = os.environ.get("EMBED_URL",   "http://192.168.10.20:1238/v1/embeddings")
EMBED_MODEL = os.environ.get("EMBED_MODEL", "nomic-embed-text")
COLLECTION  = os.environ.get("RAG_COLLECTION", "funk-docs")
VECTOR_DIM  = int(os.environ.get("RAG_VECTOR_DIM", "768"))
CHUNK_MAX   = 2000

# Dossiers (relatifs à docs_dir) exclus de l'index : rapports auto-générés par
# hermes-auto-improve (méta-commentaires sur la doc, pas de la connaissance) — ils
# noyaient la vraie doc (~84% des points). Surchargeable via RAG_EXCLUDE (séparé par ":").
EXCLUDE_DIRS = [
    p for p in os.environ.get("RAG_EXCLUDE", "hermes/builtin").split(":") if p
]


def _request(method, url, data=None):
    body = json.dumps(data).encode() if data is not None else None
    req = urllib.request.Request(
        url, data=body,
        headers={"Content-Type": "application/json"} if body else {},
        method=method
    )
    with urllib.request.urlopen(req, timeout=60) as r:
        return json.loads(r.read())


def ensure_collection():
    try:
        _request("GET", f"{QDRANT_URL}/collections/{COLLECTION}")
        print(f"Collection '{COLLECTION}' existante")
    except urllib.error.HTTPError as e:
        if e.code == 404:
            print(f"Création de la collection '{COLLECTION}'...")
            _request("PUT", f"{QDRANT_URL}/collections/{COLLECTION}", {
                "vectors": {"size": VECTOR_DIM, "distance": "Cosine"}
            })
            print("Collection créée")
        else:
            raise


def embed(text):
    # L'instance llama-embed (:1238) renvoie occasionnellement des 500 transitoires sous charge
    # soutenue (slot saturé) → retry avec backoff. Les chunks sont bornés (CHUNK_MAX=2000) donc
    # un 500 n'est pas dû à un dépassement de contexte.
    last = None
    for attempt in range(5):
        try:
            result = _request("POST", EMBED_URL, {"model": EMBED_MODEL, "input": text})
            return result["data"][0]["embedding"]
        except (urllib.error.HTTPError, urllib.error.URLError) as e:
            last = e
            code = getattr(e, "code", None)
            # Ne réessaie que sur erreurs transitoires (5xx) ou réseau
            if code is not None and code < 500:
                raise
            time.sleep(0.5 * (attempt + 1))
    raise last


def chunk_markdown(rel_path, content):
    """Découpe un fichier Markdown par sections H2, puis H3 si trop grand."""
    chunks = []

    # Séparer par H2
    parts = re.split(r'\n(?=## )', content)

    # Intro avant le premier H2
    if parts and not parts[0].strip().startswith('## '):
        intro = parts[0].strip()
        if len(intro) > 80:
            title = intro.split('\n')[0].lstrip('#').strip() or rel_path
            chunks.append({"file": rel_path, "section": title, "text": intro[:CHUNK_MAX]})
        parts = parts[1:]

    for part in parts:
        if not part.strip():
            continue
        h2_title = part.split('\n')[0].lstrip('#').strip()

        if len(part) <= CHUNK_MAX:
            chunks.append({"file": rel_path, "section": h2_title, "text": part.strip()})
        else:
            # Découper par H3
            subs = re.split(r'\n(?=### )', part)
            for sub in subs:
                if not sub.strip():
                    continue
                h3_title = sub.split('\n')[0].lstrip('#').strip()
                label = f"{h2_title} — {h3_title}" if h3_title != h2_title else h2_title
                chunks.append({"file": rel_path, "section": label, "text": sub.strip()[:CHUNK_MAX]})

    return chunks


def point_id(file_path, section):
    """ID stable uint64 = MD5(file::section)."""
    h = hashlib.md5(f"{file_path}::{section}".encode()).hexdigest()
    return int(h[:16], 16)


def ingest(docs_dir):
    ensure_collection()

    excluded = {os.path.normpath(os.path.join(docs_dir, p)) for p in EXCLUDE_DIRS}
    md_files = []
    for root, dirs, files in os.walk(docs_dir):
        # élague les dossiers cachés et exclus (ex. hermes/builtin)
        dirs[:] = sorted(
            d for d in dirs
            if not d.startswith('.')
            and os.path.normpath(os.path.join(root, d)) not in excluded
        )
        for f in sorted(files):
            if f.endswith('.md'):
                md_files.append(os.path.join(root, f))

    print(f"{len(md_files)} fichiers Markdown trouvés dans {docs_dir}")

    total = 0
    errors = 0

    for filepath in md_files:
        rel = os.path.relpath(filepath, docs_dir)
        try:
            with open(filepath, encoding='utf-8') as f:
                content = f.read()
        except OSError as e:
            print(f"  SKIP {rel}: {e}")
            continue

        chunks = chunk_markdown(rel, content)
        if not chunks:
            continue

        points = []
        for chunk in chunks:
            print(f"  embed  {rel} § {chunk['section'][:55]}")
            try:
                vector = embed(chunk['text'])
            except Exception as e:
                print(f"    ERREUR embedding: {e}")
                errors += 1
                continue

            points.append({
                "id": point_id(chunk['file'], chunk['section']),
                "vector": vector,
                "payload": {
                    "file": chunk['file'],
                    "section": chunk['section'],
                    "text": chunk['text'],
                }
            })

        if points:
            _request("PUT", f"{QDRANT_URL}/collections/{COLLECTION}/points?wait=true", {"points": points})
            total += len(points)
            print(f"  → {len(points)} chunks indexés  ({rel})")

    print(f"\nTerminé : {total} chunks dans '{COLLECTION}'" + (f" ({errors} erreurs)" if errors else ""))


if __name__ == "__main__":
    docs_dir = sys.argv[1] if len(sys.argv) > 1 else "/srv/data/rag/docs"
    if not os.path.isdir(docs_dir):
        print(f"ERREUR : {docs_dir} n'existe pas", file=sys.stderr)
        sys.exit(1)
    ingest(docs_dir)
