"""
Configuration file for search engines.
Loads search engine definitions from the user's configuration.
"""

from typing import Any, Dict, Optional
from sqlalchemy.orm import Session

from loguru import logger

from ..config.thread_settings import get_setting_from_snapshot
from ..utilities.db_utils import get_settings_manager
from .search_engine_base import _is_api_key_placeholder


def _get_setting(
    key: str,
    default_value: Any = None,
    db_session: Optional[Session] = None,
    settings_snapshot: Optional[Dict[str, Any]] = None,
    username: Optional[str] = None,
) -> Any:
    """
    Get a setting from either a database session or settings snapshot.

    Args:
        key: The setting key
        default_value: Default value if setting not found
        db_session: Database session for direct access
        settings_snapshot: Settings snapshot for thread context
        username: Username for backward compatibility

    Returns:
        The setting value or default_value if not found
    """
    # Try settings snapshot first (thread context)
    if settings_snapshot:
        try:
            return get_setting_from_snapshot(
                key, default_value, settings_snapshot=settings_snapshot
            )
        except Exception as e:
            logger.debug(f"Could not get setting {key} from snapshot: {e}")

    # Try database session if available
    if db_session:
        try:
            settings_manager = get_settings_manager(db_session, username)
            return settings_manager.get_setting(key, default_value)
        except Exception as e:
            logger.debug(f"Could not get setting {key} from db_session: {e}")

    # Return default if all methods fail
    logger.warning(
        f"Could not retrieve setting '{key}', returning default: {default_value}"
    )
    return default_value


def _extract_per_engine_config(
    raw_config: Dict[str, Any],
) -> Dict[str, Dict[str, Any]]:
    """
    Converts the "flat" configuration loaded from the settings database into
    individual settings dictionaries for each engine.

    Args:
        raw_config: The raw "flat" configuration.

    Returns:
        Configuration dictionaries indexed by engine name.

    """
    nested_config: dict[str, Any] = {}
    for key, value in raw_config.items():
        if "." in key:
            # This is a higher-level key.
            top_level_key = key.split(".")[0]
            lower_keys = ".".join(key.split(".")[1:])
            nested_config.setdefault(top_level_key, {})[lower_keys] = value
        else:
            # This is a low-level key.
            nested_config[key] = value

    # Expand all the lower-level keys.
    for key, value in nested_config.items():
        if isinstance(value, dict):
            # Expand the child keys.
            nested_config[key] = _extract_per_engine_config(value)

    return nested_config


def search_config(
    username: Optional[str] = None,
    db_session: Optional[Session] = None,
    settings_snapshot: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """
    Returns the search engine configuration loaded from the database or settings snapshot.

    Args:
        username: Username for backward compatibility (deprecated)
        db_session: Database session for direct access (preferred for web routes)
        settings_snapshot: Settings snapshot for thread context (preferred for background threads)

    Returns:
        The search engine configuration loaded from the database or snapshot.
    """
    # Extract search engine definitions
    config_data = _get_setting(
        "search.engine.web",
        {},
        db_session=db_session,
        settings_snapshot=settings_snapshot,
        username=username,
    )

    search_engines = _extract_per_engine_config(config_data)

    # Inject module/class from the hardcoded engine registry.
    # This is the single source of truth for which Python module implements
    # each engine — these values are never read from the settings DB.
    from .engine_registry import ENGINE_REGISTRY

    for name, entry in ENGINE_REGISTRY.items():
        if name in search_engines:
            search_engines[name]["module_path"] = entry.module_path
            search_engines[name]["class_name"] = entry.class_name
            if entry.full_search_module:
                search_engines[name]["full_search_module"] = (
                    entry.full_search_module
                )
                search_engines[name]["full_search_class"] = (
                    entry.full_search_class
                )

    # Add registered retrievers as available search engines
    from .retriever_registry import retriever_registry

    for name in retriever_registry.list_registered():
        search_engines[name] = {
            "module_path": ".engines.search_engine_retriever",
            "class_name": "RetrieverSearchEngine",
            "requires_api_key": False,
            "requires_llm": False,
            "description": f"LangChain retriever: {name}",
            "strengths": [
                "Domain-specific knowledge",
                "No rate limits",
                "Fast retrieval",
            ],
            "weaknesses": ["Limited to indexed content"],
            "supports_full_search": True,
            "is_retriever": True,  # Mark as retriever for identification
        }

    logger.info(
        f"Loaded {len(search_engines)} search engines from configuration file"
    )
    logger.info(f"\n  {', '.join(sorted(search_engines.keys()))} \n")

    # Register Library RAG as a search engine
    library_enabled = _get_setting(
        "search.engine.library.enabled",
        True,
        db_session=db_session,
        settings_snapshot=settings_snapshot,
        username=username,
    )

    if library_enabled:
        search_engines["library"] = {
            "module_path": ".engines.search_engine_library",
            "class_name": "LibraryRAGSearchEngine",
            "requires_llm": True,
            "display_name": "Search All Collections",
            "default_params": {},
            "description": "Search across all your document collections using semantic search",
            "strengths": [
                "Searches all your curated collections of research papers and documents",
                "Uses semantic search for better relevance",
                "Returns documents you've already saved and reviewed",
            ],
            "weaknesses": [
                "Limited to documents already in your collections",
                "Requires documents to be indexed first",
            ],
            "reliability": "High - searches all your collections",
        }
        logger.info("Registered Library RAG as search engine")

    # Register document collections as individual search engines
    if library_enabled:
        try:
            from ..database.models.library import Collection
            from ..database.session_context import get_user_db_session

            # Get username from settings_snapshot if available
            collection_username = (
                settings_snapshot.get("_username")
                if settings_snapshot
                else username
            )

            if collection_username:
                with get_user_db_session(collection_username) as session:
                    collections = session.query(Collection).all()

                    for collection in collections:
                        engine_id = f"collection_{collection.id}"
                        # Add suffix to distinguish from the all-collections search
                        display_name = f"{collection.name} (Collection)"
                        # Egress classification follows the per-collection
                        # public/private flag (default private). A "public"
                        # collection counts as a public engine (allowed under
                        # PUBLIC_ONLY); a private one is local-only. NULL
                        # (pre-migration rows) reads as private — the safe
                        # default.
                        collection_is_public = bool(
                            getattr(collection, "is_public", False)
                        )
                        # Usability flag (NOT egress): whether the LangGraph
                        # research agent offers this collection as a tool. NULL
                        # (pre-migration rows) reads as available (True). Uses
                        # the same `is not False` idiom as the rag_routes
                        # serializers so all call sites share one NULL→available
                        # default and can't drift.
                        collection_agent_enabled = (
                            getattr(collection, "agent_enabled", True)
                            is not False
                        )
                        search_engines[engine_id] = {
                            "module_path": ".engines.search_engine_collection",
                            "class_name": "CollectionSearchEngine",
                            "requires_llm": True,
                            "is_local": not collection_is_public,
                            "is_public": collection_is_public,
                            "agent_enabled": collection_agent_enabled,
                            "display_name": display_name,
                            "default_params": {
                                "collection_id": collection.id,
                                "collection_name": collection.name,
                            },
                            "description": (
                                collection.description
                                if collection.description
                                else f"Search documents in {collection.name} collection only"
                            ),
                            "strengths": [
                                f"Searches only documents in {collection.name}",
                                "Focused semantic search within specific topic area",
                                "Returns documents from a curated collection",
                            ],
                            "weaknesses": [
                                "Limited to documents in this collection",
                                "Smaller result pool than full library search",
                            ],
                            "reliability": "High - searches a specific collection",
                        }

                    logger.info(
                        f"Registered {len(collections)} document collections as search engines"
                    )
            else:
                logger.debug(
                    "No username available for collection registration"
                )
        except Exception:
            logger.warning("Could not register document collections")

    return search_engines


def get_available_engines(
    settings_snapshot: Optional[Dict[str, Any]] = None,
    use_api_key_services: bool = True,
    exclude_engines: Optional[set] = None,
) -> Dict[str, Any]:
    """
    Return search engines that are actually usable: enabled for auto-search
    and with valid API keys when required.

    This is the single shared filter used by the langgraph-agent tool
    builder so it agrees with the rest of the system on which engines are
    available.

    Args:
        settings_snapshot: Thread-safe settings snapshot.
        use_api_key_services: If False, engines that require an API key are
            excluded even when the key is present.
        exclude_engines: Additional engine names to skip (e.g. the caller's
            own name).

    Returns:
        Dict of engine_name → config for engines that passed all checks.
    """
    if not settings_snapshot:
        logger.warning(
            "get_available_engines called without settings_snapshot, "
            "returning empty dict"
        )
        return {}

    all_engines = search_config(settings_snapshot=settings_snapshot)
    excluded = set(exclude_engines) if exclude_engines else set()

    available: Dict[str, Any] = {}

    for name, config in all_engines.items():
        if name in excluded:
            continue

        # Check use_in_auto_search setting (default False)
        auto_search_key = f"search.engine.web.{name}.use_in_auto_search"
        use_in_auto = get_setting_from_snapshot(
            auto_search_key, False, settings_snapshot=settings_snapshot
        )
        if not use_in_auto:
            continue

        requires_key = config.get("requires_api_key", False)

        # Honour the use_api_key_services flag
        if requires_key and not use_api_key_services:
            continue

        # Validate the API key is actually present
        if requires_key:
            api_key = _resolve_api_key(name, config, settings_snapshot)
            if not api_key:
                logger.debug(
                    f"Skipping {name} — requires API key but none configured"
                )
                continue

        available[name] = config

    return available


def _resolve_api_key(
    engine_name: str,
    engine_config: Dict[str, Any],
    settings_snapshot: Dict[str, Any],
) -> Optional[str]:
    """
    Try to find a valid API key for *engine_name*.

    Resolution order (mirrors ``create_search_engine``):
      1. ``search.engine.web.<name>.api_key`` in the snapshot
      2. ``api_key`` inside the engine config dict

    Returns the key string or None.
    """
    api_key = None
    api_key_path = f"search.engine.web.{engine_name}.api_key"

    api_key_setting = settings_snapshot.get(api_key_path)
    if api_key_setting:
        api_key = (
            api_key_setting.get("value")
            if isinstance(api_key_setting, dict)
            else api_key_setting
        )

    if not api_key:
        api_key = engine_config.get("api_key")

    if not api_key:
        return None

    # Reject common placeholder values
    api_key_str = str(api_key).strip()
    if _is_api_key_placeholder(api_key_str):
        return None

    return api_key_str


def default_search_engine(
    username: Optional[str] = None,
    db_session: Optional[Session] = None,
    settings_snapshot: Optional[Dict[str, Any]] = None,
) -> str:
    """
    Returns the configured default search engine.

    Args:
        username: Username for backward compatibility (deprecated)
        db_session: Database session for direct access (preferred for web routes)
        settings_snapshot: Settings snapshot for thread context (preferred for background threads)

    Returns:
        The configured default search engine.
    """
    return str(
        _get_setting(
            "search.engine.DEFAULT_SEARCH_ENGINE",
            "wikipedia",
            db_session=db_session,
            settings_snapshot=settings_snapshot,
            username=username,
        )
    )
