#!/usr/bin/env python3
"""Build a small local retrieval index from non-secret homelab repo files."""

from __future__ import annotations

import argparse
import fnmatch
import json
import math
import os
import pathlib
import re
import sys
from collections import Counter


REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
DEFAULT_SOURCES_FILE = REPO_ROOT / "infra" / "ai" / "knowledge-sources.txt"
DEFAULT_EXCLUDES_FILE = REPO_ROOT / "infra" / "ai" / "knowledge-excludes.txt"
DEFAULT_INDEX_DIR = pathlib.Path(os.environ.get("LAB_AI_KNOWLEDGE_INDEX_DIR", "/data/homelab-ai/index"))
TOKEN_RE = re.compile(r"[A-Za-z0-9_./:-]{2,}")
MAX_FILE_BYTES = 512 * 1024
CHUNK_TARGET_LINES = 80
CHUNK_OVERLAP_LINES = 12


def tokenise(text: str) -> list[str]:
    return [token.lower() for token in TOKEN_RE.findall(text)]


def load_patterns(path: pathlib.Path) -> list[str]:
    patterns: list[str] = []
    for raw_line in path.read_text(encoding="utf-8").splitlines():
        line = raw_line.strip()
        if not line or line.startswith("#"):
            continue
        patterns.append(line)
    return patterns


def path_is_secret(path: pathlib.Path) -> bool:
    lowered = str(path).lower()
    secret_markers = (
        ".git/",
        ".terraform/",
        ".lab/",
        "__pycache__/",
        "terraform.tfstate",
        ".secret.",
        ".enc.",
        "id_ed25519",
        "cosign.key",
        "keys.txt",
    )
    return any(marker in lowered for marker in secret_markers)


def path_matches_any(rel_path: str, patterns: list[str]) -> bool:
    return any(fnmatch.fnmatch(rel_path, pattern) for pattern in patterns)


def candidate_files(patterns: list[str], exclude_patterns: list[str]) -> list[pathlib.Path]:
    files: set[pathlib.Path] = set()
    all_files = [path for path in REPO_ROOT.rglob("*") if path.is_file()]

    for pattern in patterns:
        if any(char in pattern for char in "*?["):
            for path in all_files:
                rel = path.relative_to(REPO_ROOT).as_posix()
                if fnmatch.fnmatch(rel, pattern):
                    files.add(path)
        else:
            path = REPO_ROOT / pattern
            if path.is_file():
                files.add(path)

    return sorted(
        path
        for path in files
        if not path_is_secret(path)
        and not path_matches_any(path.relative_to(REPO_ROOT).as_posix(), exclude_patterns)
    )


def read_text(path: pathlib.Path) -> str | None:
    try:
        if path.stat().st_size > MAX_FILE_BYTES:
            return None
        return path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        return None


def chunk_file(path: pathlib.Path, text: str) -> list[dict[str, object]]:
    rel = path.relative_to(REPO_ROOT).as_posix()
    lines = text.splitlines()
    chunks: list[dict[str, object]] = []
    start = 0

    while start < len(lines):
        end = min(start + CHUNK_TARGET_LINES, len(lines))
        body = "\n".join(lines[start:end]).strip()
        if body:
            chunks.append(
                {
                    "id": f"{rel}:{start + 1}",
                    "path": rel,
                    "start_line": start + 1,
                    "end_line": end,
                    "text": body,
                    "tokens": tokenise(body),
                }
            )
        if end == len(lines):
            break
        start = max(end - CHUNK_OVERLAP_LINES, start + 1)

    if not chunks and text.strip():
        chunks.append(
            {
                "id": f"{rel}:1",
                "path": rel,
                "start_line": 1,
                "end_line": 1,
                "text": text.strip(),
                "tokens": tokenise(text),
            }
        )

    return chunks


def build_index(files: list[pathlib.Path]) -> dict[str, object]:
    chunks: list[dict[str, object]] = []
    document_frequency: Counter[str] = Counter()

    for path in files:
        text = read_text(path)
        if text is None:
            continue
        for chunk in chunk_file(path, text):
            tokens = chunk["tokens"]
            if not isinstance(tokens, list) or not tokens:
                continue
            chunks.append(chunk)
            document_frequency.update(set(tokens))

    chunk_count = len(chunks)
    average_length = sum(len(chunk["tokens"]) for chunk in chunks) / chunk_count if chunk_count else 0
    idf = {
        token: math.log(1 + (chunk_count - freq + 0.5) / (freq + 0.5))
        for token, freq in document_frequency.items()
    }

    return {
        "version": 1,
        "repo_root": str(REPO_ROOT),
        "chunk_count": chunk_count,
        "average_length": average_length,
        "chunks": chunks,
        "idf": idf,
    }


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--sources-file", type=pathlib.Path, default=DEFAULT_SOURCES_FILE)
    parser.add_argument("--excludes-file", type=pathlib.Path, default=DEFAULT_EXCLUDES_FILE)
    parser.add_argument("--index-dir", type=pathlib.Path, default=DEFAULT_INDEX_DIR)
    args = parser.parse_args()

    if not args.sources_file.is_file():
        print(f"missing sources file: {args.sources_file}", file=sys.stderr)
        return 1

    patterns = load_patterns(args.sources_file)
    exclude_patterns = load_patterns(args.excludes_file) if args.excludes_file.is_file() else []
    files = candidate_files(patterns, exclude_patterns)
    index = build_index(files)

    args.index_dir.mkdir(parents=True, exist_ok=True)
    index_path = args.index_dir / "index.json"
    manifest_path = args.index_dir / "manifest.json"
    index_path.write_text(json.dumps(index, indent=2, sort_keys=True) + "\n", encoding="utf-8")
    manifest_path.write_text(
        json.dumps(
            {
                "sources_file": str(args.sources_file.relative_to(REPO_ROOT)),
                "excludes_file": str(args.excludes_file.relative_to(REPO_ROOT)) if args.excludes_file.is_file() else None,
                "file_count": len(files),
                "chunk_count": index["chunk_count"],
                "repo_root": str(REPO_ROOT),
            },
            indent=2,
            sort_keys=True,
        )
        + "\n",
        encoding="utf-8",
    )

    print(f"indexed {len(files)} file(s), {index['chunk_count']} chunk(s) into {index_path}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
