"""
WebSocket consumers for real-time calendar updates.

Used by:
- Mobile app for field employees to get job updates
- Web frontend for real-time calendar sync
"""
import json
import logging
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from asgiref.sync import sync_to_async
from django.contrib.contenttypes.models import ContentType

logger = logging.getLogger(__name__)


class CalendarConsumer(AsyncWebsocketConsumer):
    """
    WebSocket consumer for real-time calendar/job updates.

    Groups:
    - calendar_{tenant_schema}: All calendar updates for a tenant
    - employee_jobs_{user_id}: Jobs assigned to a specific employee
    - event_{event_id}: Updates for a specific event

    Message types:
    - event_created: New event was created
    - event_updated: Event details changed (time, status, etc.)
    - event_deleted: Event was deleted
    - job_assigned: Job was assigned to this employee
    - job_unassigned: Job was unassigned from this employee
    """

    async def connect(self):
        """Handle WebSocket connection."""
        user = self.scope.get("user")

        logger.info(
            f"CalendarConsumer Connect: User={user}, "
            f"Auth={user.is_authenticated if user else False}"
        )

        if not user or not user.is_authenticated:
            logger.warning("CalendarConsumer: Rejecting unauthenticated connection")
            await self.close()
            return

        # Store user for later use
        self.user = user
        self.groups = []

        # Add to user's personal job updates group
        self.user_group = f"employee_jobs_{user.id}"
        await self.channel_layer.group_add(self.user_group, self.channel_name)
        self.groups.append(self.user_group)

        # Add to tenant group if user has a tenant
        # Use sync_to_async for database access (tenant is a ForeignKey)
        tenant = await self._get_user_tenant(user)
        if tenant:
            self.tenant_group = f"calendar_{tenant.schema_name}"
            await self.channel_layer.group_add(self.tenant_group, self.channel_name)
            self.groups.append(self.tenant_group)
            logger.info(f"CalendarConsumer: User {user.id} joined tenant group {self.tenant_group}")
        else:
            self.tenant_group = None

        await self.accept()
        logger.info(f"CalendarConsumer: Connection accepted for user {user.id}")

        # Send initial connection confirmation
        await self.send(text_data=json.dumps({
            'type': 'connection_established',
            'user_id': user.id,
            'groups': self.groups,
        }))

    @database_sync_to_async
    def _get_user_tenant(self, user):
        """Get user's tenant in a sync context."""
        try:
            # Force refresh from database to get tenant
            user.refresh_from_db()
            return user.tenant
        except Exception:
            return None

    async def disconnect(self, close_code):
        """Handle WebSocket disconnection."""
        logger.info(f"CalendarConsumer: Disconnecting, code={close_code}")

        # Remove from all groups
        for group in getattr(self, 'groups', []):
            await self.channel_layer.group_discard(group, self.channel_name)

    async def receive(self, text_data):
        """
        Handle incoming messages from client.

        Supported messages:
        - subscribe_event: Subscribe to updates for a specific event
        - unsubscribe_event: Unsubscribe from event updates
        - ping: Keep-alive ping (responds with pong)
        """
        try:
            data = json.loads(text_data)
            message_type = data.get('type')

            if message_type == 'subscribe_event':
                event_id = data.get('event_id')
                if event_id:
                    group_name = f"event_{event_id}"
                    await self.channel_layer.group_add(group_name, self.channel_name)
                    self.groups.append(group_name)
                    await self.send(text_data=json.dumps({
                        'type': 'subscribed',
                        'event_id': event_id,
                    }))

            elif message_type == 'unsubscribe_event':
                event_id = data.get('event_id')
                if event_id:
                    group_name = f"event_{event_id}"
                    await self.channel_layer.group_discard(group_name, self.channel_name)
                    if group_name in self.groups:
                        self.groups.remove(group_name)

            elif message_type == 'ping':
                await self.send(text_data=json.dumps({'type': 'pong'}))

        except json.JSONDecodeError:
            logger.warning(f"CalendarConsumer: Invalid JSON received: {text_data}")
        except Exception as e:
            logger.error(f"CalendarConsumer: Error processing message: {e}")

    # =========================================================================
    # Event handlers - called when messages are sent to groups
    # =========================================================================

    async def event_created(self, event):
        """Handle new event creation."""
        await self.send(text_data=json.dumps({
            'type': 'event_created',
            'event': event.get('event'),
        }))

    async def event_updated(self, event):
        """Handle event update."""
        await self.send(text_data=json.dumps({
            'type': 'event_updated',
            'event': event.get('event'),
            'changed_fields': event.get('changed_fields', []),
        }))

    async def event_deleted(self, event):
        """Handle event deletion."""
        await self.send(text_data=json.dumps({
            'type': 'event_deleted',
            'event_id': event.get('event_id'),
        }))

    async def event_status_changed(self, event):
        """Handle status change (common for mobile app)."""
        await self.send(text_data=json.dumps({
            'type': 'event_status_changed',
            'event_id': event.get('event_id'),
            'old_status': event.get('old_status'),
            'new_status': event.get('new_status'),
            'event': event.get('event'),
        }))

    async def job_assigned(self, event):
        """Handle job assignment to employee."""
        await self.send(text_data=json.dumps({
            'type': 'job_assigned',
            'event': event.get('event'),
        }))

    async def job_unassigned(self, event):
        """Handle job unassignment from employee."""
        await self.send(text_data=json.dumps({
            'type': 'job_unassigned',
            'event_id': event.get('event_id'),
        }))


# =============================================================================
# Helper functions to broadcast updates
# =============================================================================

def get_event_broadcast_data(event):
    """
    Serialize an Event for WebSocket broadcast.

    Returns a dict suitable for JSON serialization.
    """
    data = {
        'id': event.id,
        'title': event.title,
        'start_time': event.start_time.isoformat() if event.start_time else None,
        'end_time': event.end_time.isoformat() if event.end_time else None,
        'status': event.status,
        'notes': event.notes,
        'created_at': event.created_at.isoformat() if event.created_at else None,
        'updated_at': event.updated_at.isoformat() if event.updated_at else None,
    }

    # Add service info if available
    if event.service:
        data['service'] = {
            'id': event.service.id,
            'name': event.service.name,
            'duration': event.service.duration,
            'price': str(event.service.price),
        }

    # Add pricing info
    if event.deposit_amount:
        data['deposit_amount'] = str(event.deposit_amount)
    if event.final_price:
        data['final_price'] = str(event.final_price)

    return data


def get_event_staff_user_ids(event):
    """
    Get user IDs of staff assigned to an event.

    Returns list of user IDs for broadcasting.
    """
    from schedule.models import Participant, Resource
    from smoothschedule.identity.users.models import User

    user_ids = set()

    try:
        user_ct = ContentType.objects.get_for_model(User)
        resource_ct = ContentType.objects.get_for_model(Resource)

        # Get direct user participants (staff role)
        for participant in event.participants.filter(
            role__in=[Participant.Role.STAFF, Participant.Role.RESOURCE],
            content_type=user_ct
        ):
            if participant.object_id:
                user_ids.add(participant.object_id)

        # Get users linked to resource participants
        for participant in event.participants.filter(
            role__in=[Participant.Role.STAFF, Participant.Role.RESOURCE],
            content_type=resource_ct
        ):
            resource = participant.content_object
            if resource and resource.user_id:
                user_ids.add(resource.user_id)

    except Exception as e:
        logger.error(f"Error getting staff user IDs for event {event.id}: {e}")

    return list(user_ids)


async def broadcast_event_update(event, update_type='event_updated', changed_fields=None, old_status=None):
    """
    Broadcast an event update to all relevant WebSocket groups.

    Args:
        event: The Event instance
        update_type: One of 'event_created', 'event_updated', 'event_deleted', 'event_status_changed'
        changed_fields: List of field names that changed (for event_updated)
        old_status: Previous status (for event_status_changed)
    """
    from channels.layers import get_channel_layer
    from django.db import connection

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

    event_data = await sync_to_async(get_event_broadcast_data)(event)
    staff_user_ids = await sync_to_async(get_event_staff_user_ids)(event)

    # Get tenant schema for group name
    tenant_schema = connection.schema_name if hasattr(connection, 'schema_name') else 'public'

    message = {
        'type': update_type,
        'event': event_data,
    }

    if changed_fields:
        message['changed_fields'] = changed_fields

    if old_status and update_type == 'event_status_changed':
        message['old_status'] = old_status
        message['new_status'] = event.status
        message['event_id'] = event.id

    if update_type == 'event_deleted':
        message['event_id'] = event.id

    # Broadcast to tenant group
    if tenant_schema != 'public':
        await channel_layer.group_send(
            f"calendar_{tenant_schema}",
            message
        )

    # Broadcast to individual employee groups
    for user_id in staff_user_ids:
        await channel_layer.group_send(
            f"employee_jobs_{user_id}",
            message
        )

    # Broadcast to event-specific group
    await channel_layer.group_send(
        f"event_{event.id}",
        message
    )

    logger.info(
        f"Broadcast {update_type} for event {event.id} to "
        f"tenant={tenant_schema}, staff={staff_user_ids}"
    )


class ResourceLocationConsumer(AsyncWebsocketConsumer):
    """
    WebSocket consumer for real-time resource location tracking.

    Used by web dashboard to show live staff location on map while en route.

    Groups:
    - resource_location_{resource_id}: Location updates for a specific resource
    """

    async def connect(self):
        """Handle WebSocket connection."""
        user = self.scope.get("user")
        self.resource_id = self.scope['url_route']['kwargs'].get('resource_id')

        logger.info(
            f"ResourceLocationConsumer Connect: User={user}, Resource={self.resource_id}, "
            f"Auth={user.is_authenticated if user else False}"
        )

        if not user or not user.is_authenticated:
            logger.warning("ResourceLocationConsumer: Rejecting unauthenticated connection")
            await self.close()
            return

        if not self.resource_id:
            logger.warning("ResourceLocationConsumer: No resource_id provided")
            await self.close()
            return

        # Store user for later use
        self.user = user
        self.group_name = f"resource_location_{self.resource_id}"

        # Add to resource location group
        await self.channel_layer.group_add(self.group_name, self.channel_name)
        await self.accept()

        logger.info(f"ResourceLocationConsumer: Connection accepted for resource {self.resource_id}")

        # Send initial connection confirmation
        await self.send(text_data=json.dumps({
            'type': 'connection_established',
            'resource_id': self.resource_id,
        }))

    async def disconnect(self, close_code):
        """Handle WebSocket disconnection."""
        logger.info(f"ResourceLocationConsumer: Disconnecting resource {getattr(self, 'resource_id', 'unknown')}, code={close_code}")

        if hasattr(self, 'group_name'):
            await self.channel_layer.group_discard(self.group_name, self.channel_name)

    async def receive(self, text_data):
        """Handle incoming messages from client (ping/pong only)."""
        try:
            data = json.loads(text_data)
            message_type = data.get('type')

            if message_type == 'ping':
                await self.send(text_data=json.dumps({'type': 'pong'}))

        except json.JSONDecodeError:
            logger.warning(f"ResourceLocationConsumer: Invalid JSON received: {text_data}")

    async def location_update(self, event):
        """Handle location update broadcast."""
        await self.send(text_data=json.dumps({
            'type': 'location_update',
            'latitude': event.get('latitude'),
            'longitude': event.get('longitude'),
            'accuracy': event.get('accuracy'),
            'heading': event.get('heading'),
            'speed': event.get('speed'),
            'timestamp': event.get('timestamp'),
            'active_job': event.get('active_job'),
        }))

    async def tracking_stopped(self, event):
        """Handle tracking stopped notification."""
        await self.send(text_data=json.dumps({
            'type': 'tracking_stopped',
            'resource_id': event.get('resource_id'),
            'reason': event.get('reason'),
        }))


async def broadcast_resource_location_update(resource_id, location_data, active_job=None):
    """
    Broadcast a location update to all connected clients watching this resource.

    Args:
        resource_id: The resource ID to broadcast to
        location_data: Dict with latitude, longitude, accuracy, heading, speed, timestamp
        active_job: Optional dict with id, title, status, status_display
    """
    from channels.layers import get_channel_layer

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

    message = {
        'type': 'location_update',
        'latitude': location_data.get('latitude'),
        'longitude': location_data.get('longitude'),
        'accuracy': location_data.get('accuracy'),
        'heading': location_data.get('heading'),
        'speed': location_data.get('speed'),
        'timestamp': location_data.get('timestamp'),
        'active_job': active_job,
    }

    await channel_layer.group_send(
        f"resource_location_{resource_id}",
        message
    )

    logger.debug(f"Broadcast location update for resource {resource_id}")
