"""
LLM Orchestrator for Cost-Optimized Blog Analysis Pipeline

Manages the flow between Sonnet classification and Opus synthesis,
with cost controls, fallback mechanisms, and progress tracking.
"""

import os
import asyncio
import logging
import re
from typing import Dict, List, Optional, Any, Callable, Tuple
from dataclasses import dataclass, asdict
from pathlib import Path
from datetime import datetime
import json

from .sonnet_classifier import SonnetContentClassifier, ContentClassification
from .opus_synthesizer import OpusStrategicSynthesizer, StrategicAnalysis
from ..blog_topic_analyzer import BlogTopicAnalyzer
from ..content_gap_analyzer import ContentGapAnalyzer

logger = logging.getLogger(__name__)

@dataclass
class PipelineConfig:
    """Configuration for LLM pipeline"""
    max_budget: float = 10.0  # Maximum cost per analysis
    sonnet_budget_ratio: float = 0.3  # 30% of budget for Sonnet
    opus_budget_ratio: float = 0.7  # 70% of budget for Opus
    
    use_traditional_fallback: bool = True  # Fall back to keyword analysis if needed
    parallel_batch_size: int = 5  # Number of parallel Sonnet batches
    
    min_engagement_for_llm: float = 2.0  # Minimum engagement rate for LLM processing
    max_items_per_source: int = 200  # Limit items per source for cost control
    
    enable_caching: bool = True  # Cache classifications to avoid reprocessing
    cache_dir: Path = Path("cache/llm_classifications")

@dataclass
class PipelineResult:
    """Result of complete LLM pipeline"""
    strategic_analysis: Optional[StrategicAnalysis]
    classified_content: Dict[str, Any]
    traditional_analysis: Dict[str, Any]
    
    pipeline_metrics: Dict[str, Any]
    cost_breakdown: Dict[str, float]
    processing_time: float
    
    success: bool
    errors: List[str]

class LLMOrchestrator:
    """
    Orchestrates the LLM-enhanced blog analysis pipeline
    with cost optimization and fallback mechanisms
    """
    
    def __init__(self, config: Optional[PipelineConfig] = None, dry_run: bool = False):
        """Initialize orchestrator with configuration"""
        self.config = config or PipelineConfig()
        self.dry_run = dry_run
        
        # Initialize components
        self.sonnet_classifier = SonnetContentClassifier(dry_run=dry_run)
        self.opus_synthesizer = OpusStrategicSynthesizer() if not dry_run else None
        self.traditional_analyzer = BlogTopicAnalyzer(Path("data/competitive_intelligence"))
        
        # Cost tracking
        self.total_cost = 0.0
        self.sonnet_cost = 0.0
        self.opus_cost = 0.0
        
        # Cache setup
        if self.config.enable_caching:
            self.config.cache_dir.mkdir(parents=True, exist_ok=True)
            
    async def run_analysis_pipeline(self,
                                   competitive_data_dir: Path,
                                   hkia_blog_dir: Path,
                                   progress_callback: Optional[Callable] = None) -> PipelineResult:
        """
        Run complete LLM-enhanced analysis pipeline
        
        Args:
            competitive_data_dir: Directory with competitive intelligence data
            hkia_blog_dir: Directory with existing HKIA blog content
            progress_callback: Optional callback for progress updates
            
        Returns:
            PipelineResult with complete analysis
        """
        start_time = datetime.now()
        errors = []
        
        try:
            # Step 1: Load and filter content
            if progress_callback:
                progress_callback("Loading competitive content...")
            content_items = self._load_competitive_content(competitive_data_dir)
            
            # Step 2: Determine processing tier for each item
            if progress_callback:
                progress_callback(f"Filtering {len(content_items)} items for processing...")
            tiered_content = self._tier_content_for_processing(content_items)
            
            # Step 3: Run traditional analysis (always, for comparison)
            if progress_callback:
                progress_callback("Running traditional keyword analysis...")
            traditional_analysis = self._run_traditional_analysis(competitive_data_dir)
            
            # Step 4: Check budget and determine LLM processing scope
            llm_items = tiered_content['full_analysis'] + tiered_content['classification']
            if not self._check_budget_feasibility(llm_items):
                if progress_callback:
                    progress_callback("Budget exceeded - reducing scope...")
                llm_items = self._reduce_scope_for_budget(llm_items)
            
            # Step 5: Run Sonnet classification
            if progress_callback:
                progress_callback(f"Classifying {len(llm_items)} items with Sonnet...")
            classified_content = await self._run_sonnet_classification(llm_items, progress_callback)
            
            # Check if Sonnet succeeded and we have budget for Opus
            if not classified_content or self.total_cost > self.config.max_budget * 0.8:
                logger.warning("Skipping Opus synthesis due to budget or classification failure")
                strategic_analysis = None
            else:
                # Step 6: Analyze HKIA coverage
                if progress_callback:
                    progress_callback("Analyzing existing HKIA blog coverage...")
                hkia_coverage = self._analyze_hkia_coverage(hkia_blog_dir)
                
                # Step 7: Run Opus synthesis
                if progress_callback:
                    progress_callback("Running strategic synthesis with Opus...")
                strategic_analysis = await self._run_opus_synthesis(
                    classified_content,
                    hkia_coverage,
                    traditional_analysis
                )
            
            processing_time = (datetime.now() - start_time).total_seconds()
            
            return PipelineResult(
                strategic_analysis=strategic_analysis,
                classified_content=classified_content or {},
                traditional_analysis=traditional_analysis,
                pipeline_metrics={
                    'total_items_processed': len(content_items),
                    'llm_items_processed': len(llm_items),
                    'cache_hits': self._get_cache_hits(),
                    'processing_tiers': {k: len(v) for k, v in tiered_content.items()}
                },
                cost_breakdown={
                    'sonnet': self.sonnet_cost,
                    'opus': self.opus_cost,
                    'total': self.total_cost
                },
                processing_time=processing_time,
                success=True,
                errors=errors
            )
            
        except Exception as e:
            logger.error(f"Pipeline failed: {e}")
            errors.append(str(e))
            
            # Return partial results with traditional analysis
            return PipelineResult(
                strategic_analysis=None,
                classified_content={},
                traditional_analysis=traditional_analysis if 'traditional_analysis' in locals() else {},
                pipeline_metrics={},
                cost_breakdown={'total': self.total_cost},
                processing_time=(datetime.now() - start_time).total_seconds(),
                success=False,
                errors=errors
            )
            
    def _load_competitive_content(self, data_dir: Path) -> List[Dict]:
        """Load all competitive content from markdown files"""
        content_items = []
        
        # Find all competitive markdown files
        for md_file in data_dir.rglob("*.md"):
            if 'backlog' in str(md_file) or 'recent' in str(md_file):
                content = self._parse_markdown_content(md_file)
                content_items.extend(content)
                
        logger.info(f"Loaded {len(content_items)} content items from {data_dir}")
        return content_items
        
    def _parse_markdown_content(self, md_file: Path) -> List[Dict]:
        """Parse content items from markdown file"""
        items = []
        
        try:
            content = md_file.read_text(encoding='utf-8')
            
            # Extract individual items (simplified parsing)
            sections = content.split('\n# ID:')
            for section in sections[1:]:  # Skip header
                item = {
                    'id': section.split('\n')[0].strip(),
                    'source': md_file.parent.parent.name,
                    'file': str(md_file)
                }
                
                # Extract title
                if '## Title:' in section:
                    title_line = section.split('## Title:')[1].split('\n')[0]
                    item['title'] = title_line.strip()
                    
                # Extract description
                if '**Description:**' in section:
                    desc = section.split('**Description:**')[1].split('**')[0]
                    item['description'] = desc.strip()
                    
                # Extract categories
                if '## Categories:' in section:
                    cat_line = section.split('## Categories:')[1].split('\n')[0]
                    item['categories'] = [c.strip() for c in cat_line.split(',')]
                    
                # Extract metrics
                if 'Views:' in section:
                    views_match = re.search(r'Views:\s*(\d+)', section)
                    if views_match:
                        item['views'] = int(views_match.group(1))
                        
                if 'Engagement_Rate:' in section:
                    eng_match = re.search(r'Engagement_Rate:\s*([\d.]+)', section)
                    if eng_match:
                        item['engagement_rate'] = float(eng_match.group(1))
                        
                items.append(item)
                
        except Exception as e:
            logger.warning(f"Error parsing {md_file}: {e}")
            
        return items
        
    def _tier_content_for_processing(self, content_items: List[Dict]) -> Dict[str, List[Dict]]:
        """Determine processing tier for each content item"""
        tiers = {
            'full_analysis': [],  # High-value content for full LLM analysis
            'classification': [],  # Medium-value for classification only
            'traditional': []  # Low-value for keyword matching only
        }
        
        for item in content_items:
            # Prioritize HVACRSchool content
            if 'hvacrschool' in item.get('source', '').lower():
                tiers['full_analysis'].append(item)
                
            # High engagement content
            elif item.get('engagement_rate', 0) > self.config.min_engagement_for_llm:
                tiers['classification'].append(item)
                
            # High view count
            elif item.get('views', 0) > 10000:
                tiers['classification'].append(item)
                
            # Everything else
            else:
                tiers['traditional'].append(item)
                
        # Apply limits
        for tier in ['full_analysis', 'classification']:
            if len(tiers[tier]) > self.config.max_items_per_source:
                # Sort by engagement and take top N
                tiers[tier] = sorted(
                    tiers[tier],
                    key=lambda x: x.get('engagement_rate', 0),
                    reverse=True
                )[:self.config.max_items_per_source]
                
        return tiers
        
    def _check_budget_feasibility(self, items: List[Dict]) -> bool:
        """Check if processing items fits within budget"""
        # Estimate costs
        estimated_sonnet_cost = len(items) * 0.002  # ~$0.002 per item
        estimated_opus_cost = 2.0  # ~$2 for synthesis
        
        total_estimate = estimated_sonnet_cost + estimated_opus_cost
        
        return total_estimate <= self.config.max_budget
        
    def _reduce_scope_for_budget(self, items: List[Dict]) -> List[Dict]:
        """Reduce items to fit budget"""
        # Calculate how many items we can afford
        available_for_sonnet = self.config.max_budget * self.config.sonnet_budget_ratio
        items_we_can_afford = int(available_for_sonnet / 0.002)  # $0.002 per item estimate
        
        # Prioritize by engagement
        sorted_items = sorted(
            items,
            key=lambda x: x.get('engagement_rate', 0),
            reverse=True
        )
        
        return sorted_items[:items_we_can_afford]
        
    def _run_traditional_analysis(self, data_dir: Path) -> Dict:
        """Run traditional keyword-based analysis"""
        try:
            analyzer = BlogTopicAnalyzer(data_dir)
            analysis = analyzer.analyze_competitive_content()
            
            return {
                'primary_topics': analysis.primary_topics,
                'secondary_topics': analysis.secondary_topics,
                'keyword_clusters': analysis.keyword_clusters,
                'content_gaps': analysis.content_gaps
            }
        except Exception as e:
            logger.error(f"Traditional analysis failed: {e}")
            return {}
            
    async def _run_sonnet_classification(self,
                                        items: List[Dict],
                                        progress_callback: Optional[Callable]) -> Dict:
        """Run Sonnet classification on items"""
        try:
            # Check cache first
            cached_items, uncached_items = self._check_classification_cache(items)
            
            if uncached_items:
                # Run classification
                result = await self.sonnet_classifier.classify_all_content(
                    uncached_items,
                    progress_callback
                )
                
                # Update cost tracking
                self.sonnet_cost = result['statistics']['total_cost']
                self.total_cost += self.sonnet_cost
                
                # Cache results
                if self.config.enable_caching:
                    self._cache_classifications(result['classifications'])
                    
                # Combine with cached
                if cached_items:
                    result['classifications'].extend(cached_items)
                    
            else:
                # All items were cached
                result = {
                    'classifications': cached_items,
                    'statistics': {'from_cache': True}
                }
                
            return result
            
        except Exception as e:
            logger.error(f"Sonnet classification failed: {e}")
            return {}
            
    async def _run_opus_synthesis(self,
                                 classified_content: Dict,
                                 hkia_coverage: Dict,
                                 traditional_analysis: Dict) -> StrategicAnalysis:
        """Run Opus strategic synthesis"""
        try:
            analysis = await self.opus_synthesizer.synthesize_competitive_landscape(
                classified_content,
                hkia_coverage,
                traditional_analysis
            )
            
            # Update cost tracking (estimate)
            self.opus_cost = 2.0  # Estimate ~$2 for Opus synthesis
            self.total_cost += self.opus_cost
            
            return analysis
            
        except Exception as e:
            logger.error(f"Opus synthesis failed: {e}")
            return None
            
    def _analyze_hkia_coverage(self, blog_dir: Path) -> Dict:
        """Analyze existing HKIA blog coverage"""
        try:
            analyzer = ContentGapAnalyzer(
                Path("data/competitive_intelligence"),
                blog_dir
            )
            coverage = analyzer._analyze_hkia_content_coverage()
            return coverage
        except Exception as e:
            logger.error(f"HKIA coverage analysis failed: {e}")
            return {}
            
    def _check_classification_cache(self, items: List[Dict]) -> Tuple[List, List]:
        """Check cache for previously classified items"""
        if not self.config.enable_caching:
            return [], items
            
        cached = []
        uncached = []
        
        for item in items:
            cache_file = self.config.cache_dir / f"{item['id']}.json"
            if cache_file.exists():
                try:
                    cached_data = json.loads(cache_file.read_text())
                    cached.append(ContentClassification(**cached_data))
                except:
                    uncached.append(item)
            else:
                uncached.append(item)
                
        logger.info(f"Cache hits: {len(cached)}, misses: {len(uncached)}")
        return cached, uncached
        
    def _cache_classifications(self, classifications: List[ContentClassification]):
        """Cache classifications for future use"""
        if not self.config.enable_caching:
            return
            
        for classification in classifications:
            cache_file = self.config.cache_dir / f"{classification.content_id}.json"
            cache_file.write_text(json.dumps(asdict(classification), indent=2))
            
    def _get_cache_hits(self) -> int:
        """Get number of cache hits in current session"""
        if not self.config.enable_caching:
            return 0
        return len(list(self.config.cache_dir.glob("*.json")))
        
    def export_pipeline_result(self, result: PipelineResult, output_dir: Path):
        """Export complete pipeline results"""
        output_dir.mkdir(parents=True, exist_ok=True)
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        
        # Export strategic analysis
        if result.strategic_analysis:
            self.opus_synthesizer.export_strategy(
                result.strategic_analysis,
                output_dir / f"strategic_analysis_{timestamp}"
            )
            
        # Export classified content
        if result.classified_content:
            classified_path = output_dir / f"classified_content_{timestamp}.json"
            classified_path.write_text(json.dumps(result.classified_content, indent=2, default=str))
            
        # Export pipeline metrics
        metrics_path = output_dir / f"pipeline_metrics_{timestamp}.json"
        metrics_data = {
            'metrics': result.pipeline_metrics,
            'cost_breakdown': result.cost_breakdown,
            'processing_time': result.processing_time,
            'success': result.success,
            'errors': result.errors
        }
        metrics_path.write_text(json.dumps(metrics_data, indent=2))
        
        logger.info(f"Exported pipeline results to {output_dir}")