"""OKF Lookup — pinpoint exact concepts from an OKF bundle.

Designed to be called by OpenCode (or any AI agent) via RUN directive
to inject precise concept context before touching code.

Search modes:
  by name      exact or fuzzy match on concept title
  by file      all concepts extracted from a source file
  by tag       filter by OKF tag (e.g. lang:python, type:Class)
  by type      filter by concept type (Function, Class, Module)

Output modes:
  full         complete concept file (default for single result)
  compact      one-line per concept: type | title | file | description
  json         machine-readable JSON array

Usage:
  # Exact / fuzzy name search
  okf lookup WorldBankConnector
  okf lookup world bank          # fuzzy, space-separated tokens

  # All concepts from a source file
  okf lookup --file StockAI/RnD/python/connectors/economic_data.py

  # Filter by type
  okf lookup --type Class connector

  # Filter by tag
  okf lookup --tag lang:python --tag type:Function fetch

  # Change bundle dir (default: ./okf_bundle)
  okf lookup --bundle ./Knowlege/okf_bundle WorldBankConnector

  # Compact listing (good for /lookup commands)
  okf lookup --compact connector

  # JSON output (for programmatic use)
  okf lookup --json WorldBankConnector

  # Limit results
  okf lookup --limit 5 fetch
"""

import argparse
import contextlib
import json
import os
import re
import sys
from pathlib import Path

CACHE_VERSION = 1


# ---------------------------------------------------------------------------
# Bundle loader (builds on pairs.parse_okf_file — single canonical reader)
# ---------------------------------------------------------------------------

def _parse_md(path: Path, bundle_dir: Path) -> dict | None:
    from okf.pairs import parse_okf_file

    raw = parse_okf_file(path)
    if raw is None:
        return None

    # Preserve lookup's contract: Index/Log concept files are not searchable
    if raw["type"] in {"Index", "Log"}:
        return None

    rel = path.relative_to(bundle_dir)
    concept_id = str(rel.with_suffix("")).replace(os.sep, "/")

    return {
        "type":        raw["type"],
        "title":       raw["title"],
        "description": raw["description"],
        "resource":    raw["resource"],
        "tags":        raw["tags"],
        "timestamp":   raw["timestamp"],
        "concept_id":  concept_id,
        "sections":    raw["sections"],
        "raw":         path.read_text(encoding="utf-8", errors="replace"),  # full file content for --full output
        "_path":       str(path),
    }


def _cache_path(bundle_dir: Path) -> Path:
    return bundle_dir / ".okf_lookup_cache.json"


def _fingerprint(bundle_dir: Path) -> dict:
    """Cheap fingerprint: {relpath: mtime_ns} for every .md file, skipping reserved names."""
    reserved = {"index.md", "log.md", "SUMMARY.md"}
    fp = {}
    for md in bundle_dir.rglob("*.md"):
        if md.name in reserved:
            continue
        rel = str(md.relative_to(bundle_dir))
        fp[rel] = md.stat().st_mtime_ns
    return fp


def load_bundle(bundle_dir: Path, use_cache: bool = True) -> list[dict]:
    cache_file = _cache_path(bundle_dir)
    current_fp = _fingerprint(bundle_dir)

    if use_cache and cache_file.exists():
        try:
            cached = json.loads(cache_file.read_text(encoding="utf-8"))
            if (
                cached.get("version") == CACHE_VERSION
                and cached.get("fingerprint") == current_fp
            ):
                return cached["concepts"]
        except Exception:
            pass

    reserved = {"index.md", "log.md", "SUMMARY.md"}
    concepts = []
    for md in sorted(bundle_dir.rglob("*.md")):
        if md.name in reserved:
            continue
        c = _parse_md(md, bundle_dir)
        if c:
            concepts.append(c)

    if use_cache:
        with contextlib.suppress(Exception):
            cache_file.write_text(
                json.dumps(
                    {"version": CACHE_VERSION, "fingerprint": current_fp, "concepts": concepts},
                    ensure_ascii=False,
                ),
                encoding="utf-8",
            )

    return concepts


# ---------------------------------------------------------------------------
# Scoring / search
# ---------------------------------------------------------------------------

def _tokenize(text: str) -> list[str]:
    """Break text into searchable tokens: split on space, camelCase, snake_case."""
    tokens = []
    for word in text.split():
        # Split snake_case first
        for part in word.split("_"):
            if not part:
                continue
            # Split camelCase (e.g. UserRepo -> User, Repo)
            parts = re.findall(r"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\d|$)|[A-Z]+", part)
            tokens.extend(p.lower() for p in parts if p)
    return tokens


def _score(concept: dict, tokens: list[str]) -> float:
    """Return a relevance score 0–1. Higher = better match."""
    if not tokens:
        return 1.0

    title       = concept["title"].lower()
    description = concept["description"].lower()
    resource    = concept["resource"].lower()
    concept_id  = concept["concept_id"].lower()
    tags        = " ".join(concept["tags"]).lower()
    # Pre-tokenized fields for substring + fuzzy matching
    title_tokens = _tokenize(concept["title"])
    id_tokens    = _tokenize(concept["concept_id"])

    score = 0.0
    for tok in tokens:
        t = tok.lower()
        # Exact title match
        if t == title:
            score += 1.0
        # Prefix match on title
        elif title.startswith(t):
            score += 0.8
        # Substring in title
        elif t in title:
            score += 0.6
        # Any tokenized subword matches (camelCase/snake_case)
        elif t in title_tokens:
            score += 0.75
        elif t in id_tokens:
            score += 0.5
        elif t in concept_id:
            score += 0.4
        elif t in resource:
            score += 0.35
        elif t in description:
            score += 0.3
        elif t in tags:
            score += 0.2
        # Acronym match: each char matches a title word initial
        else:
            # Try as acronym (e.g. "ur" matches "UserRepository")
            if len(t) >= 2 and len(title_tokens) >= len(t):
                initials = "".join(w[0] for w in title_tokens if w)
                if initials.startswith(t):
                    score += 0.7
                    continue
            # Fuzzy: count matching chars / token length
            matches = sum(1 for ch in t if ch in title)
            ratio   = matches / max(len(t), 1)
            if ratio > 0.7:
                score += ratio * 0.15

    return score / len(tokens)


def search(
    concepts:  list[dict],
    tokens:    list[str],
    file_filter: str  = "",
    type_filter: str  = "",
    tag_filters: list[str] | None = None,
    limit:     int    = 10,
    min_score: float  = 0.1,
) -> list[dict]:

    results = []
    for c in concepts:
        # hard filters
        if file_filter and file_filter.lower() not in c["resource"].lower():
            continue
        if type_filter and c["type"].lower() != type_filter.lower():
            continue
        if tag_filters:
            ctags = [t.lower() for t in c["tags"]]
            if not all(f.lower() in ctags for f in tag_filters):
                continue

        score = _score(c, tokens)
        if score >= min_score or file_filter:   # file_filter shows all matches
            results.append((score, c))

    results.sort(key=lambda x: -x[0])
    return [c for _, c in results[:limit]]


# ---------------------------------------------------------------------------
# Formatters
# ---------------------------------------------------------------------------

DIVIDER = "─" * 60


def fmt_full(concept: dict) -> str:
    """Full concept content — exactly what's in the .md file."""
    return concept["raw"]


def fmt_detail(concept: dict) -> str:
    """Rich single-concept view — most useful for OpenCode context."""
    lines = [
        DIVIDER,
        f"  {concept['type'].upper()}: {concept['title']}",
        DIVIDER,
    ]

    if concept["description"]:
        lines.append(f"  Description : {concept['description']}")

    if concept["resource"]:
        src = concept["sections"].get("source", "")
        lineno = ""
        if src:
            m = re.search(r"Lines (\d+)", src)
            if m:
                lineno = f"  line {m.group(1)}"
        lines.append(f"  Source      : {concept['resource']}{lineno}")

    lines.append(f"  Concept ID  : {concept['concept_id']}")

    if concept["tags"]:
        lines.append(f"  Tags        : {', '.join(concept['tags'])}")

    # Signature
    sig = concept["sections"].get("signature", "")
    if sig:
        sig_clean = re.sub(r"```\w*\n?", "", sig).strip()
        lines.append(f"\n  Signature:\n    {sig_clean}")

    # Docstring
    doc = concept["sections"].get("docstring", "")
    if doc:
        doc_lines = doc.strip().splitlines()
        lines.append("\n  Docstring:")
        for dl in doc_lines[:8]:
            lines.append(f"    {dl}")
        if len(doc_lines) > 8:
            lines.append(f"    ... ({len(doc_lines)-8} more lines)")

    # Parameters
    params = concept["sections"].get("parameters", "")
    if params:
        lines.append("\n  Parameters:")
        for row in params.splitlines():
            if "|" in row and "---" not in row and "Name" not in row:
                cols = [c.strip().strip("`") for c in row.split("|") if c.strip()]
                if len(cols) >= 2:
                    lines.append(f"    {cols[0]}: {cols[1]}" + (f" = {cols[2]}" if len(cols) > 2 and cols[2] != "-" else ""))

    # Returns
    ret = concept["sections"].get("returns", "")
    if ret:
        lines.append(f"\n  Returns: {ret.replace('`','').strip()}")

    # Methods (for classes)
    methods = concept["sections"].get("methods", "")
    if methods:
        mnames = re.findall(r"`(\w+)`", methods)
        if mnames:
            lines.append(f"\n  Methods: {', '.join(mnames)}")

    # Related / Relationships
    related = concept["sections"].get("relationships", "") or concept["sections"].get("related", "")
    if related:
        rnames = re.findall(r"\[([^\]]+)\]", related)
        if rnames:
            lines.append(f"\n  Related: {', '.join(rnames)}")

    lines.append(DIVIDER)
    return "\n".join(lines)


def fmt_compact(concept: dict) -> str:
    """Single line — good for listing multiple results."""
    desc = concept["description"][:60] + ("…" if len(concept["description"]) > 60 else "")
    return (
        f"[{concept['type']:<8}] {concept['title']:<30} "
        f"{concept['resource']:<40} {desc}"
    )


def fmt_json(concepts: list[dict]) -> str:
    out = []
    for c in concepts:
        out.append({
            "type":        c["type"],
            "title":       c["title"],
            "description": c["description"],
            "resource":    c["resource"],
            "concept_id":  c["concept_id"],
            "tags":        c["tags"],
            "signature":   c["sections"].get("signature", "").replace("```python", "").replace("```", "").strip(),
            "docstring":   c["sections"].get("docstring", ""),
            "methods":     re.findall(r"`(\w+)`", c["sections"].get("methods", "")),
            "returns":     c["sections"].get("returns", "").replace("`", "").strip(),
        })
    return json.dumps(out, indent=2, ensure_ascii=False)


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(
        description="Look up OKF concepts — pinpoint exact code knowledge for AI agents.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )
    p.add_argument("query",       nargs="*",           help="Search tokens (name or keywords)")
    from okf.config import _get
    from okf.config import load as load_config
    _lcfg = load_config()
    p.add_argument("--bundle",    default=_get(_lcfg, "lookup.bundle", "./okf_bundle"), help="Path to OKF bundle dir")
    p.add_argument("--file",      default="",          help="Filter by source file path (e.g. src/connectors/economic_data.py)")
    p.add_argument("--type",      default="",          help="Filter by concept type: Function | Class | Module | Dependency")
    p.add_argument("--tag",       action="append", default=[], dest="tags", help="Filter by tag (repeatable): --tag lang:python --tag type:Class")
    p.add_argument("--limit",     type=int, default=10, help="Max results (default: 10)")
    p.add_argument("--compact",   action="store_true", help="One-line output per result")
    p.add_argument("--json",      action="store_true", help="JSON output")
    p.add_argument("--full",      action="store_true", help="Show raw .md file content")
    p.add_argument("--deps", action="store_true", help="Shortcut for --type Dependency")
    p.add_argument("--exact", action="store_true", help="Exact match only (min-score=0.9, no fuzzy)")
    p.add_argument("--min-score", type=float, default=0.1, help="Minimum relevance score 0-1 (default: 0.1)")
    p.add_argument("--no-cache", action="store_true", help="Bypass and skip writing the lookup cache")
    return p


def main():
    parser = build_parser()
    args   = parser.parse_args()

    bundle_dir = Path(args.bundle).resolve()
    if not bundle_dir.exists():
        # try common locations relative to cwd
        for candidate in ["./okf_bundle", "./Knowlege/okf_bundle", "./knowledge/okf_bundle"]:
            p = Path(candidate).resolve()
            if p.exists():
                bundle_dir = p
                break
        else:
            print(f"ERROR: OKF bundle not found at {args.bundle}", file=sys.stderr)
            print("Set --bundle path/to/okf_bundle", file=sys.stderr)
            sys.exit(1)

    # Load
    concepts = load_bundle(bundle_dir, use_cache=not args.no_cache)
    if not concepts:
        print(f"ERROR: No concepts found in {bundle_dir}", file=sys.stderr)
        sys.exit(1)

    # Search
    tokens  = args.query or []
    type_filter = args.type
    if args.deps:
        type_filter = "Dependency"
    results = search(
        concepts,
        tokens     = tokens,
        file_filter= args.file,
        type_filter= type_filter,
        tag_filters= args.tags,
        limit      = args.limit,
        min_score  = 0.9 if args.exact else args.min_score,
    )

    if not results:
        print("No concepts found", file=sys.stderr)
        if tokens:
            print(f"Query: {' '.join(tokens)}", file=sys.stderr)
        sys.exit(1)

    # Output
    if args.json:
        print(fmt_json(results))
        return

    if args.compact or len(results) > 3:
        # compact list for multiple results
        print(f"\nFound {len(results)} concept(s) in {bundle_dir.name}:\n")
        for c in results:
            print(fmt_compact(c))
        print()
        if len(results) == 1:
            print(fmt_detail(results[0]))
        else:
            print("Tip: run with exact name for full detail, e.g.:")
            print(f"  okf lookup --bundle {args.bundle} \"{results[0]['title']}\"")
        return

    # 1-3 results: show full detail for each
    for i, c in enumerate(results):
        if args.full:
            print(fmt_full(c))
        else:
            print(fmt_detail(c))
        if i < len(results) - 1:
            print()


if __name__ == "__main__":
    main()
