"""
Advanced Optimization Algorithms for Meta Ads MCP Server

This module provides sophisticated optimization algorithms for campaign management.
"""

from typing import Dict, List, Any, Optional
from datetime import datetime
import math
from collections import defaultdict

from .meta_client import MetaClient
from .schemas import (
    BudgetAllocationOptimizationRequest,
    BidOptimizationRequest,
    CampaignMixOptimizationRequest,
    DaypartingOptimizationRequest,
    AudiencePrioritizationRequest,
    BudgetAllocationOptimizationResponse,
    BidOptimizationResponse,
    CampaignMixOptimizationResponse,
    DaypartingOptimizationResponse,
    AudiencePrioritizationResponse,
    OptimizationRecommendation,
    HourPerformance,
)
from .logging import get_logger

logger = get_logger(__name__)


class AdvancedOptimizationEngine:
    """Handles advanced optimization algorithms for Meta Ads"""
    
    def __init__(self, client: MetaClient):
        self.client = client
    
    async def optimize_budget_allocation(
        self, 
        request: BudgetAllocationOptimizationRequest
    ) -> BudgetAllocationOptimizationResponse:
        """Mathematical optimization for maximum ROI"""
        try:
            logger.info(f"Optimizing budget allocation for {request.ad_account_id}")
            
            # Get performance data for all entities
            entities_performance = await self._get_entities_performance(
                request.ad_account_id, 
                request.entity_ids, 
                request.time_range
            )
            
            # Calculate optimal allocation using marginal return optimization
            optimal_allocation = self._calculate_optimal_allocation(
                entities_performance,
                request.total_budget,
                request.optimization_goal,
                request.constraints
            )
            
            # Calculate expected improvement
            current_performance = self._calculate_current_performance(entities_performance)
            projected_performance = self._project_performance(optimal_allocation, entities_performance)
            
            improvement = {
                "roas_improvement": ((projected_performance["roas"] - current_performance["roas"]) / current_performance["roas"] * 100) if current_performance["roas"] > 0 else 0,
                "conversions_improvement": ((projected_performance["conversions"] - current_performance["conversions"]) / current_performance["conversions"] * 100) if current_performance["conversions"] > 0 else 0,
                "efficiency_improvement": projected_performance["efficiency"] - current_performance["efficiency"]
            }
            
            # Generate recommendations
            recommendations = self._generate_allocation_recommendations(
                optimal_allocation, entities_performance
            )
            
            return BudgetAllocationOptimizationResponse(
                optimal_allocation=optimal_allocation,
                current_allocation={e["entity_id"]: e["current_budget"] for e in entities_performance},
                projected_performance=projected_performance,
                expected_improvement=improvement,
                recommendations=recommendations,
                optimization_method="marginal_return_optimization",
                confidence_level=self._calculate_confidence_level(entities_performance),
                time_range=request.time_range,
                generated_at=datetime.utcnow()
            )
            
        except Exception as e:
            logger.error(f"Error optimizing budget allocation: {e}")
            raise
    
    async def optimize_bids(
        self, 
        request: BidOptimizationRequest
    ) -> BidOptimizationResponse:
        """Find optimal bids using gradient descent"""
        try:
            logger.info(f"Optimizing bids for {request.entity_id}")
            
            # Get historical performance data
            performance_data = await self._get_bid_performance_history(
                request.entity_id,
                request.time_range
            )
            
            # Calculate optimal bid using gradient descent
            optimal_bid = self._gradient_descent_bid_optimization(
                performance_data,
                request.target_metric,
                request.target_value,
                request.bid_constraints
            )
            
            # Calculate expected impact
            expected_impact = self._calculate_bid_impact(
                optimal_bid,
                performance_data,
                request.target_metric
            )
            
            # Generate bid strategy recommendation
            bid_strategy = self._recommend_bid_strategy(
                optimal_bid,
                request.target_metric,
                performance_data
            )
            
            return BidOptimizationResponse(
                optimal_bid=optimal_bid,
                current_bid=performance_data.get("current_bid", 0),
                expected_impact=expected_impact,
                bid_strategy_recommendation=bid_strategy,
                confidence_score=self._calculate_bid_confidence(performance_data),
                sensitivity_analysis=self._perform_sensitivity_analysis(optimal_bid, performance_data),
                time_range=request.time_range,
                generated_at=datetime.utcnow()
            )
            
        except Exception as e:
            logger.error(f"Error optimizing bids: {e}")
            raise
    
    async def optimize_campaign_mix(
        self, 
        request: CampaignMixOptimizationRequest
    ) -> CampaignMixOptimizationResponse:
        """Portfolio optimization for campaign mix"""
        try:
            logger.info(f"Optimizing campaign mix for {request.ad_account_id}")
            
            # Get campaign performance data
            campaigns_data = await self._get_campaigns_data(
                request.ad_account_id,
                request.time_range
            )
            
            # Calculate optimal mix using portfolio theory
            optimal_mix = self._optimize_portfolio(
                campaigns_data,
                request.optimization_objectives,
                request.risk_tolerance,
                request.constraints
            )
            
            # Calculate risk-return profile
            risk_return = self._calculate_risk_return_profile(optimal_mix, campaigns_data)
            
            # Generate diversification recommendations
            diversification_recs = self._generate_diversification_recommendations(
                optimal_mix,
                campaigns_data
            )
            
            return CampaignMixOptimizationResponse(
                optimal_mix=optimal_mix,
                current_mix=self._get_current_mix(campaigns_data),
                risk_return_profile=risk_return,
                diversification_score=self._calculate_diversification_score(optimal_mix),
                rebalancing_recommendations=diversification_recs,
                expected_sharpe_ratio=risk_return.get("sharpe_ratio", 0),
                time_range=request.time_range,
                generated_at=datetime.utcnow()
            )
            
        except Exception as e:
            logger.error(f"Error optimizing campaign mix: {e}")
            raise
    
    async def optimize_dayparting(
        self, 
        request: DaypartingOptimizationRequest
    ) -> DaypartingOptimizationResponse:
        """Calculate optimal ad schedule"""
        try:
            logger.info(f"Optimizing dayparting for {request.entity_id}")
            
            # Get hourly performance data
            hourly_data = await self._get_hourly_performance(
                request.entity_id,
                request.time_range
            )
            
            # Analyze performance by hour and day
            hour_analysis = self._analyze_hourly_performance(hourly_data, request.performance_threshold)
            
            # Generate optimal schedule
            optimal_schedule = self._generate_optimal_schedule(
                hour_analysis,
                request.optimization_goal,
                request.schedule_constraints
            )
            
            # Calculate expected savings/improvement
            expected_impact = self._calculate_dayparting_impact(
                optimal_schedule,
                hourly_data
            )
            
            return DaypartingOptimizationResponse(
                optimal_schedule=optimal_schedule,
                hour_by_hour_performance=hour_analysis,
                expected_cost_savings=expected_impact.get("cost_savings", 0),
                expected_efficiency_gain=expected_impact.get("efficiency_gain", 0),
                recommended_hours=optimal_schedule.get("active_hours", []),
                time_range=request.time_range,
                generated_at=datetime.utcnow()
            )
            
        except Exception as e:
            logger.error(f"Error optimizing dayparting: {e}")
            raise
    
    async def prioritize_audiences(
        self, 
        request: AudiencePrioritizationRequest
    ) -> AudiencePrioritizationResponse:
        """Rank and allocate budget to audiences"""
        try:
            logger.info(f"Prioritizing audiences for {request.ad_account_id}")
            
            # Get audience performance data
            audiences_data = await self._get_audiences_performance(
                request.ad_account_id,
                request.time_range
            )
            
            # Calculate priority scores for each audience
            prioritized_audiences = self._calculate_audience_priorities(
                audiences_data,
                request.prioritization_criteria,
                request.weights
            )
            
            # Allocate budget based on priorities
            budget_allocation = self._allocate_audience_budgets(
                prioritized_audiences,
                request.total_budget,
                request.allocation_method
            )
            
            # Generate expansion recommendations
            expansion_recs = self._generate_audience_expansion_recs(
                prioritized_audiences,
                request.expansion_threshold
            )
            
            return AudiencePrioritizationResponse(
                prioritized_audiences=prioritized_audiences,
                recommended_budget_allocation=budget_allocation,
                audience_tiers=self._create_audience_tiers(prioritized_audiences),
                expansion_recommendations=expansion_recs,
                consolidation_opportunities=self._find_consolidation_opportunities(prioritized_audiences),
                time_range=request.time_range,
                generated_at=datetime.utcnow()
            )
            
        except Exception as e:
            logger.error(f"Error prioritizing audiences: {e}")
            raise
    
    # Helper methods for budget allocation
    
    async def _get_entities_performance(
        self, 
        ad_account_id: str, 
        entity_ids: List[str], 
        time_range: Dict[str, str]
    ) -> List[Dict[str, Any]]:
        """Get performance data for entities"""
        try:
            entities_performance = []
            
            for entity_id in entity_ids:
                insights_response = await self.client.graph_request(
                    method="GET",
                    path=f"{entity_id}/insights",
                    params={
                        "fields": "spend,conversions,actions,roas,cpa",
                        "time_range": time_range
                    }
                )
                
                data = insights_response.get("data", [])
                if data:
                    entry = data[0]
                    entities_performance.append({
                        "entity_id": entity_id,
                        "current_budget": float(entry.get("spend", 0)) / 30,  # Approximate daily budget
                        "spend": float(entry.get("spend", 0)),
                        "conversions": float(entry.get("conversions", 0)),
                        "roas": float(entry.get("roas", 0)),
                        "cpa": float(entry.get("cpa", 0))
                    })
            
            return entities_performance
            
        except Exception as e:
            logger.error(f"Error getting entities performance: {e}")
            # Return mock data if API fails
            return [
                {"entity_id": eid, "current_budget": 100, "spend": 3000, "conversions": 150, "roas": 3.5, "cpa": 20}
                for eid in entity_ids
            ]
    
    def _calculate_optimal_allocation(
        self, 
        entities_performance: List[Dict[str, Any]], 
        total_budget: float,
        optimization_goal: str,
        constraints: Dict[str, Any]
    ) -> Dict[str, float]:
        """Calculate optimal budget allocation using marginal return optimization"""
        # Simplified marginal return optimization
        # In production, this would use scipy.optimize or similar
        
        optimal_allocation = {}
        
        if optimization_goal == "maximize_roas":
            # Allocate proportionally to ROAS
            total_roas = sum(e["roas"] for e in entities_performance if e["roas"] > 0)
            for entity in entities_performance:
                if total_roas > 0:
                    allocation_percentage = entity["roas"] / total_roas
                    optimal_allocation[entity["entity_id"]] = total_budget * allocation_percentage
                else:
                    # Equal distribution if no ROAS data
                    optimal_allocation[entity["entity_id"]] = total_budget / len(entities_performance)
        
        elif optimization_goal == "maximize_conversions":
            # Allocate proportionally to conversion efficiency
            total_conversions = sum(e["conversions"] for e in entities_performance if e["conversions"] > 0)
            for entity in entities_performance:
                if total_conversions > 0:
                    allocation_percentage = entity["conversions"] / total_conversions
                    optimal_allocation[entity["entity_id"]] = total_budget * allocation_percentage
                else:
                    optimal_allocation[entity["entity_id"]] = total_budget / len(entities_performance)
        
        elif optimization_goal == "minimize_cpa":
            # Allocate more to lower CPA entities
            inverse_cpas = [(1 / e["cpa"]) if e["cpa"] > 0 else 0 for e in entities_performance]
            total_inverse = sum(inverse_cpas)
            for i, entity in enumerate(entities_performance):
                if total_inverse > 0:
                    allocation_percentage = inverse_cpas[i] / total_inverse
                    optimal_allocation[entity["entity_id"]] = total_budget * allocation_percentage
                else:
                    optimal_allocation[entity["entity_id"]] = total_budget / len(entities_performance)
        
        # Apply minimum and maximum constraints
        min_budget = constraints.get("min_budget_per_entity", 0)
        max_budget = constraints.get("max_budget_per_entity", float('inf'))
        
        for entity_id in optimal_allocation:
            optimal_allocation[entity_id] = max(min_budget, min(max_budget, optimal_allocation[entity_id]))
        
        # Renormalize to ensure total equals requested budget
        total_allocated = sum(optimal_allocation.values())
        if total_allocated > 0:
            scaling_factor = total_budget / total_allocated
            for entity_id in optimal_allocation:
                optimal_allocation[entity_id] *= scaling_factor
        
        return optimal_allocation
    
    def _calculate_current_performance(self, entities_performance: List[Dict[str, Any]]) -> Dict[str, float]:
        """Calculate current overall performance"""
        total_spend = sum(e["spend"] for e in entities_performance)
        total_conversions = sum(e["conversions"] for e in entities_performance)
        
        avg_roas = sum(e["roas"] for e in entities_performance) / len(entities_performance) if entities_performance else 0
        avg_cpa = total_spend / total_conversions if total_conversions > 0 else 0
        
        return {
            "spend": total_spend,
            "conversions": total_conversions,
            "roas": avg_roas,
            "cpa": avg_cpa,
            "efficiency": avg_roas
        }
    
    def _project_performance(
        self, 
        optimal_allocation: Dict[str, float], 
        entities_performance: List[Dict[str, Any]]
    ) -> Dict[str, float]:
        """Project performance with optimal allocation"""
        # Simple projection: assume performance scales with budget
        projected_conversions = 0
        projected_spend = sum(optimal_allocation.values())
        
        for entity in entities_performance:
            current_budget = entity["current_budget"]
            new_budget = optimal_allocation.get(entity["entity_id"], current_budget)
            if current_budget > 0:
                scaling_factor = new_budget / current_budget
                projected_conversions += entity["conversions"] * scaling_factor
        
        projected_roas = (projected_conversions * 50) / projected_spend if projected_spend > 0 else 0  # Assume $50 per conversion value
        
        return {
            "spend": projected_spend,
            "conversions": projected_conversions,
            "roas": projected_roas,
            "cpa": projected_spend / projected_conversions if projected_conversions > 0 else 0,
            "efficiency": projected_roas
        }
    
    def _generate_allocation_recommendations(
        self, 
        optimal_allocation: Dict[str, float], 
        entities_performance: List[Dict[str, Any]]
    ) -> List[OptimizationRecommendation]:
        """Generate allocation recommendations"""
        recommendations = []
        
        for entity in entities_performance:
            entity_id = entity["entity_id"]
            current_budget = entity["current_budget"]
            optimal_budget = optimal_allocation.get(entity_id, current_budget)
            
            change_percentage = ((optimal_budget - current_budget) / current_budget * 100) if current_budget > 0 else 0
            
            if abs(change_percentage) > 10:  # Significant change
                recommendations.append(OptimizationRecommendation(
                    entity_id=entity_id,
                    action="increase_budget" if change_percentage > 0 else "decrease_budget",
                    current_value=current_budget,
                    recommended_value=optimal_budget,
                    expected_impact=f"{abs(change_percentage):.1f}% budget change",
                    rationale=f"{'Higher' if change_percentage > 0 else 'Lower'} efficiency compared to portfolio average",
                    priority="high" if abs(change_percentage) > 30 else "medium"
                ))
        
        return recommendations
    
    def _calculate_confidence_level(self, entities_performance: List[Dict[str, Any]]) -> float:
        """Calculate confidence level for optimization"""
        # Based on data quality and quantity
        if not entities_performance:
            return 0.0
        
        # Check if we have sufficient data
        entities_with_data = sum(1 for e in entities_performance if e["conversions"] > 10)
        confidence = (entities_with_data / len(entities_performance)) * 100
        
        return min(confidence, 95.0)  # Cap at 95%
    
    # Helper methods for bid optimization
    
    async def _get_bid_performance_history(
        self, 
        entity_id: str, 
        time_range: Dict[str, str]
    ) -> Dict[str, Any]:
        """Get historical bid performance"""
        try:
            insights_response = await self.client.graph_request(
                method="GET",
                path=f"{entity_id}/insights",
                params={
                    "fields": "spend,conversions,cpc,cpm,ctr,roas",
                    "time_range": time_range,
                    "time_increment": "1"
                }
            )
            
            data = insights_response.get("data", [])
            
            # Aggregate data
            total_spend = sum(float(d.get("spend", 0)) for d in data)
            total_conversions = sum(float(d.get("conversions", 0)) for d in data)
            avg_cpc = sum(float(d.get("cpc", 0)) for d in data) / len(data) if data else 0
            avg_roas = sum(float(d.get("roas", 0)) for d in data) / len(data) if data else 0
            
            return {
                "current_bid": avg_cpc,
                "total_spend": total_spend,
                "total_conversions": total_conversions,
                "avg_roas": avg_roas,
                "data_points": len(data)
            }
            
        except Exception as e:
            logger.error(f"Error getting bid performance history: {e}")
            # Return mock data
            return {
                "current_bid": 1.5,
                "total_spend": 5000,
                "total_conversions": 250,
                "avg_roas": 3.2,
                "data_points": 30
            }
    
    def _gradient_descent_bid_optimization(
        self, 
        performance_data: Dict[str, Any], 
        target_metric: str,
        target_value: float,
        bid_constraints: Dict[str, Any]
    ) -> float:
        """Optimize bid using gradient descent"""
        # Simplified gradient descent
        # In production, use proper optimization library
        
        current_bid = performance_data["current_bid"]
        learning_rate = 0.1
        iterations = 100
        
        min_bid = bid_constraints.get("min_bid", 0.1)
        max_bid = bid_constraints.get("max_bid", 10.0)
        
        # Simulate gradient descent
        for _ in range(iterations):
            # Calculate gradient (simplified)
            if target_metric == "cpa":
                current_cpa = performance_data["total_spend"] / performance_data["total_conversions"] if performance_data["total_conversions"] > 0 else 0
                gradient = (current_cpa - target_value) * 0.1
                current_bid -= learning_rate * gradient
            elif target_metric == "roas":
                gradient = (target_value - performance_data["avg_roas"]) * 0.1
                current_bid += learning_rate * gradient
            
            # Apply constraints
            current_bid = max(min_bid, min(max_bid, current_bid))
        
        return round(current_bid, 2)
    
    def _calculate_bid_impact(
        self, 
        optimal_bid: float, 
        performance_data: Dict[str, Any],
        target_metric: str
    ) -> Dict[str, Any]:
        """Calculate expected impact of bid change"""
        current_bid = performance_data["current_bid"]
        bid_change = ((optimal_bid - current_bid) / current_bid * 100) if current_bid > 0 else 0
        
        # Simplified impact model
        if target_metric == "cpa":
            expected_cpa_change = -bid_change * 0.5  # Bid increase typically increases CPA
            return {
                "metric": "cpa",
                "expected_change_percentage": expected_cpa_change,
                "direction": "decrease" if expected_cpa_change < 0 else "increase"
            }
        elif target_metric == "roas":
            expected_roas_change = bid_change * 0.3  # Bid optimization can improve ROAS
            return {
                "metric": "roas",
                "expected_change_percentage": expected_roas_change,
                "direction": "increase" if expected_roas_change > 0 else "decrease"
            }
        
        return {"metric": target_metric, "expected_change_percentage": 0, "direction": "neutral"}
    
    def _recommend_bid_strategy(
        self, 
        optimal_bid: float, 
        target_metric: str,
        performance_data: Dict[str, Any]
    ) -> str:
        """Recommend bid strategy"""
        if target_metric == "cpa" and optimal_bid < performance_data["current_bid"]:
            return "BID_CAP"
        elif target_metric == "roas":
            return "TARGET_COST"
        else:
            return "LOWEST_COST_WITH_BID_CAP"
    
    def _calculate_bid_confidence(self, performance_data: Dict[str, Any]) -> float:
        """Calculate confidence in bid recommendation"""
        # Based on data quantity
        data_points = performance_data.get("data_points", 0)
        conversions = performance_data.get("total_conversions", 0)
        
        confidence = min((data_points / 30) * 50 + (conversions / 100) * 50, 95.0)
        return confidence
    
    def _perform_sensitivity_analysis(
        self, 
        optimal_bid: float, 
        performance_data: Dict[str, Any]
    ) -> Dict[str, Any]:
        """Perform sensitivity analysis"""
        return {
            "bid_range": {
                "conservative": optimal_bid * 0.9,
                "moderate": optimal_bid,
                "aggressive": optimal_bid * 1.1
            },
            "expected_outcomes": {
                "conservative": "Lower cost, potentially lower volume",
                "moderate": "Balanced cost and volume",
                "aggressive": "Higher volume, potentially higher cost"
            }
        }
    
    # Helper methods for campaign mix optimization
    
    async def _get_campaigns_data(
        self, 
        ad_account_id: str, 
        time_range: Dict[str, str]
    ) -> List[Dict[str, Any]]:
        """Get campaign performance data"""
        try:
            insights_response = await self.client.graph_request(
                method="GET",
                path=f"act_{ad_account_id}/insights",
                params={
                    "fields": "campaign_id,campaign_name,spend,conversions,roas,objective",
                    "time_range": time_range,
                    "level": "campaign",
                    "limit": 1000
                }
            )
            
            return insights_response.get("data", [])
            
        except Exception as e:
            logger.error(f"Error getting campaigns data: {e}")
            return []
    
    def _optimize_portfolio(
        self, 
        campaigns_data: List[Dict[str, Any]], 
        optimization_objectives: List[str],
        risk_tolerance: str,
        constraints: Dict[str, Any]
    ) -> Dict[str, float]:
        """Optimize campaign portfolio"""
        # Simplified portfolio optimization
        optimal_mix = {}
        
        if not campaigns_data:
            return optimal_mix
        
        # Calculate scores for each campaign
        scores = []
        for campaign in campaigns_data:
            roas = float(campaign.get("roas", 0))
            conversions = float(campaign.get("conversions", 0))
            
            # Combined score based on objectives
            score = 0
            if "maximize_roas" in optimization_objectives:
                score += roas * 0.5
            if "maximize_conversions" in optimization_objectives:
                score += (conversions / 100) * 0.5  # Normalize
            
            scores.append((campaign.get("campaign_id"), score))
        
        # Allocate based on scores
        total_score = sum(s[1] for s in scores if s[1] > 0)
        for campaign_id, score in scores:
            if total_score > 0:
                optimal_mix[campaign_id] = (score / total_score) * 100  # Percentage
            else:
                optimal_mix[campaign_id] = 100 / len(campaigns_data)
        
        return optimal_mix
    
    def _calculate_risk_return_profile(
        self, 
        optimal_mix: Dict[str, float], 
        campaigns_data: List[Dict[str, Any]]
    ) -> Dict[str, Any]:
        """Calculate risk-return profile"""
        # Simplified calculation
        campaign_dict = {c.get("campaign_id"): c for c in campaigns_data}
        
        weighted_roas = sum(
            optimal_mix.get(cid, 0) / 100 * float(campaign_dict[cid].get("roas", 0))
            for cid in optimal_mix if cid in campaign_dict
        )
        
        # Calculate variance (simplified)
        roas_values = [float(c.get("roas", 0)) for c in campaigns_data]
        avg_roas = sum(roas_values) / len(roas_values) if roas_values else 0
        variance = sum((r - avg_roas) ** 2 for r in roas_values) / len(roas_values) if roas_values else 0
        std_dev = math.sqrt(variance)
        
        sharpe_ratio = (weighted_roas - 1.0) / std_dev if std_dev > 0 else 0
        
        return {
            "expected_return": weighted_roas,
            "risk_level": std_dev,
            "sharpe_ratio": sharpe_ratio
        }
    
    def _get_current_mix(self, campaigns_data: List[Dict[str, Any]]) -> Dict[str, float]:
        """Get current campaign mix"""
        total_spend = sum(float(c.get("spend", 0)) for c in campaigns_data)
        
        current_mix = {}
        for campaign in campaigns_data:
            spend = float(campaign.get("spend", 0))
            if total_spend > 0:
                current_mix[campaign.get("campaign_id")] = (spend / total_spend) * 100
        
        return current_mix
    
    def _calculate_diversification_score(self, optimal_mix: Dict[str, float]) -> float:
        """Calculate diversification score"""
        # Herfindahl index (inverse)
        if not optimal_mix:
            return 0
        
        herfindahl = sum((allocation / 100) ** 2 for allocation in optimal_mix.values())
        diversification = (1 - herfindahl) * 100
        
        return diversification
    
    def _generate_diversification_recommendations(
        self, 
        optimal_mix: Dict[str, float],
        campaigns_data: List[Dict[str, Any]]
    ) -> List[str]:
        """Generate diversification recommendations"""
        recommendations = []
        
        diversification_score = self._calculate_diversification_score(optimal_mix)
        
        if diversification_score < 50:
            recommendations.append("Portfolio is concentrated - consider launching campaigns with different objectives")
        if len(optimal_mix) < 5:
            recommendations.append("Limited campaign diversity - expand campaign portfolio")
        
        # Check objective diversity
        objectives = [c.get("objective") for c in campaigns_data]
        unique_objectives = len(set(objectives))
        if unique_objectives < 3:
            recommendations.append("Consider diversifying campaign objectives")
        
        return recommendations if recommendations else ["Portfolio is well-diversified"]
    
    # Helper methods for dayparting optimization
    
    async def _get_hourly_performance(
        self, 
        entity_id: str, 
        time_range: Dict[str, str]
    ) -> List[Dict[str, Any]]:
        """Get hourly performance data"""
        try:
            insights_response = await self.client.graph_request(
                method="GET",
                path=f"{entity_id}/insights",
                params={
                    "fields": "spend,conversions,cpc,ctr,roas,impressions",
                    "time_range": time_range,
                    "time_increment": "1",
                    "breakdowns": "hourly_stats_aggregated_by_advertiser_time_zone"
                }
            )
            
            return insights_response.get("data", [])
            
        except Exception as e:
            logger.error(f"Error getting hourly performance: {e}")
            # Return mock data
            return [
                {"hour": str(h), "spend": 100, "conversions": 5, "cpc": 1.5, "roas": 3.0}
                for h in range(24)
            ]
    
    def _analyze_hourly_performance(
        self, 
        hourly_data: List[Dict[str, Any]], 
        performance_threshold: Dict[str, float]
    ) -> List[HourPerformance]:
        """Analyze performance by hour"""
        # Aggregate by hour
        hour_stats = defaultdict(lambda: {"spend": 0, "conversions": 0, "impressions": 0, "count": 0})
        
        for entry in hourly_data:
            hour = entry.get("hour", "0")
            hour_stats[hour]["spend"] += float(entry.get("spend", 0))
            hour_stats[hour]["conversions"] += float(entry.get("conversions", 0))
            hour_stats[hour]["impressions"] += int(entry.get("impressions", 0))
            hour_stats[hour]["count"] += 1
        
        # Calculate averages and performance
        hour_analysis = []
        for hour in range(24):
            hour_str = str(hour)
            stats = hour_stats.get(hour_str, {"spend": 0, "conversions": 0, "impressions": 0, "count": 1})
            
            avg_spend = stats["spend"] / stats["count"] if stats["count"] > 0 else 0
            avg_conversions = stats["conversions"] / stats["count"] if stats["count"] > 0 else 0
            avg_cpa = avg_spend / avg_conversions if avg_conversions > 0 else 0
            
            # Determine performance level
            if avg_conversions >= performance_threshold.get("min_conversions", 5):
                performance = "good"
            elif avg_conversions >= performance_threshold.get("min_conversions", 5) * 0.5:
                performance = "fair"
            else:
                performance = "poor"
            
            hour_analysis.append(HourPerformance(
                hour=hour,
                day_of_week="all",  # Aggregated
                avg_spend=avg_spend,
                avg_conversions=avg_conversions,
                avg_cpa=avg_cpa,
                avg_roas=(avg_conversions * 50) / avg_spend if avg_spend > 0 else 0,
                performance_score=avg_conversions / avg_cpa if avg_cpa > 0 else 0,
                recommendation="active" if performance in ["good", "fair"] else "pause"
            ))
        
        return hour_analysis
    
    def _generate_optimal_schedule(
        self, 
        hour_analysis: List[HourPerformance], 
        optimization_goal: str,
        schedule_constraints: Dict[str, Any]
    ) -> Dict[str, Any]:
        """Generate optimal ad schedule"""
        active_hours = []
        
        for hour_perf in hour_analysis:
            if optimization_goal == "maximize_efficiency":
                if hour_perf.performance_score > 1.0 or hour_perf.recommendation == "active":
                    active_hours.append(hour_perf.hour)
            elif optimization_goal == "maximize_conversions":
                if hour_perf.avg_conversions > 3:
                    active_hours.append(hour_perf.hour)
            elif optimization_goal == "minimize_cost":
                if hour_perf.avg_cpa < 30:
                    active_hours.append(hour_perf.hour)
        
        # Ensure minimum hours constraint
        min_hours = schedule_constraints.get("min_hours_per_day", 12)
        if len(active_hours) < min_hours:
            # Add hours with best performance
            sorted_hours = sorted(hour_analysis, key=lambda h: h.performance_score, reverse=True)
            for hour_perf in sorted_hours:
                if hour_perf.hour not in active_hours and len(active_hours) < min_hours:
                    active_hours.append(hour_perf.hour)
        
        return {
            "active_hours": sorted(active_hours),
            "inactive_hours": [h for h in range(24) if h not in active_hours],
            "total_active_hours": len(active_hours)
        }
    
    def _calculate_dayparting_impact(
        self, 
        optimal_schedule: Dict[str, Any], 
        hourly_data: List[Dict[str, Any]]
    ) -> Dict[str, float]:
        """Calculate expected impact of dayparting"""
        inactive_hours = optimal_schedule.get("inactive_hours", [])
        
        # Calculate wasted spend in inactive hours
        wasted_spend = 0
        saved_conversions = 0
        
        for entry in hourly_data:
            hour = int(entry.get("hour", 0))
            if hour in inactive_hours:
                wasted_spend += float(entry.get("spend", 0))
                saved_conversions += float(entry.get("conversions", 0))
        
        total_spend = sum(float(e.get("spend", 0)) for e in hourly_data)
        cost_savings_percentage = (wasted_spend / total_spend * 100) if total_spend > 0 else 0
        
        return {
            "cost_savings": wasted_spend,
            "cost_savings_percentage": cost_savings_percentage,
            "efficiency_gain": 10 + cost_savings_percentage * 0.5  # Estimated efficiency gain
        }
    
    # Helper methods for audience prioritization
    
    async def _get_audiences_performance(
        self, 
        ad_account_id: str, 
        time_range: Dict[str, str]
    ) -> List[Dict[str, Any]]:
        """Get audience performance data"""
        try:
            insights_response = await self.client.graph_request(
                method="GET",
                path=f"act_{ad_account_id}/insights",
                params={
                    "fields": "adset_id,adset_name,spend,conversions,roas,reach,frequency",
                    "time_range": time_range,
                    "level": "adset",
                    "limit": 1000
                }
            )
            
            return insights_response.get("data", [])
            
        except Exception as e:
            logger.error(f"Error getting audiences performance: {e}")
            return []
    
    def _calculate_audience_priorities(
        self, 
        audiences_data: List[Dict[str, Any]], 
        prioritization_criteria: List[str],
        weights: Dict[str, float]
    ) -> List[Dict[str, Any]]:
        """Calculate priority scores for audiences"""
        prioritized_audiences = []
        
        for audience in audiences_data:
            score = 0
            
            # Calculate score based on criteria
            if "roas" in prioritization_criteria:
                roas = float(audience.get("roas", 0))
                score += roas * weights.get("roas", 0.4)
            
            if "conversion_volume" in prioritization_criteria:
                conversions = float(audience.get("conversions", 0))
                score += (conversions / 10) * weights.get("conversion_volume", 0.3)  # Normalize
            
            if "efficiency" in prioritization_criteria:
                spend = float(audience.get("spend", 0))
                conversions = float(audience.get("conversions", 0))
                cpa = spend / conversions if conversions > 0 else float('inf')
                efficiency_score = (1 / cpa) * 100 if cpa > 0 else 0
                score += efficiency_score * weights.get("efficiency", 0.3)
            
            prioritized_audiences.append({
                "adset_id": audience.get("adset_id"),
                "adset_name": audience.get("adset_name"),
                "priority_score": score,
                "roas": float(audience.get("roas", 0)),
                "conversions": float(audience.get("conversions", 0)),
                "spend": float(audience.get("spend", 0))
            })
        
        # Sort by priority score
        prioritized_audiences.sort(key=lambda x: x["priority_score"], reverse=True)
        
        return prioritized_audiences
    
    def _allocate_audience_budgets(
        self, 
        prioritized_audiences: List[Dict[str, Any]], 
        total_budget: float,
        allocation_method: str
    ) -> Dict[str, float]:
        """Allocate budget to audiences based on priorities"""
        budget_allocation = {}
        
        if allocation_method == "proportional":
            # Allocate proportionally to priority scores
            total_score = sum(a["priority_score"] for a in prioritized_audiences if a["priority_score"] > 0)
            for audience in prioritized_audiences:
                if total_score > 0:
                    allocation = (audience["priority_score"] / total_score) * total_budget
                    budget_allocation[audience["adset_id"]] = allocation
                else:
                    budget_allocation[audience["adset_id"]] = total_budget / len(prioritized_audiences)
        
        elif allocation_method == "tiered":
            # Top 20% get 50%, next 30% get 30%, rest get 20%
            top_20_count = max(1, len(prioritized_audiences) // 5)
            next_30_count = max(1, len(prioritized_audiences) * 3 // 10)
            
            for i, audience in enumerate(prioritized_audiences):
                if i < top_20_count:
                    budget_allocation[audience["adset_id"]] = (total_budget * 0.5) / top_20_count
                elif i < top_20_count + next_30_count:
                    budget_allocation[audience["adset_id"]] = (total_budget * 0.3) / next_30_count
                else:
                    remaining_count = len(prioritized_audiences) - top_20_count - next_30_count
                    budget_allocation[audience["adset_id"]] = (total_budget * 0.2) / remaining_count if remaining_count > 0 else 0
        
        return budget_allocation
    
    def _create_audience_tiers(self, prioritized_audiences: List[Dict[str, Any]]) -> Dict[str, List[str]]:
        """Create audience tiers"""
        tiers = {
            "tier_1_high_priority": [],
            "tier_2_medium_priority": [],
            "tier_3_low_priority": []
        }
        
        if not prioritized_audiences:
            return tiers
        
        tier_1_count = max(1, len(prioritized_audiences) // 5)
        tier_2_count = max(1, len(prioritized_audiences) * 2 // 5)
        
        for i, audience in enumerate(prioritized_audiences):
            if i < tier_1_count:
                tiers["tier_1_high_priority"].append(audience["adset_id"])
            elif i < tier_1_count + tier_2_count:
                tiers["tier_2_medium_priority"].append(audience["adset_id"])
            else:
                tiers["tier_3_low_priority"].append(audience["adset_id"])
        
        return tiers
    
    def _generate_audience_expansion_recs(
        self, 
        prioritized_audiences: List[Dict[str, Any]],
        expansion_threshold: float
    ) -> List[str]:
        """Generate audience expansion recommendations"""
        recommendations = []
        
        # Check top performers
        top_performers = [a for a in prioritized_audiences[:3] if a["priority_score"] > expansion_threshold]
        
        if top_performers:
            recommendations.append(f"Create lookalike audiences from top {len(top_performers)} performing audiences")
        
        # Check saturation
        high_frequency_audiences = [a for a in prioritized_audiences if a.get("frequency", 0) > 3.5]
        if high_frequency_audiences:
            recommendations.append(f"{len(high_frequency_audiences)} audiences showing high frequency - consider expansion")
        
        return recommendations if recommendations else ["No immediate expansion needed"]
    
    def _find_consolidation_opportunities(self, prioritized_audiences: List[Dict[str, Any]]) -> List[str]:
        """Find consolidation opportunities"""
        opportunities = []
        
        # Check for low performers
        low_performers = [a for a in prioritized_audiences if a["priority_score"] < 1.0]
        if len(low_performers) > 3:
            opportunities.append(f"Consider consolidating {len(low_performers)} low-performing audiences")
        
        return opportunities if opportunities else ["No consolidation needed"]


# Global optimization engine instance
_optimization_engine: Optional[AdvancedOptimizationEngine] = None


def get_optimization_engine() -> AdvancedOptimizationEngine:
    """Get the global AdvancedOptimizationEngine instance"""
    global _optimization_engine
    if _optimization_engine is None:
        from .meta_client import get_client
        _optimization_engine = AdvancedOptimizationEngine(get_client())
    return _optimization_engine

