"""
Outbound Call Scheduler
Manages scheduled outbound calls with priority queue and retry logic
"""

import asyncio
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
from dataclasses import dataclass, field
from enum import Enum
import heapq
import httpx
from pathlib import Path
import json

class CallPriority(Enum):
    HIGH = 1      # Payment reminders, urgent matters
    NORMAL = 2    # General outreach
    LOW = 3       # Marketing, surveys

class CampaignStatus(Enum):
    SCHEDULED = "scheduled"
    IN_PROGRESS = "in_progress"
    COMPLETED = "completed"
    FAILED = "failed"
    RETRY = "retry"

@dataclass(order=True)
class ScheduledCall:
    """
    Priority queue item for scheduled calls
    """
    priority: int
    scheduled_time: datetime = field(compare=False)
    call_id: str = field(compare=False)
    phone: str = field(compare=False)
    campaign_type: str = field(compare=False)
    customer_id: str = field(compare=False)
    metadata: Dict[str, Any] = field(default_factory=dict, compare=False)
    retry_count: int = field(default=0, compare=False)
    max_retries: int = field(default=3, compare=False)
    status: CampaignStatus = field(default=CampaignStatus.SCHEDULED, compare=False)

class OutboundCallScheduler:
    """
    Manages outbound call scheduling with priority queue
    """
    
    def __init__(
        self,
        api_base_url: str = "http://localhost:8000",
        webhook_url: str = "http://localhost:8001",
        max_concurrent_calls: int = 10,
        retry_delay_minutes: int = 30
    ):
        self.api_base_url = api_base_url
        self.webhook_url = webhook_url
        self.max_concurrent_calls = max_concurrent_calls
        self.retry_delay_minutes = retry_delay_minutes
        
        # Priority queue for scheduled calls
        self.call_queue: List[ScheduledCall] = []
        self.active_calls: Dict[str, ScheduledCall] = {}
        
        # Campaign configurations
        self.campaigns = {
            "payment_reminder": {
                "priority": CallPriority.HIGH,
                "retry_enabled": True,
                "max_retries": 3,
                "best_time_hours": [9, 10, 11, 14, 15, 16]  # Best hours to call
            },
            "loan_offer": {
                "priority": CallPriority.NORMAL,
                "retry_enabled": True,
                "max_retries": 2,
                "best_time_hours": [10, 11, 14, 15]
            },
            "survey": {
                "priority": CallPriority.LOW,
                "retry_enabled": False,
                "max_retries": 1,
                "best_time_hours": [10, 14, 15]
            }
        }
        
        # Statistics
        self.stats = {
            "total_scheduled": 0,
            "total_completed": 0,
            "total_failed": 0,
            "total_retries": 0
        }
    
    def schedule_call(
        self,
        phone: str,
        campaign_type: str,
        scheduled_time: Optional[datetime] = None,
        priority: Optional[CallPriority] = None,
        metadata: Dict[str, Any] = None
    ) -> str:
        """
        Schedule a new outbound call
        
        Returns:
            call_id: Unique identifier for the call
        """
        # Generate call ID
        call_id = f"CALL{datetime.now().strftime('%Y%m%d%H%M%S%f')}"
        
        # Get campaign config
        campaign_config = self.campaigns.get(
            campaign_type,
            self.campaigns["survey"]  # Default
        )
        
        # Determine priority
        if priority is None:
            priority = campaign_config["priority"]
        
        # Determine scheduled time
        if scheduled_time is None:
            # Schedule for next available time slot
            scheduled_time = self._get_next_available_slot(campaign_type)
        
        # Create scheduled call
        scheduled_call = ScheduledCall(
            priority=priority.value,
            scheduled_time=scheduled_time,
            call_id=call_id,
            phone=phone,
            campaign_type=campaign_type,
            customer_id=metadata.get("customer_id", "") if metadata else "",
            metadata=metadata or {},
            max_retries=campaign_config["max_retries"]
        )
        
        # Add to priority queue
        heapq.heappush(self.call_queue, scheduled_call)
        self.stats["total_scheduled"] += 1
        
        print(f"✅ Scheduled call {call_id} for {phone} at {scheduled_time}")
        
        return call_id
    
    def schedule_bulk_calls(
        self,
        calls: List[Dict[str, Any]]
    ) -> List[str]:
        """
        Schedule multiple calls at once
        
        Args:
            calls: List of call configurations
            
        Returns:
            List of call IDs
        """
        call_ids = []
        
        for call_config in calls:
            call_id = self.schedule_call(
                phone=call_config["phone"],
                campaign_type=call_config["campaign_type"],
                scheduled_time=call_config.get("scheduled_time"),
                priority=call_config.get("priority"),
                metadata=call_config.get("metadata")
            )
            call_ids.append(call_id)
        
        print(f"📋 Scheduled {len(call_ids)} bulk calls")
        
        return call_ids
    
    async def start_scheduler(self):
        """
        Start the call scheduler loop
        """
        print("🚀 Outbound call scheduler started")
        
        while True:
            try:
                # Check for due calls
                await self._process_due_calls()
                
                # Wait before next check
                await asyncio.sleep(10)  # Check every 10 seconds
                
            except Exception as e:
                print(f"❌ Scheduler error: {e}")
                await asyncio.sleep(30)
    
    async def _process_due_calls(self):
        """
        Process calls that are due to be made
        """
        now = datetime.now()
        
        # Check if we can make more calls
        while (
            len(self.active_calls) < self.max_concurrent_calls 
            and self.call_queue
        ):
            # Peek at next call
            next_call = self.call_queue[0]
            
            # Check if it's time
            if next_call.scheduled_time <= now:
                # Pop from queue
                scheduled_call = heapq.heappop(self.call_queue)
                
                # Make the call
                await self._initiate_call(scheduled_call)
            else:
                # Next call isn't due yet
                break
    
    async def _initiate_call(self, scheduled_call: ScheduledCall):
        """
        Initiate an outbound call
        """
        print(f"📞 Initiating call {scheduled_call.call_id} to {scheduled_call.phone}")
        
        try:
            # Update status
            scheduled_call.status = CampaignStatus.IN_PROGRESS
            self.active_calls[scheduled_call.call_id] = scheduled_call
            
            # Make API request to WhatsApp integration
            async with httpx.AsyncClient() as client:
                response = await client.post(
                    f"{self.webhook_url}/voice/outbound/trigger",
                    params={
                        "phone": scheduled_call.phone,
                        "campaign_type": scheduled_call.campaign_type
                    },
                    timeout=30.0
                )
                
                if response.status_code == 200:
                    result = response.json()
                    scheduled_call.metadata["call_sid"] = result.get("call_sid")
                    print(f"✅ Call initiated: {result.get('call_sid')}")
                    
                    # Schedule callback check
                    asyncio.create_task(
                        self._check_call_completion(scheduled_call)
                    )
                else:
                    raise Exception(f"API returned {response.status_code}")
                    
        except Exception as e:
            print(f"❌ Failed to initiate call: {e}")
            await self._handle_call_failure(scheduled_call, str(e))
    
    async def _check_call_completion(self, scheduled_call: ScheduledCall):
        """
        Check if call has completed and update status
        """
        # Wait for call to complete (max 5 minutes)
        max_wait = 300  # seconds
        waited = 0
        
        while waited < max_wait:
            await asyncio.sleep(10)
            waited += 10
            
            # Check call status via API
            # In production, this would check Twilio's call status
            # For now, simulate completion after 60 seconds
            if waited >= 60:
                await self._handle_call_completion(
                    scheduled_call,
                    success=True
                )
                break
    
    async def _handle_call_completion(
        self, 
        scheduled_call: ScheduledCall,
        success: bool = True
    ):
        """
        Handle call completion
        """
        # Remove from active calls
        if scheduled_call.call_id in self.active_calls:
            del self.active_calls[scheduled_call.call_id]
        
        if success:
            scheduled_call.status = CampaignStatus.COMPLETED
            self.stats["total_completed"] += 1
            print(f"✅ Call {scheduled_call.call_id} completed successfully")
        else:
            await self._handle_call_failure(scheduled_call, "Call failed")
    
    async def _handle_call_failure(
        self, 
        scheduled_call: ScheduledCall,
        reason: str
    ):
        """
        Handle call failure with retry logic
        """
        # Remove from active calls
        if scheduled_call.call_id in self.active_calls:
            del self.active_calls[scheduled_call.call_id]
        
        campaign_config = self.campaigns.get(scheduled_call.campaign_type, {})
        
        # Check if we should retry
        if (
            campaign_config.get("retry_enabled", False) 
            and scheduled_call.retry_count < scheduled_call.max_retries
        ):
            # Schedule retry
            scheduled_call.retry_count += 1
            scheduled_call.scheduled_time = datetime.now() + timedelta(
                minutes=self.retry_delay_minutes
            )
            scheduled_call.status = CampaignStatus.RETRY
            
            # Add back to queue
            heapq.heappush(self.call_queue, scheduled_call)
            self.stats["total_retries"] += 1
            
            print(f"🔄 Retry {scheduled_call.retry_count}/{scheduled_call.max_retries} "
                  f"scheduled for {scheduled_call.call_id}")
        else:
            # Mark as failed
            scheduled_call.status = CampaignStatus.FAILED
            self.stats["total_failed"] += 1
            
            print(f"❌ Call {scheduled_call.call_id} failed: {reason}")
    
    def _get_next_available_slot(self, campaign_type: str) -> datetime:
        """
        Get next available time slot based on campaign best practices
        """
        campaign_config = self.campaigns.get(campaign_type, {})
        best_hours = campaign_config.get("best_time_hours", [10, 14, 15])
        
        now = datetime.now()
        current_hour = now.hour
        
        # If current hour is in best hours and it's not too late
        if current_hour in best_hours and now.minute < 45:
            # Schedule for 15 minutes from now
            return now + timedelta(minutes=15)
        
        # Find next best hour today
        for hour in best_hours:
            if hour > current_hour:
                return now.replace(
                    hour=hour, 
                    minute=0, 
                    second=0, 
                    microsecond=0
                )
        
        # Schedule for first best hour tomorrow
        tomorrow = now + timedelta(days=1)
        return tomorrow.replace(
            hour=best_hours[0], 
            minute=0, 
            second=0, 
            microsecond=0
        )
    
    def get_queue_status(self) -> Dict[str, Any]:
        """
        Get current queue status
        """
        return {
            "queue_length": len(self.call_queue),
            "active_calls": len(self.active_calls),
            "stats": self.stats,
            "next_scheduled": (
                self.call_queue[0].scheduled_time.isoformat() 
                if self.call_queue 
                else None
            )
        }
    
    def get_campaign_stats(self, campaign_type: str) -> Dict[str, Any]:
        """
        Get statistics for a specific campaign
        """
        campaign_calls = [
            call for call in self.call_queue 
            if call.campaign_type == campaign_type
        ]
        
        return {
            "campaign_type": campaign_type,
            "scheduled": len([c for c in campaign_calls if c.status == CampaignStatus.SCHEDULED]),
            "in_progress": len([c for c in campaign_calls if c.status == CampaignStatus.IN_PROGRESS]),
            "completed": len([c for c in campaign_calls if c.status == CampaignStatus.COMPLETED]),
            "failed": len([c for c in campaign_calls if c.status == CampaignStatus.FAILED])
        }

# Example campaign configurations
async def schedule_payment_reminder_campaign(
    scheduler: OutboundCallScheduler,
    customers: List[Dict[str, Any]]
):
    """
    Schedule payment reminder calls for multiple customers
    """
    calls = []
    
    for customer in customers:
        # Get loan info
        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"{scheduler.api_base_url}/api/v1/loans/active",
                params={"phone": customer["phone"]}
            )
            loans = response.json()
            
            if loans:
                active_loan = loans[0]
                due_date = datetime.fromisoformat(
                    active_loan["due_date"].replace('Z', '')
                )
                
                # Schedule reminder 3 days before due date
                reminder_time = due_date - timedelta(days=3)
                
                if reminder_time > datetime.now():
                    calls.append({
                        "phone": customer["phone"],
                        "campaign_type": "payment_reminder",
                        "scheduled_time": reminder_time,
                        "priority": CallPriority.HIGH,
                        "metadata": {
                            "customer_id": customer["customer_id"],
                            "loan_id": active_loan["loan_id"],
                            "amount_due": active_loan["outstanding_balance"],
                            "due_date": active_loan["due_date"]
                        }
                    })
    
    # Schedule all calls
    call_ids = scheduler.schedule_bulk_calls(calls)
    
    print(f"📅 Scheduled {len(call_ids)} payment reminders")
    
    return call_ids

# CLI/Testing
async def main():
    """
    Test the scheduler
    """
    scheduler = OutboundCallScheduler()
    
    # Schedule some test calls
    scheduler.schedule_call(
        phone="+255712345678",
        campaign_type="payment_reminder",
        metadata={
            "customer_id": "CUST001",
            "loan_id": "LOAN001"
        }
    )
    
    scheduler.schedule_call(
        phone="+254722123456",
        campaign_type="loan_offer",
        scheduled_time=datetime.now() + timedelta(minutes=5)
    )
    
    # Print status
    status = scheduler.get_queue_status()
    print(f"\n📊 Queue Status: {json.dumps(status, indent=2)}")
    
    # Start scheduler
    await scheduler.start_scheduler()

if __name__ == "__main__":
    asyncio.run(main())
