from datetime import date, datetime, timedelta
from typing import Iterable, List, Optional, Union

from config.constants import DATE_FORMAT


DateLike = Union[str, date, datetime]


# Parsing & Formatting
def parse_date(value: DateLike) -> date:
    """
    Convert a date-like object to a datetime.date.
    """
    if isinstance(value, date) and not isinstance(value, datetime):
        return value

    if isinstance(value, datetime):
        return value.date()

    if isinstance(value, str):
        return datetime.strptime(value, DATE_FORMAT).date()

    raise TypeError(f"Unsupported date type: {type(value)}")


def format_date(value: DateLike) -> str:
    """
    Format a date-like object as YYYY-MM-DD string.
    """
    d = parse_date(value)
    return d.strftime(DATE_FORMAT)


# Validation
def validate_date_range(
    start: DateLike,
    end: Optional[DateLike],
) -> tuple[date, Optional[date]]:
    """
    Validates and normalizes a (start, end) date range.
    """
    start_d = parse_date(start)
    end_d = parse_date(end) if end is not None else None

    if end_d is not None and start_d > end_d:
        raise ValueError("start date must be <= end date")

    return start_d, end_d


# Date Arithmetic
def add_days(d: DateLike, n: int) -> date:
    """
    Add n calendar days to a date.
    """
    d = parse_date(d)
    return d + timedelta(days=n)


def days_between(start: DateLike, end: DateLike) -> int:
    """
    Number of calendar days between two dates.
    """
    start_d = parse_date(start)
    end_d = parse_date(end)
    return (end_d - start_d).days


# Sequences & Windows
def date_range(
    start: DateLike,
    end: DateLike,
    step_days: int = 1,
) -> List[date]:
    """
    Generate a list of dates from start to end inclusive.
    """
    start_d = parse_date(start)
    end_d = parse_date(end)

    if step_days <= 0:
        raise ValueError("step_days must be positive")

    dates = []
    current = start_d
    while current <= end_d:
        dates.append(current)
        current += timedelta(days=step_days)

    return dates


def rolling_windows(
    dates: Iterable[DateLike],
    window: int,
) -> List[List[date]]:
    """
    Generate rolling windows over a sequence of dates.
    """
    if window <= 0:
        raise ValueError("window must be positive")

    parsed = [parse_date(d) for d in dates]
    return [
        parsed[i : i + window]
        for i in range(len(parsed) - window + 1)
    ]


# Financial Calendar Helpers
def is_weekend(d: DateLike) -> bool:
    """
    Return True if date falls on a weekend.
    """
    d = parse_date(d)
    return d.weekday() >= 5  # 5 = Saturday, 6 = Sunday


def next_business_day(d: DateLike) -> date:
    """
    Return the next business day, skipping weekends.
    """
    d = parse_date(d)
    next_d = d + timedelta(days=1)

    while is_weekend(next_d):
        next_d += timedelta(days=1)

    return next_d


def previous_business_day(d: DateLike) -> date:
    """
    Return the previous business day, skipping weekends.
    """
    d = parse_date(d)
    prev_d = d - timedelta(days=1)

    while is_weekend(prev_d):
        prev_d -= timedelta(days=1)

    return prev_d


# Option-Specific (Calendar Only)
def is_month_end(d: DateLike) -> bool:
    """
    Return True if date is the last calendar day of the month.
    """
    d = parse_date(d)
    return add_days(d, 1).month != d.month


def third_friday(year: int, month: int) -> date:
    """
    Compute the third Friday of a given month.
    """
    d = date(year, month, 1)
    friday_count = 0

    while True:
        if d.weekday() == 4:  # Friday
            friday_count += 1
            if friday_count == 3:
                return d
        d += timedelta(days=1)


def standard_option_expiry(year: int, month: int) -> date:
    """
    Return the standard monthly option expiration date.
    """
    return third_friday(year, month)