# createsonline/ai/models.py
"""
AI-enhanced model base classes for CREATESONLINE.

Provides AIBaseModel and IntelligentMixin that add AI capabilities
(predictions, embeddings, smart defaults) to SQLAlchemy models.
"""

import time
import hashlib
from typing import Any, Callable, Dict, List, Optional


class IntelligentMixin:
    """
    Mixin that adds AI capabilities to any model.
    
    Add to SQLAlchemy models to enable:
    - Auto-computed fields (predictions, summaries)
    - Embedding generation for semantic search
    - Smart default values
    - Data validation with AI
    
    Usage::
    
        from createsonline.ai.models import IntelligentMixin
        from createsonline.db import Model
        
        class Product(Model, IntelligentMixin):
            __tablename__ = 'products'
            id = Column(Integer, primary_key=True)
            name = Column(String)
            description = Column(Text)
            
            # AI-computed fields
            _ai_fields = {
                'category': {'type': 'classification', 'source': 'description'},
                'embedding': {'type': 'embedding', 'source': 'description'},
            }
    """
    
    # Override in subclass to define AI-computed fields
    _ai_fields: Dict[str, dict] = {}
    _ai_cache: Dict[str, Any] = {}
    _ai_cache_ttl: float = 3600.0  # 1 hour
    
    def compute_ai_field(self, field_name: str, ai_service=None) -> Any:
        """Compute an AI field value."""
        config = self._ai_fields.get(field_name)
        if not config:
            raise ValueError(f"Unknown AI field: {field_name}")
        
        # Check cache
        cache_key = f"{type(self).__name__}:{id(self)}:{field_name}"
        cached = self._ai_cache.get(cache_key)
        if cached and (time.time() - cached['time']) < self._ai_cache_ttl:
            return cached['value']
        
        # Get source data
        source = config.get('source', '')
        source_value = getattr(self, source, '') if source else ''
        
        field_type = config.get('type', 'text')
        
        if ai_service:
            if field_type == 'classification':
                result = ai_service.classify(str(source_value))
            elif field_type == 'embedding':
                result = ai_service.get_embedding(str(source_value))
            elif field_type == 'summary':
                result = ai_service.summarize(str(source_value))
            elif field_type == 'sentiment':
                result = ai_service.analyze_sentiment(str(source_value))
            else:
                result = ai_service.generate_text(str(source_value))
        else:
            # Fallback: basic computed values without AI service
            if field_type == 'embedding':
                result = self._simple_embedding(str(source_value))
            elif field_type == 'sentiment':
                result = self._simple_sentiment(str(source_value))
            else:
                result = str(source_value)
        
        self._ai_cache[cache_key] = {'value': result, 'time': time.time()}
        return result
    
    def compute_all_ai_fields(self, ai_service=None) -> dict:
        """Compute all AI fields and return as dict."""
        return {
            name: self.compute_ai_field(name, ai_service)
            for name in self._ai_fields
        }
    
    def similarity_to(self, other, field: str = None) -> float:
        """
        Compute similarity between this instance and another.
        Uses embeddings if available, otherwise text similarity.
        """
        if field:
            val1 = str(getattr(self, field, ''))
            val2 = str(getattr(other, field, ''))
        else:
            val1 = str(self)
            val2 = str(other)
        
        emb1 = self._simple_embedding(val1)
        emb2 = self._simple_embedding(val2)
        
        import math
        dot = sum(a * b for a, b in zip(emb1, emb2))
        na = math.sqrt(sum(a * a for a in emb1))
        nb = math.sqrt(sum(b * b for b in emb2))
        if na == 0 or nb == 0:
            return 0.0
        return dot / (na * nb)
    
    @staticmethod
    def _simple_embedding(text: str, dim: int = 64) -> List[float]:
        """Simple hash-based embedding for fallback."""
        import math
        vec = [0.0] * dim
        words = text.lower().split()
        for word in words:
            h = int(hashlib.md5(word.encode()).hexdigest(), 16)
            idx = h % dim
            sign = 1 if (h >> dim) & 1 == 0 else -1
            vec[idx] += sign
        norm = math.sqrt(sum(x * x for x in vec))
        if norm > 0:
            vec = [x / norm for x in vec]
        return vec
    
    @staticmethod
    def _simple_sentiment(text: str) -> dict:
        """Simple rule-based sentiment analysis for fallback."""
        positive = {
            'good', 'great', 'excellent', 'amazing', 'wonderful', 'fantastic',
            'love', 'best', 'happy', 'perfect', 'awesome', 'beautiful',
            'brilliant', 'outstanding', 'superb', 'nice', 'liked', 'enjoy',
        }
        negative = {
            'bad', 'terrible', 'awful', 'horrible', 'worst', 'hate', 'poor',
            'ugly', 'broken', 'failed', 'disappointing', 'sad', 'angry',
            'annoying', 'useless', 'waste', 'boring', 'slow',
        }
        
        words = set(text.lower().split())
        pos_count = len(words & positive)
        neg_count = len(words & negative)
        total = pos_count + neg_count
        
        if total == 0:
            return {'label': 'neutral', 'score': 0.5}
        
        pos_ratio = pos_count / total
        if pos_ratio > 0.6:
            return {'label': 'positive', 'score': pos_ratio}
        elif pos_ratio < 0.4:
            return {'label': 'negative', 'score': 1 - pos_ratio}
        else:
            return {'label': 'neutral', 'score': 0.5}
    
    def get_smart_defaults(self) -> dict:
        """Get smart default values for the model based on existing data patterns."""
        defaults = {}
        for field_name, config in self._ai_fields.items():
            if 'default' in config:
                defaults[field_name] = config['default']
        return defaults
    
    def __repr_ai__(self) -> str:
        ai_info = ', '.join(f"{k}={v.get('type', '?')}" for k, v in self._ai_fields.items())
        return f"AI({ai_info})" if ai_info else "AI(none)"


class AIBaseModel(IntelligentMixin):
    """
    Base class for AI-enhanced models.
    
    Extends IntelligentMixin with:
    - Auto-registration with AIQueryProcessor
    - Change tracking for AI field recomputation
    - Batch processing support
    
    Usage::
    
        class Article(AIBaseModel):
            __tablename__ = 'articles'
            
            _ai_fields = {
                'summary': {'type': 'summary', 'source': 'content'},
                'tags': {'type': 'classification', 'source': 'content'},
            }
    """
    
    # Registry of all AI models
    _registry: Dict[str, type] = {}
    
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        # Auto-register subclasses
        name = getattr(cls, '__tablename__', cls.__name__.lower())
        AIBaseModel._registry[name] = cls
    
    @classmethod
    def get_registered_models(cls) -> Dict[str, type]:
        """Get all registered AI model classes."""
        return dict(cls._registry)
    
    @classmethod
    def batch_compute(cls, instances: list, field_name: str,
                      ai_service=None) -> List[Any]:
        """Compute an AI field for a batch of instances."""
        results = []
        for inst in instances:
            if isinstance(inst, IntelligentMixin):
                results.append(inst.compute_ai_field(field_name, ai_service))
            else:
                results.append(None)
        return results
    
    def to_ai_dict(self) -> dict:
        """Convert model to dict including AI-computed fields."""
        base = {}
        # Get regular fields
        if hasattr(self, '__table__'):
            for col in self.__table__.columns:
                base[col.name] = getattr(self, col.name, None)
        
        # Add AI fields
        for field_name in self._ai_fields:
            try:
                base[f'_ai_{field_name}'] = self.compute_ai_field(field_name)
            except Exception:
                base[f'_ai_{field_name}'] = None
        
        return base
