"""
RAG Service Factory

Provides get_rag_service() for creating LibraryRAGService instances
with appropriate settings. Extracted from rag_routes.py to avoid
circular imports (service → routes).
"""

import json
from typing import Optional

from loguru import logger

from ...constants import (
    DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS,
    DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS_JSON,
)
from ...database.models.library import Collection
from ...database.session_context import get_user_db_session
from ...utilities.db_utils import get_settings_manager
from ...utilities.type_utils import to_bool
from ..services.library_rag_service import LibraryRAGService


def _get_default_text_separators(settings):
    """Return configured default text separators, parsing string values if needed."""
    default_text_separators = settings.get_setting(
        "local_search_text_separators",
        DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS_JSON,
    )
    if isinstance(default_text_separators, str):
        # A value that is not valid JSON (e.g. a not-yet-migrated corrupt row)
        # falls back to the default separators — migration #4298 heals existing
        # corrupt data.
        try:
            default_text_separators = json.loads(default_text_separators)
        except json.JSONDecodeError:
            logger.warning(
                "Invalid JSON for local_search_text_separators: {!r} — using default separators",
                default_text_separators,
            )
            default_text_separators = DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS

    if not isinstance(default_text_separators, list):
        default_text_separators = DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS

    return default_text_separators


def _enforce_embeddings_policy(
    embedding_provider: str, settings_manager, username: str
) -> None:
    """Pre-flight egress-policy check before constructing the RAG service.

    Fails BEFORE the first chunk is processed (per the plan's pre-flight
    requirement) — important because indexing a large corpus can take 10+
    minutes; we want the user to see a clear policy error immediately, not
    after embeddings have already been generated for hundreds of chunks.

    No-op when the egress policy does not require local embeddings —
    i.e. when ``embeddings.require_local`` is False AND the scope does not
    imply it. Under PRIVATE_ONLY the requirement is forced regardless of
    the flag (see context_from_snapshot).
    """
    # Lazy import to avoid pulling the security module on every factory call.
    from ...security.egress.policy import (
        DEFAULT_EGRESS_SCOPE,
        Decision,
        PolicyDeniedError,
        context_from_snapshot,
        evaluate_embeddings,
    )

    # We don't have a full settings snapshot here, only a SettingsManager.
    # Build a minimal snapshot for the policy module — only the keys it
    # reads for scope coupling + embedding classification matter.
    scope = (
        settings_manager.get_setting("policy.egress_scope")
        or DEFAULT_EGRESS_SCOPE
    )
    require_local_flag = to_bool(
        settings_manager.get_setting("embeddings.require_local") or False
    )
    base_url = settings_manager.get_setting("embeddings.openai.base_url")
    ollama_url = settings_manager.get_setting("embeddings.ollama.url")
    # evaluate_embeddings() classifies the ollama embeddings endpoint from
    # embeddings.ollama.url OR, when that's unset, llm.ollama.url. Omitting
    # the llm.* fallback here would misclassify a user who only configured
    # the shared llm.ollama.url as "remote" and wrongly deny local ollama
    # embeddings. Populate both so the classification matches runtime.
    llm_ollama_url = settings_manager.get_setting("llm.ollama.url")
    snapshot = {
        "policy.egress_scope": scope,
        "embeddings.require_local": require_local_flag,
        "embeddings.openai.base_url": base_url or "",
        "embeddings.ollama.url": ollama_url or "",
        "llm.ollama.url": llm_ollama_url or "",
    }
    # Build the ctx from the ACTUAL scope so PRIVATE_ONLY forces local
    # embeddings even when the raw flag is False. primary_engine="library"
    # is concrete.
    try:
        ctx = context_from_snapshot(snapshot, "library", username=username)
    except PolicyDeniedError:
        raise
    except ValueError as exc:
        raise PolicyDeniedError(
            Decision(False, "invalid_policy_config"),
            target=embedding_provider,
        ) from exc
    # No-op unless the (scope-aware) policy requires local embeddings.
    if not ctx.require_local_embeddings:
        return

    decision = evaluate_embeddings(
        embedding_provider, ctx, settings_snapshot=snapshot
    )
    if not decision.allowed:
        logger.bind(policy_audit=True).warning(
            "embeddings provider denied by egress policy",
            provider=embedding_provider,
            reason=decision.reason,
        )
        raise PolicyDeniedError(decision, target=embedding_provider)


def get_rag_service(
    username: str,
    collection_id: Optional[str] = None,
    use_defaults: bool = False,
    db_password: Optional[str] = None,
) -> LibraryRAGService:
    """
    Get RAG service instance with appropriate settings.

    Args:
        username: Username for database access and settings lookup
        collection_id: Optional collection UUID to load stored settings from
        use_defaults: When True, ignore stored collection settings and use
            current defaults. Pass True on force-reindex so that the new
            default embedding model is picked up.
        db_password: Optional database password for encrypted databases

    If collection_id is provided:
    - Uses collection's stored settings if they exist (unless use_defaults=True)
    - Uses current defaults for new collections (and stores them)

    If no collection_id:
    - Uses current default settings
    """
    # Use get_user_db_session so that settings are readable from background
    # threads (no Flask app context).  Without an explicit db_session,
    # get_settings_manager falls back to JSON defaults only, and the
    # local_search_* keys have no JSON defaults — causing user-configured
    # embedding settings to be silently ignored.  See #3453.
    with get_user_db_session(username, db_password) as db_session:
        settings = get_settings_manager(
            db_session=db_session, username=username
        )

        # Get current default settings.
        # The local_search_* keys are written by the embedding-settings page
        # and have no JSON defaults file yet, so explicit fallbacks are
        # required to avoid TypeError / None propagation on fresh installs.
        raw_embedding_model = settings.get_setting(
            "local_search_embedding_model"
        )
        raw_embedding_provider = settings.get_setting(
            "local_search_embedding_provider"
        )
        # Warn on silent fallback so a regression of #3453 is visible in logs
        # instead of being masked by `or`-chained defaults.  On fresh installs
        # this fires legitimately until the user saves settings; in a
        # regression it would fire on every indexing call.
        if not raw_embedding_model and not raw_embedding_provider:
            logger.warning(
                "local_search embedding settings are empty; falling back to "
                "hardcoded defaults (sentence_transformers/all-MiniLM-L6-v2). "
                "Expected on fresh installs before settings are saved; "
                "otherwise check that db_session is being passed to "
                "SettingsManager (see #3453)."
            )
        default_embedding_model = raw_embedding_model or "all-MiniLM-L6-v2"
        default_embedding_provider = (
            raw_embedding_provider or "sentence_transformers"
        )
        default_chunk_size = int(
            settings.get_setting("local_search_chunk_size") or 1000
        )
        default_chunk_overlap = int(
            settings.get_setting("local_search_chunk_overlap") or 200
        )
        default_splitter_type = (
            settings.get_setting("local_search_splitter_type") or "recursive"
        )
        default_text_separators = _get_default_text_separators(settings)
        default_distance_metric = (
            settings.get_setting("local_search_distance_metric") or "cosine"
        )
        default_normalize_vectors = settings.get_bool_setting(
            "local_search_normalize_vectors"
        )
        default_index_type = (
            settings.get_setting("local_search_index_type") or "flat"
        )

        # If collection_id provided, check for stored settings
        if collection_id:
            collection = (
                db_session.query(Collection).filter_by(id=collection_id).first()
            )

            if collection and collection.embedding_model and not use_defaults:
                # Use collection's stored settings
                logger.info(
                    f"Using stored settings for collection {collection_id}: "
                    f"{collection.embedding_model_type.value if collection.embedding_model_type else 'unknown'}/{collection.embedding_model}"
                )
                # Egress policy pre-flight (R9-07 / plan landmine #3):
                # block before any chunk is generated when the stored
                # provider conflicts with require_local. Critical for
                # collections indexed pre-policy-rollout with OpenAI.
                effective_provider = (
                    collection.embedding_model_type.value
                    if collection.embedding_model_type
                    else default_embedding_provider
                )
                _enforce_embeddings_policy(
                    effective_provider, settings, username
                )

                # Handle normalize_vectors - may be stored as string in some
                # cases
                coll_normalize = collection.normalize_vectors
                if coll_normalize is not None:
                    coll_normalize = to_bool(coll_normalize)
                else:
                    coll_normalize = default_normalize_vectors

                def _col(stored, default):
                    """Use stored collection value if not None, else default."""
                    return stored if stored is not None else default

                return LibraryRAGService(
                    username=username,
                    embedding_model=collection.embedding_model,
                    embedding_provider=collection.embedding_model_type.value
                    if collection.embedding_model_type
                    else default_embedding_provider,
                    chunk_size=_col(collection.chunk_size, default_chunk_size),
                    chunk_overlap=_col(
                        collection.chunk_overlap, default_chunk_overlap
                    ),
                    splitter_type=_col(
                        collection.splitter_type, default_splitter_type
                    ),
                    text_separators=_col(
                        collection.text_separators, default_text_separators
                    ),
                    distance_metric=_col(
                        collection.distance_metric, default_distance_metric
                    ),
                    normalize_vectors=coll_normalize,
                    index_type=_col(collection.index_type, default_index_type),
                    db_password=db_password,
                )
            if collection:
                # New collection - use defaults and store them
                logger.info(
                    f"New collection {collection_id}, using and storing default settings"
                )

                # Egress policy pre-flight.
                _enforce_embeddings_policy(
                    default_embedding_provider, settings, username
                )

                # Create service with defaults
                return LibraryRAGService(
                    username=username,
                    embedding_model=default_embedding_model,
                    embedding_provider=default_embedding_provider,
                    chunk_size=default_chunk_size,
                    chunk_overlap=default_chunk_overlap,
                    splitter_type=default_splitter_type,
                    text_separators=default_text_separators,
                    distance_metric=default_distance_metric,
                    normalize_vectors=default_normalize_vectors,
                    index_type=default_index_type,
                    db_password=db_password,
                )

                # Store settings on collection (will be done during indexing)
                # Note: We don't store here because we don't have
                # embedding_dimension yet.  It will be stored in
                # index_collection when first document is indexed.

        # No collection or fallback - use current defaults
        # Egress policy pre-flight.
        _enforce_embeddings_policy(
            default_embedding_provider, settings, username
        )
        return LibraryRAGService(
            username=username,
            embedding_model=default_embedding_model,
            embedding_provider=default_embedding_provider,
            chunk_size=default_chunk_size,
            chunk_overlap=default_chunk_overlap,
            splitter_type=default_splitter_type,
            text_separators=default_text_separators,
            distance_metric=default_distance_metric,
            normalize_vectors=default_normalize_vectors,
            index_type=default_index_type,
            db_password=db_password,
        )
