"""
Persistent knowledge base with TTL support.

Stores structured knowledge entries with:
- Automatic expiration (TTL)
- Tag-based organization
- Vector embeddings for semantic search
- Source tracking
"""

from __future__ import annotations

import json
import sqlite3
import threading
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional

from navig.memory.embeddings import EmbeddingProvider


def _debug_log(message: str) -> None:
    """Simple debug logging wrapper."""
    try:
        from navig.debug_logger import DebugLogger
        logger = DebugLogger()
        logger.log_operation("memory", {"message": message})
    except Exception:
        pass  # Fail silently if logging unavailable


@dataclass
class KnowledgeEntry:
    """A knowledge base entry."""

    id: str = field(default_factory=lambda: str(uuid.uuid4()))
    key: str = ""  # Unique key for deduplication
    content: str = ""
    summary: Optional[str] = None
    tags: list[str] = field(default_factory=list)
    source: str = ""  # Where this knowledge came from
    created_at: datetime = field(default_factory=datetime.utcnow)
    expires_at: Optional[datetime] = None
    metadata: dict = field(default_factory=dict)
    embedding: Optional[list[float]] = None

    def to_dict(self) -> dict:
        """Convert to dictionary for storage."""
        return {
            'id': self.id,
            'key': self.key,
            'content': self.content,
            'summary': self.summary,
            'tags': json.dumps(self.tags),
            'source': self.source,
            'created_at': self.created_at.isoformat(),
            'expires_at': self.expires_at.isoformat() if self.expires_at else None,
            'metadata': json.dumps(self.metadata),
            'embedding': json.dumps(self.embedding) if self.embedding else None,
        }

    @classmethod
    def from_dict(cls, data: dict) -> 'KnowledgeEntry':
        """Create from dictionary."""
        return cls(
            id=data['id'],
            key=data['key'],
            content=data['content'],
            summary=data.get('summary'),
            tags=json.loads(data['tags']) if isinstance(data['tags'], str) else data['tags'],
            source=data.get('source', ''),
            created_at=datetime.fromisoformat(data['created_at']),
            expires_at=datetime.fromisoformat(data['expires_at']) if data.get('expires_at') else None,
            metadata=json.loads(data['metadata']) if isinstance(data['metadata'], str) else data.get('metadata', {}),
            embedding=json.loads(data['embedding']) if data.get('embedding') else None,
        )

    @property
    def is_expired(self) -> bool:
        """Check if entry has expired."""
        if self.expires_at is None:
            return False
        return datetime.utcnow() > self.expires_at


class KnowledgeBase:
    """
    Persistent knowledge store with vector search.
    
    Stores structured knowledge with:
    - Unique keys for upsert operations
    - TTL-based expiration
    - Tag-based filtering
    - Optional vector embeddings for semantic search
    
    Usage:
        kb = KnowledgeBase(
            db_path=Path.home() / '.navig' / 'knowledge.db',
            embedding_provider=LocalEmbeddingProvider(),
        )
        
        # Add knowledge
        entry = KnowledgeEntry(
            key='project-info',
            content='This project uses Python 3.10+',
            tags=['project', 'python'],
            source='user',
        )
        kb.upsert(entry)
        
        # Search
        results = kb.search('python version', limit=5)
    """

    SCHEMA_VERSION = 1

    def __init__(
        self,
        db_path: Path,
        embedding_provider: Optional[EmbeddingProvider] = None,
        auto_expire: bool = True,
    ):
        self.db_path = db_path
        self.embedding_provider = embedding_provider
        self.auto_expire = auto_expire
        self._local = threading.local()
        self._lock = threading.Lock()

        # Ensure directory exists
        db_path.parent.mkdir(parents=True, exist_ok=True)

        # Initialize schema
        self._init_schema()

        # Clean expired entries on startup
        if auto_expire:
            self._clean_expired()

    def _get_conn(self) -> sqlite3.Connection:
        """Get thread-local connection."""
        if not hasattr(self._local, 'conn') or self._local.conn is None:
            self._local.conn = sqlite3.connect(
                str(self.db_path),
                check_same_thread=False,
            )
            self._local.conn.row_factory = sqlite3.Row
            self._local.conn.execute('PRAGMA journal_mode=WAL')
        return self._local.conn

    def _init_schema(self) -> None:
        """Initialize database schema."""
        conn = self._get_conn()

        conn.executescript('''
            CREATE TABLE IF NOT EXISTS knowledge (
                id TEXT PRIMARY KEY,
                key TEXT UNIQUE NOT NULL,
                content TEXT NOT NULL,
                summary TEXT,
                tags TEXT DEFAULT '[]',
                source TEXT DEFAULT '',
                created_at TEXT NOT NULL,
                expires_at TEXT,
                metadata TEXT DEFAULT '{}',
                embedding TEXT
            );
            
            CREATE INDEX IF NOT EXISTS idx_knowledge_key 
                ON knowledge(key);
            CREATE INDEX IF NOT EXISTS idx_knowledge_source 
                ON knowledge(source);
            CREATE INDEX IF NOT EXISTS idx_knowledge_expires 
                ON knowledge(expires_at);
        ''')

        conn.commit()
        _debug_log(f"KnowledgeBase initialized at {self.db_path}")

    def _clean_expired(self) -> int:
        """Remove expired entries."""
        conn = self._get_conn()
        now = datetime.utcnow().isoformat()

        with self._lock:
            cursor = conn.execute(
                'DELETE FROM knowledge WHERE expires_at IS NOT NULL AND expires_at < ?',
                (now,)
            )
            conn.commit()

        deleted = cursor.rowcount
        if deleted > 0:
            _debug_log(f"Cleaned {deleted} expired knowledge entries")
        return deleted

    def upsert(
        self,
        entry: KnowledgeEntry,
        compute_embedding: bool = True,
    ) -> KnowledgeEntry:
        """
        Insert or update a knowledge entry.
        
        If an entry with the same key exists, it will be updated.
        
        Args:
            entry: The knowledge entry
            compute_embedding: Whether to compute embedding vector
            
        Returns:
            The stored entry
        """
        # Compute embedding if provider available
        if compute_embedding and self.embedding_provider and not entry.embedding:
            text = entry.summary or entry.content
            entry.embedding = self.embedding_provider.embed_text(text)

        conn = self._get_conn()
        data = entry.to_dict()

        with self._lock:
            conn.execute('''
                INSERT INTO knowledge (
                    id, key, content, summary, tags, source,
                    created_at, expires_at, metadata, embedding
                ) VALUES (
                    :id, :key, :content, :summary, :tags, :source,
                    :created_at, :expires_at, :metadata, :embedding
                )
                ON CONFLICT(key) DO UPDATE SET
                    content = excluded.content,
                    summary = excluded.summary,
                    tags = excluded.tags,
                    source = excluded.source,
                    expires_at = excluded.expires_at,
                    metadata = excluded.metadata,
                    embedding = excluded.embedding
            ''', data)
            conn.commit()

        _debug_log(f"Upserted knowledge entry: {entry.key}")
        return entry

    def get(self, key: str) -> Optional[KnowledgeEntry]:
        """Get entry by key."""
        conn = self._get_conn()

        cursor = conn.execute(
            'SELECT * FROM knowledge WHERE key = ?',
            (key,)
        )
        row = cursor.fetchone()

        if not row:
            return None

        entry = KnowledgeEntry.from_dict(dict(row))

        # Check expiration
        if entry.is_expired:
            self.delete(key)
            return None

        return entry

    def get_by_id(self, id: str) -> Optional[KnowledgeEntry]:
        """Get entry by ID."""
        conn = self._get_conn()

        cursor = conn.execute(
            'SELECT * FROM knowledge WHERE id = ?',
            (id,)
        )
        row = cursor.fetchone()

        if not row:
            return None

        entry = KnowledgeEntry.from_dict(dict(row))

        if entry.is_expired:
            return None

        return entry

    def delete(self, key: str) -> bool:
        """Delete entry by key."""
        conn = self._get_conn()

        with self._lock:
            cursor = conn.execute(
                'DELETE FROM knowledge WHERE key = ?',
                (key,)
            )
            conn.commit()

        return cursor.rowcount > 0

    def list_by_tag(
        self,
        tag: str,
        limit: int = 50,
    ) -> list[KnowledgeEntry]:
        """Get all entries with a specific tag."""
        conn = self._get_conn()
        now = datetime.utcnow().isoformat()

        # JSON contains check
        cursor = conn.execute('''
            SELECT * FROM knowledge 
            WHERE tags LIKE ? 
            AND (expires_at IS NULL OR expires_at > ?)
            ORDER BY created_at DESC
            LIMIT ?
        ''', (f'%"{tag}"%', now, limit))

        return [
            KnowledgeEntry.from_dict(dict(row))
            for row in cursor.fetchall()
        ]

    def list_by_source(
        self,
        source: str,
        limit: int = 50,
    ) -> list[KnowledgeEntry]:
        """Get all entries from a specific source."""
        conn = self._get_conn()
        now = datetime.utcnow().isoformat()

        cursor = conn.execute('''
            SELECT * FROM knowledge 
            WHERE source = ?
            AND (expires_at IS NULL OR expires_at > ?)
            ORDER BY created_at DESC
            LIMIT ?
        ''', (source, now, limit))

        return [
            KnowledgeEntry.from_dict(dict(row))
            for row in cursor.fetchall()
        ]

    def search(
        self,
        query: str,
        limit: int = 10,
        min_similarity: float = 0.5,
        tags: Optional[list[str]] = None,
    ) -> list[tuple[KnowledgeEntry, float]]:
        """
        Semantic search across knowledge base.
        
        Args:
            query: Search query
            limit: Maximum results
            min_similarity: Minimum cosine similarity threshold
            tags: Optional tag filter
            
        Returns:
            List of (entry, similarity) tuples sorted by relevance
        """
        if not self.embedding_provider:
            # Fall back to text search
            return [(entry, 1.0) for entry in self.text_search(query, limit, tags)]

        # Get query embedding
        query_embedding = self.embedding_provider.embed_text(query)

        # Get all entries (with optional tag filter)
        conn = self._get_conn()
        now = datetime.utcnow().isoformat()

        if tags:
            tag_conditions = ' AND '.join(
                f"tags LIKE '%\"{tag}\"%'" for tag in tags
            )
            cursor = conn.execute(f'''
                SELECT * FROM knowledge 
                WHERE embedding IS NOT NULL
                AND (expires_at IS NULL OR expires_at > ?)
                AND {tag_conditions}
            ''', (now,))
        else:
            cursor = conn.execute('''
                SELECT * FROM knowledge 
                WHERE embedding IS NOT NULL
                AND (expires_at IS NULL OR expires_at > ?)
            ''', (now,))

        # Compute similarities
        results = []
        for row in cursor.fetchall():
            entry = KnowledgeEntry.from_dict(dict(row))
            if entry.embedding:
                similarity = self.embedding_provider.similarity(
                    query_embedding,
                    entry.embedding,
                )
                if similarity >= min_similarity:
                    results.append((entry, similarity))

        # Sort by similarity
        results.sort(key=lambda x: x[1], reverse=True)

        return results[:limit]

    def text_search(
        self,
        query: str,
        limit: int = 10,
        tags: Optional[list[str]] = None,
    ) -> list[KnowledgeEntry]:
        """
        Full-text search (fallback when no embeddings).
        
        Args:
            query: Search query
            limit: Maximum results
            tags: Optional tag filter
            
        Returns:
            Matching entries
        """
        conn = self._get_conn()
        now = datetime.utcnow().isoformat()
        pattern = f'%{query}%'

        sql = '''
            SELECT * FROM knowledge 
            WHERE (content LIKE ? OR summary LIKE ?)
            AND (expires_at IS NULL OR expires_at > ?)
        '''
        params = [pattern, pattern, now]

        if tags:
            for tag in tags:
                sql += " AND tags LIKE ?"
                params.append(f'%"{tag}"%')

        sql += ' ORDER BY created_at DESC LIMIT ?'
        params.append(limit)

        cursor = conn.execute(sql, params)

        return [
            KnowledgeEntry.from_dict(dict(row))
            for row in cursor.fetchall()
        ]

    def add_with_ttl(
        self,
        key: str,
        content: str,
        ttl_hours: int = 24,
        **kwargs,
    ) -> KnowledgeEntry:
        """
        Add knowledge with automatic expiration.
        
        Args:
            key: Unique key
            content: Knowledge content
            ttl_hours: Hours until expiration
            **kwargs: Additional KnowledgeEntry fields
            
        Returns:
            Stored entry
        """
        expires_at = datetime.utcnow() + timedelta(hours=ttl_hours)

        entry = KnowledgeEntry(
            key=key,
            content=content,
            expires_at=expires_at,
            **kwargs,
        )

        return self.upsert(entry)

    def count(self) -> int:
        """Get total number of non-expired entries."""
        conn = self._get_conn()
        now = datetime.utcnow().isoformat()

        cursor = conn.execute(
            'SELECT COUNT(*) FROM knowledge WHERE expires_at IS NULL OR expires_at > ?',
            (now,)
        )
        return cursor.fetchone()[0]

    def clear(self) -> int:
        """Delete all entries."""
        conn = self._get_conn()

        with self._lock:
            cursor = conn.execute('DELETE FROM knowledge')
            conn.commit()

        deleted = cursor.rowcount
        _debug_log(f"Cleared knowledge base ({deleted} entries)")
        return deleted

    def export_entries(self) -> list[dict]:
        """Export all entries as dictionaries."""
        conn = self._get_conn()
        now = datetime.utcnow().isoformat()

        cursor = conn.execute(
            'SELECT * FROM knowledge WHERE expires_at IS NULL OR expires_at > ?',
            (now,)
        )

        return [dict(row) for row in cursor.fetchall()]

    def import_entries(
        self,
        entries: list[dict],
        overwrite: bool = False,
    ) -> int:
        """
        Import entries from dictionaries.
        
        Args:
            entries: List of entry dictionaries
            overwrite: Whether to overwrite existing keys
            
        Returns:
            Number of entries imported
        """
        imported = 0

        for data in entries:
            try:
                entry = KnowledgeEntry.from_dict(data)

                if not overwrite and self.get(entry.key):
                    continue

                self.upsert(entry, compute_embedding=False)
                imported += 1

            except Exception as e:
                _debug_log(f"Failed to import entry: {e}")

        return imported

    def close(self) -> None:
        """Close database connection."""
        if hasattr(self._local, 'conn') and self._local.conn:
            self._local.conn.close()
            self._local.conn = None
