#!/usr/bin/env python3
"""
meshdb_agent.py - MeshPOP Search Engine (Standalone Agent) v2.0
Deployed to each server for local indexing and search.
SQLite FTS5-based full-text search with BM25 ranking.

Usage:
    meshdb_agent.py index [path]       Index a directory (default: $HOME)
    meshdb_agent.py index-all          Auto-discover and index all projects
    meshdb_agent.py index-full [home]  Deep recursive scan of entire home
    meshdb_agent.py search <query>     Full-text search
    meshdb_agent.py find <query>       Filename search
    meshdb_agent.py read <query>       Read best matching document
    meshdb_agent.py status             Show database statistics
    meshdb_agent.py json search <q>    JSON output mode
"""

import os
import sys
import json
import time
import hashlib
import sqlite3
import platform
from datetime import datetime

VERSION = "2.0.0"
DB_DIR = os.path.expanduser("~/.mpop")
DB_PATH = os.path.join(DB_DIR, "meshdb.db")
MAX_CONTENT = 500_000  # 500KB per file
MAX_FILE_SIZE = 2_000_000  # 2MB - skip larger files

# ==================== File Type Detection ====================

TYPE_MAP = {
    ".py": "python", ".js": "javascript", ".ts": "typescript",
    ".jsx": "react", ".tsx": "react", ".rs": "rust", ".go": "go",
    ".java": "java", ".rb": "ruby", ".php": "php", ".c": "c",
    ".cpp": "cpp", ".h": "header", ".hpp": "header",
    ".sh": "shell", ".bash": "shell", ".zsh": "shell", ".fish": "shell",
    ".md": "markdown", ".txt": "text", ".rst": "text",
    ".json": "json", ".yaml": "yaml", ".yml": "yaml", ".toml": "toml",
    ".xml": "xml", ".html": "html", ".css": "css", ".scss": "scss",
    ".sql": "sql", ".r": "r", ".R": "r",
    ".dockerfile": "docker", ".conf": "config", ".cfg": "config",
    ".ini": "config", ".env": "config", ".properties": "config",
    ".csv": "data", ".tsv": "data",
    ".tex": "latex", ".bib": "latex",
    ".vue": "vue", ".svelte": "svelte",
    ".swift": "swift", ".kt": "kotlin", ".scala": "scala",
    ".lua": "lua", ".perl": "perl", ".pl": "perl",
    ".nginx": "config", ".tf": "terraform", ".hcl": "terraform",
    ".log": "log",
}

INDEXABLE_EXTENSIONS = set(TYPE_MAP.keys()) | {
    ".gitignore", ".dockerignore", ".editorconfig",
    ".prettierrc", ".eslintrc", ".babelrc",
}

# Directories to always skip during walk
SKIP_DIRS = {
    "node_modules", ".git", "__pycache__", ".venv", "venv",
    ".tox", ".mypy_cache", ".pytest_cache", "dist", "build",
    ".next", ".nuxt", ".cache", ".parcel-cache",
    "target", ".cargo", "vendor", ".gradle",
    ".idea", ".vscode", ".DS_Store",
    "env", ".env", "eggs", "*.egg-info",
    ".terraform", ".serverless",
}

# Directories to skip when scanning from HOME level (heavy/non-project)
SKIP_HOME_DIRS = {
    # System / OS
    "Library", "Applications", ".Trash", "Movies", "Music", "Pictures",
    "Downloads", "Desktop", "Documents", "Public",
    "Public", "Downloads", "Documents", "Desktop", "Videos", "Photos", "Templates", "Music",
    # Package managers / runtimes
    ".local", ".npm", ".cargo", ".rustup", ".gradle", ".m2",
    ".docker", ".kube", ".minikube", ".cache", ".gem",
    ".cpan", ".config", ".ssh", ".gnupg", "go",
    ".pyenv", ".nvm", ".rbenv", ".sdkman",
    ".mpop", ".vscode", ".cursor",
    # Large data dirs (corpus, models - too big to index content)
    "stanza_resources", "models", "open-webui-env",
    "executor_venv", "llama.cpp", "stable-diffusion-webui",
    "snap", "chromadb-data",
}

# Large data directories to scan shallowly (only top-level files)
SHALLOW_SCAN_DIRS = {
    "corpus", "corpus_raw", "corpus_raw2", "corpus_backup", "corpus_fast",
    "corpus_catalog", "builds", "builds_wiki_full", "builds_wiki_mini",
    "wiki_ppmi", "wiki_ppmi_large", "wiki_ppmi_out",
    "ppmi", "ppmi_out", "ppmi_archive", "ppmi_all", "ppmi_backup", "ppmi_data",
    "cc100_data", "cc100_ppmi_out", "cc_news",
    "news_data", "news_pipeline",
    "wikidata", "wikidata_output", "wikipedia",
}


def detect_type(filepath):
    basename = os.path.basename(filepath).lower()
    if basename == "dockerfile":
        return "docker"
    if basename == "makefile":
        return "makefile"
    if basename == "jenkinsfile":
        return "jenkins"
    if basename in (".gitignore", ".dockerignore"):
        return "config"
    ext = os.path.splitext(filepath)[1].lower()
    return TYPE_MAP.get(ext, "unknown")


def file_hash(filepath):
    try:
        h = hashlib.md5()
        with open(filepath, "rb") as f:
            for chunk in iter(lambda: f.read(8192), b""):
                h.update(chunk)
        return h.hexdigest()
    except Exception:
        return None


def read_file_content(filepath, max_length=None):
    max_length = max_length or MAX_CONTENT
    try:
        with open(filepath, "r", encoding="utf-8", errors="replace") as f:
            return f.read(max_length)
    except Exception:
        return ""


def format_size(size):
    for unit in ["B", "KB", "MB", "GB"]:
        if size < 1024:
            return f"{size:.1f}{unit}"
        size /= 1024
    return f"{size:.1f}TB"


# ==================== Database ====================

def init_db(db_path=None):
    db_path = db_path or DB_PATH
    os.makedirs(os.path.dirname(db_path), exist_ok=True)
    conn = sqlite3.connect(db_path)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA journal_mode=WAL")
    conn.execute("PRAGMA busy_timeout=10000")

    conn.execute("""
        CREATE TABLE IF NOT EXISTS documents (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            filepath TEXT UNIQUE,
            filename TEXT,
            content TEXT,
            doc_type TEXT,
            file_size INTEGER,
            file_hash TEXT,
            source_dir TEXT,
            hostname TEXT,
            indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            modified_at TIMESTAMP
        )
    """)

    # FTS5 virtual table
    try:
        conn.execute("""
            CREATE VIRTUAL TABLE IF NOT EXISTS documents_fts USING fts5(
                filename, content, doc_type,
                content='documents',
                content_rowid='id',
                tokenize='unicode61'
            )
        """)
    except sqlite3.OperationalError:
        pass

    # Triggers for FTS sync
    for trigger_sql in [
        """CREATE TRIGGER IF NOT EXISTS documents_ai AFTER INSERT ON documents BEGIN
            INSERT INTO documents_fts(rowid, filename, content, doc_type)
            VALUES (new.id, new.filename, new.content, new.doc_type);
        END""",
        """CREATE TRIGGER IF NOT EXISTS documents_ad AFTER DELETE ON documents BEGIN
            INSERT INTO documents_fts(documents_fts, rowid, filename, content, doc_type)
            VALUES ('delete', old.id, old.filename, old.content, old.doc_type);
        END""",
        """CREATE TRIGGER IF NOT EXISTS documents_au AFTER UPDATE ON documents BEGIN
            INSERT INTO documents_fts(documents_fts, rowid, filename, content, doc_type)
            VALUES ('delete', old.id, old.filename, old.content, old.doc_type);
            INSERT INTO documents_fts(rowid, filename, content, doc_type)
            VALUES (new.id, new.filename, new.content, new.doc_type);
        END""",
    ]:
        try:
            conn.execute(trigger_sql)
        except sqlite3.OperationalError:
            pass

    conn.commit()
    return conn


# ==================== Scanning ====================

def should_skip_dir(dirname):
    return dirname in SKIP_DIRS or dirname.startswith(".")


def should_index_file(filepath):
    ext = os.path.splitext(filepath)[1].lower()
    basename = os.path.basename(filepath).lower()
    if basename in ("dockerfile", "makefile", "jenkinsfile", ".gitignore", ".dockerignore"):
        return True
    return ext in INDEXABLE_EXTENSIONS


def scan_directory(dirpath):
    """Recursively scan a directory for indexable files."""
    files = []
    for root, dirs, filenames in os.walk(dirpath):
        dirs[:] = [d for d in dirs if not should_skip_dir(d)]
        for fname in filenames:
            fpath = os.path.join(root, fname)
            if should_index_file(fpath):
                try:
                    sz = os.path.getsize(fpath)
                    if sz <= MAX_FILE_SIZE:
                        files.append(fpath)
                except OSError:
                    pass
    return files


def scan_directory_shallow(dirpath):
    """Scan only top-level files in a directory (no recursion)."""
    files = []
    try:
        for fname in os.listdir(dirpath):
            fpath = os.path.join(dirpath, fname)
            if os.path.isfile(fpath) and should_index_file(fpath):
                try:
                    sz = os.path.getsize(fpath)
                    if sz <= MAX_FILE_SIZE:
                        files.append(fpath)
                except OSError:
                    pass
    except PermissionError:
        pass
    return files


def scan_home_full(home_dir):
    """
    Deep scan of entire home directory with smart filtering:
    - Skip SKIP_HOME_DIRS completely
    - Shallow scan SHALLOW_SCAN_DIRS (top-level files only)
    - Full recursive scan everything else
    - Also index standalone files in home root
    """
    home_dir = os.path.abspath(os.path.expanduser(home_dir))
    all_files = []

    # 1) Standalone files in home root (*.py, *.sh, *.md, etc.)
    for fname in os.listdir(home_dir):
        fpath = os.path.join(home_dir, fname)
        if os.path.isfile(fpath) and should_index_file(fpath):
            try:
                sz = os.path.getsize(fpath)
                if sz <= MAX_FILE_SIZE:
                    all_files.append(fpath)
            except OSError:
                pass

    # 2) Subdirectories
    for entry in os.listdir(home_dir):
        subpath = os.path.join(home_dir, entry)
        if not os.path.isdir(subpath):
            continue
        if entry.startswith(".") or entry in SKIP_HOME_DIRS:
            continue
        if entry in SHALLOW_SCAN_DIRS:
            # Only top-level files
            shallow = scan_directory_shallow(subpath)
            all_files.extend(shallow)
        else:
            # Full recursive scan
            deep = scan_directory(subpath)
            all_files.extend(deep)

    return all_files


# ==================== Indexing ====================

def index_files(conn, files, source_dir, hostname=None):
    """Index a list of file paths into the database."""
    hostname = hostname or platform.node()
    stats = {"total": len(files), "new": 0, "updated": 0, "skipped": 0, "errors": 0}

    for fpath in files:
        try:
            fhash = file_hash(fpath)
            existing = conn.execute(
                "SELECT file_hash FROM documents WHERE filepath = ?", (fpath,)
            ).fetchone()
            if existing and existing["file_hash"] == fhash:
                stats["skipped"] += 1
                continue

            content = read_file_content(fpath)
            fstat = os.stat(fpath)
            doc_type = detect_type(fpath)
            filename = os.path.basename(fpath)

            is_update = existing is not None

            if is_update:
                conn.execute("""
                    UPDATE documents SET filename=?, content=?, doc_type=?,
                    file_size=?, file_hash=?, source_dir=?, hostname=?,
                    indexed_at=CURRENT_TIMESTAMP, modified_at=?
                    WHERE filepath=?
                """, (filename, content, doc_type, fstat.st_size, fhash,
                      source_dir, hostname,
                      datetime.fromtimestamp(fstat.st_mtime).isoformat(),
                      fpath))
                stats["updated"] += 1
            else:
                conn.execute("""
                    INSERT INTO documents (filepath, filename, content, doc_type,
                    file_size, file_hash, source_dir, hostname, modified_at)
                    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
                """, (fpath, filename, content, doc_type, fstat.st_size, fhash,
                      source_dir, hostname,
                      datetime.fromtimestamp(fstat.st_mtime).isoformat()))
                stats["new"] += 1

            if (stats["new"] + stats["updated"]) % 500 == 0:
                conn.commit()

        except Exception as e:
            stats["errors"] += 1

    conn.commit()
    return stats


def index_directory(conn, dirpath, replace=False):
    """Index a single directory recursively."""
    hostname = platform.node()
    dirpath = os.path.abspath(os.path.expanduser(dirpath))
    if not os.path.isdir(dirpath):
        return {"error": f"Not a directory: {dirpath}"}
    files = scan_directory(dirpath)
    return index_files(conn, files, dirpath, hostname)


# ==================== Search ====================

def search(conn, query, limit=20, doc_type=None, source_dir=None):
    where_clauses = []
    params = []

    if doc_type:
        where_clauses.append("d.doc_type = ?")
        params.append(doc_type)
    if source_dir:
        where_clauses.append("d.source_dir LIKE ?")
        params.append(f"%{source_dir}%")

    where_sql = (" AND " + " AND ".join(where_clauses)) if where_clauses else ""

    sql = f"""
        SELECT d.*, bm25(documents_fts, 10.0, 5.0, 1.0) as rank
        FROM documents_fts fts
        JOIN documents d ON d.id = fts.rowid
        WHERE documents_fts MATCH ?
        {where_sql}
        ORDER BY rank
        LIMIT ?
    """
    params = [query] + params + [limit]

    try:
        rows = conn.execute(sql, params).fetchall()
        return [dict(r) for r in rows]
    except sqlite3.OperationalError:
        return search_like(conn, query, limit, doc_type, source_dir)


def search_like(conn, query, limit=20, doc_type=None, source_dir=None):
    where_clauses = ["(content LIKE ? OR filename LIKE ?)"]
    params = [f"%{query}%", f"%{query}%"]

    if doc_type:
        where_clauses.append("doc_type = ?")
        params.append(doc_type)
    if source_dir:
        where_clauses.append("source_dir LIKE ?")
        params.append(f"%{source_dir}%")

    where_sql = " AND ".join(where_clauses)
    sql = f"SELECT * FROM documents WHERE {where_sql} ORDER BY file_size ASC LIMIT ?"
    params.append(limit)
    rows = conn.execute(sql, params).fetchall()
    return [dict(r) for r in rows]


def search_filename(conn, query, limit=10):
    rows = conn.execute(
        "SELECT * FROM documents WHERE filename LIKE ? ORDER BY file_size ASC LIMIT ?",
        (f"%{query}%", limit)
    ).fetchall()
    return [dict(r) for r in rows]


def read_doc(conn, query):
    row = conn.execute(
        "SELECT * FROM documents WHERE filepath LIKE ? OR filename LIKE ? LIMIT 1",
        (f"%{query}%", f"%{query}%")
    ).fetchone()
    return dict(row) if row else None


def get_status(conn):
    total = conn.execute("SELECT COUNT(*) FROM documents").fetchone()[0]
    types = conn.execute(
        "SELECT doc_type, COUNT(*) as cnt FROM documents GROUP BY doc_type ORDER BY cnt DESC"
    ).fetchall()
    dirs = conn.execute(
        "SELECT source_dir, COUNT(*) as cnt FROM documents GROUP BY source_dir ORDER BY cnt DESC"
    ).fetchall()
    db_size = os.path.getsize(DB_PATH) if os.path.exists(DB_PATH) else 0

    return {
        "total_documents": total,
        "database_size": format_size(db_size),
        "database_path": DB_PATH,
        "hostname": platform.node(),
        "types": {r[0]: r[1] for r in types},
        "directories": {r[0]: r[1] for r in dirs},
    }


# ==================== Auto-discover Projects ====================

def discover_projects(home_dir=None):
    """Find project directories under home (dirs with .git, package.json, etc.)"""
    home_dir = home_dir or os.path.expanduser("~")
    markers = {".git", "package.json", "Cargo.toml", "go.mod", "setup.py",
               "pyproject.toml", "Makefile", "CMakeLists.txt", "pom.xml",
               "build.gradle", "Gemfile", "requirements.txt", "docker-compose.yml"}
    projects = []

    for entry in os.scandir(home_dir):
        if not entry.is_dir() or entry.name in SKIP_HOME_DIRS or entry.name.startswith("."):
            continue
        try:
            children = set(os.listdir(entry.path))
            if children & markers:
                projects.append(entry.path)
            else:
                # Check one level deeper
                for sub in os.scandir(entry.path):
                    if sub.is_dir() and not sub.name.startswith(".") and sub.name not in SKIP_DIRS:
                        try:
                            sub_children = set(os.listdir(sub.path))
                            if sub_children & markers:
                                projects.append(sub.path)
                        except PermissionError:
                            pass
        except PermissionError:
            pass

    return sorted(projects)


# ==================== API Layer ====================

def api_search(query, limit=20, doc_type=None, source_dir=None, db_path=None):
    conn = init_db(db_path)
    t0 = time.time()
    results = search(conn, query, limit, doc_type, source_dir)
    elapsed = (time.time() - t0) * 1000
    conn.close()
    return {"query": query, "count": len(results), "elapsed_ms": round(elapsed, 1),
            "results": [{
                "filepath": r["filepath"], "filename": r["filename"],
                "doc_type": r["doc_type"], "file_size": r.get("file_size", 0),
                "rank": r.get("rank", 0),
                "snippet": (r.get("content") or "")[:200]
            } for r in results]}


def api_index(dirpath, replace=False, db_path=None):
    conn = init_db(db_path)
    t0 = time.time()
    stats = index_directory(conn, dirpath, replace)
    elapsed = time.time() - t0
    conn.close()
    stats["elapsed_seconds"] = round(elapsed, 1)
    stats["directory"] = dirpath
    return stats


def api_find(query, limit=10, db_path=None):
    conn = init_db(db_path)
    results = search_filename(conn, query, limit)
    conn.close()
    return {"query": query, "count": len(results),
            "results": [{"filepath": r["filepath"], "filename": r["filename"],
                         "doc_type": r["doc_type"], "file_size": r.get("file_size", 0)}
                        for r in results]}


def api_read(query, db_path=None):
    conn = init_db(db_path)
    doc = read_doc(conn, query)
    conn.close()
    if doc:
        return {"filepath": doc["filepath"], "filename": doc["filename"],
                "doc_type": doc["doc_type"], "content": doc.get("content", ""),
                "file_size": doc.get("file_size", 0)}
    return {"error": f"No document found matching: {query}"}


def api_status(db_path=None):
    conn = init_db(db_path)
    status = get_status(conn)
    conn.close()
    return status


def api_index_all(home_dir=None, replace=False, db_path=None):
    """Index only marker-based projects (lightweight)."""
    projects = discover_projects(home_dir)
    conn = init_db(db_path)
    results = []
    total_stats = {"total": 0, "new": 0, "updated": 0, "skipped": 0, "errors": 0}

    for proj in projects:
        t0 = time.time()
        stats = index_directory(conn, proj, replace)
        elapsed = time.time() - t0
        stats["directory"] = proj
        stats["elapsed_seconds"] = round(elapsed, 1)
        results.append(stats)

        for k in total_stats:
            total_stats[k] += stats.get(k, 0)

        print(f"  [{len(results):3d}/{len(projects)}] {proj} "
              f"({stats.get('new',0)} new, {stats.get('updated',0)} updated, "
              f"{elapsed:.1f}s)")

    conn.close()
    return {"projects": len(projects), "totals": total_stats, "details": results}


def api_index_full(home_dir=None, db_path=None):
    """Deep full scan of entire home directory."""
    home_dir = home_dir or os.path.expanduser("~")
    print(f"  Scanning {home_dir} (full deep scan)...")
    t0 = time.time()
    files = scan_home_full(home_dir)
    scan_time = time.time() - t0
    print(f"  Found {len(files)} files in {scan_time:.1f}s")

    conn = init_db(db_path)
    hostname = platform.node()

    # Group files by their top-level parent dir for source_dir
    groups = {}
    for fpath in files:
        rel = os.path.relpath(fpath, home_dir)
        parts = rel.split(os.sep)
        if len(parts) > 1:
            source = os.path.join(home_dir, parts[0])
        else:
            source = home_dir  # standalone file in home root
        groups.setdefault(source, []).append(fpath)

    total_stats = {"total": 0, "new": 0, "updated": 0, "skipped": 0, "errors": 0}
    done = 0
    total_dirs = len(groups)

    for source_dir, group_files in sorted(groups.items()):
        done += 1
        t1 = time.time()
        stats = index_files(conn, group_files, source_dir, hostname)
        elapsed = time.time() - t1

        for k in total_stats:
            total_stats[k] += stats.get(k, 0)

        if stats.get("new", 0) > 0 or stats.get("updated", 0) > 0:
            print(f"  [{done:3d}/{total_dirs}] {source_dir} "
                  f"({len(group_files)} files, {stats.get('new',0)} new, "
                  f"{stats.get('updated',0)} updated, {elapsed:.1f}s)")

    conn.close()
    total_elapsed = time.time() - t0
    total_stats["elapsed_seconds"] = round(total_elapsed, 1)
    total_stats["home_dir"] = home_dir
    total_stats["groups"] = total_dirs
    return total_stats


# ==================== CLI ====================

def parse_cli_options(args):
    """Parse --limit, --type, --dir from args, return (query_str, options)."""
    opts = {"limit": 20, "type": None, "dir": None}
    query_parts = []
    i = 0
    while i < len(args):
        if args[i] == "--limit" and i + 1 < len(args):
            opts["limit"] = int(args[i + 1])
            i += 2
        elif args[i] == "--type" and i + 1 < len(args):
            opts["type"] = args[i + 1]
            i += 2
        elif args[i] == "--dir" and i + 1 < len(args):
            opts["dir"] = args[i + 1]
            i += 2
        elif args[i].startswith("--"):
            i += 1  # skip unknown flags
        else:
            query_parts.append(args[i])
            i += 1
    return " ".join(query_parts), opts



def main():
    args = sys.argv[1:]
    if not args:
        print(f"meshdb_agent v{VERSION} - MeshPOP Search Engine")
        print(f"Host: {platform.node()}")
        print(f"DB: {DB_PATH}")
        print()
        print("Commands:")
        print("  index [path]     Index a single directory")
        print("  index-all        Auto-discover and index projects")
        print("  index-full [home] Deep scan entire home directory")
        print("  search <query>   Full-text search")
        print("  find <query>     Filename search")
        print("  read <query>     Read matching document")
        print("  status           Database statistics")
        print("  json <cmd> ...   JSON output mode")
        return

    cmd = args[0]
    json_mode = False

    if cmd == "json":
        json_mode = True
        args = args[1:]
        cmd = args[0] if args else "status"

    if cmd == "index":
        dirpath = args[1] if len(args) > 1 else os.path.expanduser("~")
        replace = "--replace" in args
        print(f"Indexing: {dirpath}")
        result = api_index(dirpath, replace)
        if json_mode:
            print(json.dumps(result, indent=2))
        else:
            print(f"  Total: {result['total']} files")
            print(f"  New: {result['new']}, Updated: {result['updated']}, "
                  f"Skipped: {result['skipped']}, Errors: {result['errors']}")
            print(f"  Time: {result['elapsed_seconds']}s")

    elif cmd == "index-all":
        home_dir = args[1] if len(args) > 1 else None
        replace = "--replace" in args
        print(f"Auto-discovering projects in {home_dir or '~'}...")
        result = api_index_all(home_dir, replace)
        if json_mode:
            print(json.dumps(result, indent=2))
        else:
            t = result["totals"]
            print(f"\nDone! {result['projects']} projects, "
                  f"{t['total']} files ({t['new']} new, {t['updated']} updated, "
                  f"{t['errors']} errors)")

    elif cmd == "index-full":
        home_dir = args[1] if len(args) > 1 else None
        print(f"Full deep scan of {home_dir or '~'}...")
        result = api_index_full(home_dir)
        if json_mode:
            print(json.dumps(result, indent=2))
        else:
            print(f"\nDone! {result['total']} files in {result['groups']} directories")
            print(f"  New: {result['new']}, Updated: {result['updated']}, "
                  f"Skipped: {result['skipped']}, Errors: {result['errors']}")
            print(f"  Time: {result['elapsed_seconds']}s")

    elif cmd == "search":
        query, opts = parse_cli_options(args[1:])
        if not query:
            print("Usage: meshdb_agent.py search <query> [--limit N] [--type T] [--dir D]")
            return
        result = api_search(query, limit=opts["limit"], doc_type=opts["type"], source_dir=opts["dir"])
        if json_mode:
            print(json.dumps(result, indent=2))
        else:
            print(f"Search: '{query}' ({result['count']} results, {result['elapsed_ms']}ms)\n")
            for i, r in enumerate(result["results"], 1):
                print(f"  {i}. [{r['doc_type']}] {r['filepath']}")
                snippet = r.get("snippet", "").strip().replace("\n", " ")[:120]
                if snippet:
                    print(f"     {snippet}")

    elif cmd == "find":
        query, opts = parse_cli_options(args[1:])
        if not query:
            print("Usage: meshdb_agent.py find <query> [--limit N]")
            return
        result = api_find(query, limit=opts["limit"])
        if json_mode:
            print(json.dumps(result, indent=2))
        else:
            print(f"Find: '{query}' ({result['count']} results)\n")
            for i, r in enumerate(result["results"], 1):
                print(f"  {i}. [{r['doc_type']}] {r['filepath']}")

    elif cmd == "read":
        query, _ = parse_cli_options(args[1:])
        if not query:
            print("Usage: meshdb_agent.py read <filepath>")
            return
        result = api_read(query)
        if json_mode:
            print(json.dumps(result, indent=2))
        else:
            if "error" in result:
                print(f"  {result['error']}")
            else:
                print(f"File: {result['filepath']} [{result['doc_type']}]")
                print("=" * 60)
                print(result.get("content", "")[:5000])

    elif cmd == "status":
        result = api_status()
        if json_mode:
            print(json.dumps(result, indent=2))
        else:
            print(f"meshdb_agent v{VERSION}")
            print(f"  Host: {result['hostname']}")
            print(f"  DB: {result['database_path']} ({result['database_size']})")
            print(f"  Documents: {result['total_documents']}")
            print(f"\n  Types:")
            for dtype, cnt in sorted(result["types"].items(), key=lambda x: -x[1])[:15]:
                print(f"    {dtype:<15} {cnt:>6}")
            print(f"\n  Directories ({len(result['directories'])}):")
            for d, cnt in sorted(result["directories"].items(), key=lambda x: -x[1])[:15]:
                print(f"    {d:<50} {cnt:>6}")

    else:
        print(f"Unknown command: {cmd}")
        print("Commands: index, index-all, index-full, search, find, read, status")


if __name__ == "__main__":
    main()
