"""
Availability Service - Resource capacity checking with concurrency management and time blocks
"""
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from django.db.models import Q
from .models import Event, Location, Participant, Resource, TimeBlock


class LocationService:
    """
    Service for location business logic with atomic operations.

    Centralizes:
    - Primary location management (atomic)
    - Quota checking (used by HasQuota and set_active)
    - Multi-location feature checks (used by serializers)
    """

    # ========== Quota & Feature Checks (Centralized) ==========

    @staticmethod
    def count_active_locations(business):
        """Count active locations for quota/validation."""
        return Location.objects.filter(business=business, is_active=True).count()

    @staticmethod
    def get_location_limit(business):
        """
        Get max_locations limit for a business.
        Returns: int (limit) or None (unlimited)
        """
        return business.get_limit('max_locations')

    @staticmethod
    def can_add_location(business):
        """
        Check if business can add a new active location.
        Used by HasQuota permission and set_active action.

        Returns: (can_add: bool, error_message: str | None)
        """
        current = LocationService.count_active_locations(business)
        limit = LocationService.get_location_limit(business)

        # None = unlimited
        if limit is None:
            return True, None

        if current >= limit:
            return False, f"Location quota reached ({limit}). Upgrade your plan for more locations."

        return True, None

    @staticmethod
    def requires_location_on_entities(business):
        """
        Check if location is required on Event/Resource/TimeBlock for this business.
        True when: has multi_location feature AND >1 active location exists.

        Used by serializer validation (centralized check).
        """
        if not business:
            return False
        has_multi = business.has_feature('multi_location')
        if not has_multi:
            return False
        location_count = LocationService.count_active_locations(business)
        return location_count > 1

    # ========== Invariant Validation ==========

    @staticmethod
    def validate_at_least_one_active(business, excluding_location_id=None):
        """
        Validate that at least one active location will remain.
        Called before deactivating or deleting a location.

        Returns: (is_valid: bool, error_message: str | None)
        """
        active_query = Location.objects.filter(business=business, is_active=True)
        if excluding_location_id:
            active_query = active_query.exclude(pk=excluding_location_id)

        if not active_query.exists():
            return False, "Cannot deactivate or delete the last active location."
        return True, None

    # ========== Primary Location Management (Atomic) ==========

    @staticmethod
    @transaction.atomic
    def set_primary(business, location_id):
        """
        Atomically set a location as primary.
        Unsets any existing primary for the business.

        Returns: Updated location
        Raises: Location.DoesNotExist if not found
        """
        # Lock and get the target location
        location = Location.objects.select_for_update().get(
            pk=location_id,
            business=business,
        )

        # Unset any existing primary (excluding the target)
        Location.objects.filter(
            business=business,
            is_primary=True,
        ).exclude(pk=location.pk).update(is_primary=False)

        # Set new primary
        location.is_primary = True
        location.save(update_fields=['is_primary', 'updated_at'])

        return location

    @staticmethod
    @transaction.atomic
    def promote_next_primary(business, excluding_location_id):
        """
        Promote the next location to primary after deleting current primary.
        Called from perform_destroy when deleting a primary location.

        Returns: Promoted location or None if no other locations exist
        """
        next_location = Location.objects.filter(
            business=business,
            is_active=True,
        ).exclude(pk=excluding_location_id).order_by('display_order').first()

        if next_location:
            # Unset any existing primary (should be none, but safety)
            Location.objects.filter(
                business=business,
                is_primary=True,
            ).exclude(pk=next_location.pk).update(is_primary=False)

            next_location.is_primary = True
            next_location.save(update_fields=['is_primary', 'updated_at'])

        return next_location


class AvailabilityService:
    """
    Service for checking resource availability with concurrency limits and time blocks.

    CRITICAL Features:
    - Handles max_concurrent_events==0 as unlimited capacity
    - Filters out CANCELED events to prevent ghost bookings
    - Uses correct overlap logic: start < query_end AND end > query_start
    - Checks business-level and resource-level time blocks
    - Returns soft block warnings separately from hard blocks
    """

    @staticmethod
    def check_availability(resource, start_time, end_time, exclude_event_id=None, booking_context='staff_scheduling', service=None):
        """
        Check if resource has capacity for a new/updated event.

        Args:
            resource (Resource): The resource to check
            start_time (datetime): Proposed event start
            end_time (datetime): Proposed event end
            exclude_event_id (int, optional): Event ID to exclude (when updating)
            booking_context (str, optional): Context for booking ('customer_booking' or 'staff_scheduling')
                - 'customer_booking': Business hours enforced as HARD blocks
                - 'staff_scheduling': Business hours soft warnings only (default)
            service (Service, optional): Service being booked - uses its prep_time and takedown_time
                for buffer calculations. If not provided, uses resource.buffer_duration.

        Returns:
            tuple: (is_available: bool, reason: str, soft_block_warnings: list)
            - is_available: False if hard-blocked or capacity exceeded
            - reason: Human-readable explanation
            - soft_block_warnings: List of soft block warnings (can be overridden)
        """
        from datetime import timedelta
        soft_block_warnings = []

        # Calculate buffer times from service or resource
        if service:
            prep_buffer = timedelta(minutes=service.prep_time)
            takedown_buffer = timedelta(minutes=service.takedown_time)
        else:
            prep_buffer = resource.buffer_duration
            takedown_buffer = resource.buffer_duration

        # Calculate the full window including prep and takedown
        full_start = start_time - prep_buffer
        full_end = end_time + takedown_buffer

        # Step 1: Check time blocks for the SERVICE window (business hours, soft blocks)
        block_result = AvailabilityService._check_time_blocks(
            resource, start_time, end_time, booking_context=booking_context
        )

        if block_result['hard_blocked']:
            return False, block_result['reason'], []

        soft_block_warnings.extend(block_result['soft_warnings'])

        # Step 2: Check HARD blocks only for prep/takedown windows
        # (Prep/takedown can extend outside business hours but not through hard blocks)
        if prep_buffer.total_seconds() > 0:
            prep_block_result = AvailabilityService._check_time_blocks(
                resource, full_start, start_time, booking_context='buffer_check'
            )
            if prep_block_result['hard_blocked']:
                return False, f"Prep time blocked: {prep_block_result['reason']}", []

        if takedown_buffer.total_seconds() > 0:
            takedown_block_result = AvailabilityService._check_time_blocks(
                resource, end_time, full_end, booking_context='buffer_check'
            )
            if takedown_block_result['hard_blocked']:
                return False, f"Takedown time blocked: {takedown_block_result['reason']}", []

        # Step 3: Calculate search window for event conflicts (includes buffers)
        query_start = full_start
        query_end = full_end

        # Step 4: Find all events for this resource
        resource_content_type = ContentType.objects.get_for_model(Resource)

        resource_participants = Participant.objects.filter(
            content_type=resource_content_type,
            object_id=resource.id,
            role=Participant.Role.RESOURCE
        ).select_related('event')

        # Step 5: Filter for overlapping events
        overlapping_events = []
        for participant in resource_participants:
            event = participant.event

            # Skip if this is the event being updated
            # CRITICAL: Convert exclude_event_id to int for comparison (frontend may send string)
            if exclude_event_id and event.id == int(exclude_event_id):
                continue

            # CRITICAL: Skip cancelled events (prevents ghost bookings)
            if event.status == Event.Status.CANCELED:
                continue

            # CRITICAL: Check overlap using correct logic
            # Overlap exists when: event.start < query_end AND event.end > query_start
            if event.start_time < query_end and event.end_time > query_start:
                overlapping_events.append(event)

        current_count = len(overlapping_events)

        # Step 6: Check capacity limit

        # CRITICAL: Handle infinite capacity (0 = unlimited)
        if resource.max_concurrent_events == 0:
            return True, "Unlimited capacity resource", soft_block_warnings

        # Check if we've hit the limit
        if current_count >= resource.max_concurrent_events:
            return False, (
                f"Resource capacity exceeded. "
                f"{current_count}/{resource.max_concurrent_events} slots occupied."
            ), []

        # Available!
        return True, f"Available ({current_count + 1}/{resource.max_concurrent_events} slots)", soft_block_warnings

    @staticmethod
    def _check_time_blocks(resource, start_time, end_time, booking_context='staff_scheduling'):
        """
        Check if a time period is blocked by any time blocks.

        Checks both business-level blocks (resource=null) and resource-level blocks.
        Business-level blocks are checked first as they apply to all resources.

        For BUSINESS_HOURS blocks, the logic is INVERTED:
        - BUSINESS_HOURS blocks define WHEN business IS OPEN
        - Times OUTSIDE the block's window are blocked
        - Times WITHIN the block's window are allowed

        Args:
            resource (Resource): The resource to check
            start_time (datetime): Proposed event start
            end_time (datetime): Proposed event end
            booking_context (str): Context for booking:
                - 'customer_booking': Business hours enforced as HARD blocks
                - 'staff_scheduling': Business hours soft warnings only (default)
                - 'buffer_check': Only check HARD blocks (for prep/takedown windows)

        Returns:
            dict: {
                'hard_blocked': bool,
                'reason': str,
                'soft_warnings': list of warning strings
            }
        """
        result = {
            'hard_blocked': False,
            'reason': '',
            'soft_warnings': []
        }

        # Get active time blocks (business-level + resource-level)
        blocks = TimeBlock.objects.filter(
            Q(resource__isnull=True) | Q(resource=resource),
            is_active=True
        ).order_by('resource')  # Business blocks first (null sorts first)

        # Separate business hours blocks from regular blocks
        business_hours_blocks = [b for b in blocks if b.is_business_hours]
        regular_blocks = [b for b in blocks if not b.is_business_hours]

        # For buffer_check context, skip business hours entirely
        # (prep/takedown can extend outside business hours)
        if booking_context != 'buffer_check':
            # Check business hours first (if any are defined)
            # For business hours, we check if time is OUTSIDE the defined hours
            if business_hours_blocks:
                is_within_business_hours = False
                for block in business_hours_blocks:
                    # blocks_datetime_range returns True if the time overlaps with the block
                    if block.blocks_datetime_range(start_time, end_time):
                        is_within_business_hours = True
                        break

                if not is_within_business_hours:
                    # Time is OUTSIDE business hours
                    if booking_context == 'customer_booking':
                        result['hard_blocked'] = True
                        result['reason'] = "Outside business hours"
                        return result
                    elif booking_context == 'staff_scheduling':
                        result['soft_warnings'].append("Outside business hours")

        # Check regular blocks (closures, unavailable, etc.)
        for block in regular_blocks:
            if block.blocks_datetime_range(start_time, end_time):
                if block.block_type == TimeBlock.BlockType.HARD:
                    # Hard block - immediately return unavailable
                    level = "Business closed" if block.resource is None else f"{resource.name} unavailable"
                    result['hard_blocked'] = True
                    result['reason'] = f"{level}: {block.title}"
                    return result
                else:
                    # Soft block - add warning but continue (skip for buffer_check)
                    if booking_context != 'buffer_check':
                        level = "Business advisory" if block.resource is None else f"{resource.name} advisory"
                        result['soft_warnings'].append(f"{level}: {block.title}")

        return result

    @staticmethod
    def check_availability_simple(resource, start_time, end_time, exclude_event_id=None):
        """
        Simple availability check that returns just (bool, str) for backwards compatibility.

        Use check_availability() for full soft block warning support.
        """
        is_available, reason, _ = AvailabilityService.check_availability(
            resource, start_time, end_time, exclude_event_id
        )
        return is_available, reason
