from django.db.models.signals import post_save, post_delete, pre_save
from django.dispatch import receiver
from .models import EPGSource, EPGData
from .tasks import refresh_epg_data, delete_epg_refresh_task_by_id
from core.scheduling import create_or_update_periodic_task, delete_periodic_task
from core.utils import is_protected_path, send_websocket_update
import json
import logging
import os

logger = logging.getLogger(__name__)

@receiver(post_save, sender=EPGSource)
def trigger_refresh_on_new_epg_source(sender, instance, created, **kwargs):
    # Trigger refresh only if the source is newly created, active, and not a dummy EPG
    if created and instance.is_active and instance.source_type != 'dummy':
        refresh_epg_data.delay(instance.id)

@receiver(post_save, sender=EPGSource)
def create_dummy_epg_data(sender, instance, created, **kwargs):
    """
    Automatically create EPGData for dummy EPG sources when they are created.
    This allows channels to be assigned to dummy EPGs immediately without
    requiring a refresh first.
    """
    if instance.source_type == 'dummy':
        # Ensure dummy EPGs always have idle status and no status message
        if instance.status != EPGSource.STATUS_IDLE or instance.last_message:
            instance.status = EPGSource.STATUS_IDLE
            instance.last_message = None
            instance.save(update_fields=['status', 'last_message'])

        # Create a URL-friendly tvg_id from the dummy EPG name
        # Replace spaces and special characters with underscores
        friendly_tvg_id = instance.name.replace(' ', '_').replace('-', '_')
        # Remove any characters that aren't alphanumeric or underscores
        friendly_tvg_id = ''.join(c for c in friendly_tvg_id if c.isalnum() or c == '_')
        # Convert to lowercase for consistency
        friendly_tvg_id = friendly_tvg_id.lower()
        # Prefix with 'dummy_' to make it clear this is a dummy EPG
        friendly_tvg_id = f"dummy_{friendly_tvg_id}"

        # Create or update the EPGData record
        epg_data, data_created = EPGData.objects.get_or_create(
            tvg_id=friendly_tvg_id,
            epg_source=instance,
            defaults={
                'name': instance.name,
                'icon_url': None
            }
        )

        # Update name if it changed and record already existed
        if not data_created and epg_data.name != instance.name:
            epg_data.name = instance.name
            epg_data.save(update_fields=['name'])

        if data_created:
            logger.info(f"Auto-created EPGData for dummy EPG source: {instance.name} (ID: {instance.id})")

            # Send websocket update to notify frontend that EPG data has been created
            # This allows the channel form to immediately show the new dummy EPG without refreshing
            send_websocket_update('updates', 'update', {
                'type': 'epg_data_created',
                'source_id': instance.id,
                'source_name': instance.name,
                'epg_data_id': epg_data.id
            })
        else:
            logger.debug(f"EPGData already exists for dummy EPG source: {instance.name} (ID: {instance.id})")

@receiver(post_save, sender=EPGSource)
def create_or_update_refresh_task(sender, instance, created, update_fields=None, **kwargs):
    """
    Create or update a Celery Beat periodic task when an EPGSource is created/updated.
    Skip creating tasks for dummy EPG sources as they don't need refreshing.
    Supports both interval-based and cron-based scheduling via the shared utility.
    """
    # Skip task creation for dummy EPGs
    if instance.source_type == 'dummy':
        # If there's an existing task, disable it
        if instance.refresh_task:
            instance.refresh_task.enabled = False
            instance.refresh_task.save(update_fields=['enabled'])
        return

    # Skip rescheduling when only non-schedule fields were saved (e.g. status/last_message
    # updates from the refresh task itself). We only need to reschedule when schedule-relevant
    # fields change or when _cron_expression was explicitly set by the serializer.
    SCHEDULE_FIELDS = {'refresh_interval', 'is_active', 'refresh_task'}
    if (
        not created
        and update_fields is not None
        and not (set(update_fields) & SCHEDULE_FIELDS)
        and not hasattr(instance, '_cron_expression')
    ):
        return

    task_name = f"epg_source-refresh-{instance.id}"
    should_be_enabled = instance.is_active

    # Read cron_expression from transient attribute set by the serializer.
    # If not set (e.g. save came from a task updating status/last_message),
    # preserve the existing crontab so we don't accidentally revert to interval.
    if hasattr(instance, "_cron_expression"):
        cron_expr = instance._cron_expression
    else:
        cron_expr = ""
        try:
            existing_task = instance.refresh_task
            if existing_task and existing_task.crontab:
                ct = existing_task.crontab
                cron_expr = f"{ct.minute} {ct.hour} {ct.day_of_month} {ct.month_of_year} {ct.day_of_week}"
        except Exception:
            pass

    task = create_or_update_periodic_task(
        task_name=task_name,
        celery_task_path="apps.epg.tasks.refresh_epg_data",
        kwargs={"source_id": instance.id},
        interval_hours=int(instance.refresh_interval),
        cron_expression=cron_expr,
        enabled=should_be_enabled,
    )

    if instance.refresh_task != task:
        instance.refresh_task = task
        instance.save(update_fields=["refresh_task"])

@receiver(post_delete, sender=EPGSource)
def delete_refresh_task(sender, instance, **kwargs):
    """
    Delete the associated Celery Beat periodic task when an EPGSource is deleted.
    """
    try:
        # First try the foreign key relationship to find the task ID
        task = None
        if instance.refresh_task:
            logger.info(f"Found task via foreign key: {instance.refresh_task.id} for EPGSource {instance.id}")
            task = instance.refresh_task

            # Store task ID before deletion if we need to bypass the helper function
            if task:
                delete_epg_refresh_task_by_id(instance.id)
        else:
            # Otherwise use the helper function
            delete_epg_refresh_task_by_id(instance.id)
    except Exception as e:
        logger.error(f"Error in delete_refresh_task signal handler: {str(e)}", exc_info=True)

@receiver(pre_save, sender=EPGSource)
def update_status_on_active_change(sender, instance, **kwargs):
    """
    When an EPGSource's is_active field changes, update the status accordingly.
    For dummy EPGs, always ensure status is idle and no status message.
    """
    # Dummy EPGs should always be idle with no status message
    if instance.source_type == 'dummy':
        instance.status = EPGSource.STATUS_IDLE
        instance.last_message = None
        return

    if instance.pk:  # Only for existing records, not new ones
        try:
            # Get the current record from the database
            old_instance = EPGSource.objects.get(pk=instance.pk)

            # If is_active changed, update the status
            if old_instance.is_active != instance.is_active:
                if instance.is_active:
                    # When activating, set status to idle
                    instance.status = 'idle'
                else:
                    # When deactivating, set status to disabled
                    instance.status = 'disabled'
        except EPGSource.DoesNotExist:
            # New record, will use default status
            pass

@receiver(post_delete, sender=EPGSource)
def delete_cached_files(sender, instance, **kwargs):
    """
    Delete cached files associated with an EPGSource when it's deleted.
    Only deletes files that aren't in protected directories.
    """
    # Check and delete the main file path if not protected
    if instance.file_path and os.path.exists(instance.file_path):
        if is_protected_path(instance.file_path):
            logger.info(f"Skipping deletion of protected file: {instance.file_path}")
        else:
            try:
                os.remove(instance.file_path)
                logger.info(f"Deleted cached file: {instance.file_path}")
            except OSError as e:
                logger.error(f"Error deleting cached file {instance.file_path}: {e}")

    # Check and delete the extracted file path if it exists, is different from main path, and not protected
    if instance.extracted_file_path and os.path.exists(instance.extracted_file_path) and instance.extracted_file_path != instance.file_path:
        if is_protected_path(instance.extracted_file_path):
            logger.info(f"Skipping deletion of protected extracted file: {instance.extracted_file_path}")
        else:
            try:
                os.remove(instance.extracted_file_path)
                logger.info(f"Deleted extracted file: {instance.extracted_file_path}")
            except OSError as e:
                logger.error(f"Error deleting extracted file {instance.extracted_file_path}: {e}")
