"""Bucket date range utilities for time dimension analytics.

This module provides utilities for calculating canonical date ranges
for time dimension buckets (week, month, quarter) and clamping them
to filter date ranges.

Time dimensions in analytics queries group data by calendar periods.
When users apply date filters, the bucket labels may extend beyond
the filter range. This module calculates both:
- Canonical boundaries: the full calendar period (e.g., full week Mon-Sun)
- Clamped boundaries: intersection with the user's filter date range

Example:
    >>> from testio_mcp.utilities.bucket_utils import get_bucket_boundaries
    >>> # User filters Nov 30 - Dec 30, but week 48 is Nov 25 - Dec 1
    >>> boundaries = get_bucket_boundaries(
    ...     "week", "2024-W48",
    ...     filter_start=date(2024, 11, 30),
    ...     filter_end=date(2024, 12, 30)
    ... )
    >>> boundaries.start  # Clamped to filter
    date(2024, 11, 30)
    >>> boundaries.full_start  # Canonical week start
    date(2024, 11, 25)
"""

import calendar
import re
from datetime import date, timedelta
from typing import NamedTuple


class BucketBoundaries(NamedTuple):
    """Canonical and clamped boundaries for a time bucket.

    Attributes:
        start: Actual start date (clamped to filter range)
        end: Actual end date (clamped to filter range)
        full_start: Canonical bucket start (e.g., Monday of the week)
        full_end: Canonical bucket end (e.g., Sunday of the week)
    """

    start: date
    end: date
    full_start: date
    full_end: date


def get_week_boundaries(week_label: str) -> tuple[date, date]:
    """Calculate canonical boundaries for a week bucket.

    Uses SQLite strftime %W week numbering where:
    - Week 00 contains January 1 (unless Jan 1 is Monday, then it's week 01)
    - Weeks start on Monday
    - Week numbers are 00-53

    Args:
        week_label: Week in "YYYY-WNN" format (e.g., "2024-W48")

    Returns:
        Tuple of (monday_date, sunday_date) for the week

    Raises:
        ValueError: If week_label format is invalid

    Example:
        >>> get_week_boundaries("2024-W48")
        (date(2024, 11, 25), date(2024, 12, 1))
    """
    match = re.match(r"^(\d{4})-W(\d{1,2})$", week_label)
    if not match:
        raise ValueError(f"Invalid week label format: '{week_label}'. Expected 'YYYY-WNN'")

    year = int(match.group(1))
    week_num = int(match.group(2))

    # Validate week number (strftime %W produces 00-53)
    if week_num < 0 or week_num > 53:
        raise ValueError(f"Invalid week number: {week_num}. Must be 0-53.")

    # Calculate Monday of the requested week using strftime %W logic:
    # - Week 01 starts on the first Monday of the year
    # - Week 00 contains days before the first Monday (if Jan 1 is not Monday)
    # - If Jan 1 is Monday, there is no week 00 for that year
    jan1 = date(year, 1, 1)
    jan1_weekday = jan1.weekday()  # 0=Monday, 6=Sunday

    # Find first Monday of the year (start of week 01)
    # If Jan 1 is Monday, first_monday = Jan 1
    # Otherwise, first_monday = next Monday after Jan 1
    days_until_monday = (7 - jan1_weekday) % 7
    first_monday = jan1 + timedelta(days=days_until_monday)

    if week_num == 0:
        # Week 00: days before first Monday
        # Monday of week 00 is 7 days before first_monday
        monday = first_monday - timedelta(days=7)
    else:
        # Week N: first_monday + (N-1) weeks
        monday = first_monday + timedelta(weeks=week_num - 1)

    sunday = monday + timedelta(days=6)

    return (monday, sunday)


def get_month_boundaries(month_label: str) -> tuple[date, date]:
    """Calculate canonical boundaries for a month bucket.

    Args:
        month_label: Month in "YYYY-MM" format (e.g., "2024-11")

    Returns:
        Tuple of (first_day, last_day) for the month

    Raises:
        ValueError: If month_label format is invalid

    Example:
        >>> get_month_boundaries("2024-11")
        (date(2024, 11, 1), date(2024, 11, 30))
    """
    match = re.match(r"^(\d{4})-(\d{2})$", month_label)
    if not match:
        raise ValueError(f"Invalid month label format: '{month_label}'. Expected 'YYYY-MM'")

    year = int(match.group(1))
    month = int(match.group(2))

    # Validate month
    if month < 1 or month > 12:
        raise ValueError(f"Invalid month: {month}. Must be 1-12.")

    first_day = date(year, month, 1)
    _, last_day_num = calendar.monthrange(year, month)
    last_day = date(year, month, last_day_num)

    return (first_day, last_day)


def get_quarter_boundaries(quarter_label: str) -> tuple[date, date]:
    """Calculate canonical boundaries for a quarter bucket.

    Args:
        quarter_label: Quarter in "YYYY-QN" format (e.g., "2024-Q4")

    Returns:
        Tuple of (first_day, last_day) for the quarter

    Raises:
        ValueError: If quarter_label format is invalid

    Example:
        >>> get_quarter_boundaries("2024-Q4")
        (date(2024, 10, 1), date(2024, 12, 31))
    """
    match = re.match(r"^(\d{4})-Q([1-4])$", quarter_label)
    if not match:
        raise ValueError(f"Invalid quarter label format: '{quarter_label}'. Expected 'YYYY-QN'")

    year = int(match.group(1))
    quarter = int(match.group(2))

    # Q1: Jan-Mar, Q2: Apr-Jun, Q3: Jul-Sep, Q4: Oct-Dec
    quarter_start_months = {1: 1, 2: 4, 3: 7, 4: 10}
    quarter_end_months = {1: 3, 2: 6, 3: 9, 4: 12}

    start_month = quarter_start_months[quarter]
    end_month = quarter_end_months[quarter]

    first_day = date(year, start_month, 1)
    _, last_day_num = calendar.monthrange(year, end_month)
    last_day = date(year, end_month, last_day_num)

    return (first_day, last_day)


def get_bucket_boundaries(
    dimension: str,
    bucket_label: str | None,
    filter_start: date | None = None,
    filter_end: date | None = None,
) -> BucketBoundaries | None:
    """Calculate bucket boundaries for a time dimension, with optional clamping.

    This is the main entry point for bucket boundary calculation. It:
    1. Parses the bucket label to get canonical date range
    2. Clamps the range to the filter dates (if provided)
    3. Returns both canonical and clamped boundaries

    Args:
        dimension: Time dimension name ("week", "month", or "quarter")
        bucket_label: The bucket label value (e.g., "2024-W48", "2024-11", "2024-Q4")
        filter_start: Optional filter start date for clamping
        filter_end: Optional filter end date for clamping

    Returns:
        BucketBoundaries with canonical and clamped dates, or None if label is None

    Raises:
        ValueError: If dimension is not a time dimension or label format is invalid

    Example:
        >>> get_bucket_boundaries("week", "2024-W48", date(2024, 11, 30), date(2024, 12, 30))
        BucketBoundaries(
            start=date(2024, 11, 30),    # Clamped to filter start
            end=date(2024, 12, 1),       # Full bucket end (Sunday)
            full_start=date(2024, 11, 25), # Canonical Monday
            full_end=date(2024, 12, 1)    # Canonical Sunday
        )
    """
    if bucket_label is None:
        return None

    # Get canonical boundaries based on dimension
    boundary_functions = {
        "week": get_week_boundaries,
        "month": get_month_boundaries,
        "quarter": get_quarter_boundaries,
    }

    if dimension not in boundary_functions:
        raise ValueError(f"Not a time dimension: '{dimension}'. Expected: week, month, quarter")

    full_start, full_end = boundary_functions[dimension](bucket_label)

    # Clamp to filter range
    start = full_start
    end = full_end

    if filter_start is not None:
        start = max(start, filter_start)
    if filter_end is not None:
        end = min(end, filter_end)

    return BucketBoundaries(
        start=start,
        end=end,
        full_start=full_start,
        full_end=full_end,
    )


def format_bucket_metadata(boundaries: BucketBoundaries) -> dict[str, str]:
    """Format bucket boundaries as ISO date strings for JSON response.

    Args:
        boundaries: BucketBoundaries to format

    Returns:
        Dictionary with start, end, full_start, full_end as ISO date strings

    Example:
        >>> boundaries = BucketBoundaries(
        ...     start=date(2024, 11, 30),
        ...     end=date(2024, 12, 1),
        ...     full_start=date(2024, 11, 25),
        ...     full_end=date(2024, 12, 1),
        ... )
        >>> format_bucket_metadata(boundaries)
        {'start': '2024-11-30', 'end': '2024-12-01', ...}
    """
    return {
        "start": boundaries.start.isoformat(),
        "end": boundaries.end.isoformat(),
        "full_start": boundaries.full_start.isoformat(),
        "full_end": boundaries.full_end.isoformat(),
    }
