"""
Phase 33: Full-Text Search & Discovery for the Brynq Platform.

Unified search across agents, tasks, messages, jobs, services, and social
posts.  Includes TF-IDF-like scoring, trending discovery, popular agents,
activity feed, and autocomplete — all built on stdlib only (no external
search libraries).

Storage is file-based, consistent with the rest of the codebase.
"""

import json
import math
import re
import time
from collections import Counter, defaultdict
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Any, Optional

from brynq.server import (
    BROKER_DIR,
    CHANNELS_DIR,
    SESSIONS_DIR,
    _read_jsonl_sync,
)

# ── Paths ──────────────────────────────────────────────────────────────────

PROFILES_DIR = BROKER_DIR / "profiles"
TASKS_DIR = BROKER_DIR / "tasks"
JOBS_DIR = BROKER_DIR / "jobs"
SERVICES_DIR = BROKER_DIR / "marketplace" / "services"
SOCIAL_POSTS_DIR = BROKER_DIR / "social" / "posts"

for _d in (PROFILES_DIR, TASKS_DIR, JOBS_DIR, SERVICES_DIR, SOCIAL_POSTS_DIR):
    _d.mkdir(parents=True, exist_ok=True)

# ── Stop Words ─────────────────────────────────────────────────────────────

STOP_WORDS: set[str] = {
    "a", "an", "the", "and", "or", "but", "in", "on", "at", "to", "for",
    "of", "with", "by", "from", "is", "it", "as", "be", "was", "are",
    "been", "being", "have", "has", "had", "do", "does", "did", "will",
    "would", "could", "should", "may", "might", "shall", "can", "this",
    "that", "these", "those", "i", "you", "he", "she", "we", "they",
    "me", "him", "her", "us", "them", "my", "your", "his", "its", "our",
    "their", "not", "no", "so", "if", "up", "out", "about", "into",
    "then", "than", "too", "very", "just", "also", "all", "each",
    "every", "any", "some", "such",
}

# ── Tokenizer ──────────────────────────────────────────────────────────────

_SPLIT_RE = re.compile(r"[^a-z0-9]+")


def _tokenize(text: str) -> list[str]:
    """Split text on whitespace/punctuation, lowercase, remove stop words."""
    if not text:
        return []
    tokens = _SPLIT_RE.split(text.lower())
    return [t for t in tokens if t and t not in STOP_WORDS and len(t) > 1]


# ── Data Loaders ───────────────────────────────────────────────────────────


def _load_all_profiles() -> list[dict]:
    """Load all profile JSON files from the profiles directory."""
    profiles: list[dict] = []
    if not PROFILES_DIR.exists():
        return profiles
    for f in sorted(PROFILES_DIR.glob("*.json")):
        if f.name.startswith("_"):
            continue
        try:
            profiles.append(json.loads(f.read_text("utf-8")))
        except (json.JSONDecodeError, OSError):
            continue
    return profiles


def _load_all_tasks() -> list[dict]:
    """Load all task JSON files from the tasks directory."""
    tasks: list[dict] = []
    if not TASKS_DIR.exists():
        return tasks
    for f in sorted(TASKS_DIR.glob("*.json")):
        try:
            tasks.append(json.loads(f.read_text("utf-8")))
        except (json.JSONDecodeError, OSError):
            continue
    return tasks


def _load_all_messages() -> list[dict]:
    """Load all messages from all channel JSONL files."""
    messages: list[dict] = []
    if not CHANNELS_DIR.exists():
        return messages
    for f in sorted(CHANNELS_DIR.glob("*.jsonl")):
        channel_name = f.stem
        for entry in _read_jsonl_sync(f):
            if "channel" not in entry:
                entry["channel"] = channel_name
            messages.append(entry)
    return messages


def _load_all_jobs() -> list[dict]:
    """Load all job JSON files from the jobs directory."""
    jobs: list[dict] = []
    if not JOBS_DIR.exists():
        return jobs
    for f in sorted(JOBS_DIR.glob("*.json")):
        try:
            jobs.append(json.loads(f.read_text("utf-8")))
        except (json.JSONDecodeError, OSError):
            continue
    return jobs


def _load_all_services() -> list[dict]:
    """Load all marketplace service JSON files."""
    services: list[dict] = []
    if not SERVICES_DIR.exists():
        return services
    for f in sorted(SERVICES_DIR.glob("*.json")):
        try:
            services.append(json.loads(f.read_text("utf-8")))
        except (json.JSONDecodeError, OSError):
            continue
    return services


def _load_all_social_posts() -> list[dict]:
    """Load all social post JSON files."""
    posts: list[dict] = []
    if not SOCIAL_POSTS_DIR.exists():
        return posts
    for f in sorted(SOCIAL_POSTS_DIR.glob("*.json")):
        try:
            posts.append(json.loads(f.read_text("utf-8")))
        except (json.JSONDecodeError, OSError):
            continue
    return posts


# ── Snippet Extraction ─────────────────────────────────────────────────────


def _extract_snippet(text: str, query: str, context_chars: int = 60) -> str:
    """Return a snippet of *text* centred on the first occurrence of *query*.

    If *query* is not found (or empty), return the first *context_chars*
    characters of *text*.
    """
    if not text:
        return ""
    if not query:
        return text[:context_chars * 2].strip()

    lower_text = text.lower()
    lower_query = query.lower()
    idx = lower_text.find(lower_query)
    if idx == -1:
        return text[:context_chars * 2].strip()

    start = max(0, idx - context_chars)
    end = min(len(text), idx + len(query) + context_chars)
    snippet = text[start:end].strip()
    if start > 0:
        snippet = "..." + snippet
    if end < len(text):
        snippet = snippet + "..."
    return snippet


# ── Scoring Helpers ────────────────────────────────────────────────────────


def _recency_boost(timestamp_str: str, max_boost: float = 0.3) -> float:
    """Return a small boost [0, max_boost] for recent items.

    Items from the last 24 hours get the full boost.  The boost decays
    linearly over 7 days and is 0 after that.
    """
    if not timestamp_str:
        return 0.0
    try:
        ts = datetime.fromisoformat(timestamp_str)
        if ts.tzinfo is None:
            ts = ts.replace(tzinfo=timezone.utc)
        now = datetime.now(timezone.utc)
        age_hours = max(0.0, (now - ts).total_seconds() / 3600.0)
        if age_hours <= 24:
            return max_boost
        if age_hours >= 168:  # 7 days
            return 0.0
        return max_boost * (1.0 - (age_hours - 24) / 144.0)
    except (ValueError, TypeError):
        return 0.0


def _score_document(
    query_tokens: list[str],
    title_text: str,
    body_text: str,
    timestamp_str: str = "",
) -> float:
    """TF-IDF-like score for a document against query tokens.

    Title matches score higher than body matches.  Exact (full-query)
    matches get a bonus.  Recent items receive a recency boost.
    """
    if not query_tokens:
        return 0.0

    title_tokens = _tokenize(title_text)
    body_tokens = _tokenize(body_text)

    title_counter = Counter(title_tokens)
    body_counter = Counter(body_tokens)

    total_doc_tokens = len(title_tokens) + len(body_tokens) + 1  # avoid div/0

    score = 0.0
    for qt in query_tokens:
        # Title match — weighted 3x
        tf_title = title_counter.get(qt, 0)
        if tf_title > 0:
            score += 3.0 * (1 + math.log(1 + tf_title))

        # Body match — weighted 1x
        tf_body = body_counter.get(qt, 0)
        if tf_body > 0:
            score += 1.0 * (1 + math.log(1 + tf_body))

        # Partial / substring match in the raw lowered text
        if qt in title_text.lower():
            score += 1.5
        if qt in body_text.lower():
            score += 0.5

    # Exact query substring match bonus (full query in title or body)
    full_query = " ".join(query_tokens)
    if full_query and full_query in title_text.lower():
        score += 5.0
    if full_query and full_query in body_text.lower():
        score += 2.0

    # Normalise slightly by doc length to avoid long docs always winning
    score = score / (1 + math.log(total_doc_tokens))

    # Recency boost — only when there is already a textual match
    if score > 0:
        score += _recency_boost(timestamp_str)

    return round(score, 4)


# ── Inverted Index ─────────────────────────────────────────────────────────


class _InvertedIndex:
    """Simple in-memory inverted index for fast keyword lookup."""

    def __init__(self) -> None:
        # token -> set of (doc_type, doc_id)
        self.index: dict[str, set[tuple[str, str]]] = defaultdict(set)
        # (doc_type, doc_id) -> stored doc dict
        self.docs: dict[tuple[str, str], dict] = {}
        self.built_at: float = 0.0

    def add(self, doc_type: str, doc_id: str, tokens: list[str], doc: dict) -> None:
        key = (doc_type, doc_id)
        self.docs[key] = doc
        for token in tokens:
            self.index[token].add(key)

    def lookup(self, tokens: list[str]) -> list[tuple[str, str]]:
        """Return doc keys matching ANY of the given tokens."""
        result: set[tuple[str, str]] = set()
        for token in tokens:
            result.update(self.index.get(token, set()))
        return list(result)

    def clear(self) -> None:
        self.index.clear()
        self.docs.clear()
        self.built_at = 0.0


_index = _InvertedIndex()


def build_index() -> dict:
    """Scan all data sources and (re)build the in-memory inverted index.

    Returns a summary dict with counts per content type.
    """
    _index.clear()
    counts: dict[str, int] = {}

    # Profiles
    profiles = _load_all_profiles()
    counts["agents"] = len(profiles)
    for p in profiles:
        doc_id = p.get("session_id", "")
        tokens = (
            _tokenize(p.get("session_name", ""))
            + _tokenize(p.get("bio", ""))
            + _tokenize(" ".join(p.get("skills", [])))
            + _tokenize(p.get("platform", ""))
        )
        _index.add("agent", doc_id, tokens, p)

    # Tasks
    tasks = _load_all_tasks()
    counts["tasks"] = len(tasks)
    for t in tasks:
        doc_id = t.get("task_id", "")
        tokens = _tokenize(t.get("title", "")) + _tokenize(t.get("description", ""))
        _index.add("task", doc_id, tokens, t)

    # Messages
    messages = _load_all_messages()
    counts["messages"] = len(messages)
    for m in messages:
        doc_id = m.get("id", "")
        tokens = (
            _tokenize(m.get("message", ""))
            + _tokenize(m.get("session_name", ""))
        )
        _index.add("message", doc_id, tokens, m)

    # Jobs
    jobs = _load_all_jobs()
    counts["jobs"] = len(jobs)
    for j in jobs:
        doc_id = j.get("job_id", "")
        tokens = (
            _tokenize(j.get("title", ""))
            + _tokenize(j.get("description", ""))
            + _tokenize(" ".join(j.get("required_skills", [])))
        )
        _index.add("job", doc_id, tokens, j)

    # Services
    services = _load_all_services()
    counts["services"] = len(services)
    for s in services:
        doc_id = s.get("service_id", "")
        tokens = (
            _tokenize(s.get("title", ""))
            + _tokenize(s.get("description", ""))
            + _tokenize(s.get("category", ""))
        )
        _index.add("service", doc_id, tokens, s)

    # Social posts
    posts = _load_all_social_posts()
    counts["social_posts"] = len(posts)
    for sp in posts:
        doc_id = sp.get("post_id", "")
        tokens = (
            _tokenize(sp.get("content", ""))
            + _tokenize(sp.get("author_name", ""))
            + _tokenize(" ".join(sp.get("tags", [])))
        )
        _index.add("social_post", doc_id, tokens, sp)

    _index.built_at = time.time()
    return {"indexed": counts, "built_at": _index.built_at}


# ── Unified Search ─────────────────────────────────────────────────────────


def search(
    query: str,
    types: Optional[list[str]] = None,
    limit: int = 20,
) -> list[dict]:
    """Search across ALL content types (or a subset).

    Args:
        query: Free-text query string.
        types: Optional list of content types to include. Valid values:
            agent, task, message, job, service, social_post.
            None means search all.
        limit: Maximum results to return.

    Returns:
        List of result dicts sorted by score descending.  Each dict:
            type, id, title, snippet, score, timestamp
    """
    if not query or not query.strip():
        return []

    query_tokens = _tokenize(query)
    if not query_tokens:
        return []

    allowed_types = set(types) if types else None

    results: list[dict] = []

    # Profiles / agents
    if allowed_types is None or "agent" in allowed_types:
        for p in _load_all_profiles():
            title_text = p.get("session_name", "")
            body_text = (
                p.get("bio", "") + " " + " ".join(p.get("skills", []))
            )
            score = _score_document(query_tokens, title_text, body_text, p.get("updated", ""))
            if score > 0:
                results.append({
                    "type": "agent",
                    "id": p.get("session_id", ""),
                    "title": title_text,
                    "snippet": _extract_snippet(body_text, query),
                    "score": score,
                    "timestamp": p.get("updated", p.get("created", "")),
                })

    # Tasks
    if allowed_types is None or "task" in allowed_types:
        for t in _load_all_tasks():
            title_text = t.get("title", "")
            body_text = t.get("description", "")
            score = _score_document(query_tokens, title_text, body_text, t.get("created", ""))
            if score > 0:
                results.append({
                    "type": "task",
                    "id": t.get("task_id", ""),
                    "title": title_text,
                    "snippet": _extract_snippet(body_text, query),
                    "score": score,
                    "timestamp": t.get("created", ""),
                })

    # Messages
    if allowed_types is None or "message" in allowed_types:
        for m in _load_all_messages():
            title_text = m.get("session_name", "")
            body_text = m.get("message", "")
            score = _score_document(query_tokens, title_text, body_text, m.get("timestamp", ""))
            if score > 0:
                results.append({
                    "type": "message",
                    "id": m.get("id", ""),
                    "title": f"#{m.get('channel', '?')} by {title_text}",
                    "snippet": _extract_snippet(body_text, query),
                    "score": score,
                    "timestamp": m.get("timestamp", ""),
                })

    # Jobs
    if allowed_types is None or "job" in allowed_types:
        for j in _load_all_jobs():
            title_text = j.get("title", "")
            body_text = (
                j.get("description", "") + " " + " ".join(j.get("required_skills", []))
            )
            score = _score_document(query_tokens, title_text, body_text, j.get("created", ""))
            if score > 0:
                results.append({
                    "type": "job",
                    "id": j.get("job_id", ""),
                    "title": title_text,
                    "snippet": _extract_snippet(body_text, query),
                    "score": score,
                    "timestamp": j.get("created", ""),
                })

    # Services
    if allowed_types is None or "service" in allowed_types:
        for s in _load_all_services():
            title_text = s.get("title", "")
            body_text = (
                s.get("description", "") + " " + s.get("category", "")
            )
            score = _score_document(query_tokens, title_text, body_text, s.get("created", ""))
            if score > 0:
                results.append({
                    "type": "service",
                    "id": s.get("service_id", ""),
                    "title": title_text,
                    "snippet": _extract_snippet(body_text, query),
                    "score": score,
                    "timestamp": s.get("created", ""),
                })

    # Social posts
    if allowed_types is None or "social_post" in allowed_types:
        for sp in _load_all_social_posts():
            title_text = sp.get("author_name", "")
            body_text = sp.get("content", "")
            score = _score_document(query_tokens, title_text, body_text, sp.get("created_at", ""))
            if score > 0:
                results.append({
                    "type": "social_post",
                    "id": sp.get("post_id", ""),
                    "title": f"Post by {title_text}",
                    "snippet": _extract_snippet(body_text, query),
                    "score": score,
                    "timestamp": sp.get("created_at", ""),
                })

    results.sort(key=lambda r: r["score"], reverse=True)
    return results[:limit]


# ── Type-Specific Searches ─────────────────────────────────────────────────


def search_agents(
    query: str,
    filters: Optional[dict] = None,
) -> list[dict]:
    """Search profiles by name, bio, skills, platform.

    Args:
        query: Free-text query string.
        filters: Optional dict with keys: platform, availability, skill.

    Returns:
        List of matching profile dicts with a ``search_score`` field,
        sorted by score descending.
    """
    if not query or not query.strip():
        return []

    query_tokens = _tokenize(query)
    if not query_tokens:
        return []

    filters = filters or {}
    scored: list[tuple[float, dict]] = []

    for p in _load_all_profiles():
        # Apply filters first
        if filters.get("platform") and p.get("platform", "").lower() != filters["platform"].lower():
            continue
        if filters.get("availability") and p.get("availability") != filters["availability"]:
            continue
        if filters.get("skill"):
            bot_skills = [s.lower() for s in p.get("skills", [])]
            if filters["skill"].lower() not in bot_skills:
                continue

        title_text = p.get("session_name", "")
        body_text = (
            p.get("bio", "")
            + " " + " ".join(p.get("skills", []))
            + " " + p.get("platform", "")
        )
        score = _score_document(query_tokens, title_text, body_text, p.get("updated", ""))
        if score > 0:
            result = {**p, "search_score": score}
            scored.append((score, result))

    scored.sort(key=lambda x: -x[0])
    return [item[1] for item in scored]


def search_tasks(
    query: str,
    status: Optional[str] = None,
) -> list[dict]:
    """Search tasks by title and description.

    Args:
        query: Free-text query string.
        status: Optional status filter (pending, accepted, completed, failed).

    Returns:
        List of matching task dicts with ``search_score``, sorted by score.
    """
    if not query or not query.strip():
        return []

    query_tokens = _tokenize(query)
    if not query_tokens:
        return []

    scored: list[tuple[float, dict]] = []

    for t in _load_all_tasks():
        if status and t.get("status") != status:
            continue

        title_text = t.get("title", "")
        body_text = t.get("description", "")
        score = _score_document(query_tokens, title_text, body_text, t.get("created", ""))
        if score > 0:
            result = {**t, "search_score": score}
            scored.append((score, result))

    scored.sort(key=lambda x: -x[0])
    return [item[1] for item in scored]


def search_messages(
    query: str,
    channel: Optional[str] = None,
    sender: Optional[str] = None,
    limit: int = 50,
) -> list[dict]:
    """Search channel messages by text content.

    Args:
        query: Free-text query string.
        channel: Optional channel filter.
        sender: Optional sender name filter (substring, case-insensitive).
        limit: Maximum results.

    Returns:
        List of matching message dicts with ``search_score``, sorted by score.
    """
    if not query or not query.strip():
        return []

    query_tokens = _tokenize(query)
    if not query_tokens:
        return []

    sender_lower = sender.lower() if sender else None
    scored: list[tuple[float, dict]] = []

    messages = _load_all_messages()
    for m in messages:
        if channel and m.get("channel") != channel:
            continue
        if sender_lower and sender_lower not in m.get("session_name", "").lower():
            continue

        title_text = m.get("session_name", "")
        body_text = m.get("message", "")
        score = _score_document(query_tokens, title_text, body_text, m.get("timestamp", ""))
        if score > 0:
            result = {**m, "search_score": score}
            scored.append((score, result))

    scored.sort(key=lambda x: -x[0])
    return [item[1] for item in scored[:limit]]


def search_jobs(
    query: str,
    status: Optional[str] = None,
    skill: Optional[str] = None,
) -> list[dict]:
    """Search job postings by title, description, skills.

    Args:
        query: Free-text query string.
        status: Optional status filter.
        skill: Optional required-skill filter.

    Returns:
        List of matching job dicts with ``search_score``, sorted by score.
    """
    if not query or not query.strip():
        return []

    query_tokens = _tokenize(query)
    if not query_tokens:
        return []

    scored: list[tuple[float, dict]] = []

    for j in _load_all_jobs():
        if status and j.get("status") != status:
            continue
        if skill:
            all_skills = [s.lower() for s in j.get("required_skills", []) + j.get("preferred_skills", [])]
            if skill.lower() not in all_skills:
                continue

        title_text = j.get("title", "")
        body_text = (
            j.get("description", "")
            + " " + " ".join(j.get("required_skills", []))
        )
        score = _score_document(query_tokens, title_text, body_text, j.get("created", ""))
        if score > 0:
            result = {**j, "search_score": score}
            scored.append((score, result))

    scored.sort(key=lambda x: -x[0])
    return [item[1] for item in scored]


def search_services(
    query: str,
    category: Optional[str] = None,
) -> list[dict]:
    """Search marketplace services by title, description, category.

    Args:
        query: Free-text query string.
        category: Optional category filter.

    Returns:
        List of matching service dicts with ``search_score``, sorted by score.
    """
    if not query or not query.strip():
        return []

    query_tokens = _tokenize(query)
    if not query_tokens:
        return []

    scored: list[tuple[float, dict]] = []

    for s in _load_all_services():
        if category and s.get("category") != category:
            continue

        title_text = s.get("title", "")
        body_text = (
            s.get("description", "")
            + " " + s.get("category", "")
        )
        score = _score_document(query_tokens, title_text, body_text, s.get("created", ""))
        if score > 0:
            result = {**s, "search_score": score}
            scored.append((score, result))

    scored.sort(key=lambda x: -x[0])
    return [item[1] for item in scored]


# ── Trending Discovery ─────────────────────────────────────────────────────


def get_trending(hours: int = 24, limit: int = 10) -> list[dict]:
    """Find trending topics by analysing recent message frequency.

    Scans all channel messages from the last *hours* hours, tokenises them,
    and returns the most frequent terms (excluding stop words) as topics.

    Returns:
        List of dicts: {topic, count, sample_messages}
    """
    cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
    token_counts: Counter = Counter()
    token_messages: dict[str, list[str]] = defaultdict(list)

    for m in _load_all_messages():
        ts_str = m.get("timestamp", "")
        try:
            ts = datetime.fromisoformat(ts_str)
            if ts.tzinfo is None:
                ts = ts.replace(tzinfo=timezone.utc)
            if ts < cutoff:
                continue
        except (ValueError, TypeError):
            continue

        text = m.get("message", "")
        tokens = _tokenize(text)
        for token in set(tokens):  # count each token once per message
            token_counts[token] += 1
            if len(token_messages[token]) < 3:
                token_messages[token].append(text[:120])

    trending: list[dict] = []
    for topic, count in token_counts.most_common(limit):
        trending.append({
            "topic": topic,
            "count": count,
            "sample_messages": token_messages[topic],
        })

    return trending


def get_popular_agents(limit: int = 10) -> list[dict]:
    """Return the most active / highest-reputation agents.

    Agents are ranked by a composite score: reputation * 10 + tasks_completed.

    Returns:
        List of profile dicts with an added ``popularity_score`` field.
    """
    profiles = _load_all_profiles()
    scored: list[tuple[float, dict]] = []
    for p in profiles:
        rep = p.get("reputation", 5.0)
        completed = p.get("tasks_completed", 0)
        popularity = rep * 10 + completed
        result = {**p, "popularity_score": round(popularity, 1)}
        scored.append((popularity, result))

    scored.sort(key=lambda x: -x[0])
    return [item[1] for item in scored[:limit]]


def get_recent_activity(limit: int = 20) -> list[dict]:
    """Return a unified activity feed across all content types, most recent first.

    Each entry: {type, id, title, summary, timestamp}.
    """
    activities: list[dict] = []

    # Recent tasks
    for t in _load_all_tasks():
        activities.append({
            "type": "task",
            "id": t.get("task_id", ""),
            "title": t.get("title", ""),
            "summary": f"[{t.get('status', '?')}] {t.get('description', '')[:100]}",
            "timestamp": t.get("created", ""),
        })

    # Recent messages (last N)
    messages = _load_all_messages()
    messages.sort(key=lambda m: m.get("timestamp", ""), reverse=True)
    for m in messages[:limit * 2]:
        activities.append({
            "type": "message",
            "id": m.get("id", ""),
            "title": f"#{m.get('channel', '?')} by {m.get('session_name', '?')}",
            "summary": m.get("message", "")[:120],
            "timestamp": m.get("timestamp", ""),
        })

    # Recent jobs
    for j in _load_all_jobs():
        activities.append({
            "type": "job",
            "id": j.get("job_id", ""),
            "title": j.get("title", ""),
            "summary": f"[{j.get('status', '?')}] {j.get('description', '')[:100]}",
            "timestamp": j.get("created", ""),
        })

    # Recent services
    for s in _load_all_services():
        activities.append({
            "type": "service",
            "id": s.get("service_id", ""),
            "title": s.get("title", ""),
            "summary": f"[{s.get('category', '?')}] {s.get('description', '')[:100]}",
            "timestamp": s.get("created", ""),
        })

    # Recent social posts
    for sp in _load_all_social_posts():
        activities.append({
            "type": "social_post",
            "id": sp.get("post_id", ""),
            "title": f"Post by {sp.get('author_name', '?')}",
            "summary": sp.get("content", "")[:120],
            "timestamp": sp.get("created_at", ""),
        })

    activities.sort(key=lambda a: a.get("timestamp", ""), reverse=True)
    return activities[:limit]


# ── Autocomplete ───────────────────────────────────────────────────────────


def autocomplete(prefix: str, limit: int = 5) -> list[str]:
    """Return quick suggestions matching *prefix*.

    Sources: agent names, skill names, common search terms from the
    inverted index (if built), and content type keywords.

    Args:
        prefix: The prefix to match (case-insensitive).
        limit: Maximum suggestions to return.

    Returns:
        List of suggestion strings, sorted by relevance.
    """
    if not prefix or not prefix.strip():
        return []

    prefix_lower = prefix.strip().lower()
    candidates: Counter = Counter()

    # Agent names
    for p in _load_all_profiles():
        name = p.get("session_name", "")
        if name.lower().startswith(prefix_lower):
            candidates[name] += 5

        # Skills
        for skill in p.get("skills", []):
            if skill.lower().startswith(prefix_lower):
                candidates[skill] += 3

    # Job titles and skills
    for j in _load_all_jobs():
        title = j.get("title", "")
        if title.lower().startswith(prefix_lower):
            candidates[title] += 2
        for skill in j.get("required_skills", []):
            if skill.lower().startswith(prefix_lower):
                candidates[skill] += 2

    # Service titles
    for s in _load_all_services():
        title = s.get("title", "")
        if title.lower().startswith(prefix_lower):
            candidates[title] += 2

    # Task titles
    for t in _load_all_tasks():
        title = t.get("title", "")
        if title.lower().startswith(prefix_lower):
            candidates[title] += 1

    # From the inverted index (if populated)
    for token in _index.index:
        if token.startswith(prefix_lower):
            freq = len(_index.index[token])
            candidates[token] += freq

    # Return top suggestions
    return [term for term, _ in candidates.most_common(limit)]
