"""
Corpus search — find matching world models by keyword/factor name similarity.

Searches afs_world_models for stored world models whose factor_names
overlap with the query keywords. Jaccard similarity scoring, matching
the JS corpusService.js implementation.

This is the Python-side equivalent of corpusService.js corpusSearch().
Used by afs_decide to skip LLM world model construction when a corpus
template matches the user's question.

Usage:
    from afs.store.corpus_search import corpus_search, extract_keywords

    keywords = extract_keywords("Which synthesizer should I buy?",
                                ["Prophet 6", "OB-8x"])
    hit = corpus_search(keywords, threshold=0.6)
    if hit:
        world_model = hit["world_model"]  # ready for colony
"""

import re
import logging
from typing import Optional

from .world_model_store import _get_collection

logger = logging.getLogger(__name__)


def extract_keywords(question: str, options: list = None) -> list[str]:
    """Extract searchable keywords from question + option names.

    No LLM needed — pure string ops. Produces factor-name-like tokens
    that can match against stored factor_names via Jaccard.

    Strategy:
    1. Tokenize question into lowercase words
    2. Remove stop words
    3. Add option names as keywords
    4. Deduplicate, return sorted
    """
    stop_words = {
        'a', 'an', 'the', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
        'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
        'should', 'may', 'might', 'shall', 'can', 'need', 'dare', 'ought',
        'used', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from',
        'as', 'into', 'through', 'during', 'before', 'after', 'above', 'below',
        'between', 'out', 'off', 'over', 'under', 'again', 'further', 'then',
        'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'each',
        'every', 'both', 'few', 'more', 'most', 'other', 'some', 'such', 'no',
        'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very',
        'just', 'because', 'but', 'and', 'or', 'if', 'while', 'that', 'which',
        'what', 'who', 'whom', 'this', 'these', 'those', 'am', 'i', 'me', 'my',
        'we', 'our', 'you', 'your', 'he', 'him', 'his', 'she', 'her', 'it',
        'its', 'they', 'them', 'their', 'myself', 'yourself', 'himself',
        'herself', 'itself', 'ourselves', 'themselves',
        'second', 'opinion', 'decide', 'decision', 'help', 'choose', 'best',
        'compare', 'vs', 'versus', 'between', 'should', 'which', 'what',
    }

    keywords = set()

    # Tokenize question
    if question:
        tokens = re.findall(r'[a-zA-Z_][a-zA-Z0-9_]*', question.lower())
        for tok in tokens:
            if tok not in stop_words and len(tok) > 2:
                keywords.add(tok)
                # Also add underscored compounds (e.g., "market size" → "market_size")
                # by keeping multi-word patterns
        # Look for compound terms with underscores already in the question
        compounds = re.findall(r'[a-z]+_[a-z]+(?:_[a-z]+)*', question.lower())
        keywords.update(compounds)

    # Add option names
    if options:
        for opt in options:
            name = opt if isinstance(opt, str) else opt.get("name", "")
            if name:
                tokens = re.findall(r'[a-zA-Z_][a-zA-Z0-9_]*', name.lower())
                for tok in tokens:
                    if tok not in stop_words and len(tok) > 2:
                        keywords.add(tok)

    return sorted(keywords)


def jaccard_similarity(set_a: set, set_b: set) -> float:
    """Case-insensitive Jaccard similarity between two sets."""
    a = {s.lower() for s in set_a}
    b = {s.lower() for s in set_b}
    intersection = a & b
    union = a | b
    if not union:
        return 0.0
    return len(intersection) / len(union)


def corpus_search(keywords: list[str], threshold: float = 0.4,
                  max_candidates: int = 200) -> Optional[dict]:
    """Search afs_world_models for the best matching world model.

    Args:
        keywords: list of factor-name-like tokens to match
        threshold: minimum Jaccard similarity (0.4 = loose match for templates)
        max_candidates: max documents to scan

    Returns:
        dict with { world_model, factor_names, similarity, source_label,
                    strategy_count, created_at } or None
    """
    coll = _get_collection()
    if coll is None:
        return None

    if not keywords:
        return None

    query_set = set(keywords)

    try:
        # Query recent corpus artifacts (sorted by recency, capped)
        cursor = coll.find(
            {},
            {"factor_names": 1, "world_model": 1, "source_label": 1,
             "strategy_count": 1, "created_at": 1}
        ).sort("created_at", -1).limit(max_candidates)

        best_hit = None
        best_sim = 0.0

        for doc in cursor:
            stored_factors = doc.get("factor_names", [])
            if not stored_factors:
                continue

            sim = jaccard_similarity(query_set, set(stored_factors))
            if sim > best_sim and sim >= threshold:
                best_sim = sim
                best_hit = {
                    "world_model": doc.get("world_model"),
                    "factor_names": stored_factors,
                    "similarity": round(sim, 4),
                    "source_label": doc.get("source_label", ""),
                    "strategy_count": doc.get("strategy_count", 0),
                    "created_at": doc.get("created_at"),
                }

        if best_hit:
            logger.info(
                f"[corpus_search] Hit: similarity={best_hit['similarity']:.2f}, "
                f"factors={best_hit['factor_names'][:5]}, "
                f"source='{best_hit['source_label'][:60]}'"
            )

        return best_hit

    except Exception as e:
        logger.warning(f"[corpus_search] MongoDB query failed: {e}")
        return None
