import csv
import io
from datetime import datetime, timedelta
from typing import List, Dict, Optional, Tuple
from django.db import transaction
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.core.cache import cache
from django.db.models import Q, Count, Avg
from .models import Channel, Zone, ChannelZone, Show, EPGEntry, Jingle, Codec


class ChannelService:
    """
    Service class for Channel-related business logic.
    
    Handles complex operations for channel management,
    zone assignments, and configuration validation.
    """
    
    @staticmethod
    def create_channel_with_zones(channel_data: Dict, zone_configs: List[Dict]) -> Channel:
        """
        Create a channel with multiple zone configurations.
        
        Args:
            channel_data: Dictionary containing channel information
            zone_configs: List of zone configuration dictionaries
        
        Returns:
            Channel: Created channel instance
        
        Raises:
            ValidationError: If validation fails
        """
        with transaction.atomic():
            # Create the channel
            channel = Channel.objects.create(**channel_data)
            
            # Create zone configurations
            for config in zone_configs:
                zone_id = config.pop('zone_id')
                zone = Zone.objects.get(id=zone_id, is_active=True)
                
                ChannelZone.objects.create(
                    channel=channel,
                    zone=zone,
                    **config
                )
            
            # Clear related caches
            cache.delete_many([
                'channels_list',
                'channels_stats',
                f'channel_zones_{channel.id}'
            ])
            
            return channel
    
    @staticmethod
    def update_channel_zones(channel: Channel, zone_configs: List[Dict]) -> None:
        """
        Update channel zone configurations.
        
        Args:
            channel: Channel instance to update
            zone_configs: List of new zone configurations
        
        Raises:
            ValidationError: If validation fails
        """
        with transaction.atomic():
            # Remove existing configurations
            channel.channel_zones.all().delete()
            
            # Create new configurations
            for config in zone_configs:
                zone_id = config.pop('zone_id')
                zone = Zone.objects.get(id=zone_id, is_active=True)
                
                ChannelZone.objects.create(
                    channel=channel,
                    zone=zone,
                    **config
                )
            
            # Clear related caches
            cache.delete_many([
                'channels_list',
                f'channel_detail_{channel.id}',
                f'channel_zones_{channel.id}'
            ])
    
    @staticmethod
    def get_channel_statistics() -> Dict:
        """
        Get comprehensive channel statistics.
        
        Returns:
            Dict: Statistics including counts and averages
        """
        cache_key = 'channels_stats'
        stats = cache.get(cache_key)
        
        if stats is None:
            stats = {
                'total_channels': Channel.objects.count(),
                'active_channels': Channel.objects.filter(is_active=True).count(),
                'hd_channels': Channel.objects.filter(is_hd=True).count(),
                'uhd_channels': Channel.objects.filter(is_4k=True).count(),
                'channels_by_type': dict(
                    Channel.objects.values('channel_type')
                    .annotate(count=Count('id'))
                    .values_list('channel_type', 'count')
                ),
                'zones_coverage': dict(
                    Zone.objects.annotate(
                        channels_count=Count('zone_channels')
                    ).values_list('name', 'channels_count')
                ),
                'avg_zones_per_channel': ChannelZone.objects.values('channel')
                .annotate(zones_count=Count('zone'))
                .aggregate(avg=Avg('zones_count'))['avg'] or 0
            }
            
            # Cache for 1 hour
            cache.set(cache_key, stats, 3600)
        
        return stats
    
    @staticmethod
    def validate_channel_configuration(channel: Channel) -> List[str]:
        """
        Validate channel configuration and return warnings.
        
        Args:
            channel: Channel instance to validate
        
        Returns:
            List[str]: List of validation warnings
        """
        warnings = []
        
        # Check if channel has any zone configurations
        if not channel.channel_zones.exists():
            warnings.append("Channel has no zone configurations")
        
        # Check for inactive zones
        inactive_zones = channel.channel_zones.filter(zone__is_active=False)
        if inactive_zones.exists():
            zone_names = ', '.join(inactive_zones.values_list('zone__name', flat=True))
            warnings.append(f"Channel is configured for inactive zones: {zone_names}")
        
        # Check for missing codecs
        configs_without_video = channel.channel_zones.filter(video_codec__isnull=True)
        if configs_without_video.exists():
            warnings.append("Some zone configurations are missing video codec")
        
        configs_without_audio = channel.channel_zones.filter(audio_codec__isnull=True)
        if configs_without_audio.exists():
            warnings.append("Some zone configurations are missing audio codec")
        
        # Check for expired configurations
        expired_configs = channel.channel_zones.filter(
            end_date__lt=timezone.now().date()
        )
        if expired_configs.exists():
            warnings.append("Channel has expired zone configurations")
        
        # Check for jingles
        if not channel.jingles.filter(is_active=True).exists():
            warnings.append("Channel has no active jingles")
        
        return warnings


class EPGService:
    """
    Service class for EPG (Electronic Program Guide) operations.
    
    Handles EPG data import, export, scheduling validation,
    and conflict resolution.
    """
    
    @staticmethod
    def import_epg_from_csv(csv_content: str, channel_id: int, overwrite: bool = False) -> Dict:
        """
        Import EPG entries from CSV content.
        
        Args:
            csv_content: CSV content as string
            channel_id: Target channel ID
            overwrite: Whether to overwrite existing entries
        
        Returns:
            Dict: Import results with counts and errors
        """
        results = {
            'success_count': 0,
            'error_count': 0,
            'errors': [],
            'warnings': []
        }
        
        try:
            channel = Channel.objects.get(id=channel_id)
        except Channel.DoesNotExist:
            results['errors'].append('Channel not found')
            return results
        
        # Parse CSV content
        csv_file = io.StringIO(csv_content)
        reader = csv.DictReader(csv_file)
        
        with transaction.atomic():
            for row_num, row in enumerate(reader, start=2):  # Start from 2 (header is row 1)
                try:
                    # Parse required fields
                    show_title = row.get('show_title', '').strip()
                    start_time_str = row.get('start_time', '').strip()
                    end_time_str = row.get('end_time', '').strip()
                    
                    if not all([show_title, start_time_str, end_time_str]):
                        results['errors'].append(
                            f"Row {row_num}: Missing required fields (show_title, start_time, end_time)"
                        )
                        results['error_count'] += 1
                        continue
                    
                    # Parse datetime fields
                    try:
                        start_time = datetime.strptime(start_time_str, '%Y-%m-%d %H:%M')
                        end_time = datetime.strptime(end_time_str, '%Y-%m-%d %H:%M')
                        start_time = timezone.make_aware(start_time)
                        end_time = timezone.make_aware(end_time)
                    except ValueError as e:
                        results['errors'].append(
                            f"Row {row_num}: Invalid datetime format - {str(e)}"
                        )
                        results['error_count'] += 1
                        continue
                    
                    # Validate time range
                    if end_time <= start_time:
                        results['errors'].append(
                            f"Row {row_num}: End time must be after start time"
                        )
                        results['error_count'] += 1
                        continue
                    
                    # Find or create show
                    show, created = Show.objects.get_or_create(
                        title=show_title,
                        defaults={
                            'show_type': row.get('show_type', 'other'),
                            'description': row.get('description', ''),
                            'content_rating': row.get('content_rating', 'general'),
                            'language': row.get('language', 'en'),
                            'year': int(row.get('year', timezone.now().year))
                        }
                    )
                    
                    # Check for conflicts if not overwriting
                    if not overwrite:
                        conflicts = EPGEntry.objects.filter(
                            channel=channel,
                            start_time__lt=end_time,
                            end_time__gt=start_time
                        )
                        
                        if conflicts.exists():
                            conflict = conflicts.first()
                            results['warnings'].append(
                                f"Row {row_num}: Schedule conflict with existing entry "
                                f"'{conflict.show.title}' - skipped"
                            )
                            continue
                    else:
                        # Remove conflicting entries
                        EPGEntry.objects.filter(
                            channel=channel,
                            start_time__lt=end_time,
                            end_time__gt=start_time
                        ).delete()
                    
                    # Create EPG entry
                    EPGEntry.objects.create(
                        channel=channel,
                        show=show,
                        start_time=start_time,
                        end_time=end_time,
                        episode_title=row.get('episode_title', ''),
                        episode_number=int(row.get('episode_number', 0)) or None,
                        season_number=int(row.get('season_number', 0)) or None,
                        episode_description=row.get('episode_description', ''),
                        is_premiere=row.get('is_premiere', '').lower() == 'true',
                        is_finale=row.get('is_finale', '').lower() == 'true',
                        is_repeat=row.get('is_repeat', '').lower() == 'true',
                        is_live=row.get('is_live', '').lower() == 'true',
                        audio_language=row.get('audio_language', 'en'),
                        subtitle_language=row.get('subtitle_language', '')
                    )
                    
                    results['success_count'] += 1
                    
                except Exception as e:
                    results['errors'].append(
                        f"Row {row_num}: Unexpected error - {str(e)}"
                    )
                    results['error_count'] += 1
        
        # Clear EPG caches
        cache.delete_many([
            f'epg_entries_{channel_id}',
            'epg_stats',
            f'channel_detail_{channel_id}'
        ])
        
        return results
    
    @staticmethod
    def export_epg_to_csv(channel_id: Optional[int] = None, 
                         start_date: Optional[datetime] = None,
                         end_date: Optional[datetime] = None) -> str:
        """
        Export EPG entries to CSV format.
        
        Args:
            channel_id: Optional channel ID filter
            start_date: Optional start date filter
            end_date: Optional end date filter
        
        Returns:
            str: CSV content
        """
        # Build query
        queryset = EPGEntry.objects.select_related('channel', 'show')
        
        if channel_id:
            queryset = queryset.filter(channel_id=channel_id)
        
        if start_date:
            queryset = queryset.filter(start_time__gte=start_date)
        
        if end_date:
            queryset = queryset.filter(end_time__lte=end_date)
        
        queryset = queryset.order_by('channel__name', 'start_time')
        
        # Generate CSV
        output = io.StringIO()
        writer = csv.writer(output)
        
        # Write header
        writer.writerow([
            'channel_name', 'show_title', 'show_type', 'start_time', 'end_time',
            'episode_title', 'episode_number', 'season_number', 'episode_description',
            'is_premiere', 'is_finale', 'is_repeat', 'is_live',
            'audio_language', 'subtitle_language', 'content_rating',
            'description', 'language', 'year'
        ])
        
        # Write data rows
        for entry in queryset:
            writer.writerow([
                entry.channel.name,
                entry.show.title,
                entry.show.show_type,
                entry.start_time.strftime('%Y-%m-%d %H:%M'),
                entry.end_time.strftime('%Y-%m-%d %H:%M'),
                entry.episode_title or '',
                entry.episode_number or '',
                entry.season_number or '',
                entry.episode_description or '',
                entry.is_premiere,
                entry.is_finale,
                entry.is_repeat,
                entry.is_live,
                entry.audio_language,
                entry.subtitle_language or '',
                entry.show.content_rating,
                entry.show.description or '',
                entry.show.language,
                entry.show.year or ''
            ])
        
        return output.getvalue()
    
    @staticmethod
    def get_schedule_conflicts(channel_id: int, days: int = 7) -> List[Dict]:
        """
        Get scheduling conflicts for a channel.
        
        Args:
            channel_id: Channel ID to check
            days: Number of days to check ahead
        
        Returns:
            List[Dict]: List of conflict information
        """
        start_date = timezone.now()
        end_date = start_date + timedelta(days=days)
        
        entries = EPGEntry.objects.filter(
            channel_id=channel_id,
            start_time__gte=start_date,
            start_time__lte=end_date
        ).order_by('start_time')
        
        conflicts = []
        previous_entry = None
        
        for entry in entries:
            if previous_entry and entry.start_time < previous_entry.end_time:
                conflicts.append({
                    'entry1': {
                        'id': previous_entry.id,
                        'show': previous_entry.show.title,
                        'start_time': previous_entry.start_time,
                        'end_time': previous_entry.end_time
                    },
                    'entry2': {
                        'id': entry.id,
                        'show': entry.show.title,
                        'start_time': entry.start_time,
                        'end_time': entry.end_time
                    },
                    'overlap_duration': (previous_entry.end_time - entry.start_time).total_seconds() / 60
                })
            
            previous_entry = entry
        
        return conflicts
    
    @staticmethod
    def get_epg_statistics(channel_id: Optional[int] = None) -> Dict:
        """
        Get EPG statistics.
        
        Args:
            channel_id: Optional channel ID filter
        
        Returns:
            Dict: EPG statistics
        """
        cache_key = f'epg_stats_{channel_id or "all"}'
        stats = cache.get(cache_key)
        
        if stats is None:
            queryset = EPGEntry.objects.all()
            if channel_id:
                queryset = queryset.filter(channel_id=channel_id)
            
            now = timezone.now()
            today = now.date()
            
            stats = {
                'total_entries': queryset.count(),
                'entries_today': queryset.filter(
                    start_time__date=today
                ).count(),
                'entries_this_week': queryset.filter(
                    start_time__gte=today - timedelta(days=today.weekday())
                ).count(),
                'live_entries': queryset.filter(is_live=True).count(),
                'premiere_entries': queryset.filter(is_premiere=True).count(),
                'repeat_entries': queryset.filter(is_repeat=True).count(),
                'currently_airing': queryset.filter(
                    start_time__lte=now,
                    end_time__gt=now
                ).count(),
                'upcoming_today': queryset.filter(
                    start_time__date=today,
                    start_time__gt=now
                ).count()
            }
            
            # Cache for 30 minutes
            cache.set(cache_key, stats, 1800)
        
        return stats


class JingleService:
    """
    Service class for Jingle management operations.
    
    Handles jingle scheduling, priority management,
    and ad break coordination.
    """
    
    @staticmethod
    def get_jingles_for_channel(channel_id: int, jingle_type: Optional[str] = None) -> List[Jingle]:
        """
        Get active jingles for a channel, ordered by priority.
        
        Args:
            channel_id: Channel ID
            jingle_type: Optional jingle type filter
        
        Returns:
            List[Jingle]: Ordered list of jingles
        """
        queryset = Jingle.objects.filter(
            channel_id=channel_id,
            is_active=True
        ).order_by('-priority', 'name')
        
        if jingle_type:
            queryset = queryset.filter(jingle_type=jingle_type)
        
        return list(queryset)
    
    @staticmethod
    def validate_jingle_files(jingle: Jingle) -> List[str]:
        """
        Validate jingle file requirements.
        
        Args:
            jingle: Jingle instance to validate
        
        Returns:
            List[str]: List of validation errors
        """
        errors = []
        
        # Check if at least one file is provided
        if not jingle.audio_file and not jingle.video_file:
            errors.append("At least one file (audio or video) must be provided")
        
        # Validate file formats (basic check)
        if jingle.audio_file:
            audio_ext = jingle.audio_file.name.split('.')[-1].lower()
            if audio_ext not in ['mp3', 'wav', 'aac', 'ogg']:
                errors.append(f"Unsupported audio format: {audio_ext}")
        
        if jingle.video_file:
            video_ext = jingle.video_file.name.split('.')[-1].lower()
            if video_ext not in ['mp4', 'avi', 'mov', 'mkv']:
                errors.append(f"Unsupported video format: {video_ext}")
        
        return errors
    
    @staticmethod
    def get_ad_break_schedule(channel_id: int, date: datetime) -> Dict:
        """
        Get ad break schedule for a channel on a specific date.
        
        Args:
            channel_id: Channel ID
            date: Target date
        
        Returns:
            Dict: Ad break schedule information
        """
        # Get EPG entries for the date
        epg_entries = EPGEntry.objects.filter(
            channel_id=channel_id,
            start_time__date=date.date()
        ).order_by('start_time')
        
        # Get ad break jingles
        ad_start_jingles = JingleService.get_jingles_for_channel(
            channel_id, 'ad_start'
        )
        ad_end_jingles = JingleService.get_jingles_for_channel(
            channel_id, 'ad_end'
        )
        
        schedule = {
            'date': date.date(),
            'channel_id': channel_id,
            'programs': [],
            'ad_breaks': [],
            'total_ad_time': timedelta(0)
        }
        
        for entry in epg_entries:
            program_info = {
                'epg_entry_id': entry.id,
                'show_title': entry.show.title,
                'start_time': entry.start_time,
                'end_time': entry.end_time,
                'duration': entry.duration,
                'ad_breaks': []
            }
            
            # Calculate ad break positions (example: every 15 minutes for shows > 30 minutes)
            if entry.duration.total_seconds() > 1800:  # 30 minutes
                break_interval = timedelta(minutes=15)
                current_time = entry.start_time + break_interval
                
                while current_time < entry.end_time - timedelta(minutes=5):
                    ad_break = {
                        'time': current_time,
                        'start_jingles': [j.name for j in ad_start_jingles[:1]],
                        'end_jingles': [j.name for j in ad_end_jingles[:1]],
                        'estimated_duration': timedelta(minutes=3)  # Standard ad break
                    }
                    
                    program_info['ad_breaks'].append(ad_break)
                    schedule['ad_breaks'].append(ad_break)
                    schedule['total_ad_time'] += ad_break['estimated_duration']
                    
                    current_time += break_interval
            
            schedule['programs'].append(program_info)
        
        return schedule


class CodecService:
    """
    Service class for Codec management operations.
    
    Handles codec validation, compatibility checks,
    and encoding recommendations.
    """
    
    @staticmethod
    def get_compatible_codecs(codec_type: str, container_format: Optional[str] = None) -> List[Codec]:
        """
        Get compatible codecs for a given type and container.
        
        Args:
            codec_type: Type of codec (video, audio, container)
            container_format: Optional container format filter
        
        Returns:
            List[Codec]: List of compatible codecs
        """
        queryset = Codec.objects.filter(
            codec_type=codec_type,
            is_active=True
        ).order_by('name')
        
        # Add container compatibility logic here if needed
        # This is a simplified version
        
        return list(queryset)
    
    @staticmethod
    def validate_codec_configuration(video_codec: Codec, audio_codec: Codec, 
                                   container_codec: Codec) -> List[str]:
        """
        Validate codec configuration compatibility.
        
        Args:
            video_codec: Video codec instance
            audio_codec: Audio codec instance
            container_codec: Container codec instance
        
        Returns:
            List[str]: List of compatibility warnings
        """
        warnings = []
        
        # Basic type validation
        if video_codec.codec_type != 'video':
            warnings.append("Selected video codec is not a video codec")
        
        if audio_codec.codec_type != 'audio':
            warnings.append("Selected audio codec is not an audio codec")
        
        if container_codec.codec_type != 'container':
            warnings.append("Selected container format is not a container codec")
        
        # Add specific compatibility rules here
        # This is a simplified example
        
        return warnings
    
    @staticmethod
    def get_encoding_recommendations(resolution_width: int, resolution_height: int,
                                   frame_rate: float) -> Dict:
        """
        Get encoding recommendations based on resolution and frame rate.
        
        Args:
            resolution_width: Video width in pixels
            resolution_height: Video height in pixels
            frame_rate: Frame rate in fps
        
        Returns:
            Dict: Encoding recommendations
        """
        # Calculate total pixels
        total_pixels = resolution_width * resolution_height
        
        # Basic recommendations based on resolution
        if total_pixels >= 3840 * 2160:  # 4K
            video_bitrate_range = (15000, 25000)  # kbps
            audio_bitrate_range = (256, 320)
            recommended_codecs = ['h265', 'av1']
        elif total_pixels >= 1920 * 1080:  # 1080p
            video_bitrate_range = (4000, 8000)
            audio_bitrate_range = (128, 256)
            recommended_codecs = ['h264', 'h265']
        elif total_pixels >= 1280 * 720:  # 720p
            video_bitrate_range = (2000, 4000)
            audio_bitrate_range = (96, 128)
            recommended_codecs = ['h264']
        else:  # SD
            video_bitrate_range = (500, 2000)
            audio_bitrate_range = (64, 96)
            recommended_codecs = ['h264', 'mpeg2']
        
        # Adjust for frame rate
        if frame_rate > 30:
            video_bitrate_range = (
                int(video_bitrate_range[0] * 1.5),
                int(video_bitrate_range[1] * 1.5)
            )
        
        return {
            'resolution': f"{resolution_width}x{resolution_height}",
            'frame_rate': frame_rate,
            'video_bitrate_range': video_bitrate_range,
            'audio_bitrate_range': audio_bitrate_range,
            'recommended_video_codecs': recommended_codecs,
            'recommended_audio_codecs': ['aac', 'mp3'],
            'recommended_containers': ['mp4', 'mkv']
        }