"""
Signals for the schedule app.

Handles:
1. Auto-attaching plugins from GlobalEventPlugin rules when events are created
2. Rescheduling Celery tasks when events are modified (time/duration changes)
3. Scheduling/cancelling Celery tasks when EventPlugins are created/deleted/modified
4. Cancelling tasks when Events are deleted or cancelled
5. Broadcasting real-time updates via WebSocket for calendar sync
6. Customer notification hooks on status changes
"""
import logging
from django.db.models.signals import post_save, pre_save, post_delete, pre_delete
from django.dispatch import Signal, receiver

logger = logging.getLogger(__name__)


# =============================================================================
# Custom Signals for Status Changes
# =============================================================================

# Fired when an event status changes (hook point for custom behavior)
# Provides: event, old_status, new_status, changed_by, tenant, skip_notifications
event_status_changed = Signal()

# Fired when a customer should be notified about an event
# Provides: event, notification_type, tenant
customer_notification_requested = Signal()


# ============================================================================
# WebSocket Broadcasting Helpers
# ============================================================================

def broadcast_event_change_sync(event, update_type, changed_fields=None, old_status=None):
    """
    Synchronous wrapper to broadcast event changes via WebSocket.

    Uses async_to_sync to call the async broadcast function from signals.
    """
    from asgiref.sync import async_to_sync
    from channels.layers import get_channel_layer

    channel_layer = get_channel_layer()
    if not channel_layer:
        logger.debug("No channel layer configured, skipping WebSocket broadcast")
        return

    try:
        from .consumers import broadcast_event_update
        async_to_sync(broadcast_event_update)(
            event,
            update_type=update_type,
            changed_fields=changed_fields,
            old_status=old_status
        )
    except Exception as e:
        logger.error(f"Failed to broadcast event change: {e}")


@receiver(post_save, sender='schedule.Event')
def auto_attach_global_plugins(sender, instance, created, **kwargs):
    """
    When a new event is created, automatically attach all active GlobalEventPlugin rules.
    """
    if not created:
        return

    from .models import GlobalEventPlugin, EventPlugin

    # Get all active global rules
    global_rules = GlobalEventPlugin.objects.filter(is_active=True)

    attached_count = 0
    for rule in global_rules:
        event_plugin = rule.apply_to_event(instance)
        if event_plugin:
            attached_count += 1
            logger.info(
                f"Auto-attached plugin '{rule.plugin_installation}' to event '{instance}' "
                f"via global rule (trigger={rule.trigger}, offset={rule.offset_minutes})"
            )

    if attached_count > 0:
        logger.info(f"Auto-attached {attached_count} plugins to new event '{instance}'")


@receiver(pre_save, sender='schedule.Event')
def track_event_changes(sender, instance, **kwargs):
    """
    Track changes to event timing before save.
    Store old values on the instance for post_save comparison.
    """
    if instance.pk:
        try:
            from .models import Event
            old_instance = Event.objects.get(pk=instance.pk)
            instance._old_start_time = old_instance.start_time
            instance._old_end_time = old_instance.end_time
        except sender.DoesNotExist:
            instance._old_start_time = None
            instance._old_end_time = None
    else:
        instance._old_start_time = None
        instance._old_end_time = None


@receiver(post_save, sender='schedule.Event')
def reschedule_event_plugins_on_change(sender, instance, created, **kwargs):
    """
    When an event's timing changes, update any scheduled Celery tasks for its plugins.
    This handles both time changes and duration changes (via end_time).
    """
    if created:
        # New events don't have existing tasks to reschedule
        return

    old_start = getattr(instance, '_old_start_time', None)
    old_end = getattr(instance, '_old_end_time', None)

    if old_start is None and old_end is None:
        return

    # Check if timing actually changed
    start_changed = old_start and old_start != instance.start_time
    end_changed = old_end and old_end != instance.end_time

    if not start_changed and not end_changed:
        return

    logger.info(
        f"Event '{instance}' timing changed. "
        f"Start: {old_start} -> {instance.start_time}, "
        f"End: {old_end} -> {instance.end_time}"
    )

    # Reschedule all active time-based event plugins
    reschedule_event_celery_tasks(instance, start_changed, end_changed)


def reschedule_event_celery_tasks(event, start_changed=True, end_changed=True):
    """
    Reschedule Celery tasks for an event's plugins when timing changes.

    Args:
        event: The Event instance
        start_changed: Whether start_time changed
        end_changed: Whether end_time changed
    """
    from .models import EventPlugin

    # Get all active, time-based plugins for this event
    time_based_triggers = ['before_start', 'at_start', 'after_start', 'after_end']

    plugins_to_update = event.event_plugins.filter(
        is_active=True,
        trigger__in=time_based_triggers
    )

    for event_plugin in plugins_to_update:
        # Only reschedule if the relevant time changed
        affects_start = event_plugin.trigger in ['before_start', 'at_start', 'after_start']
        affects_end = event_plugin.trigger == 'after_end'

        if (affects_start and start_changed) or (affects_end and end_changed):
            new_execution_time = event_plugin.get_execution_time()
            if new_execution_time:
                logger.info(
                    f"Rescheduling plugin '{event_plugin.plugin_installation}' for event '{event}' "
                    f"to new execution time: {new_execution_time}"
                )
                # TODO: Integrate with Celery beat to reschedule the actual task
                # For now, we log the intent. The actual Celery integration
                # will be handled by the task execution system.
                schedule_event_plugin_task(event_plugin, new_execution_time)


def schedule_event_plugin_task(event_plugin, execution_time):
    """
    Schedule a Celery task for an event plugin at a specific time.

    This function handles creating or updating Celery beat entries
    for time-based event plugin execution.
    """
    from django.utils import timezone

    # Don't schedule tasks in the past
    if execution_time < timezone.now():
        logger.debug(
            f"Skipping scheduling for event plugin {event_plugin.id} - "
            f"execution time {execution_time} is in the past"
        )
        return

    # Get or create the Celery task entry
    # Using django-celery-beat's PeriodicTask model if available
    try:
        from django_celery_beat.models import PeriodicTask, ClockedSchedule

        # Create a clocked schedule for the specific execution time
        clocked_schedule, _ = ClockedSchedule.objects.get_or_create(
            clocked_time=execution_time
        )

        # Task name is unique per event-plugin combination
        task_name = f"event_plugin_{event_plugin.id}"

        import json

        # Create or update the periodic task
        task, created = PeriodicTask.objects.update_or_create(
            name=task_name,
            defaults={
                'task': 'schedule.tasks.execute_event_plugin',
                'clocked': clocked_schedule,
                'one_off': True,  # Run only once
                'enabled': event_plugin.is_active,
                'kwargs': json.dumps({
                    'event_plugin_id': event_plugin.id,
                    'event_id': event_plugin.event_id,
                }),
            }
        )

        action = "Created" if created else "Updated"
        logger.info(f"{action} Celery task '{task_name}' for execution at {execution_time}")

    except ImportError:
        # django-celery-beat not installed, fall back to simple delay
        logger.warning(
            "django-celery-beat not installed. "
            "Event plugin scheduling will use basic Celery delay."
        )
    except Exception as e:
        logger.error(f"Failed to schedule event plugin task: {e}")


@receiver(post_save, sender='schedule.GlobalEventPlugin')
def apply_global_plugin_to_existing_events(sender, instance, created, **kwargs):
    """
    When a new GlobalEventPlugin rule is created, apply it to all existing events
    if apply_to_existing is True.
    """
    if not created:
        return

    if not instance.is_active:
        return

    if not instance.apply_to_existing:
        logger.info(
            f"Global plugin rule '{instance}' will only apply to future events"
        )
        return

    count = instance.apply_to_all_events()
    logger.info(
        f"Applied global plugin rule '{instance}' to {count} existing events"
    )


# ============================================================================
# EventPlugin Scheduling Signals
# ============================================================================

@receiver(post_save, sender='schedule.EventPlugin')
def schedule_event_plugin_on_create(sender, instance, created, **kwargs):
    """
    When an EventPlugin is created or updated, schedule its Celery task
    if it has a time-based trigger.
    """
    # Only schedule time-based triggers
    time_based_triggers = ['before_start', 'at_start', 'after_start', 'after_end']

    if instance.trigger not in time_based_triggers:
        return

    if not instance.is_active:
        # If deactivated, cancel any existing task
        from .tasks import cancel_event_plugin_task
        cancel_event_plugin_task(instance.id)
        return

    execution_time = instance.get_execution_time()
    if execution_time:
        schedule_event_plugin_task(instance, execution_time)


@receiver(pre_save, sender='schedule.EventPlugin')
def track_event_plugin_active_change(sender, instance, **kwargs):
    """
    Track if is_active changed so we can cancel tasks when deactivated.
    """
    if instance.pk:
        try:
            from .models import EventPlugin
            old_instance = EventPlugin.objects.get(pk=instance.pk)
            instance._was_active = old_instance.is_active
        except sender.DoesNotExist:
            instance._was_active = None
    else:
        instance._was_active = None


@receiver(post_delete, sender='schedule.EventPlugin')
def cancel_event_plugin_on_delete(sender, instance, **kwargs):
    """
    When an EventPlugin is deleted, cancel its scheduled Celery task.
    """
    from .tasks import cancel_event_plugin_task
    cancel_event_plugin_task(instance.id)


# ============================================================================
# Event Deletion/Cancellation Signals
# ============================================================================

@receiver(pre_delete, sender='schedule.Event')
def cancel_event_tasks_on_delete(sender, instance, **kwargs):
    """
    When an Event is deleted, cancel all its scheduled plugin tasks.
    """
    from .tasks import cancel_event_tasks
    cancel_event_tasks(instance.id)


@receiver(pre_save, sender='schedule.Event')
def track_event_status_change(sender, instance, **kwargs):
    """
    Track status changes to detect cancellation.
    """
    if instance.pk:
        try:
            from .models import Event
            old_instance = Event.objects.get(pk=instance.pk)
            instance._old_status = old_instance.status
        except sender.DoesNotExist:
            instance._old_status = None
    else:
        instance._old_status = None


@receiver(post_save, sender='schedule.Event')
def cancel_event_tasks_on_cancel(sender, instance, created, **kwargs):
    """
    When an Event is cancelled, cancel all its scheduled plugin tasks.
    """
    if created:
        return

    from .models import Event

    old_status = getattr(instance, '_old_status', None)

    # If status changed to cancelled, cancel all tasks
    if old_status != Event.Status.CANCELED and instance.status == Event.Status.CANCELED:
        from .tasks import cancel_event_tasks
        logger.info(f"Event '{instance}' was cancelled, cancelling all plugin tasks")
        cancel_event_tasks(instance.id)


# ============================================================================
# WebSocket Broadcasting Signals
# ============================================================================

@receiver(post_save, sender='schedule.Event')
def broadcast_event_save(sender, instance, created, **kwargs):
    """
    Broadcast event creation/update via WebSocket for real-time calendar sync.
    """
    old_status = getattr(instance, '_old_status', None)
    old_start = getattr(instance, '_old_start_time', None)
    old_end = getattr(instance, '_old_end_time', None)

    if created:
        # New event created
        broadcast_event_change_sync(instance, 'event_created')
        logger.info(f"Broadcast event_created for event {instance.id}")

    elif old_status and old_status != instance.status:
        # Status changed
        broadcast_event_change_sync(
            instance,
            'event_status_changed',
            old_status=old_status
        )
        logger.info(
            f"Broadcast event_status_changed for event {instance.id}: "
            f"{old_status} -> {instance.status}"
        )

    else:
        # Other update - determine what changed
        changed_fields = []

        if old_start and old_start != instance.start_time:
            changed_fields.append('start_time')
        if old_end and old_end != instance.end_time:
            changed_fields.append('end_time')

        # Always broadcast updates for changes
        broadcast_event_change_sync(
            instance,
            'event_updated',
            changed_fields=changed_fields if changed_fields else None
        )
        logger.info(f"Broadcast event_updated for event {instance.id}")


@receiver(pre_delete, sender='schedule.Event')
def broadcast_event_delete(sender, instance, **kwargs):
    """
    Broadcast event deletion via WebSocket.
    """
    # Store the event data before deletion for broadcasting
    broadcast_event_change_sync(instance, 'event_deleted')
    logger.info(f"Broadcast event_deleted for event {instance.id}")


# =============================================================================
# Customer Notification Signal Handlers
# =============================================================================

@receiver(event_status_changed)
def handle_event_status_change_notifications(sender, event, old_status, new_status, changed_by, tenant, **kwargs):
    """
    Handle customer notifications when event status changes.

    This is a hook point for custom notification behavior.
    Default behavior: queue customer notifications for certain transitions.
    """
    from .models import Event

    skip_notifications = kwargs.get('skip_notifications', False)
    if skip_notifications:
        logger.debug(f"Notifications skipped for event {event.id} status change")
        return

    # Define which transitions should notify customers
    NOTIFY_TRANSITIONS = {
        (Event.Status.SCHEDULED, Event.Status.EN_ROUTE): 'en_route_notification',
        (Event.Status.EN_ROUTE, Event.Status.IN_PROGRESS): 'arrived_notification',
        (Event.Status.IN_PROGRESS, Event.Status.COMPLETED): 'completed_notification',
    }

    notification_type = NOTIFY_TRANSITIONS.get((old_status, new_status))

    if notification_type:
        # Fire the notification signal
        customer_notification_requested.send(
            sender=sender,
            event=event,
            notification_type=notification_type,
            tenant=tenant,
        )
        logger.info(f"Requested {notification_type} for event {event.id}")


@receiver(event_status_changed)
def handle_event_status_change_plugins(sender, event, old_status, new_status, changed_by, tenant, **kwargs):
    """
    Execute plugins attached to status change events.
    """
    from .models import Event

    try:
        if new_status == Event.Status.COMPLETED:
            event.execute_plugins(trigger='on_complete')
        elif new_status == Event.Status.CANCELED:
            event.execute_plugins(trigger='on_cancel')
    except Exception as e:
        logger.warning(f"Error executing event plugins on status change: {e}")


@receiver(customer_notification_requested)
def send_customer_notification_task(sender, event, notification_type, tenant, **kwargs):
    """
    Queue customer notification via Celery task.

    This is the default handler that queues notifications.
    Can be replaced or supplemented by custom handlers.
    """
    try:
        from smoothschedule.communication.mobile.tasks import send_customer_status_notification

        send_customer_status_notification.delay(
            tenant_id=tenant.id,
            event_id=event.id,
            notification_type=notification_type,
        )
        logger.debug(f"Queued {notification_type} for event {event.id}")

    except Exception as e:
        # Don't fail the status change if notification fails
        logger.warning(f"Failed to queue customer notification: {e}")


# =============================================================================
# Helper function for status changes
# =============================================================================

def emit_status_change(event, old_status, new_status, changed_by, tenant, skip_notifications=False):
    """
    Emit the event_status_changed signal.

    This should be called after a successful status transition.
    The StatusMachine uses this to trigger hooks.

    Args:
        event: The Event instance
        old_status: Previous status value
        new_status: New status value
        changed_by: User who made the change
        tenant: Business tenant
        skip_notifications: If True, signal receivers should skip notifications
    """
    event_status_changed.send(
        sender=event.__class__,
        event=event,
        old_status=old_status,
        new_status=new_status,
        changed_by=changed_by,
        tenant=tenant,
        skip_notifications=skip_notifications,
    )


# =============================================================================
# TimeBlock (Time-Off Request) Signals
# =============================================================================

# Custom signal for time-off request notifications
time_off_request_submitted = Signal()

# Fields that trigger re-approval when changed on an approved block
TIME_BLOCK_APPROVAL_FIELDS = [
    'title', 'start_date', 'end_date', 'all_day', 'start_time', 'end_time',
    'recurrence_type', 'recurrence_pattern', 'recurrence_start', 'recurrence_end',
]


@receiver(pre_save, sender='schedule.TimeBlock')
def track_time_block_changes(sender, instance, **kwargs):
    """
    Track changes to TimeBlock before save and reset approval if needed.

    When a staff member edits an already-approved time block:
    1. Tracks which fields changed
    2. If approval-sensitive fields changed, resets status to PENDING
    3. Marks the instance so post_save knows to send notifications

    This ensures that any edit to an approved time-off request triggers
    a new approval workflow.
    """
    instance._needs_re_approval_notification = False

    if instance.pk:
        try:
            from .models import TimeBlock
            old_instance = TimeBlock.objects.get(pk=instance.pk)
            instance._old_approval_status = old_instance.approval_status
            instance._was_approved = old_instance.approval_status == TimeBlock.ApprovalStatus.APPROVED

            # Track which approval-sensitive fields changed
            instance._changed_fields = []
            for field in TIME_BLOCK_APPROVAL_FIELDS:
                old_value = getattr(old_instance, field)
                new_value = getattr(instance, field)
                if old_value != new_value:
                    instance._changed_fields.append(field)

            # If this was an approved block and significant fields changed,
            # reset to PENDING status (treated as a new request)
            if instance._was_approved and instance._changed_fields:
                # Only reset if the user editing is not self-approving
                # (owners/managers edits stay approved)
                created_by = instance.created_by
                if created_by and not created_by.can_self_approve_time_off():
                    logger.info(
                        f"TimeBlock {instance.id} was approved but modified "
                        f"(changed: {instance._changed_fields}). "
                        f"Resetting to PENDING for re-approval."
                    )
                    instance.approval_status = TimeBlock.ApprovalStatus.PENDING
                    instance.reviewed_by = None
                    instance.reviewed_at = None
                    instance.review_notes = ''
                    instance._needs_re_approval_notification = True

        except sender.DoesNotExist:
            instance._old_approval_status = None
            instance._was_approved = False
            instance._changed_fields = []
    else:
        instance._old_approval_status = None
        instance._was_approved = False
        instance._changed_fields = []


def is_notifications_available():
    """Check if the notifications app is installed and migrated."""
    try:
        from smoothschedule.communication.notifications.models import Notification
        Notification.objects.exists()
        return True
    except Exception:
        return False


def create_notification_safe(recipient, actor, verb, action_object=None, target=None, data=None):
    """Safely create a notification, handling import and creation errors."""
    if not is_notifications_available():
        logger.debug("notifications app not available, skipping notification creation")
        return None

    try:
        from smoothschedule.communication.notifications.models import Notification
        return Notification.objects.create(
            recipient=recipient,
            actor=actor,
            verb=verb,
            action_object=action_object,
            target=target,
            data=data or {}
        )
    except Exception as e:
        logger.error(f"Failed to create notification for {recipient}: {e}")
        return None


@receiver(post_save, sender='schedule.TimeBlock')
def notify_managers_on_pending_time_off(sender, instance, created, **kwargs):
    """
    When a TimeBlock is created with PENDING approval status or when
    an approved TimeBlock is modified, notify all managers and owners.

    Handles two scenarios:
    1. New time-off request created with PENDING status
    2. Approved time-off request modified (reset to PENDING)
    """
    from .models import TimeBlock

    # Check if this is a new pending request OR a modified approved request
    is_new_pending = created and instance.approval_status == TimeBlock.ApprovalStatus.PENDING
    needs_re_approval = getattr(instance, '_needs_re_approval_notification', False)

    if not is_new_pending and not needs_re_approval:
        return

    # Only for resource-level blocks (staff time-off)
    if not instance.resource:
        return

    # Get the requester
    requester = instance.created_by
    if not requester:
        return

    # Determine notification type for logging and data
    if needs_re_approval:
        changed_fields = getattr(instance, '_changed_fields', [])
        verb = 'modified time off request'
        notification_type = 'time_off_request_modified'
        logger.info(
            f"Time-off request modified by {requester.get_full_name() or requester.email} "
            f"for resource '{instance.resource.name}' (changed: {changed_fields}). "
            f"Request returned to pending status for re-approval."
        )
    else:
        verb = 'requested time off'
        notification_type = 'time_off_request'
        logger.info(
            f"Time-off request submitted by {requester.get_full_name() or requester.email} "
            f"for resource '{instance.resource.name}'"
        )

    # Find all managers and owners to notify
    from smoothschedule.identity.users.models import User

    reviewers = User.objects.filter(
        role__in=[User.Role.TENANT_OWNER, User.Role.TENANT_MANAGER],
        is_active=True
    )

    # Create in-app notifications for each reviewer
    for reviewer in reviewers:
        notification = create_notification_safe(
            recipient=reviewer,
            actor=requester,
            verb=verb,
            action_object=instance,
            target=instance.resource,
            data={
                'time_block_id': instance.id,
                'title': instance.title,
                'resource_name': instance.resource.name if instance.resource else None,
                'requester_name': requester.get_full_name() or requester.email,
                'type': notification_type,
                'is_modification': needs_re_approval,
                'changed_fields': getattr(instance, '_changed_fields', []) if needs_re_approval else [],
            }
        )
        if notification:
            logger.debug(f"Created time-off notification for {reviewer.email}")

    # Fire custom signal for additional handlers (e.g., email notifications)
    time_off_request_submitted.send(
        sender=sender,
        time_block=instance,
        requester=requester,
        reviewers=list(reviewers),
        is_modification=needs_re_approval,
        changed_fields=getattr(instance, '_changed_fields', []) if needs_re_approval else [],
    )


@receiver(time_off_request_submitted)
def send_time_off_email_notifications(sender, time_block, requester, reviewers, **kwargs):
    """
    Send email notifications to reviewers when a time-off request is submitted or modified.
    """
    from django.core.mail import send_mail
    from django.conf import settings

    # Check if this is a modification of an existing approved request
    is_modification = kwargs.get('is_modification', False)
    changed_fields = kwargs.get('changed_fields', [])

    # Get the resource name for the email
    resource_name = time_block.resource.name if time_block.resource else 'Unknown'
    requester_name = requester.get_full_name() or requester.email

    # Build date description
    if time_block.recurrence_type == 'NONE':
        if time_block.start_date == time_block.end_date:
            date_desc = time_block.start_date.strftime('%B %d, %Y') if time_block.start_date else 'Date not specified'
        else:
            start = time_block.start_date.strftime('%B %d, %Y') if time_block.start_date else ''
            end = time_block.end_date.strftime('%B %d, %Y') if time_block.end_date else ''
            date_desc = f"{start} - {end}"
    else:
        date_desc = f"Recurring ({time_block.get_recurrence_type_display()})"

    # Different subject and message for new vs modified requests
    if is_modification:
        subject = f"Modified Time-Off Request: {requester_name} - {time_block.title or 'Time Off'}"
        changed_fields_str = ', '.join(changed_fields) if changed_fields else 'unspecified fields'
        message = f"""
A previously approved time-off request has been modified and requires re-approval.

Requester: {requester_name}
Resource: {resource_name}
Title: {time_block.title or 'Time Off'}
Date(s): {date_desc}
Description: {time_block.description or 'No description provided'}

Modified fields: {changed_fields_str}

The request has been returned to pending status and needs your review.
Please log in to review this request.
"""
    else:
        subject = f"Time-Off Request: {requester_name} - {time_block.title or 'Time Off'}"
        message = f"""
A new time-off request has been submitted and needs your review.

Requester: {requester_name}
Resource: {resource_name}
Title: {time_block.title or 'Time Off'}
Date(s): {date_desc}
Description: {time_block.description or 'No description provided'}

Please log in to review this request.
"""

    # Send email to each reviewer
    for reviewer in reviewers:
        if reviewer.email:
            try:
                send_mail(
                    subject=subject,
                    message=message,
                    from_email=settings.DEFAULT_FROM_EMAIL,
                    recipient_list=[reviewer.email],
                    fail_silently=True,
                )
                logger.info(f"Sent time-off request email to {reviewer.email}")
            except Exception as e:
                logger.warning(f"Failed to send time-off email to {reviewer.email}: {e}")
