"""
Memory Ingestion Pipeline
Автоматичне витягування фактів/пам'яті з діалогів

Етапи:
1. PII Scrubber - виявлення та редакція персональних даних
2. Memory Candidate Extractor - класифікація та витягування
3. Dedup & Merge - дедуплікація схожих пам'ятей
4. Write - збереження в SQL + Vector + Graph
5. Audit Log - запис в аудит
"""

import re
import hashlib
from typing import List, Dict, Any, Optional, Tuple
from datetime import datetime, timedelta
from uuid import UUID, uuid4
from enum import Enum
import structlog
from pydantic import BaseModel

logger = structlog.get_logger()


class MemoryType(str, Enum):
    EPISODIC = "episodic"      # Події/факти про взаємодію
    SEMANTIC = "semantic"      # Стійкі вподобання/профіль
    PROCEDURAL = "procedural"  # Як робити щось


class MemoryCategory(str, Enum):
    PREFERENCE = "preference"           # Вподобання користувача
    FACT = "fact"                       # Факт про користувача
    TOPIC_INTEREST = "topic_interest"   # Інтерес до теми
    ROLE = "role"                       # Роль (інвестор, інженер)
    INTERACTION = "interaction"         # Тип взаємодії
    FEEDBACK = "feedback"               # Відгук/оцінка
    OPT_OUT = "opt_out"                 # Заборона збереження


class PIIType(str, Enum):
    PHONE = "phone"
    EMAIL = "email"
    ADDRESS = "address"
    PASSPORT = "passport"
    CARD_NUMBER = "card_number"
    NAME = "name"
    LOCATION = "location"


class MemoryCandidate(BaseModel):
    """Кандидат на збереження в пам'ять"""
    content: str
    summary: str
    memory_type: MemoryType
    category: MemoryCategory
    importance: float  # 0.0 - 1.0
    confidence: float  # 0.0 - 1.0
    ttl_days: Optional[int] = None
    source_message_ids: List[str] = []
    metadata: Dict[str, Any] = {}


class PIIDetection(BaseModel):
    """Результат виявлення PII"""
    pii_type: PIIType
    start: int
    end: int
    original: str
    redacted: str


# =============================================================================
# 1. PII SCRUBBER
# =============================================================================

class PIIScrubber:
    """Виявлення та редакція персональних даних"""
    
    # Регулярні вирази для PII
    PATTERNS = {
        PIIType.PHONE: [
            r'\+?38?\s?0?\d{2}[\s\-]?\d{3}[\s\-]?\d{2}[\s\-]?\d{2}',  # UA phones
            r'\+?\d{1,3}[\s\-]?\(?\d{2,3}\)?[\s\-]?\d{3}[\s\-]?\d{2}[\s\-]?\d{2}',
        ],
        PIIType.EMAIL: [
            r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
        ],
        PIIType.CARD_NUMBER: [
            r'\b\d{4}[\s\-]?\d{4}[\s\-]?\d{4}[\s\-]?\d{4}\b',
        ],
        PIIType.PASSPORT: [
            r'\b[A-Z]{2}\d{6}\b',  # UA passport
        ],
    }
    
    def detect(self, text: str) -> List[PIIDetection]:
        """Виявити всі PII в тексті"""
        detections = []
        
        for pii_type, patterns in self.PATTERNS.items():
            for pattern in patterns:
                for match in re.finditer(pattern, text, re.IGNORECASE):
                    detections.append(PIIDetection(
                        pii_type=pii_type,
                        start=match.start(),
                        end=match.end(),
                        original=match.group(),
                        redacted=self._redact(pii_type, match.group())
                    ))
        
        return detections
    
    def _redact(self, pii_type: PIIType, value: str) -> str:
        """Редагувати PII значення"""
        if pii_type == PIIType.EMAIL:
            parts = value.split('@')
            return f"{parts[0][:2]}***@{parts[1]}" if len(parts) == 2 else "[EMAIL]"
        elif pii_type == PIIType.PHONE:
            return f"***{value[-4:]}" if len(value) > 4 else "[PHONE]"
        elif pii_type == PIIType.CARD_NUMBER:
            return f"****{value[-4:]}"
        else:
            return f"[{pii_type.value.upper()}]"
    
    def scrub(self, text: str) -> Tuple[str, List[PIIDetection], bool]:
        """
        Очистити текст від PII
        Returns: (cleaned_text, detections, has_pii)
        """
        detections = self.detect(text)
        
        if not detections:
            return text, [], False
        
        # Сортувати за позицією (з кінця) для правильної заміни
        detections.sort(key=lambda x: x.start, reverse=True)
        
        cleaned = text
        for detection in detections:
            cleaned = cleaned[:detection.start] + detection.redacted + cleaned[detection.end:]
        
        return cleaned, detections, True


# =============================================================================
# 2. MEMORY CANDIDATE EXTRACTOR
# =============================================================================

class MemoryExtractor:
    """Витягування кандидатів на пам'ять з повідомлень"""
    
    # Ключові фрази для категорій
    CATEGORY_PATTERNS = {
        MemoryCategory.PREFERENCE: [
            r'я (хочу|бажаю|віддаю перевагу|люблю|не люблю)',
            r'мені (подобається|не подобається|зручніше)',
            r'(краще|гірше) для мене',
        ],
        MemoryCategory.ROLE: [
            r'я (інвестор|інженер|розробник|науковець|журналіст|модератор)',
            r'працюю (як|в галузі)',
            r'моя (роль|посада|професія)',
        ],
        MemoryCategory.TOPIC_INTEREST: [
            r'цікавить (мене )?(BioMiner|EcoMiner|токеноміка|governance|стейкінг)',
            r'хочу (дізнатися|розібратися) (в|з)',
            r'питання (про|щодо|стосовно)',
        ],
        MemoryCategory.OPT_OUT: [
            r'(не |НЕ )?(запам[\'ʼ]ятов|запамʼятовуй|запамятовуй)',
            r'забудь (мене|це|все)',
            r'вимкни (пам[\'ʼ]ять|память)',
        ],
    }
    
    # Важливість за категорією
    IMPORTANCE_WEIGHTS = {
        MemoryCategory.PREFERENCE: 0.7,
        MemoryCategory.ROLE: 0.8,
        MemoryCategory.TOPIC_INTEREST: 0.6,
        MemoryCategory.FACT: 0.5,
        MemoryCategory.OPT_OUT: 1.0,  # Найвища важливість
    }
    
    def extract(
        self, 
        messages: List[Dict[str, Any]],
        context: Optional[Dict[str, Any]] = None
    ) -> List[MemoryCandidate]:
        """
        Витягнути кандидатів на пам'ять з повідомлень
        
        Args:
            messages: Список повідомлень [{role, content, message_id, ...}]
            context: Додатковий контекст (group_id, user_id, etc.)
        
        Returns:
            Список MemoryCandidate
        """
        candidates = []
        
        for msg in messages:
            if msg.get('role') != 'user':
                continue
            
            content = msg.get('content', '')
            message_id = msg.get('message_id', str(uuid4()))
            
            # Перевірити opt-out фрази
            opt_out = self._check_opt_out(content)
            if opt_out:
                candidates.append(opt_out)
                candidates[-1].source_message_ids = [message_id]
                continue
            
            # Шукати інші категорії
            for category, patterns in self.CATEGORY_PATTERNS.items():
                if category == MemoryCategory.OPT_OUT:
                    continue
                    
                for pattern in patterns:
                    if re.search(pattern, content, re.IGNORECASE):
                        candidate = self._create_candidate(
                            content=content,
                            category=category,
                            message_id=message_id,
                            context=context
                        )
                        if candidate:
                            candidates.append(candidate)
                        break
        
        return candidates
    
    def _check_opt_out(self, content: str) -> Optional[MemoryCandidate]:
        """Перевірити на opt-out фразу"""
        for pattern in self.CATEGORY_PATTERNS[MemoryCategory.OPT_OUT]:
            match = re.search(pattern, content, re.IGNORECASE)
            if match:
                # Визначити тип opt-out
                if 'забудь' in content.lower():
                    action = 'forget'
                    summary = "Користувач просить видалити пам'ять"
                else:
                    action = 'disable'
                    summary = "Користувач просить не запам'ятовувати"
                
                return MemoryCandidate(
                    content=content,
                    summary=summary,
                    memory_type=MemoryType.SEMANTIC,
                    category=MemoryCategory.OPT_OUT,
                    importance=1.0,
                    confidence=0.95,
                    metadata={'action': action}
                )
        return None
    
    def _create_candidate(
        self,
        content: str,
        category: MemoryCategory,
        message_id: str,
        context: Optional[Dict[str, Any]] = None
    ) -> Optional[MemoryCandidate]:
        """Створити кандидата на пам'ять"""
        
        # Визначити тип пам'яті
        if category in [MemoryCategory.PREFERENCE, MemoryCategory.ROLE]:
            memory_type = MemoryType.SEMANTIC
            ttl_days = None  # Безстроково
        else:
            memory_type = MemoryType.EPISODIC
            ttl_days = 90  # 3 місяці
        
        # Створити короткий summary
        summary = self._generate_summary(content, category)
        
        return MemoryCandidate(
            content=content,
            summary=summary,
            memory_type=memory_type,
            category=category,
            importance=self.IMPORTANCE_WEIGHTS.get(category, 0.5),
            confidence=0.7,  # Базова впевненість, можна підвищити через LLM
            ttl_days=ttl_days,
            source_message_ids=[message_id],
            metadata=context or {}
        )
    
    def _generate_summary(self, content: str, category: MemoryCategory) -> str:
        """Згенерувати короткий summary"""
        # Простий варіант - перші 100 символів
        # В production використовувати LLM
        summary = content[:100]
        if len(content) > 100:
            summary += "..."
        return f"[{category.value}] {summary}"


# =============================================================================
# 3. DEDUP & MERGE
# =============================================================================

class MemoryDeduplicator:
    """Дедуплікація та об'єднання схожих пам'ятей"""
    
    def __init__(self, similarity_threshold: float = 0.85):
        self.similarity_threshold = similarity_threshold
    
    def deduplicate(
        self,
        new_candidates: List[MemoryCandidate],
        existing_memories: List[Dict[str, Any]]
    ) -> Tuple[List[MemoryCandidate], List[Dict[str, Any]]]:
        """
        Дедуплікувати нових кандидатів проти існуючих пам'ятей
        
        Returns:
            (candidates_to_create, memories_to_update)
        """
        to_create = []
        to_update = []
        
        for candidate in new_candidates:
            # Шукати схожу пам'ять
            similar = self._find_similar(candidate, existing_memories)
            
            if similar:
                # Оновити існуючу пам'ять
                to_update.append({
                    'memory_id': similar['memory_id'],
                    'content': candidate.content,
                    'summary': candidate.summary,
                    'importance': max(candidate.importance, similar.get('importance', 0)),
                    'source_message_ids': list(set(
                        similar.get('source_message_ids', []) + 
                        candidate.source_message_ids
                    ))
                })
            else:
                to_create.append(candidate)
        
        return to_create, to_update
    
    def _find_similar(
        self,
        candidate: MemoryCandidate,
        existing: List[Dict[str, Any]]
    ) -> Optional[Dict[str, Any]]:
        """Знайти схожу пам'ять"""
        candidate_hash = self._content_hash(candidate.content)
        
        for memory in existing:
            # Швидка перевірка за хешем
            if self._content_hash(memory.get('content', '')) == candidate_hash:
                return memory
            
            # Перевірка за категорією + summary
            if (memory.get('category') == candidate.category.value and
                self._text_similarity(candidate.summary, memory.get('summary', '')) > self.similarity_threshold):
                return memory
        
        return None
    
    def _content_hash(self, content: str) -> str:
        """Обчислити хеш контенту"""
        normalized = content.lower().strip()
        return hashlib.md5(normalized.encode()).hexdigest()
    
    def _text_similarity(self, text1: str, text2: str) -> float:
        """Проста подібність тексту (Jaccard)"""
        if not text1 or not text2:
            return 0.0
        
        words1 = set(text1.lower().split())
        words2 = set(text2.lower().split())
        
        intersection = len(words1 & words2)
        union = len(words1 | words2)
        
        return intersection / union if union > 0 else 0.0


# =============================================================================
# 4. MEMORY INGESTION PIPELINE
# =============================================================================

class MemoryIngestionPipeline:
    """
    Повний пайплайн витягування та збереження пам'яті
    """
    
    def __init__(self, db=None, vector_store=None, graph_store=None):
        self.db = db
        self.vector_store = vector_store
        self.graph_store = graph_store
        
        self.pii_scrubber = PIIScrubber()
        self.extractor = MemoryExtractor()
        self.deduplicator = MemoryDeduplicator()
    
    async def process_conversation(
        self,
        messages: List[Dict[str, Any]],
        user_id: Optional[str] = None,
        platform_user_id: Optional[str] = None,
        group_id: Optional[str] = None,
        conversation_id: Optional[str] = None
    ) -> Dict[str, Any]:
        """
        Обробити розмову та витягнути пам'ять
        
        Returns:
            {
                "memories_created": int,
                "memories_updated": int,
                "pii_detected": bool,
                "opt_out_requested": bool,
                "details": [...]
            }
        """
        result = {
            "memories_created": 0,
            "memories_updated": 0,
            "pii_detected": False,
            "opt_out_requested": False,
            "details": []
        }
        
        # 1. PII Scrubbing
        cleaned_messages = []
        for msg in messages:
            if msg.get('role') == 'user':
                cleaned, detections, has_pii = self.pii_scrubber.scrub(msg.get('content', ''))
                if has_pii:
                    result["pii_detected"] = True
                    logger.info("pii_detected", 
                               count=len(detections),
                               types=[d.pii_type.value for d in detections])
                cleaned_messages.append({**msg, 'content': cleaned, '_pii_detected': has_pii})
            else:
                cleaned_messages.append(msg)
        
        # 2. Extract candidates
        context = {
            'user_id': user_id,
            'platform_user_id': platform_user_id,
            'group_id': group_id,
            'conversation_id': conversation_id
        }
        candidates = self.extractor.extract(cleaned_messages, context)
        
        # Перевірити opt-out
        for candidate in candidates:
            if candidate.category == MemoryCategory.OPT_OUT:
                result["opt_out_requested"] = True
                await self._handle_opt_out(candidate, context)
                result["details"].append({
                    "type": "opt_out",
                    "action": candidate.metadata.get('action'),
                    "summary": candidate.summary
                })
        
        # Якщо opt-out — не зберігати інші пам'яті
        if result["opt_out_requested"]:
            return result
        
        # 3. Dedup against existing
        existing_memories = []
        if self.db:
            existing_memories = await self._get_existing_memories(
                user_id=user_id,
                platform_user_id=platform_user_id,
                group_id=group_id
            )
        
        to_create, to_update = self.deduplicator.deduplicate(candidates, existing_memories)
        
        # 4. Write to storage
        for candidate in to_create:
            memory_id = await self._create_memory(candidate, context)
            if memory_id:
                result["memories_created"] += 1
                result["details"].append({
                    "type": "created",
                    "memory_id": str(memory_id),
                    "category": candidate.category.value,
                    "summary": candidate.summary
                })
        
        for update in to_update:
            success = await self._update_memory(update)
            if success:
                result["memories_updated"] += 1
                result["details"].append({
                    "type": "updated",
                    "memory_id": update['memory_id'],
                    "summary": update.get('summary')
                })
        
        # 5. Audit log
        await self._log_ingestion(result, context)
        
        logger.info("ingestion_complete",
                   created=result["memories_created"],
                   updated=result["memories_updated"],
                   pii=result["pii_detected"],
                   opt_out=result["opt_out_requested"])
        
        return result
    
    async def _handle_opt_out(
        self,
        candidate: MemoryCandidate,
        context: Dict[str, Any]
    ):
        """Обробити opt-out запит"""
        action = candidate.metadata.get('action', 'disable')
        group_id = context.get('group_id')
        platform_user_id = context.get('platform_user_id')
        
        if not platform_user_id:
            return
        
        if self.db:
            if action == 'forget' and group_id:
                # Повне видалення в групі
                await self.db.execute(
                    "SELECT memory_forget_in_group($1::uuid, $2)",
                    group_id, platform_user_id
                )
            else:
                # Просто відключити збереження
                if group_id:
                    await self.db.execute("""
                        UPDATE group_members 
                        SET no_memory_in_group = TRUE 
                        WHERE group_id = $1::uuid AND platform_user_id = $2
                    """, group_id, platform_user_id)
                else:
                    await self.db.execute("""
                        UPDATE memory_consent 
                        SET memory_enabled = FALSE, updated_at = NOW()
                        WHERE platform_user_id = $1
                    """, platform_user_id)
    
    async def _get_existing_memories(
        self,
        user_id: Optional[str],
        platform_user_id: Optional[str],
        group_id: Optional[str]
    ) -> List[Dict[str, Any]]:
        """Отримати існуючі пам'яті"""
        if not self.db:
            return []
        
        query = """
            SELECT memory_id, content, summary, category, importance, source_message_ids
            FROM memories
            WHERE is_active = TRUE
        """
        params = []
        
        if group_id:
            query += " AND group_id = $1::uuid"
            params.append(group_id)
            if platform_user_id:
                query += " AND platform_user_id = $2"
                params.append(platform_user_id)
        elif user_id:
            query += " AND user_id = $1::uuid AND group_id IS NULL"
            params.append(user_id)
        elif platform_user_id:
            query += " AND platform_user_id = $1 AND group_id IS NULL"
            params.append(platform_user_id)
        else:
            return []
        
        rows = await self.db.fetch(query, *params)
        return [dict(row) for row in rows]
    
    async def _create_memory(
        self,
        candidate: MemoryCandidate,
        context: Dict[str, Any]
    ) -> Optional[UUID]:
        """Створити нову пам'ять"""
        if not self.db:
            return uuid4()  # Mock для тестування
        
        memory_id = uuid4()
        
        # Calculate expires_at
        expires_at = None
        if candidate.ttl_days:
            expires_at = datetime.now() + timedelta(days=candidate.ttl_days)
        
        await self.db.execute("""
            INSERT INTO memories (
                memory_id, user_id, platform_user_id, group_id,
                memory_type, category, content, summary,
                importance, confidence, ttl_days, expires_at,
                source_message_ids, extraction_method, metadata
            ) VALUES (
                $1, $2::uuid, $3, $4::uuid,
                $5, $6, $7, $8,
                $9, $10, $11, $12,
                $13, $14, $15
            )
        """,
            memory_id,
            context.get('user_id'),
            context.get('platform_user_id'),
            context.get('group_id'),
            candidate.memory_type.value,
            candidate.category.value,
            candidate.content,
            candidate.summary,
            candidate.importance,
            candidate.confidence,
            candidate.ttl_days,
            expires_at,
            candidate.source_message_ids,
            'pipeline',
            candidate.metadata
        )
        
        # Зберегти embedding якщо є vector store
        if self.vector_store:
            await self._store_embedding(memory_id, candidate, context)
        
        # Зберегти в граф якщо є graph store
        if self.graph_store:
            await self._store_graph_relation(memory_id, candidate, context)
        
        return memory_id
    
    async def _update_memory(self, update: Dict[str, Any]) -> bool:
        """Оновити існуючу пам'ять"""
        if not self.db:
            return True
        
        await self.db.execute("""
            UPDATE memories
            SET content = $2, summary = $3, importance = $4,
                source_message_ids = $5, updated_at = NOW()
            WHERE memory_id = $1::uuid
        """,
            update['memory_id'],
            update['content'],
            update['summary'],
            update['importance'],
            update['source_message_ids']
        )
        return True
    
    async def _store_embedding(
        self,
        memory_id: UUID,
        candidate: MemoryCandidate,
        context: Dict[str, Any]
    ):
        """Зберегти embedding в vector store"""
        # Реалізація залежить від vector store (Qdrant, pgvector)
        pass
    
    async def _store_graph_relation(
        self,
        memory_id: UUID,
        candidate: MemoryCandidate,
        context: Dict[str, Any]
    ):
        """Зберегти зв'язок в graph store"""
        # Реалізація для Neo4j
        pass
    
    async def _log_ingestion(
        self,
        result: Dict[str, Any],
        context: Dict[str, Any]
    ):
        """Записати в аудит"""
        if not self.db:
            return
        
        await self.db.execute("""
            INSERT INTO memory_events (
                user_id, group_id, action, actor, new_value
            ) VALUES (
                $1::uuid, $2::uuid, 'ingestion', 'pipeline', $3
            )
        """,
            context.get('user_id'),
            context.get('group_id'),
            result
        )
