"""
Recurring task date calculation utilities.

Supported recurrence types:
  - monthly_date:    config = {"day": 1}
  - every_n_weeks:   config = {"weeks": 2, "start_date": "2025-01-01"}
  - monthly_weekday: config = {"weekday": 0, "week_numbers": [2, 4]}
                               weekday: 0=Mon..6=Sun
                               week_numbers: [1..5]
"""

import calendar
from datetime import date, timedelta
from typing import Any


def get_next_due_date(recurrence_type: str, recurrence_config: dict[str, Any], after_date: date) -> date:
    """Return the next due date strictly after `after_date`."""
    if recurrence_type == "monthly_date":
        return _next_monthly_date(recurrence_config, after_date)
    elif recurrence_type == "every_n_weeks":
        return _next_every_n_weeks(recurrence_config, after_date)
    elif recurrence_type == "monthly_weekday":
        return _next_monthly_weekday(recurrence_config, after_date)
    else:
        raise ValueError(f"Unknown recurrence_type: {recurrence_type}")


def _next_monthly_date(config: dict, after: date) -> date:
    """E.g. every 1st of the month."""
    day = config["day"]
    # Try current month first
    year, month = after.year, after.month
    # Clamp day to last day of month
    last_day = calendar.monthrange(year, month)[1]
    clamped = min(day, last_day)
    candidate = date(year, month, clamped)
    if candidate > after:
        return candidate
    # Move to next month
    if month == 12:
        year, month = year + 1, 1
    else:
        month += 1
    last_day = calendar.monthrange(year, month)[1]
    return date(year, month, min(day, last_day))


def _next_every_n_weeks(config: dict, after: date) -> date:
    """E.g. every 2 weeks from a start date."""
    weeks = config["weeks"]
    start = date.fromisoformat(config["start_date"])
    interval = timedelta(weeks=weeks)
    if start > after:
        return start
    # Walk forward from start until we pass after
    candidate = start
    while candidate <= after:
        candidate += interval
    return candidate


def _next_monthly_weekday(config: dict, after: date) -> date:
    """E.g. every 2nd and 4th Monday of the month.
    weekday: 0=Mon..6=Sun, week_numbers: [1,2,3,4,5]
    """
    weekday = config["weekday"]
    week_numbers = sorted(config["week_numbers"])

    def occurrences_in_month(year: int, month: int):
        """Return all dates matching weekday+week_numbers in given month."""
        dates = []
        first = date(year, month, 1)
        # Find first occurrence of weekday in month
        diff = (weekday - first.weekday()) % 7
        first_match = first + timedelta(days=diff)
        for wn in week_numbers:
            d = first_match + timedelta(weeks=wn - 1)
            if d.month == month:
                dates.append(d)
        return sorted(dates)

    # Search current month and next few months
    year, month = after.year, after.month
    for _ in range(14):  # safety: max 14 months ahead
        for d in occurrences_in_month(year, month):
            if d > after:
                return d
        # Advance one month
        if month == 12:
            year, month = year + 1, 1
        else:
            month += 1

    raise ValueError("Could not find next occurrence within 14 months")
