"""
Dashboard metrics service for Aimantis PMS.

Provides centralized, reusable calculation logic for:
- Average Daily Rate (ADR)
- Revenue per Available Room (RevPAR)
- Other derived hospitality metrics

Uses proper PMS stay-night expansion logic consistent with
ISTAT, city tax, and guest night services.

ORM Optimization Strategy:
- NO select_related() is used because we only access property_id (FK integer),
  not the full property object. Traversing FK objects would be wasteful.
- .only() explicitly selects the minimal fields required for calculations.
- This prevents N+1 queries while avoiding Django FieldError from deferred fields.
- For multi-tenant scale, structure_id filtering is applied at the DB level.
"""

from datetime import date, timedelta
from decimal import Decimal, ROUND_HALF_UP
from typing import Dict, List, Optional, Tuple

from django.db.models import Q
from bookings.models import Booking


class StayNightRevenue:
    """
    Represents revenue allocated to a single occupied room night.
    
    Immutable dataclass-like structure for tracking per-night revenue.
    """
    __slots__ = ['booking_id', 'property_id', 'night_date', 'revenue']
    
    def __init__(self, booking_id: int, property_id: int, night_date: date, revenue: Decimal):
        self.booking_id = booking_id
        self.property_id = property_id
        self.night_date = night_date
        self.revenue = revenue


def calculate_per_night_revenue(booking: Booking) -> Decimal:
    """
    Calculate per-night revenue for a booking using BASE PRICE only.
    
    ADR Formula: base_price / length_of_stay
    
    IMPORTANT:
    - Uses base_price (room revenue ONLY)
    - Excludes: cleaning_fee, city_tax, other_extra_fees, subtotal, total_price
    - This is the hospitality industry standard for ADR calculation
    
    Args:
        booking: Booking instance with base_price and length_of_stay
        
    Returns:
        Decimal per-night revenue, or Decimal('0') if invalid
    """
    if not booking.length_of_stay or booking.length_of_stay <= 0:
        return Decimal('0')
    
    # Use base_price (room revenue), NOT total_price (includes fees)
    base_price = booking.base_price or Decimal('0')
    length_of_stay = Decimal(str(booking.length_of_stay))
    
    if length_of_stay == 0:
        return Decimal('0')
    
    return base_price / length_of_stay


def expand_booking_to_revenue_nights(
    booking: Booking,
    window_start: date,
    window_end: date,
) -> List[StayNightRevenue]:
    """
    Expand a single booking into individual revenue nights within a window.
    
    This is the core PMS logic that properly handles:
    - check_in_date is INCLUDED
    - check_out_date is EXCLUDED
    - Overlap calculation with the analysis window
    - Per-night revenue allocation
    
    Args:
        booking: Booking object
        window_start: Start of analysis window (inclusive)
        window_end: End of analysis window (exclusive)
        
    Returns:
        List of StayNightRevenue objects for each occupied night in window
    """
    # Validate booking has required data
    if not booking.check_in_date or not booking.check_out_date:
        return []
    
    if not booking.property_id:
        return []
    
    # Calculate overlap with window
    effective_start = max(booking.check_in_date, window_start)
    effective_end = min(booking.check_out_date, window_end)
    
    # No overlap
    if effective_start >= effective_end:
        return []
    
    # Calculate per-night revenue
    per_night_revenue = calculate_per_night_revenue(booking)
    
    if per_night_revenue <= 0:
        return []
    
    # Generate revenue nights (check_out_date is EXCLUDED)
    revenue_nights = []
    current_date = effective_start
    
    while current_date < effective_end:
        revenue_nights.append(
            StayNightRevenue(
                booking_id=booking.id,
                property_id=booking.property_id,
                night_date=current_date,
                revenue=per_night_revenue,
            )
        )
        current_date += timedelta(days=1)
    
    return revenue_nights


def get_bookings_for_window(
    structure_id: Optional[int],
    window_start: date,
    window_end: date,
    only_checked_in: bool = False,
) -> List[Booking]:
    """
    Fetch bookings overlapping a date window with proper filtering.
    
    Uses PMS overlap logic:
    - booking.check_in_date < window_end
    - booking.check_out_date > window_start
    
    Args:
        structure_id: Structure ID for multi-tenant filtering (None = all)
        window_start: Window start date (inclusive)
        window_end: Window end date (exclusive)
        only_checked_in: If True, only include is_checked_in=True bookings
                         (legacy parameter, default=False for active-stay logic)
        
    Returns:
        List of Booking objects overlapping the window
    """
    # Base overlap filter (active stay logic)
    filters = Q(
        check_in_date__lt=window_end,
        check_out_date__gt=window_start,
    )
    
    # Add structure filter
    if structure_id:
        filters &= Q(structure_id=structure_id)
    
    # Add check-in status filter (optional, for backward compatibility)
    if only_checked_in:
        filters &= Q(is_checked_in=True)
    
    # Execute query with production-grade ORM optimization
    # WHY NO select_related():
    # - We only access booking.property_id (FK integer field), not booking.property object
    # - Accessing booking.property.name or other FK attributes would require select_related
    # - Since we don't traverse FK objects, select_related would waste memory/CPU
    # - .only() ensures we fetch only the minimal fields needed for ADR calculation
    #
    # WHY these specific .only() fields:
    # - id: Required for booking identification in revenue night expansion
    # - property_id: Required for room count tracking (FK integer, no FK traversal)
    # - structure_id: Required for multi-tenant filtering validation
    # - check_in_date: Required for active stay logic (check_in <= target < check_out)
    # - check_out_date: Required for active stay logic (checkout day is EXCLUDED)
    # - length_of_stay: Required for per-night revenue calculation (base_price / nights)
    # - base_price: REQUIRED for ADR (room revenue only, excludes fees)
    # - platform: Required for event metadata (channel attribution)
    return list(
        Booking.objects.filter(filters)
        # NO select_related - only property_id is accessed, not booking.property object
        .only(
            'id',
            'property_id',
            'structure_id',
            'check_in_date',
            'check_out_date',
            'length_of_stay',
            'base_price',  # ADR uses base_price (room revenue), NOT total_price
            'platform',
        )
    )


def calculate_adr_for_window(
    structure_id: Optional[int],
    window_start: date,
    window_end: date,
    only_checked_in: bool = False,
) -> float:
    """
    Calculate Average Daily Rate (ADR) for a date window.
    
    ADR Formula (Hospitality Industry Standard):
        ADR = Total Room Revenue / Occupied Room Nights
    
    Where:
    - Total Room Revenue = SUM(base_price for all active bookings)
    - Occupied Room Nights = COUNT(stay nights within window)
    - Uses base_price ONLY (excludes cleaning fees, city tax, extra services)
    
    Business Rules:
    - Active stay: check_in_date <= target_date < check_out_date
    - Check-out day is NOT counted as occupied
    - Zero-division protection: returns 0.0 if no occupied nights
    - Result rounded to 2 decimals using ROUND_HALF_UP
    
    Args:
        structure_id: Structure ID for filtering (None = all structures)
        window_start: Window start date (inclusive)
        window_end: Window end date (exclusive)
        only_checked_in: If True, only count checked-in bookings (legacy, default=False)
        
    Returns:
        ADR as float rounded to 2 decimals, or 0.0 if no occupied nights
    """
    # Fetch overlapping bookings with optimized queryset
    bookings = get_bookings_for_window(
        structure_id=structure_id,
        window_start=window_start,
        window_end=window_end,
        only_checked_in=only_checked_in,
    )
    
    if not bookings:
        return 0.0
    
    # Expand all bookings to individual revenue nights
    all_revenue_nights: List[StayNightRevenue] = []
    
    for booking in bookings:
        revenue_nights = expand_booking_to_revenue_nights(
            booking=booking,
            window_start=window_start,
            window_end=window_end,
        )
        all_revenue_nights.extend(revenue_nights)
    
    # Defensive zero-division protection
    if not all_revenue_nights:
        return 0.0
    
    # Calculate total revenue and occupied nights
    total_revenue = sum(night.revenue for night in all_revenue_nights)
    occupied_nights = len(all_revenue_nights)
    
    # Final zero-division check (should never reach here due to above check)
    if occupied_nights == 0:
        return 0.0
    
    # Calculate ADR and round to 2 decimals
    adr = total_revenue / occupied_nights
    adr_rounded = adr.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
    
    return float(adr_rounded)


def calculate_all_adr_metrics(
    structure_id: Optional[int],
    today: date,
) -> Dict[str, float]:
    """
    Calculate all ADR metrics (today, 7-day, 30-day) in one optimized call.
    
    This minimizes database queries by batching booking fetches.
    
    Args:
        structure_id: Structure ID for filtering (None = all structures)
        today: Current date for calculations
        
    Returns:
        Dict with keys: today, next_7_days, next_30_days
    """
    # Define windows
    windows = {
        'today': (today, today + timedelta(days=1)),
        'next_7_days': (today, today + timedelta(days=7)),
        'next_30_days': (today, today + timedelta(days=30)),
    }
    
    # Calculate ADR for each window
    # All windows use active-stay logic (date-based), not check-in status
    # This ensures consistency across all metrics
    results = {}
    
    for metric_name, (window_start, window_end) in windows.items():
        # Use active-stay logic for all windows (only_checked_in=False)
        results[metric_name] = calculate_adr_for_window(
            structure_id=structure_id,
            window_start=window_start,
            window_end=window_end,
            only_checked_in=False,
        )
    
    return results


# ============================================================================
# UNIT-TESTABLE HELPER METHODS
# These functions are designed to be easily testable in isolation.
# ============================================================================

def is_active_stay_on_date(
    check_in_date: date,
    check_out_date: date,
    target_date: date,
) -> bool:
    """
    Determine if a booking is active on a specific date.
    
    Active Stay Logic (PMS Standard):
    - check_in_date <= target_date < check_out_date
    - Check-out day is EXCLUSIVE (guest leaves, room becomes available)
    
    This is a pure function with no side effects, ideal for unit testing.
    
    Args:
        check_in_date: Booking check-in date
        check_out_date: Booking check-out date
        target_date: Date to check activity
        
    Returns:
        True if booking is active on target_date, False otherwise
        
    Examples:
        >>> is_active_stay_on_date(date(2026, 5, 14), date(2026, 5, 19), date(2026, 5, 14))
        True
        >>> is_active_stay_on_date(date(2026, 5, 14), date(2026, 5, 19), date(2026, 5, 19))
        False  # Check-out day is NOT active
        >>> is_active_stay_on_date(date(2026, 5, 14), date(2026, 5, 19), date(2026, 5, 13))
        False  # Before check-in
    """
    return check_in_date <= target_date < check_out_date


def calculate_occupancy_percentage(
    occupied_nights: int,
    total_possible_nights: int,
) -> float:
    """
    Calculate occupancy percentage with defensive zero-division protection.
    
    Formula: (occupied_nights / total_possible_nights) * 100
    
    Args:
        occupied_nights: Number of occupied room nights
        total_possible_nights: Total available room nights
        
    Returns:
        Occupancy percentage (0-100), rounded to 2 decimals
        Returns 0.0 if total_possible_nights is 0
        
    Examples:
        >>> calculate_occupancy_percentage(20, 100)
        20.0
        >>> calculate_occupancy_percentage(0, 100)
        0.0
        >>> calculate_occupancy_percentage(50, 0)
        0.0  # Defensive zero-division
    """
    if total_possible_nights == 0:
        return 0.0
    
    occupancy = (occupied_nights / total_possible_nights) * 100
    return round(occupancy, 2)


def calculate_adr_from_bookings(
    bookings: List[Booking],
    window_start: date,
    window_end: date,
) -> float:
    """
    Calculate ADR from a list of bookings within a date window.
    
    This is a pure calculation function that accepts pre-fetched bookings,
    making it ideal for unit testing without database dependencies.
    
    ADR Formula:
        ADR = SUM(base_price for occupied nights) / COUNT(occupied room nights)
    
    Args:
        bookings: List of Booking objects (must have base_price, length_of_stay,
                  check_in_date, check_out_date, property_id)
        window_start: Window start date (inclusive)
        window_end: Window end date (exclusive)
        
    Returns:
        ADR as float rounded to 2 decimals, or 0.0 if no occupied nights
        
    Examples:
        >>> bookings = [MockBooking(base_price=270, length_of_stay=5, ...)]
        >>> calculate_adr_from_bookings(bookings, date(2026, 5, 14), date(2026, 5, 15))
        54.0  # 270 / 5 = 54 per night
    """
    if not bookings:
        return 0.0
    
    # Expand all bookings to revenue nights
    all_revenue_nights: List[StayNightRevenue] = []
    
    for booking in bookings:
        revenue_nights = expand_booking_to_revenue_nights(
            booking=booking,
            window_start=window_start,
            window_end=window_end,
        )
        all_revenue_nights.extend(revenue_nights)
    
    # Defensive zero-division protection
    if not all_revenue_nights:
        return 0.0
    
    # Calculate ADR
    total_revenue = sum(night.revenue for night in all_revenue_nights)
    occupied_nights = len(all_revenue_nights)
    
    if occupied_nights == 0:
        return 0.0
    
    adr = total_revenue / occupied_nights
    adr_rounded = adr.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
    
    return float(adr_rounded)
