"""
Second Brain Service - Semantic knowledge management.

Manages the user's knowledge base with:
- Semantic search via embeddings
- Two-layer scoping (personal + tenant)
- Weight management (base, recency, usage)
- Query analytics
"""

from __future__ import annotations

import logging
import time
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any

from lightwave.ai.embeddings import EmbeddingService
from lightwave.schema.pydantic.models.constitution import SecondBrainEntryContext

if TYPE_CHECKING:
    from django.contrib.auth.models import AbstractUser

logger = logging.getLogger(__name__)


class SecondBrainService:
    """
    Service for managing and querying the second brain knowledge base.

    The second brain has two layers:
    - Personal (tenant=None): Follows user across all tenants
    - Tenant-specific: Only available in that tenant context

    Usage:
        service = SecondBrainService(user, "tenant_schema")

        # Query with semantic search
        entries = service.query("tax strategies", max_results=10)

        # Add new entry
        entry = service.add_entry(
            title="Tax Planning",
            content="Key insights about...",
            entry_type="insight",
            tags=["tax", "planning"],
        )
    """

    def __init__(
        self,
        user: "AbstractUser",
        tenant_schema: str,
        embedding_provider: str = "openai",
    ):
        """
        Initialize the second brain service.

        Args:
            user: The authenticated Django user
            tenant_schema: PostgreSQL schema name for tenant context
            embedding_provider: Provider for embeddings (openai, anthropic, local)
        """
        self.user = user
        self.tenant_schema = tenant_schema
        self.embedding_service = EmbeddingService(provider=embedding_provider)

    def query(
        self,
        query: str,
        max_results: int = 10,
        min_score: float = 0.3,
        include_personal: bool = True,
        include_tenant: bool = True,
        entry_types: list[str] | None = None,
        tags: list[str] | None = None,
    ) -> list[SecondBrainEntryContext]:
        """
        Query the second brain with semantic search.

        Combines personal and tenant-specific entries, ranked by
        similarity score multiplied by effective weight.

        Args:
            query: Search query text
            max_results: Maximum number of results
            min_score: Minimum similarity score (0.0-1.0)
            include_personal: Include personal (tenant=None) entries
            include_tenant: Include current tenant's entries
            entry_types: Filter by entry types (note, insight, etc.)
            tags: Filter by tags (any match)

        Returns:
            List of SecondBrainEntryContext sorted by score * weight
        """
        start_time = time.time()

        try:
            # Import Django models
            from apps.core.knowledge.models import SecondBrainEntry, SecondBrainQuery
            from django.db.models import Q
        except ImportError:
            logger.warning("Second brain models not available")
            return []

        # Build query filter
        q_filter = Q(user=self.user, embedding__isnull=False)

        # Layer filtering
        layer_filter = Q()
        if include_personal:
            layer_filter |= Q(tenant__isnull=True)
        if include_tenant:
            layer_filter |= Q(tenant__schema_name=self.tenant_schema)
        q_filter &= layer_filter

        # Entry type filtering
        if entry_types:
            q_filter &= Q(entry_type__in=entry_types)

        # Tag filtering (any match)
        if tags:
            # JSONField contains any of the tags
            tag_filter = Q()
            for tag in tags:
                tag_filter |= Q(tags__contains=[tag])
            q_filter &= tag_filter

        # Get matching entries
        entries = SecondBrainEntry.objects.filter(q_filter)

        if not entries.exists():
            return []

        # Generate query embedding
        query_result = self.embedding_service.embed("query", "temp", query)
        query_embedding = query_result.embedding

        # Score entries by similarity * weight
        scored: list[tuple[Any, float, float]] = []
        for entry in entries:
            similarity = self.embedding_service.cosine_similarity(
                query_embedding,
                entry.embedding,
            )
            if similarity >= min_score:
                combined_score = similarity * entry.effective_weight
                scored.append((entry, similarity, combined_score))

        # Sort by combined score
        scored.sort(key=lambda x: x[2], reverse=True)

        # Take top results and mark accessed
        results = []
        entry_ids = []
        for entry, similarity, _score in scored[:max_results]:
            entry.mark_accessed()
            context = entry.to_constitution_context()
            # Add similarity to context for debugging
            context["similarity_score"] = similarity
            results.append(SecondBrainEntryContext.model_validate(context))
            entry_ids.append(str(entry.id))

        # Log query for analytics
        query_time_ms = int((time.time() - start_time) * 1000)
        try:
            SecondBrainQuery.objects.create(
                user=self.user,
                query=query[:500],  # Truncate long queries
                agent_type="second_brain_service",
                entries_returned=entry_ids,
                entry_count=len(results),
                query_time_ms=query_time_ms,
            )
        except Exception as e:
            logger.warning(f"Failed to log query: {e}")

        return results

    def add_entry(
        self,
        title: str,
        content: str,
        entry_type: str = "note",
        tags: list[str] | None = None,
        source_url: str = "",
        base_weight: float = 1.0,
        tenant_specific: bool = False,
    ) -> Any:
        """
        Add a new entry to the second brain.

        Automatically generates embedding for semantic search.

        Args:
            title: Entry title
            content: Full content
            entry_type: Type of entry (note, insight, reference, etc.)
            tags: Topic tags for categorization
            source_url: Original source URL if applicable
            base_weight: User-assigned importance (0.0-2.0)
            tenant_specific: If True, scoped to current tenant

        Returns:
            The created SecondBrainEntry instance
        """
        try:
            from apps.core.knowledge.models import SecondBrainEntry
            from apps.core.tenant.models import Tenant
        except ImportError:
            raise RuntimeError("Second brain models not available")

        # Get tenant if tenant-specific
        tenant = None
        if tenant_specific:
            tenant = Tenant.objects.get(schema_name=self.tenant_schema)

        # Create entry
        entry = SecondBrainEntry.objects.create(
            user=self.user,
            tenant=tenant,
            title=title,
            content=content,
            entry_type=entry_type,
            tags=tags or [],
            source_url=source_url,
            base_weight=base_weight,
        )

        # Generate embedding
        self._update_entry_embedding(entry)

        return entry

    def update_entry(
        self,
        entry_id: str,
        title: str | None = None,
        content: str | None = None,
        tags: list[str] | None = None,
        base_weight: float | None = None,
    ) -> Any:
        """
        Update an existing entry.

        Regenerates embedding if content changes.

        Args:
            entry_id: UUID of the entry
            title: New title (if provided)
            content: New content (if provided)
            tags: New tags (if provided)
            base_weight: New base weight (if provided)

        Returns:
            The updated SecondBrainEntry instance
        """
        try:
            from apps.core.knowledge.models import SecondBrainEntry
        except ImportError:
            raise RuntimeError("Second brain models not available")

        entry = SecondBrainEntry.objects.get(id=entry_id, user=self.user)

        content_changed = False
        if title is not None:
            entry.title = title
            content_changed = True
        if content is not None:
            entry.content = content
            content_changed = True
        if tags is not None:
            entry.tags = tags
        if base_weight is not None:
            entry.base_weight = base_weight

        entry.save()

        # Regenerate embedding if content changed
        if content_changed:
            self._update_entry_embedding(entry)

        return entry

    def delete_entry(self, entry_id: str) -> None:
        """
        Delete an entry from the second brain.

        Args:
            entry_id: UUID of the entry to delete
        """
        try:
            from apps.core.knowledge.models import SecondBrainEntry
        except ImportError:
            raise RuntimeError("Second brain models not available")

        entry = SecondBrainEntry.objects.get(id=entry_id, user=self.user)
        entry.delete()

    def update_all_embeddings(self, force: bool = False) -> int:
        """
        Update embeddings for all entries that need it.

        Args:
            force: If True, regenerate all embeddings even if current

        Returns:
            Number of entries updated
        """
        try:
            from apps.core.knowledge.models import SecondBrainEntry
            from django.db.models import F, Q
        except ImportError:
            return 0

        # Find entries needing update
        q_filter = Q(user=self.user)
        if not force:
            # Only entries without embedding or with stale embedding
            q_filter &= Q(embedding__isnull=True) | Q(embedding_updated_at__lt=F("updated_at"))

        entries = SecondBrainEntry.objects.filter(q_filter)

        count = 0
        for entry in entries:
            try:
                self._update_entry_embedding(entry)
                count += 1
            except Exception as e:
                logger.error(f"Failed to update embedding for {entry.id}: {e}")

        return count

    def apply_recency_decay(self, decay_rate: float = 0.01) -> int:
        """
        Apply time-based decay to all entries' recency weights.

        Call this periodically (e.g., daily via Celery task).

        Args:
            decay_rate: Amount to decay per call (default 1%)

        Returns:
            Number of entries updated
        """
        try:
            from apps.core.knowledge.models import SecondBrainEntry
        except ImportError:
            return 0

        entries = SecondBrainEntry.objects.filter(user=self.user)

        count = 0
        for entry in entries:
            entry.apply_recency_decay(decay_rate)
            count += 1

        return count

    def get_analytics(self) -> dict[str, Any]:
        """
        Get analytics about second brain usage.

        Returns:
            Dictionary with usage statistics
        """
        try:
            from apps.core.knowledge.models import SecondBrainEntry, SecondBrainQuery
            from django.db.models import Avg, Count, Sum
        except ImportError:
            return {}

        entries = SecondBrainEntry.objects.filter(user=self.user)
        queries = SecondBrainQuery.objects.filter(user=self.user)

        return {
            "total_entries": entries.count(),
            "personal_entries": entries.filter(tenant__isnull=True).count(),
            "tenant_entries": entries.filter(tenant__schema_name=self.tenant_schema).count(),
            "entries_with_embeddings": entries.filter(embedding__isnull=False).count(),
            "total_queries": queries.count(),
            "avg_query_time_ms": queries.aggregate(avg=Avg("query_time_ms"))["avg"],
            "total_access_count": entries.aggregate(total=Sum("access_count"))["total"] or 0,
            "top_entry_types": list(entries.values("entry_type").annotate(count=Count("id")).order_by("-count")[:5]),
        }

    def _update_entry_embedding(self, entry: Any) -> None:
        """
        Update the embedding for a single entry.

        Args:
            entry: SecondBrainEntry instance
        """
        # Combine title and content for embedding
        text = f"{entry.title}\n\n{entry.content}"

        # Generate embedding
        result = self.embedding_service.embed(
            entity_type="second_brain",
            entity_id=str(entry.id),
            text=text,
        )

        # Update entry
        entry.embedding = result.embedding
        entry.embedding_model = f"{self.embedding_service.provider}:{self.embedding_service.model}"
        entry.embedding_updated_at = datetime.now(timezone.utc)
        entry.save(
            update_fields=[
                "embedding",
                "embedding_model",
                "embedding_updated_at",
                "updated_at",
            ]
        )


# =============================================================================
# CONVENIENCE FUNCTIONS
# =============================================================================


def query_second_brain(
    user: "AbstractUser",
    tenant_schema: str,
    query: str,
    max_results: int = 10,
) -> list[SecondBrainEntryContext]:
    """
    Query the second brain for a user.

    Convenience function that creates a service and queries in one call.

    Args:
        user: The authenticated Django user
        tenant_schema: PostgreSQL schema name
        query: Search query text
        max_results: Maximum number of results

    Returns:
        List of matching entries
    """
    service = SecondBrainService(user, tenant_schema)
    return service.query(query, max_results=max_results)


def update_entry_embedding(
    user: "AbstractUser",
    tenant_schema: str,
    entry_id: str,
) -> None:
    """
    Update the embedding for a specific entry.

    Args:
        user: The entry owner
        tenant_schema: PostgreSQL schema name
        entry_id: UUID of the entry
    """
    try:
        from apps.core.knowledge.models import SecondBrainEntry
    except ImportError:
        raise RuntimeError("Second brain models not available")

    entry = SecondBrainEntry.objects.get(id=entry_id, user=user)
    service = SecondBrainService(user, tenant_schema)
    service._update_entry_embedding(entry)
