#
#   Imandra Inc.
#
#   codelogician/doc/search.py
#

import json
from pathlib import Path
from typing import Any, Final

from fuzzysearch import find_near_matches

from .md_utils import parse_frontmatter

SKILL_DIR: Final = (Path(__file__).parent / 'skill').resolve()

# =====
# # Search schema and help
# =====

SEARCH_SCHEMA: Final[dict[str, Any]] = {
    'type': 'object',
    'properties': {
        'scope': {
            'type': 'string',
            'enum': ['all', 'error', 'prelude'],
            'description': 'Search scope. "all" searches markdown files, "error" searches error_corpus.json, "prelude" searches prelude.json.',
        },
        'query': {
            'type': 'string',
            'description': 'The search query string for fuzzy matching.',
        },
        'max_results': {
            'type': 'integer',
            'default': 10,
            'description': 'Maximum number of results to return.',
        },
    },
    'required': ['query'],
}

SEARCH_HELP: Final = f"""\
Search documentation, error corpus, and prelude reference.

Subcommands:
  search <QUERY>          Fuzzy match markdown file names and descriptions
  search error <QUERY>    Search error_corpus.json (common IML errors and fixes)
  search prelude <QUERY>  Search prelude.json (built-in modules and functions)
  search schema           Print the JSON schema for --json requests

Options:
  --json    Pass a JSON search request instead of a plain query
  -n        Maximum number of results (default: 10)

JSON schema for --json:
{json.dumps(SEARCH_SCHEMA, indent=2)}
"""


# =====
# # Internals
# =====


def _fuzzy_score(query: str, text: str) -> float | None:
    """Return fuzzy match distance between query and text, or None if no match."""
    if not text:
        return None
    query_lower = query.lower()
    text_lower = text.lower()
    if query_lower in text_lower:
        return 0.0
    max_l_dist = max(1, len(query) // 3)
    matches = find_near_matches(query_lower, text_lower, max_l_dist=max_l_dist)
    if matches:
        return min(m.dist for m in matches)
    return None


def _best_score(query: str, fields: list[str]) -> float | None:
    """Return the best fuzzy score across multiple text fields."""
    best: float | None = None
    for text in fields:
        score = _fuzzy_score(query, text)
        if score is not None:
            if best is None or score < best:
                best = score
            if best == 0.0:
                break
    return best


# Markdown file search
# ====================


def collect_md_files(
    skill_dir: Path | None = None,
) -> list[tuple[Path, dict[str, Any] | None]]:
    """Collect all markdown files under skill/ with their parsed frontmatter."""
    root = (skill_dir or SKILL_DIR).resolve()
    results = []
    for md_path in sorted(root.rglob('*.md')):
        text = md_path.read_text()
        fm, _ = parse_frontmatter(text)
        results.append((md_path, fm))
    return results


def collect_iml_files(
    skill_dir: Path | None = None,
) -> list[tuple[Path, dict[str, Any] | None]]:
    """Collect all IML files under skill/ with no frontmatter."""
    root = (skill_dir or SKILL_DIR).resolve()
    results = []
    for iml_path in sorted(root.rglob('*.iml')):
        results.append((iml_path, None))
    return results


def search_md(
    query: str, *, skill_dir: Path | None = None, max_results: int = 10
) -> list[dict[str, Any]]:
    """Fuzzy match markdown files by name and description.

    Returns list of dicts with keys: path, score, content, name (optional),
    description (optional). Results are sorted by score ascending (0 = exact match).
    """
    root = (skill_dir or SKILL_DIR).resolve()
    scored: list[tuple[float, Path, dict[str, Any] | None]] = []

    for md_path, fm in collect_md_files(root):
        rel = md_path.relative_to(root)
        stem = md_path.stem
        name = fm.get('name', stem) if fm else stem
        desc = fm.get('description', '') if fm else ''

        score = _best_score(query, [name, stem, desc, str(rel)])
        if score is not None:
            scored.append((score, md_path, fm))

    scored.sort(key=lambda x: x[0])
    results: list[dict[str, Any]] = []
    for dist, md_path, fm in scored[:max_results]:
        _, body = parse_frontmatter(md_path.read_text())
        entry: dict[str, Any] = {
            'path': str(md_path.relative_to(root)),
            'score': dist,
            'content': body,
        }
        if fm:
            if fm.get('name'):
                entry['name'] = fm['name']
            if fm.get('description'):
                entry['description'] = fm['description']
        results.append(entry)
    return results


# Error corpus search
# ====================


def load_error_corpus(skill_dir: Path | None = None) -> list[dict[str, Any]]:
    root = skill_dir or SKILL_DIR
    p = root / 'error-fix-data' / 'error_corpus.json'
    data = json.loads(p.read_text())
    return data['items']


def search_error_corpus(
    query: str, *, skill_dir: Path | None = None, max_results: int = 10
) -> list[dict[str, Any]]:
    """Search error_corpus.json by name, msg_str, kind, tags, and explanation."""
    scored: list[tuple[float, dict[str, Any]]] = []

    for item in load_error_corpus(skill_dir):
        fields = [
            item.get('name', ''),
            item.get('msg_str', ''),
            item.get('kind', ''),
            item.get('explanation', '') or '',
            item.get('solution_description', '') or '',
            ' '.join(item.get('tags', [])),
        ]
        score = _best_score(query, fields)
        if score is not None:
            scored.append((score, item))

    scored.sort(key=lambda x: x[0])
    return [item for _, item in scored[:max_results]]


# Prelude search
# ====================


def load_prelude(skill_dir: Path | None = None) -> list[dict[str, Any]]:
    root = skill_dir or SKILL_DIR
    p = root / 'reference' / 'prelude' / 'prelude.json'
    return json.loads(p.read_text())


def search_prelude(
    query: str, *, skill_dir: Path | None = None, max_results: int = 10
) -> list[dict[str, Any]]:
    """Search prelude.json by module, name, signature, and doc."""
    scored: list[tuple[float, dict[str, Any]]] = []

    for entry in load_prelude(skill_dir):
        fields = [
            entry.get('name', ''),
            entry.get('module', ''),
            entry.get('signature', ''),
            entry.get('doc', '') or '',
            entry.get('pattern', '') or '',
        ]
        score = _best_score(query, fields)
        if score is not None:
            scored.append((score, entry))

    scored.sort(key=lambda x: x[0])
    return [item for _, item in scored[:max_results]]


# =====
# # JSON search interface
# =====


def handle_json_search(
    raw: str, *, skill_dir: Path | None = None
) -> list[dict[str, Any]]:
    """Handle a JSON search request conforming to SEARCH_SCHEMA."""
    req = json.loads(raw)
    query = req['query']
    scope = req.get('scope', 'all')
    max_results = req.get('max_results', 10)

    if scope == 'error':
        return search_error_corpus(query, skill_dir=skill_dir, max_results=max_results)
    elif scope == 'prelude':
        return search_prelude(query, skill_dir=skill_dir, max_results=max_results)
    else:
        return search_md(query, skill_dir=skill_dir, max_results=max_results)
