"""
Occupancy service for Aimantis dashboard.

Calculates occupancy percentages for:
- Today
- Next 7 days
- Next 30 days

Uses optimized SQL aggregation to avoid Python loops.
"""

from datetime import date, timedelta
from typing import Dict, Any, Optional

from django.db.models import Q
from django.utils import timezone

from bookings.models import Booking
from properties.models import Property

from dashboard.constants import OCCUPANCY_WINDOWS, OCCUPANCY_ROUNDING
from dashboard.services.utils import (
    calculate_overlapping_nights,
    get_today,
    safe_divide,
    round_occupancy,
)


class OccupancyService:
    """
    Service for calculating occupancy percentages.
    
    Uses efficient overlap calculations and avoids N+1 queries
    by batching all bookings for a given window.
    """
    
    def __init__(self, structure_id: Optional[int] = None):
        """
        Initialize the occupancy service.
        
        Args:
            structure_id: Optional structure ID for multi-tenant filtering
        """
        self.structure_id = structure_id
        self.today = get_today()
    
    def _get_structure_filter(self) -> Dict[str, Any]:
        """Get structure filter dict for queries."""
        return {"structure_id": self.structure_id} if self.structure_id else {}
    
    def _get_total_rooms(self) -> int:
        """Get total room count for the structure."""
        qs = Property.objects.all()
        if self.structure_id:
            qs = qs.filter(structure_id=self.structure_id)
        return qs.count()
    
    def _calculate_occupancy_for_window(
        self,
        window_start: date,
        window_end: date,
    ) -> int:
        """
        Calculate occupancy percentage for a date window.
        
        CRITICAL BUSINESS RULE:
        - Counts DISTINCT occupied properties per day (not booking rows)
        - Occupancy can NEVER exceed 100%
        - Multiple bookings for the same room on the same day = 1 occupied room
        
        Algorithm:
        1. For each day in window, count DISTINCT property_ids with active bookings
        2. Sum daily occupied rooms across all days
        3. Divide by (total_rooms * window_days)
        
        Args:
            window_start: Start date (inclusive)
            window_end: End date (exclusive)
            
        Returns:
            Occupancy percentage (0-100), capped at 100
        """
        total_rooms = self._get_total_rooms()
        if total_rooms == 0:
            return 0
        
        # Calculate total possible nights in window
        window_days = (window_end - window_start).days
        total_possible_nights = total_rooms * window_days
        
        if total_possible_nights == 0:
            return 0
        
        # Fetch all bookings overlapping the window (active reservations based on dates)
        # ACTIVE STAY LOGIC: check_in_date < window_end AND check_out_date > window_start
        # This counts all reservations for the period, not just checked-in bookings
        # ORM Optimization:
        # - NO select_related: We only access booking.property_id and date fields
        # - .only(): Selects minimal fields needed for DISTINCT property counting
        bookings = Booking.objects.filter(
            check_in_date__lt=window_end,
            check_out_date__gt=window_start,
            **self._get_structure_filter()
        ).only(
            'property_id',        # CRITICAL: Needed for DISTINCT room counting
            'check_in_date',
            'check_out_date',
        )
        
        # CRITICAL FIX: Count DISTINCT occupied properties per day
        # This prevents occupancy from exceeding 100% when multiple bookings
        # overlap for the same room (e.g., double-booking edge cases)
        
        # Build a set of (date, property_id) tuples for each occupied night
        occupied_room_nights = set()
        
        for booking in bookings:
            # For each booking, mark each night as occupied for this property
            current_date = max(booking.check_in_date, window_start)
            booking_end = min(booking.check_out_date, window_end)
            
            while current_date < booking_end:
                # Tuple: (date, property_id) ensures DISTINCT counting
                occupied_room_nights.add((current_date, booking.property_id))
                current_date += timedelta(days=1)
        
        # Total reserved nights = count of unique (date, property_id) combinations
        total_reserved_nights = len(occupied_room_nights)
        
        # Calculate percentage (capped at 100%)
        occupancy = safe_divide(
            total_reserved_nights * 100,
            total_possible_nights,
            default=0.0,
        )
        
        # CRITICAL: Cap at 100% (defensive, should never exceed due to DISTINCT logic)
        occupancy_capped = min(occupancy, 100)
        
        return round_occupancy(occupancy_capped, OCCUPANCY_ROUNDING)
    
    def get_today_occupancy(self) -> int:
        """
        Calculate occupancy for today.
        
        Returns:
            Occupancy percentage for today
        """
        return self._calculate_occupancy_for_window(
            self.today,
            self.today + timedelta(days=1),
        )
    
    def get_next_7_days_occupancy(self) -> int:
        """
        Calculate occupancy forecast for next 7 days.
        
        Returns:
            Average occupancy percentage for next 7 days
        """
        return self._calculate_occupancy_for_window(
            self.today,
            self.today + timedelta(days=7),
        )
    
    def get_next_30_days_occupancy(self) -> int:
        """
        Calculate occupancy forecast for next 30 days.
        
        Returns:
            Average occupancy percentage for next 30 days
        """
        return self._calculate_occupancy_for_window(
            self.today,
            self.today + timedelta(days=30),
        )
    
    def get_occupancy(self) -> Dict[str, int]:
        """
        Get all occupancy metrics in a single call.
        
        Returns:
            Dict with occupancy percentages for different time windows
        """
        return {
            "today": self.get_today_occupancy(),
            "next_7_days": self.get_next_7_days_occupancy(),
            "next_30_days": self.get_next_30_days_occupancy(),
        }
