from __future__ import annotations

import asyncio
from typing import Any, Dict, List, Optional, Callable, Awaitable
from dataclasses import dataclass
from enum import Enum
import time

from .logging import get_logger
from .meta_client import MetaClient
from .cache import get_cache_manager, cache_key, cache_ttl_for_endpoint
from .rate_limiter import get_rate_limiter, RateLimitExceededError
from .util import chunk

logger = get_logger(__name__)


class BulkOperationType(str, Enum):
    """Types of bulk operations."""
    FETCH = "fetch"
    UPDATE = "update"
    CREATE = "create"
    DELETE = "delete"
    INSIGHTS = "insights"


@dataclass
class BulkOperationConfig:
    """Configuration for bulk operations."""
    max_concurrent: int = 10
    chunk_size: int = 50
    retry_attempts: int = 3
    retry_delay: float = 1.0
    timeout: float = 30.0
    enable_caching: bool = True
    cache_ttl: Optional[int] = None
    rate_limit_delay: float = 0.1


@dataclass
class BulkOperationResult:
    """Result of a bulk operation."""
    success: bool
    data: Optional[Any] = None
    error: Optional[str] = None
    operation_id: Optional[str] = None
    duration: float = 0.0
    retry_count: int = 0


@dataclass
class BulkOperationStats:
    """Statistics for bulk operations."""
    total_operations: int = 0
    successful_operations: int = 0
    failed_operations: int = 0
    total_duration: float = 0.0
    average_duration: float = 0.0
    cache_hits: int = 0
    cache_misses: int = 0
    rate_limit_delays: int = 0


class BulkOperationManager:
    """Manages bulk operations with parallel execution and error handling."""
    
    def __init__(self, client: MetaClient, config: Optional[BulkOperationConfig] = None):
        self.client = client
        self.config = config or BulkOperationConfig()
        self.cache_manager = get_cache_manager()
        self.rate_limiter = get_rate_limiter()
        self._semaphore = asyncio.Semaphore(self.config.max_concurrent)
        self._stats = BulkOperationStats()
    
    async def _execute_with_retry(
        self,
        operation: Callable[[], Awaitable[Any]],
        operation_id: str,
        max_retries: Optional[int] = None
    ) -> BulkOperationResult:
        """Execute an operation with retry logic."""
        max_retries = max_retries or self.config.retry_attempts
        start_time = time.time()
        
        for attempt in range(max_retries + 1):
            try:
                async with self._semaphore:
                    result = await operation()
                    duration = time.time() - start_time
                    
                    return BulkOperationResult(
                        success=True,
                        data=result,
                        operation_id=operation_id,
                        duration=duration,
                        retry_count=attempt
                    )
                    
            except RateLimitExceededError as e:
                if attempt < max_retries:
                    delay = e.retry_after + self.config.rate_limit_delay
                    logger.warning(
                        "Rate limit exceeded, retrying",
                        operation_id=operation_id,
                        attempt=attempt + 1,
                        delay=delay
                    )
                    await asyncio.sleep(delay)
                    self._stats.rate_limit_delays += 1
                    continue
                else:
                    duration = time.time() - start_time
                    return BulkOperationResult(
                        success=False,
                        error=f"Rate limit exceeded: {str(e)}",
                        operation_id=operation_id,
                        duration=duration,
                        retry_count=attempt
                    )
                    
            except Exception as e:
                if attempt < max_retries:
                    delay = self.config.retry_delay * (2 ** attempt)
                    logger.warning(
                        "Operation failed, retrying",
                        operation_id=operation_id,
                        attempt=attempt + 1,
                        error=str(e),
                        delay=delay
                    )
                    await asyncio.sleep(delay)
                    continue
                else:
                    duration = time.time() - start_time
                    return BulkOperationResult(
                        success=False,
                        error=str(e),
                        operation_id=operation_id,
                        duration=duration,
                        retry_count=attempt
                    )
        
        # Should never reach here
        duration = time.time() - start_time
        return BulkOperationResult(
            success=False,
            error="Max retries exceeded",
            operation_id=operation_id,
            duration=duration,
            retry_count=max_retries
        )
    
    async def _fetch_cached_or_execute(
        self,
        cache_key: str,
        operation: Callable[[], Awaitable[Any]],
        ttl: Optional[int] = None
    ) -> Any:
        """Fetch from cache or execute operation."""
        if not self.config.enable_caching:
            return await operation()
        
        # Try cache first
        cached_result = await self.cache_manager.get(cache_key)
        if cached_result is not None:
            self._stats.cache_hits += 1
            logger.debug("Cache hit for bulk operation", cache_key=cache_key)
            return cached_result
        
        # Execute operation
        self._stats.cache_misses += 1
        result = await operation()
        
        # Cache the result
        if result is not None:
            cache_ttl = ttl or self.config.cache_ttl
            await self.cache_manager.set(cache_key, result, cache_ttl)
        
        return result
    
    async def fetch_campaigns(
        self,
        ad_account_id: str,
        fields: List[str],
        filters: Optional[Dict[str, Any]] = None,
        limit: int = 1000
    ) -> List[BulkOperationResult]:
        """Fetch campaigns in parallel with chunking."""
        logger.info(
            "Starting bulk campaign fetch",
            ad_account_id=ad_account_id,
            fields=fields,
            limit=limit
        )
        
        # First, get total count
        count_result = await self._execute_with_retry(
            lambda: self.client.get(
                f"{ad_account_id}/campaigns",
                params={"fields": "id", "limit": 1}
            ),
            "count_campaigns"
        )
        
        if not count_result.success:
            return [count_result]
        
        total_count = count_result.data.get("data", [])
        if not total_count:
            return [BulkOperationResult(success=True, data=[])]
        
        # Calculate chunks needed
        chunks_needed = (limit + self.config.chunk_size - 1) // self.config.chunk_size
        chunk_tasks = []
        
        for i in range(chunks_needed):
            chunk_offset = i * self.config.chunk_size
            chunk_limit = min(self.config.chunk_size, limit - chunk_offset)
            chunk_cache_key = cache_key(
                "campaigns",
                ad_account_id,
                "fields", ",".join(fields),
                "offset", chunk_offset,
                "limit", chunk_limit,
                "filters", str(filters) if filters else "none"
            )
            task = self._execute_with_retry(
                lambda offset=chunk_offset, limit_size=chunk_limit, key=chunk_cache_key: self._fetch_cached_or_execute(
                    key,
                    lambda offset=offset, limit_size=limit_size: self.client.get(
                        f"{ad_account_id}/campaigns",
                        params={
                            "fields": ",".join(fields),
                            "limit": limit_size,
                            "offset": offset,
                            **(filters or {})
                        }
                    ),
                    cache_ttl_for_endpoint("campaigns")
                ),
                f"fetch_campaigns_chunk_{i}"
            )
            chunk_tasks.append(task)
        
        # Execute all chunks in parallel
        results = await asyncio.gather(*chunk_tasks, return_exceptions=True)
        
        # Process results
        processed_results = []
        all_campaigns = []
        
        for i, result in enumerate(results):
            if isinstance(result, Exception):
                processed_results.append(BulkOperationResult(
                    success=False,
                    error=str(result),
                    operation_id=f"fetch_campaigns_chunk_{i}"
                ))
            elif isinstance(result, BulkOperationResult):
                processed_results.append(result)
                if result.success and result.data:
                    campaigns = result.data.get("data", [])
                    all_campaigns.extend(campaigns)
            else:
                # Direct result from cache
                campaigns = result.get("data", []) if isinstance(result, dict) else []
                all_campaigns.extend(campaigns)
                processed_results.append(BulkOperationResult(
                    success=True,
                    data=result,
                    operation_id=f"fetch_campaigns_chunk_{i}"
                ))
        
        # Update stats
        self._stats.total_operations += len(processed_results)
        self._stats.successful_operations += sum(1 for r in processed_results if r.success)
        self._stats.failed_operations += sum(1 for r in processed_results if not r.success)
        
        logger.info(
            "Bulk campaign fetch completed",
            total_campaigns=len(all_campaigns),
            successful_chunks=sum(1 for r in processed_results if r.success),
            failed_chunks=sum(1 for r in processed_results if not r.success)
        )
        
        return processed_results
    
    async def fetch_insights(
        self,
        ad_account_id: str,
        object_ids: List[str],
        fields: List[str],
        level: str = "ad",
        time_range: Optional[Dict[str, str]] = None,
        breakdowns: Optional[List[str]] = None
    ) -> List[BulkOperationResult]:
        """Fetch insights for multiple objects in parallel."""
        logger.info(
            "Starting bulk insights fetch",
            ad_account_id=ad_account_id,
            object_count=len(object_ids),
            insights_level=level
        )
        
        # Chunk object IDs
        object_chunks = list(chunk(object_ids, self.config.chunk_size))
        insight_tasks = []
        
        for i, chunk_ids in enumerate(object_chunks):
            chunk_object_ids = list(chunk_ids)
            chunk_cache_key = cache_key(
                "insights",
                ad_account_id,
                "level", level,
                "fields", ",".join(fields),
                "objects", ",".join(chunk_object_ids),
                "time_range", str(time_range) if time_range else "none",
                "breakdowns", ",".join(breakdowns) if breakdowns else "none"
            )
            task = self._execute_with_retry(
                lambda object_ids=chunk_object_ids, key=chunk_cache_key: self._fetch_cached_or_execute(
                    key,
                    lambda object_ids=object_ids: self.client.get(
                        f"{ad_account_id}/insights",
                        params={
                            "fields": ",".join(fields),
                            "level": level,
                            "filtering": [{"field": "id", "operator": "IN", "value": object_ids}],
                            "time_range": time_range,
                            "breakdowns": ",".join(breakdowns) if breakdowns else None
                        }
                    ),
                    cache_ttl_for_endpoint("insights")
                ),
                f"fetch_insights_chunk_{i}"
            )
            insight_tasks.append(task)
        
        # Execute all chunks in parallel
        results = await asyncio.gather(*insight_tasks, return_exceptions=True)
        
        # Process results
        processed_results = []
        all_insights = []
        
        for i, result in enumerate(results):
            if isinstance(result, Exception):
                processed_results.append(BulkOperationResult(
                    success=False,
                    error=str(result),
                    operation_id=f"fetch_insights_chunk_{i}"
                ))
            elif isinstance(result, BulkOperationResult):
                processed_results.append(result)
                if result.success and result.data:
                    insights = result.data.get("data", [])
                    all_insights.extend(insights)
            else:
                # Direct result from cache
                insights = result.get("data", []) if isinstance(result, dict) else []
                all_insights.extend(insights)
                processed_results.append(BulkOperationResult(
                    success=True,
                    data=result,
                    operation_id=f"fetch_insights_chunk_{i}"
                ))
        
        # Update stats
        self._stats.total_operations += len(processed_results)
        self._stats.successful_operations += sum(1 for r in processed_results if r.success)
        self._stats.failed_operations += sum(1 for r in processed_results if not r.success)
        
        logger.info(
            "Bulk insights fetch completed",
            total_insights=len(all_insights),
            successful_chunks=sum(1 for r in processed_results if r.success),
            failed_chunks=sum(1 for r in processed_results if not r.success)
        )
        
        return processed_results
    
    async def bulk_update(
        self,
        updates: List[Dict[str, Any]],
        operation_type: str = "campaigns"
    ) -> List[BulkOperationResult]:
        """Perform bulk updates using Meta's batch API."""
        logger.info(
            "Starting bulk update",
            operation_type=operation_type,
            update_count=len(updates)
        )
        
        # Chunk updates for batch API (max 50 per batch)
        update_chunks = list(chunk(updates, 50))
        update_tasks = []
        
        for i, chunk_updates in enumerate(update_chunks):
            # Prepare batch operations
            batch_operations = []
            for update in chunk_updates:
                object_id = update.get("id")
                update_data = {k: v for k, v in update.items() if k != "id"}
                
                batch_operations.append({
                    "method": "POST",
                    "relative_url": f"{object_id}",
                    "body": "&".join([f"{k}={v}" for k, v in update_data.items()])
                })
            
            task = self._execute_with_retry(
                lambda batch_ops=list(batch_operations): self.client.batch(batch_ops),
                f"bulk_update_batch_{i}"
            )
            update_tasks.append(task)
        
        # Execute all batches in parallel
        results = await asyncio.gather(*update_tasks, return_exceptions=True)
        
        # Process results
        processed_results = []
        all_responses = []
        
        for i, result in enumerate(results):
            if isinstance(result, Exception):
                processed_results.append(BulkOperationResult(
                    success=False,
                    error=str(result),
                    operation_id=f"bulk_update_batch_{i}"
                ))
            elif isinstance(result, BulkOperationResult):
                processed_results.append(result)
                if result.success and result.data:
                    all_responses.extend(result.data)
            else:
                # Direct result
                if isinstance(result, list):
                    all_responses.extend(result)
                processed_results.append(BulkOperationResult(
                    success=True,
                    data=result,
                    operation_id=f"bulk_update_batch_{i}"
                ))
        
        # Update stats
        self._stats.total_operations += len(processed_results)
        self._stats.successful_operations += sum(1 for r in processed_results if r.success)
        self._stats.failed_operations += sum(1 for r in processed_results if not r.success)
        
        logger.info(
            "Bulk update completed",
            total_responses=len(all_responses),
            successful_batches=sum(1 for r in processed_results if r.success),
            failed_batches=sum(1 for r in processed_results if not r.success)
        )
        
        return processed_results
    
    async def parallel_insights_by_campaign(
        self,
        ad_account_id: str,
        campaign_ids: List[str],
        fields: List[str],
        time_range: Optional[Dict[str, str]] = None
    ) -> Dict[str, Any]:
        """Fetch insights for multiple campaigns in parallel and aggregate results."""
        logger.info(
            "Starting parallel insights by campaign",
            ad_account_id=ad_account_id,
            campaign_count=len(campaign_ids)
        )
        
        # Create tasks for each campaign
        insight_tasks = []
        
        for campaign_id in campaign_ids:
            campaign_cache_key = cache_key(
                "campaign_insights",
                campaign_id,
                "fields", ",".join(fields),
                "time_range", str(time_range) if time_range else "none"
            )
            task = self._execute_with_retry(
                lambda cid=campaign_id, key=campaign_cache_key: self._fetch_cached_or_execute(
                    key,
                    lambda cid=cid: self.client.get(
                        f"{cid}/insights",
                        params={
                            "fields": ",".join(fields),
                            "time_range": time_range
                        }
                    ),
                    cache_ttl_for_endpoint("insights")
                ),
                f"insights_campaign_{campaign_id}"
            )
            insight_tasks.append((campaign_id, task))
        
        # Execute all tasks in parallel
        results = await asyncio.gather(
            *[task for _, task in insight_tasks],
            return_exceptions=True
        )
        
        # Aggregate results by campaign
        aggregated_results = {}
        successful_campaigns = 0
        failed_campaigns = 0
        
        for i, (campaign_id, _) in enumerate(insight_tasks):
            result = results[i]
            
            if isinstance(result, Exception):
                aggregated_results[campaign_id] = {
                    "success": False,
                    "error": str(result)
                }
                failed_campaigns += 1
            elif isinstance(result, BulkOperationResult):
                if result.success and result.data:
                    insights = result.data.get("data", [])
                    aggregated_results[campaign_id] = {
                        "success": True,
                        "insights": insights[0] if insights else {}
                    }
                    successful_campaigns += 1
                else:
                    aggregated_results[campaign_id] = {
                        "success": False,
                        "error": result.error
                    }
                    failed_campaigns += 1
            else:
                # Direct result from cache
                insights = result.get("data", []) if isinstance(result, dict) else []
                aggregated_results[campaign_id] = {
                    "success": True,
                    "insights": insights[0] if insights else {}
                }
                successful_campaigns += 1
        
        logger.info(
            "Parallel insights by campaign completed",
            successful_campaigns=successful_campaigns,
            failed_campaigns=failed_campaigns
        )
        
        return aggregated_results
    
    def get_stats(self) -> BulkOperationStats:
        """Get current operation statistics."""
        if self._stats.total_operations > 0:
            self._stats.average_duration = self._stats.total_duration / self._stats.total_operations
        return self._stats
    
    def reset_stats(self) -> None:
        """Reset operation statistics."""
        self._stats = BulkOperationStats()


# Utility functions for common bulk operations
async def fetch_all_campaigns(
    client: MetaClient,
    ad_account_id: str,
    fields: List[str] = None,
    config: Optional[BulkOperationConfig] = None
) -> List[Dict[str, Any]]:
    """Fetch all campaigns for an ad account with parallel processing."""
    if fields is None:
        fields = ["id", "name", "status", "objective", "created_time", "updated_time"]
    
    manager = BulkOperationManager(client, config)
    results = await manager.fetch_campaigns(ad_account_id, fields)
    
    # Extract successful results
    all_campaigns = []
    for result in results:
        if result.success and result.data:
            campaigns = result.data.get("data", [])
            all_campaigns.extend(campaigns)
    
    return all_campaigns


async def fetch_campaign_insights_bulk(
    client: MetaClient,
    ad_account_id: str,
    campaign_ids: List[str],
    fields: List[str] = None,
    time_range: Optional[Dict[str, str]] = None,
    config: Optional[BulkOperationConfig] = None
) -> Dict[str, Any]:
    """Fetch insights for multiple campaigns in parallel."""
    if fields is None:
        fields = ["impressions", "spend", "clicks", "ctr", "cpm", "cpc"]
    
    manager = BulkOperationManager(client, config)
    return await manager.parallel_insights_by_campaign(
        ad_account_id, campaign_ids, fields, time_range
    )


async def bulk_update_campaign_status(
    client: MetaClient,
    campaign_ids: List[str],
    status: str,
    config: Optional[BulkOperationConfig] = None
) -> List[BulkOperationResult]:
    """Update status for multiple campaigns in bulk."""
    updates = [{"id": cid, "status": status} for cid in campaign_ids]
    
    manager = BulkOperationManager(client, config)
    return await manager.bulk_update(updates, "campaigns")
