"""
Django AppConfig for the ``library`` app.

Registers a startup probe that runs once per Python process and yells if
Mnemosyne is misconfigured in a way that would cause silent zero-result
searches — missing embedding model, missing Neo4j index, or a dimension
mismatch between the model and an existing index. Loud ERROR lines are the
only defence against "search works but returns nothing", which is
indistinguishable from "search works and matched nothing" unless you read
the stderr of a different container.

The probe is deliberately best-effort: it cannot crash the process even if
Neo4j is unreachable, because a transient DB blip on startup should not
take down the whole app. Nothing hard-gates on the vector indexes — the
``init`` sidecar only runs ``migrate`` + ``load_library_types`` (vector
indexes cannot be created before the system embedding model is configured
in the admin, which is a manual step after first boot). This probe is the
only way an operator learns that the manual
``setup_neo4j_indexes`` step was skipped or fell out of sync with the
current system model.
"""

import logging
import os
import sys

from django.apps import AppConfig

logger = logging.getLogger(__name__)


# Index names we expect setup_neo4j_indexes to have created. Kept in sync
# with library/management/commands/setup_neo4j_indexes.py. A test asserts
# they stay in sync.
_EXPECTED_VECTOR_INDEXES = (
    "chunk_embedding_index",
    "concept_embedding_index",
    "image_embedding_index",
)
_EXPECTED_FULLTEXT_INDEXES = (
    "chunk_text_fulltext",
    "concept_name_fulltext",
    "item_title_fulltext",
    "library_name_fulltext",
)


def _should_skip_probe() -> bool:
    """
    Decide whether to skip the startup probe.

    Skip when:
    - Running a management command other than the long-running servers
      (migrate, makemigrations, setup_neo4j_indexes, load_library_types,
      collectstatic, test, shell). The probe would just spam stderr for
      ops work that doesn't care about Neo4j index state.
    - Neo4j bolt URL is unset (build-time ``collectstatic`` stubs, CI
      unit tests without real infra).
    - Running under pytest (any argv contains ``pytest`` or a test runner
      env var is set).
    """
    if "pytest" in sys.argv[0] or "PYTEST_CURRENT_TEST" in os.environ:
        return True
    if os.environ.get("DJANGO_SKIP_STARTUP_PROBE") == "1":
        return True
    # Typical Django command invocations where the probe is noise.
    skip_commands = {
        "migrate",
        "makemigrations",
        "setup_neo4j_indexes",
        "load_library_types",
        "collectstatic",
        "test",
        "shell",
        "check",
        "dbshell",
        "showmigrations",
        "squashmigrations",
        "createsuperuser",
        "help",
    }
    if len(sys.argv) >= 2 and sys.argv[1] in skip_commands:
        return True
    # No Neo4j endpoint configured — probably a build or local dev without
    # graph infrastructure. Don't pretend we can check.
    if not os.environ.get("NEOMODEL_NEO4J_BOLT_URL"):
        return True
    return False


def _is_web_process() -> bool:
    """
    True when running inside the web (gunicorn / runserver) process.

    The reachability collector must only register here: ``/metrics`` is served
    by the web process, and registering in the Celery worker would both probe
    the GPU endpoints from a process whose metrics nobody scrapes and risk
    duplicate registration. Celery launches via ``celery`` argv; management
    commands are excluded above.
    """
    argv0 = sys.argv[0]
    if "celery" in argv0 or (len(sys.argv) >= 2 and sys.argv[1] == "celery"):
        return False
    if "pytest" in argv0 or "PYTEST_CURRENT_TEST" in os.environ:
        return False
    # gunicorn (prod) or runserver (dev).
    if "gunicorn" in argv0:
        return True
    if len(sys.argv) >= 2 and sys.argv[1] == "runserver":
        return True
    return False


def _run_startup_probe():
    """
    Emit ERROR/WARNING logs if the stack is misconfigured for search.

    Each check is individually guarded so a single unreachable dependency
    doesn't mask the other findings. Returns nothing; side effect is log
    output.
    """
    from neomodel import db

    # --- 1. System embedding model --------------------------------------
    embedding_dim = None
    embedding_model_label = "<unknown>"
    try:
        from llm_manager.models import LLMModel

        model = LLMModel.get_system_embedding_model()
        if not model:
            logger.error(
                "No system embedding model configured. Search will return "
                "zero results until one is set in the LLM admin."
            )
        elif not model.vector_dimensions:
            logger.error(
                "System embedding model '%s: %s' has no vector_dimensions "
                "set. Neo4j vector indexes cannot be validated and search "
                "quality will be unpredictable.",
                model.api.name,
                model.name,
            )
            embedding_model_label = f"{model.api.name}: {model.name}"
        else:
            embedding_dim = model.vector_dimensions
            embedding_model_label = f"{model.api.name}: {model.name}"
            logger.info(
                "System embedding model: %s (%dd)",
                embedding_model_label,
                embedding_dim,
            )
    except Exception as exc:
        logger.warning(
            "Startup probe could not read system embedding model: %s", exc
        )

    # --- 2. Neo4j indexes present & correctly dimensioned ---------------
    try:
        results, _ = db.cypher_query(
            "SHOW INDEXES YIELD name, type, options RETURN name, type, options"
        )
    except Exception as exc:
        logger.warning(
            "Startup probe could not list Neo4j indexes: %s. Search "
            "degradation will only surface at query time.",
            exc,
        )
        return

    present = {}
    for row in results:
        name, idx_type, options = row[0], row[1], row[2]
        present[name] = (idx_type, options)

    # Missing vector indexes
    for name in _EXPECTED_VECTOR_INDEXES:
        if name not in present:
            logger.error(
                "Neo4j vector index '%s' is missing. Configure the system "
                "embedding model in /admin/llm_manager/llmmodel/, then run "
                "'docker compose exec app python manage.py "
                "setup_neo4j_indexes' to create it.",
                name,
            )
            continue
        # Dimension check against the embedding model.
        if embedding_dim is None:
            continue
        idx_type, options = present[name]
        config = (options or {}).get("indexConfig") or {}
        raw_dim = config.get("vector.dimensions")
        try:
            existing_dim = int(raw_dim) if raw_dim is not None else None
        except (TypeError, ValueError):
            existing_dim = None
        if existing_dim is not None and existing_dim != embedding_dim:
            logger.error(
                "Neo4j index '%s' has %d dimensions but system embedding "
                "model %s reports %d. Re-run 'setup_neo4j_indexes --drop' "
                "and re-embed all content — search will return empty or "
                "wrong results until this is fixed.",
                name,
                existing_dim,
                embedding_model_label,
                embedding_dim,
            )

    # Missing fulltext indexes
    for name in _EXPECTED_FULLTEXT_INDEXES:
        if name not in present:
            logger.error(
                "Neo4j full-text index '%s' is missing. Full-text search "
                "will silently return no matches. Run 'setup_neo4j_indexes'.",
                name,
            )


class LibraryConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "library"
    verbose_name = "Library"

    def ready(self):
        if _is_web_process():
            from library.health_collector import register

            register()
