"""
Intelligence Bridge - Connect SparkNet to Spark Intelligence infrastructure.

This module integrates SparkNet's local learning with Spark Intelligence's:
- FastEmbed (BAAI/bge-small-en-v1.5) for semantic embeddings
- Memory Store for hybrid search (FTS5 BM25 + vector similarity)
- Mind API for distributed memory sync

This enables semantic recall instead of just keyword matching.
"""

import os
import sys
import json
import time
import hashlib
from pathlib import Path
from typing import List, Dict, Any, Optional, Tuple
from dataclasses import dataclass, field
from enum import Enum

# Spark Intelligence paths
SPARK_INTELLIGENCE_PATH = Path.home() / "Desktop" / "vibeship-spark-intelligence"
SPARK_PULSE_PATH = Path.home() / "Desktop" / "vibeship-spark-pulse"


class BridgeStatus(Enum):
    """Status of the intelligence bridge."""
    READY = "ready"
    EMBEDDINGS_ONLY = "embeddings_only"
    MEMORY_ONLY = "memory_only"
    DEGRADED = "degraded"
    OFFLINE = "offline"


@dataclass
class SemanticResult:
    """Result from semantic search."""
    spark_id: str
    content: str
    score: float
    domain: Optional[str] = None
    spark_type: Optional[str] = None
    source: str = "local"  # "local", "memory_store", "mind"
    bm25_score: Optional[float] = None
    cosine_score: Optional[float] = None


@dataclass
class BridgeStats:
    """Statistics for the intelligence bridge."""
    embeddings_available: bool = False
    memory_store_available: bool = False
    mind_available: bool = False
    total_embeddings: int = 0
    total_synced: int = 0
    model_name: str = ""


class IntelligenceBridge:
    """
    Connect SparkNet to Spark Intelligence infrastructure.

    Provides:
    - Semantic embeddings via FastEmbed
    - Hybrid search via Memory Store (BM25 + vectors)
    - Sync to Mind API for distributed memory

    Falls back gracefully when components are unavailable.
    """

    # Default embedding dimension for BAAI/bge-small-en-v1.5
    EMBEDDING_DIM = 384

    # Mind API defaults
    MIND_API_URL = "http://localhost:8080"
    DEFAULT_USER_ID = "550e8400-e29b-41d4-a716-446655440000"

    def __init__(
        self,
        mind_url: Optional[str] = None,
        user_id: Optional[str] = None,
        enable_embeddings: bool = True,
        enable_memory_store: bool = True,
        enable_mind_sync: bool = True,
    ):
        """
        Initialize the Intelligence Bridge.

        Args:
            mind_url: Mind API URL (default: http://localhost:8080)
            user_id: User ID for Mind API
            enable_embeddings: Enable semantic embeddings
            enable_memory_store: Enable hybrid memory store
            enable_mind_sync: Enable Mind API sync
        """
        self.mind_url = mind_url or self.MIND_API_URL
        self.user_id = user_id or self.DEFAULT_USER_ID

        self._enable_embeddings = enable_embeddings
        self._enable_memory_store = enable_memory_store
        self._enable_mind_sync = enable_mind_sync

        # Lazy-loaded components
        self._embedder = None
        self._memory_store = None
        self._mind_bridge = None

        # Track what's available
        self._embeddings_available = False
        self._memory_store_available = False
        self._mind_available = False

        # Embedding cache
        self._embedding_cache: Dict[str, List[float]] = {}

        # Sync tracking
        self._sync_state_path = Path.home() / ".sparknet" / "intelligence_sync.json"
        self._synced_sparks: set = set()
        self._load_sync_state()

        # Try to initialize components
        self._init_components()

    def _init_components(self):
        """Initialize available components."""
        # Try to load embedding module
        if self._enable_embeddings:
            self._embeddings_available = self._init_embeddings()

        # Try to load memory store
        if self._enable_memory_store:
            self._memory_store_available = self._init_memory_store()

        # Check Mind API availability
        if self._enable_mind_sync:
            self._mind_available = self._check_mind()

    def _init_embeddings(self) -> bool:
        """Initialize FastEmbed embeddings."""
        try:
            # First try to import from Spark Intelligence
            if SPARK_INTELLIGENCE_PATH.exists():
                sys.path.insert(0, str(SPARK_INTELLIGENCE_PATH))
                from lib.embeddings import embed_text, embed_texts
                self._embed_text = embed_text
                self._embed_texts = embed_texts
                return True
        except ImportError:
            pass

        # Try direct fastembed import
        try:
            from fastembed import TextEmbedding
            model_name = os.environ.get("SPARK_EMBED_MODEL", "BAAI/bge-small-en-v1.5")
            self._embedder = TextEmbedding(model_name=model_name)
            self._embed_text = self._local_embed_text
            self._embed_texts = self._local_embed_texts
            return True
        except ImportError:
            pass

        # Fallback: no embeddings
        self._embed_text = lambda x: None
        self._embed_texts = lambda x: None
        return False

    def _local_embed_text(self, text: str) -> Optional[List[float]]:
        """Embed text using local fastembed."""
        if not self._embedder:
            return None
        try:
            embeddings = list(self._embedder.embed([text]))
            return embeddings[0].tolist() if embeddings else None
        except Exception:
            return None

    def _local_embed_texts(self, texts: List[str]) -> Optional[List[List[float]]]:
        """Embed multiple texts using local fastembed."""
        if not self._embedder:
            return None
        try:
            embeddings = list(self._embedder.embed(texts))
            return [e.tolist() for e in embeddings]
        except Exception:
            return None

    def _init_memory_store(self) -> bool:
        """Initialize Memory Store for hybrid search."""
        try:
            if SPARK_INTELLIGENCE_PATH.exists():
                sys.path.insert(0, str(SPARK_INTELLIGENCE_PATH))
                from lib.memory_store import upsert_entry, retrieve as ms_retrieve
                self._upsert_entry = upsert_entry
                self._ms_retrieve = ms_retrieve
                return True
        except ImportError:
            pass

        return False

    def _check_mind(self) -> bool:
        """Check if Mind API is available."""
        try:
            import requests
            response = requests.get(f"{self.mind_url}/health", timeout=2)
            return response.status_code == 200
        except Exception:
            return False

    def _load_sync_state(self):
        """Load sync state from disk."""
        try:
            if self._sync_state_path.exists():
                with open(self._sync_state_path) as f:
                    data = json.load(f)
                    self._synced_sparks = set(data.get("synced_sparks", []))
        except Exception:
            self._synced_sparks = set()

    def _save_sync_state(self):
        """Save sync state to disk."""
        try:
            self._sync_state_path.parent.mkdir(parents=True, exist_ok=True)
            with open(self._sync_state_path, "w") as f:
                json.dump({
                    "synced_sparks": list(self._synced_sparks),
                    "last_sync": time.time(),
                }, f)
        except Exception:
            pass

    # =========================================================================
    # Core Methods
    # =========================================================================

    def embed(self, text: str) -> Optional[List[float]]:
        """
        Generate embedding for text.

        Uses FastEmbed (BAAI/bge-small-en-v1.5) for 384-dim vectors.

        Args:
            text: Text to embed

        Returns:
            Embedding vector or None if unavailable
        """
        if not self._embeddings_available:
            return None

        # Check cache
        cache_key = hashlib.md5(text.encode()).hexdigest()[:16]
        if cache_key in self._embedding_cache:
            return self._embedding_cache[cache_key]

        # Generate embedding
        embedding = self._embed_text(text)

        if embedding:
            self._embedding_cache[cache_key] = embedding

        return embedding

    def embed_batch(self, texts: List[str]) -> Optional[List[List[float]]]:
        """
        Generate embeddings for multiple texts.

        Args:
            texts: List of texts to embed

        Returns:
            List of embedding vectors or None if unavailable
        """
        if not self._embeddings_available or not texts:
            return None

        return self._embed_texts(texts)

    def semantic_recall(
        self,
        query: str,
        sparks: List[Any],  # List[Spark]
        limit: int = 10,
        hybrid: bool = True,
    ) -> List[SemanticResult]:
        """
        Find related sparks using semantic similarity.

        Args:
            query: Search query
            sparks: List of Spark objects to search
            limit: Maximum results
            hybrid: Use hybrid search (BM25 + semantic) if available

        Returns:
            List of SemanticResult sorted by relevance
        """
        if not sparks:
            return []

        # If memory store available and hybrid requested, use it
        if self._memory_store_available and hybrid:
            return self._hybrid_recall(query, sparks, limit)

        # Fall back to pure semantic search
        if self._embeddings_available:
            return self._semantic_only_recall(query, sparks, limit)

        # Fall back to keyword matching
        return self._keyword_recall(query, sparks, limit)

    def _hybrid_recall(
        self,
        query: str,
        sparks: List[Any],
        limit: int,
    ) -> List[SemanticResult]:
        """Hybrid search using Memory Store."""
        # First ensure sparks are in memory store
        self._sync_to_memory_store(sparks)

        # Retrieve using hybrid search
        results = self._ms_retrieve(
            query,
            project_key="sparknet",
            limit=limit,
            candidate_limit=limit * 5,
        )

        # Map results back to sparks
        spark_map = {s.id: s for s in sparks}
        semantic_results = []

        for r in results:
            spark_id = r.get("entry_id", "")
            if spark_id in spark_map:
                spark = spark_map[spark_id]
                # Handle spark_type safely (might be None or an enum)
                spark_type_val = getattr(spark, "spark_type", None)
                if spark_type_val is not None and hasattr(spark_type_val, "value"):
                    spark_type_str = spark_type_val.value
                else:
                    spark_type_str = str(spark_type_val) if spark_type_val else None

                semantic_results.append(SemanticResult(
                    spark_id=spark_id,
                    content=spark.content,
                    score=r.get("score", 0.5),
                    domain=getattr(spark, "domain", None),
                    spark_type=spark_type_str,
                    source="memory_store",
                    bm25_score=r.get("bm25"),
                    cosine_score=r.get("score") - (r.get("bm25", 0) * 0.6) if r.get("bm25") else None,
                ))

        return semantic_results[:limit]

    def _semantic_only_recall(
        self,
        query: str,
        sparks: List[Any],
        limit: int,
    ) -> List[SemanticResult]:
        """Pure semantic search using cosine similarity."""
        query_embedding = self.embed(query)
        if not query_embedding:
            return self._keyword_recall(query, sparks, limit)

        # Embed all sparks and compute similarity
        results = []
        for spark in sparks:
            spark_embedding = self.embed(spark.content)
            if spark_embedding:
                similarity = self._cosine_similarity(query_embedding, spark_embedding)
                # Handle spark_type safely (might be None or an enum)
                spark_type_val = getattr(spark, "spark_type", None)
                if spark_type_val is not None and hasattr(spark_type_val, "value"):
                    spark_type_str = spark_type_val.value
                else:
                    spark_type_str = str(spark_type_val) if spark_type_val else None

                results.append(SemanticResult(
                    spark_id=spark.id,
                    content=spark.content,
                    score=similarity,
                    domain=getattr(spark, "domain", None),
                    spark_type=spark_type_str,
                    source="local",
                    cosine_score=similarity,
                ))

        # Sort by score descending
        results.sort(key=lambda x: x.score, reverse=True)
        return results[:limit]

    def _keyword_recall(
        self,
        query: str,
        sparks: List[Any],
        limit: int,
    ) -> List[SemanticResult]:
        """Fallback keyword matching."""
        query_lower = query.lower()
        query_words = set(query_lower.split())

        results = []
        for spark in sparks:
            content_lower = spark.content.lower()

            # Simple scoring: count matching words
            content_words = set(content_lower.split())
            matching = query_words & content_words

            if matching or query_lower in content_lower:
                # Score based on word overlap
                score = len(matching) / max(len(query_words), 1)
                if query_lower in content_lower:
                    score = max(score, 0.5)  # Boost for substring match

                # Handle spark_type safely (might be None or an enum)
                spark_type_val = getattr(spark, "spark_type", None)
                if spark_type_val is not None and hasattr(spark_type_val, "value"):
                    spark_type_str = spark_type_val.value
                else:
                    spark_type_str = str(spark_type_val) if spark_type_val else None

                results.append(SemanticResult(
                    spark_id=spark.id,
                    content=spark.content,
                    score=score,
                    domain=getattr(spark, "domain", None),
                    spark_type=spark_type_str,
                    source="keyword",
                ))

        results.sort(key=lambda x: x.score, reverse=True)
        return results[:limit]

    def _cosine_similarity(self, a: List[float], b: List[float]) -> float:
        """Compute cosine similarity between two vectors."""
        if len(a) != len(b):
            return 0.0

        dot_product = sum(x * y for x, y in zip(a, b))
        norm_a = sum(x * x for x in a) ** 0.5
        norm_b = sum(x * x for x in b) ** 0.5

        if norm_a == 0 or norm_b == 0:
            return 0.0

        return dot_product / (norm_a * norm_b)

    # =========================================================================
    # Memory Store Integration
    # =========================================================================

    def _sync_to_memory_store(self, sparks: List[Any]):
        """Sync sparks to memory store for hybrid search."""
        if not self._memory_store_available:
            return

        for spark in sparks:
            # Skip if already synced
            if spark.id in self._synced_sparks:
                continue

            try:
                self._upsert_entry(
                    memory_id=spark.id,
                    content=spark.content,
                    scope="project",
                    project_key="sparknet",
                    category=getattr(spark, "spark_type", {}).value if hasattr(spark, "spark_type") else "pattern",
                    created_at=time.time(),
                    source="sparknet",
                    meta={
                        "domain": getattr(spark, "domain", None),
                        "confidence": getattr(spark, "confidence", 0.7),
                        "tags": getattr(spark, "tags", []),
                    },
                )
                self._synced_sparks.add(spark.id)
            except Exception:
                pass

        self._save_sync_state()

    # =========================================================================
    # Mind API Integration
    # =========================================================================

    def sync_to_mind(self, spark: Any, approved: bool = True) -> Tuple[bool, Optional[str]]:
        """
        Sync an approved spark to Mind API.

        Args:
            spark: The spark to sync
            approved: Whether the spark has been approved (required)

        Returns:
            Tuple of (success, memory_id or error)
        """
        if not approved:
            return (False, "Spark must be approved before syncing to Mind")

        if not self._mind_available:
            return (False, "Mind API not available")

        try:
            import requests

            # Map spark type to Mind temporal level
            temporal_level = self._spark_to_temporal_level(spark)

            # Create Mind memory
            memory_data = {
                "user_id": self.user_id,
                "content": spark.content,
                "content_type": self._spark_to_content_type(spark),
                "temporal_level": temporal_level,
                "salience": getattr(spark, "confidence", 0.7),
            }

            response = requests.post(
                f"{self.mind_url}/v1/memories/",
                json=memory_data,
                timeout=10,
            )

            if response.status_code == 200:
                data = response.json()
                memory_id = data.get("memory_id")
                return (True, memory_id)
            else:
                return (False, f"Mind API error: {response.status_code}")

        except Exception as e:
            return (False, str(e))

    def retrieve_from_mind(
        self,
        query: str,
        limit: int = 10,
    ) -> List[Dict[str, Any]]:
        """
        Retrieve relevant memories from Mind API.

        Args:
            query: Search query
            limit: Maximum results

        Returns:
            List of memory dicts from Mind
        """
        if not self._mind_available:
            return []

        try:
            import requests

            response = requests.post(
                f"{self.mind_url}/v1/memories/retrieve",
                json={"query": query, "limit": limit},
                timeout=10,
            )

            if response.status_code == 200:
                return response.json().get("memories", [])
            return []

        except Exception:
            return []

    def _spark_to_temporal_level(self, spark: Any) -> int:
        """Map spark type to Mind temporal level (1-4)."""
        spark_type = getattr(spark, "spark_type", None)
        if spark_type:
            type_value = spark_type.value if hasattr(spark_type, "value") else str(spark_type)
            mapping = {
                "eureka": 3,      # Seasonal - discoveries
                "lesson": 3,      # Seasonal - learned lessons
                "pattern": 3,     # Seasonal - recognized patterns
                "principle": 4,   # Identity - core principles
                "preference": 4,  # Identity - preferences
                "gotcha": 2,      # Situational - pitfalls
                "shortcut": 2,    # Situational - shortcuts
            }
            return mapping.get(type_value, 3)
        return 3  # Default to seasonal

    def _spark_to_content_type(self, spark: Any) -> str:
        """Map spark type to Mind content type."""
        spark_type = getattr(spark, "spark_type", None)
        if spark_type:
            type_value = spark_type.value if hasattr(spark_type, "value") else str(spark_type)
            mapping = {
                "eureka": "observation",
                "lesson": "reflection",
                "pattern": "observation",
                "principle": "principle",
                "preference": "preference",
                "gotcha": "observation",
                "shortcut": "observation",
            }
            return mapping.get(type_value, "observation")
        return "observation"

    # =========================================================================
    # Status and Stats
    # =========================================================================

    def status(self) -> BridgeStatus:
        """Get current bridge status."""
        if self._embeddings_available and self._memory_store_available:
            return BridgeStatus.READY
        elif self._embeddings_available:
            return BridgeStatus.EMBEDDINGS_ONLY
        elif self._memory_store_available:
            return BridgeStatus.MEMORY_ONLY
        elif self._embeddings_available or self._memory_store_available:
            return BridgeStatus.DEGRADED
        else:
            return BridgeStatus.OFFLINE

    def stats(self) -> BridgeStats:
        """Get bridge statistics."""
        return BridgeStats(
            embeddings_available=self._embeddings_available,
            memory_store_available=self._memory_store_available,
            mind_available=self._mind_available,
            total_embeddings=len(self._embedding_cache),
            total_synced=len(self._synced_sparks),
            model_name=os.environ.get("SPARK_EMBED_MODEL", "BAAI/bge-small-en-v1.5"),
        )

    def refresh(self):
        """Refresh component availability."""
        self._init_components()


# ============================================================================
# Module-level singleton
# ============================================================================

_bridge_instance: Optional[IntelligenceBridge] = None


def get_intelligence_bridge() -> IntelligenceBridge:
    """Get or create the global IntelligenceBridge instance."""
    global _bridge_instance
    if _bridge_instance is None:
        _bridge_instance = IntelligenceBridge()
    return _bridge_instance


def embed_spark(content: str) -> Optional[List[float]]:
    """Convenience function to embed text."""
    return get_intelligence_bridge().embed(content)


def semantic_search(
    query: str,
    sparks: List[Any],
    limit: int = 10,
) -> List[SemanticResult]:
    """Convenience function for semantic search."""
    return get_intelligence_bridge().semantic_recall(query, sparks, limit)


def sync_to_mind(spark: Any) -> Tuple[bool, Optional[str]]:
    """Convenience function to sync spark to Mind."""
    return get_intelligence_bridge().sync_to_mind(spark)
