"""
Demo Date Utilities for Temporal Determinism
Adjusts dates from seed data to be relative to demo session creation time
"""

from datetime import datetime, timezone, timedelta
from typing import Optional
import pytz

# Fixed base reference date for all demo data
# This is the "day 0" that all seed data is defined relative to
BASE_REFERENCE_DATE = datetime(2025, 1, 15, 6, 0, 0, tzinfo=timezone.utc)


def get_base_reference_date(session_created_at: Optional[datetime] = None) -> datetime:
    """
    Get the base reference date for demo data.
    
    If session_created_at is provided, calculate relative to it.
    Otherwise, use current time (for backwards compatibility with seed scripts).
    
    Returns:
        Base reference date at 6 AM UTC
    """
    if session_created_at:
        if session_created_at.tzinfo is None:
            session_created_at = session_created_at.replace(tzinfo=timezone.utc)
        # Reference is session creation time at 6 AM that day
        return session_created_at.replace(
            hour=6, minute=0, second=0, microsecond=0
        )
    # Fallback for seed scripts: use today at 6 AM
    now = datetime.now(timezone.utc)
    return now.replace(hour=6, minute=0, second=0, microsecond=0)


def adjust_date_for_demo(
    original_date: Optional[datetime],
    session_created_at: datetime
) -> Optional[datetime]:
    """
    Adjust a date from seed data to be relative to demo session creation time.

    This function calculates the offset between the original date and BASE_REFERENCE_DATE,
    then applies that offset to the session creation time.

    Example:
        # Original seed date: 2025-01-20 06:00 (BASE_REFERENCE + 5 days)
        # Demo session created: 2025-12-16 10:00
        # Offset: 5 days
        # Result: 2025-12-21 10:00 (session + 5 days)
    """
    if original_date is None:
        return None

    # Ensure timezone-aware datetimes
    if original_date.tzinfo is None:
        original_date = original_date.replace(tzinfo=timezone.utc)
    if session_created_at.tzinfo is None:
        session_created_at = session_created_at.replace(tzinfo=timezone.utc)

    # Calculate offset from base reference
    offset = original_date - BASE_REFERENCE_DATE

    # Apply offset to session creation date
    return session_created_at + offset


def calculate_edge_case_times(session_created_at: datetime) -> dict:
    """
    Calculate deterministic edge case times for demo sessions.
    
    These times are designed to always create specific demo scenarios:
    - One late delivery (should have arrived hours ago)
    - One overdue production batch (should have started hours ago)
    - One in-progress batch (started recently)
    - One upcoming batch (starts soon)
    - One arriving-soon delivery (arrives in a few hours)
    
    Returns:
        {
            'late_delivery_expected': session - 4h,
            'overdue_batch_planned_start': session - 2h,
            'in_progress_batch_actual_start': session - 1h45m,
            'upcoming_batch_planned_start': session + 1h30m,
            'arriving_soon_delivery_expected': session + 2h30m,
            'evening_batch_planned_start': today 17:00,
            'tomorrow_morning_planned_start': tomorrow 05:00
        }
    """
    if session_created_at.tzinfo is None:
        session_created_at = session_created_at.replace(tzinfo=timezone.utc)
    
    # Calculate today at 6 AM (base reference)
    base_reference = get_base_reference_date(session_created_at)
    
    # Calculate tomorrow at 6 AM
    tomorrow_base = base_reference + timedelta(days=1)
    
    return {
        'late_delivery_expected': session_created_at - timedelta(hours=4),
        'overdue_batch_planned_start': session_created_at - timedelta(hours=2),
        'in_progress_batch_actual_start': session_created_at - timedelta(hours=1, minutes=45),
        'upcoming_batch_planned_start': session_created_at + timedelta(hours=1, minutes=30),
        'arriving_soon_delivery_expected': session_created_at + timedelta(hours=2, minutes=30),
        'evening_batch_planned_start': base_reference.replace(hour=17, minute=0, second=0, microsecond=0),
        'tomorrow_morning_planned_start': tomorrow_base.replace(hour=5, minute=0, second=0, microsecond=0)
    }


def ensure_future_time(
    target_time: datetime,
    reference_time: datetime,
    min_hours_ahead: float = 1.0
) -> datetime:
    """
    Ensure a target time is in the future relative to reference time.
    
    If target_time is in the past or too close to reference_time,
    shift it forward by at least min_hours_ahead.
    """
    if target_time.tzinfo is None:
        target_time = target_time.replace(tzinfo=timezone.utc)
    if reference_time.tzinfo is None:
        reference_time = reference_time.replace(tzinfo=timezone.utc)
    
    time_diff = (target_time - reference_time).total_seconds() / 3600
    
    if time_diff < min_hours_ahead:
        # Shift forward to ensure minimum hours ahead
        return reference_time + timedelta(hours=min_hours_ahead)
    
    return target_time


def resolve_time_marker(
    time_marker: str,
    session_created_at: datetime,
    base_reference_date: datetime = BASE_REFERENCE_DATE
) -> datetime:
    """
    Resolve time markers like "BASE_TS + 1h30m" to actual datetimes.
    
    Supports markers in the format:
    - "BASE_TS + XhYm" (e.g., "BASE_TS + 1h30m")
    - "BASE_TS - XhYm" (e.g., "BASE_TS - 2h")
    - "BASE_TS + Xd" (e.g., "BASE_TS + 2d")
    - "BASE_TS - Xd" (e.g., "BASE_TS - 1d")
    
    Args:
        time_marker: Time marker string to resolve
        session_created_at: Demo session creation time
        base_reference_date: Base reference date for calculation
        
    Returns:
        Resolved datetime adjusted for demo session
        
    Raises:
        ValueError: If time_marker format is invalid
        
    Examples:
        >>> resolve_time_marker("BASE_TS + 1h30m", session_time)
        >>> # Returns session_created_at + 1h30m
        >>> resolve_time_marker("BASE_TS - 2h", session_time)
        >>> # Returns session_created_at - 2h
    """
    if not time_marker or not time_marker.startswith("BASE_TS"):
        raise ValueError(f"Invalid time marker format: {time_marker}")
    
    # Extract the offset part
    offset_part = time_marker[7:].strip()  # Remove "BASE_TS "
    
    if not offset_part:
        # Just "BASE_TS" - return session_created_at
        return session_created_at
    
    # Handle complex multi-operation markers like "- 30d 6h + 4h 5m"
    # Split by operators to handle multiple operations
    import re
    
    # Parse all operations in the format: [operator][value]
    # Pattern matches: optional whitespace, operator (+/-), number with optional decimal, unit (d/h/m)
    pattern = r'\s*([+-])\s*(\d+\.?\d*)\s*([dhm])'
    operations = []
    
    # Find all operations in the string
    for match in re.finditer(pattern, offset_part):
        operator = match.group(1)
        value = float(match.group(2))
        unit = match.group(3)
        operations.append((operator, value, unit))
    
    if not operations:
        # Fallback to old simple parsing for backwards compatibility
        operator = offset_part[0]
        value_part = offset_part[1:].strip()
        
        if operator not in ['+', '-']:
            raise ValueError(f"Invalid operator in time marker: {time_marker}")
        
        # Parse time components (supports decimals like 0.5d, 1.25h)
        days = 0.0
        hours = 0.0
        minutes = 0.0

        if 'd' in value_part:
            # Handle days (supports decimals like 0.5d = 12 hours)
            day_part, rest = value_part.split('d', 1)
            days = float(day_part)
            value_part = rest

        if 'h' in value_part:
            # Handle hours (supports decimals like 1.5h = 1h30m)
            hour_part, rest = value_part.split('h', 1)
            hours = float(hour_part)
            value_part = rest

        if 'm' in value_part:
            # Handle minutes (supports decimals like 30.5m)
            minute_part = value_part.split('m', 1)[0]
            minutes = float(minute_part)

        # Calculate offset using float values
        offset = timedelta(days=days, hours=hours, minutes=minutes)
        
        if operator == '+':
            return session_created_at + offset
        else:
            return session_created_at - offset
    
    # Process multiple operations
    result_time = session_created_at
    
    for operator, value, unit in operations:
        if unit == 'd':
            offset = timedelta(days=value)
        elif unit == 'h':
            offset = timedelta(hours=value)
        elif unit == 'm':
            offset = timedelta(minutes=value)
        else:
            raise ValueError(f"Invalid time unit '{unit}' in time marker: {time_marker}")
        
        if operator == '+':
            result_time = result_time + offset
        elif operator == '-':
            result_time = result_time - offset
        else:
            raise ValueError(f"Invalid operator '{operator}' in time marker: {time_marker}")
    
    return result_time


def shift_to_session_time(
    original_offset_days: int,
    original_hour: int,
    original_minute: int,
    session_created_at: datetime,
    base_reference: Optional[datetime] = None
) -> datetime:
    """
    Shift a time from seed data to demo session time with same-day preservation.
    
    Ensures that:
    1. Items scheduled for "today" (offset_days=0) remain on the same day as session creation
    2. Future items stay in the future, past items stay in the past
    3. Times don't shift to invalid moments (e.g., past times for pending items)
    
    Examples:
        # Session created at noon, item originally scheduled for morning
        >>> session = datetime(2025, 12, 12, 12, 0, tzinfo=timezone.utc)
        >>> result = shift_to_session_time(0, 6, 0, session)  # Today at 06:00
        >>> # Returns today at 13:00 (shifted forward to stay in future)

        # Session created at noon, item originally scheduled for evening
        >>> result = shift_to_session_time(0, 18, 0, session)  # Today at 18:00
        >>> # Returns today at 18:00 (already in future)
    """
    if session_created_at.tzinfo is None:
        session_created_at = session_created_at.replace(tzinfo=timezone.utc)
    
    if base_reference is None:
        base_reference = get_base_reference_date(session_created_at)
    
    # Calculate original time
    original_time = base_reference.replace(
        hour=original_hour,
        minute=original_minute,
        second=0,
        microsecond=0
    ) + timedelta(days=original_offset_days)
    
    # Calculate offset from base reference
    offset = original_time - base_reference
    
    # Apply offset to session creation date
    new_time = session_created_at + offset
    
    # Ensure the time is in the future for pending items
    if original_offset_days >= 0:  # Future or today
        new_time = ensure_future_time(new_time, session_created_at, min_hours_ahead=0.5)
    
    return new_time


def get_working_hours_time(
    target_date: datetime,
    hours_from_start: float = 2.0
) -> datetime:
    """
    Get a time within working hours (8 AM - 6 PM) for a given date.
    
    Args:
        target_date: The date to calculate time for
        hours_from_start: Hours from working day start (8 AM)
    
    Returns:
        Datetime within working hours
    """
    if target_date.tzinfo is None:
        target_date = target_date.replace(tzinfo=timezone.utc)
    
    # Working hours: 8 AM - 6 PM (10 hours)
    working_start = target_date.replace(hour=8, minute=0, second=0, microsecond=0)
    working_end = target_date.replace(hour=18, minute=0, second=0, microsecond=0)
    
    # Calculate time within working hours
    result_time = working_start + timedelta(hours=hours_from_start)
    
    # Ensure it's within working hours
    if result_time > working_end:
        result_time = working_end
    
    return result_time


def get_next_workday(date: datetime) -> datetime:
    """
    Get the next workday (Monday-Friday), skipping weekends.
    
    If date is Friday, returns Monday.
    If date is Saturday, returns Monday.
    Otherwise returns next day.
    """
    if date.tzinfo is None:
        date = date.replace(tzinfo=timezone.utc)
    
    next_day = date + timedelta(days=1)
    
    # Skip weekends
    while next_day.weekday() >= 5:  # 5=Saturday, 6=Sunday
        next_day += timedelta(days=1)
    
    return next_day


def get_previous_workday(date: datetime) -> datetime:
    """
    Get the previous workday (Monday-Friday), skipping weekends.
    
    If date is Monday, returns Friday.
    If date is Sunday, returns Friday.
    Otherwise returns previous day.
    """
    if date.tzinfo is None:
        date = date.replace(tzinfo=timezone.utc)
    
    prev_day = date - timedelta(days=1)
    
    # Skip weekends
    while prev_day.weekday() >= 5:  # 5=Saturday, 6=Sunday
        prev_day -= timedelta(days=1)
    
    return prev_day


def format_iso_with_timezone(dt: datetime) -> str:
    """
    Format datetime as ISO 8601 with timezone, replacing Z with +00:00 for compatibility.
    """
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    
    iso_string = dt.isoformat()
    return iso_string.replace('+00:00', 'Z') if iso_string.endswith('+00:00') else iso_string