"""
Management command to create Neo4j indexes for Mnemosyne content graph.

Creates:
- Vector indexes (dynamic dimensions from system embedding model) for Chunk, Concept, and ImageEmbedding
- Full-text indexes for text search on Chunk.text_preview and Concept.name
- Constraint indexes enforced by neomodel (unique properties)
"""

import logging

from django.core.management.base import BaseCommand
from neomodel import db

logger = logging.getLogger(__name__)

# Default vector dimensions (used when no system embedding model is configured)
DEFAULT_VECTOR_DIMENSIONS = 4096

# Full-text index definitions: (index_name, label, properties)
FULLTEXT_INDEXES = [
    ("chunk_text_fulltext", "Chunk", ["text_preview"]),
    ("concept_name_fulltext", "Concept", ["name"]),
    ("item_title_fulltext", "Item", ["title"]),
    ("library_name_fulltext", "Library", ["name"]),
]


def _get_vector_dimensions():
    """
    Get vector dimensions from the system embedding model.

    Falls back to DEFAULT_VECTOR_DIMENSIONS if no model is configured
    or the model has no vector_dimensions set.

    :returns: Tuple of (dimensions, source_description).
    """
    try:
        from llm_manager.models import LLMModel

        model = LLMModel.get_system_embedding_model()
        if model and model.vector_dimensions:
            return model.vector_dimensions, f"{model.api.name}: {model.name}"
    except Exception:
        pass

    return DEFAULT_VECTOR_DIMENSIONS, "default (no system embedding model)"


class Command(BaseCommand):
    help = (
        "Create Neo4j vector, full-text, and constraint indexes "
        "for the Mnemosyne content graph. Vector dimensions are read "
        "from the system embedding model."
    )

    def add_arguments(self, parser):
        parser.add_argument(
            "--drop",
            action="store_true",
            help="Drop existing indexes before recreating them",
        )
        parser.add_argument(
            "--dimensions",
            type=int,
            default=0,
            help="Override vector dimensions (default: read from system embedding model)",
        )

    def handle(self, *args, **options):
        drop = options["drop"]
        override_dims = options["dimensions"]

        # Resolve vector dimensions
        if override_dims > 0:
            dimensions = override_dims
            source = f"CLI override ({override_dims})"
        else:
            dimensions, source = _get_vector_dimensions()

        self.stdout.write(
            self.style.HTTP_INFO(
                f"Vector dimensions: {dimensions} (source: {source})"
            )
        )

        # Vector index definitions (dynamic dimensions)
        vector_indexes = [
            ("chunk_embedding_index", "Chunk", "embedding", dimensions, "cosine"),
            ("concept_embedding_index", "Concept", "embedding", dimensions, "cosine"),
            ("image_embedding_index", "ImageEmbedding", "embedding", dimensions, "cosine"),
        ]

        # Get existing indexes
        existing_indexes = self._get_existing_indexes()

        if drop:
            self._drop_indexes(existing_indexes, vector_indexes)
            existing_indexes = self._get_existing_indexes()

        # Create vector indexes
        for name, label, prop, dims, similarity in vector_indexes:
            if name in existing_indexes:
                self.stdout.write(
                    self.style.NOTICE(f"Vector index '{name}' already exists, skipping")
                )
                continue
            try:
                cypher = (
                    f"CREATE VECTOR INDEX {name} IF NOT EXISTS "
                    f"FOR (n:{label}) ON (n.{prop}) "
                    f"OPTIONS {{indexConfig: {{"
                    f"`vector.dimensions`: {dims}, "
                    f"`vector.similarity_function`: '{similarity}'"
                    f"}}}}"
                )
                db.cypher_query(cypher)
                self.stdout.write(
                    self.style.SUCCESS(
                        f"Created vector index: {name} ({label}.{prop}, {dims}d {similarity})"
                    )
                )
            except Exception as e:
                self.stderr.write(
                    self.style.ERROR(f"Failed to create vector index '{name}': {e}")
                )

        # Create full-text indexes
        for name, label, properties in FULLTEXT_INDEXES:
            if name in existing_indexes:
                self.stdout.write(
                    self.style.NOTICE(
                        f"Full-text index '{name}' already exists, skipping"
                    )
                )
                continue
            try:
                props_str = ", ".join(f"n.{p}" for p in properties)
                cypher = (
                    f"CREATE FULLTEXT INDEX {name} IF NOT EXISTS "
                    f"FOR (n:{label}) ON EACH [{props_str}]"
                )
                db.cypher_query(cypher)
                self.stdout.write(
                    self.style.SUCCESS(
                        f"Created full-text index: {name} ({label}: {', '.join(properties)})"
                    )
                )
            except Exception as e:
                self.stderr.write(
                    self.style.ERROR(f"Failed to create full-text index '{name}': {e}")
                )

        # Install neomodel constraints (unique indexes from model definitions)
        try:
            from neomodel import install_all_labels

            install_all_labels()
            self.stdout.write(
                self.style.SUCCESS("Installed neomodel constraint indexes")
            )
        except Exception as e:
            self.stderr.write(
                self.style.ERROR(f"Failed to install neomodel labels: {e}")
            )

        self.stdout.write(self.style.SUCCESS("\nNeo4j index setup complete."))

    def _get_existing_indexes(self):
        """Return set of existing index names."""
        try:
            results, _ = db.cypher_query("SHOW INDEXES YIELD name RETURN name")
            return {row[0] for row in results}
        except Exception:
            return set()

    def _drop_indexes(self, existing_indexes, vector_indexes):
        """Drop all Mnemosyne-managed indexes."""
        managed_names = {name for name, *_ in vector_indexes} | {
            name for name, *_ in FULLTEXT_INDEXES
        }
        for name in managed_names & existing_indexes:
            try:
                db.cypher_query(f"DROP INDEX {name} IF EXISTS")
                self.stdout.write(self.style.WARNING(f"Dropped index: {name}"))
            except Exception as e:
                self.stderr.write(
                    self.style.ERROR(f"Failed to drop index '{name}': {e}")
                )
