#!/usr/bin/env python3
"""
Query the Fable knowledge base — "What would Fable do here?"

BM25-ranked full-text search over Fable's authored corpus + narration.
Returns the most analogous passages Fable actually wrote, with source paths
so the agent can open the full doc for the authoring shape.

Usage:
  python3 query_kb.py "rate limit idempotency key"
  python3 query_kb.py "auth redirect" --kind narration   # only narration cadence
  python3 query_kb.py "migration journal" --kind doc -k 8 # only docs, top 8
  python3 query_kb.py "image pipeline" --project multideal

FTS5 query syntax is supported: "exact phrase", term*, a OR b, a NOT b.
"""
import argparse, os, re, sqlite3, sys


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('query')
    ap.add_argument('--kind', choices=['doc', 'narration', 'all'], default='all')
    ap.add_argument('--project', default=None)
    ap.add_argument('-k', type=int, default=5)
    ap.add_argument('--root', default=os.path.expanduser('~/.claude/fable'))
    ap.add_argument('--full', action='store_true', help='print full chunk text, not a snippet')
    ap.add_argument('--raw', action='store_true', help='pass query to FTS5 verbatim (default OR-joins bare terms)')
    args = ap.parse_args()

    db = os.path.join(os.path.expanduser(args.root), 'fable.db')
    if not os.path.exists(db):
        sys.exit(f'no KB at {db} — run: python3 build_kb.py')
    con = sqlite3.connect(db)

    # Default to OR across bare terms so partial matches still rank (BM25 favors
    # chunks that hit more terms). Power users keep AND/phrases via quotes or operators.
    q = args.query
    has_ops = any(t in q for t in ('"', '*', ' OR ', ' NOT ', ' AND ', '(')) or args.raw
    if not has_ops and len(q.split()) > 1:
        toks = [t for t in re.findall(r'[A-Za-z0-9_]+', q) if t]
        q = ' OR '.join(toks)

    where, params = ['chunks MATCH ?'], [q]
    if args.kind != 'all':
        where.append('kind = ?'); params.append(args.kind)
    if args.project:
        where.append('project = ?'); params.append(args.project)
    col = 'text' if args.full else "snippet(chunks, 0, '«', '»', ' … ', 18)"
    sql = (f'SELECT bm25(chunks) AS score, kind, project, source, heading, {col} '
           f'FROM chunks WHERE {" AND ".join(where)} ORDER BY score LIMIT ?')
    params.append(args.k)

    try:
        rows = con.execute(sql, params).fetchall()
    except sqlite3.OperationalError as e:
        sys.exit(f'query error: {e}\n(check FTS5 syntax — quote phrases, escape special chars)')

    if not rows:
        print(f'no matches for: {args.query!r}')
        return
    print(f'Fable precedent for: {args.query!r}  ({len(rows)} hits)\n')
    for score, kind, proj, src, heading, text in rows:
        tag = f'[{kind}·{proj}]'
        print(f'── {tag}  {src}  › {heading}   (score {score:.2f})')
        print(f'   {text.strip()}\n')


if __name__ == '__main__':
    main()
