import datetime

__all__ = [
    "parse_date",
    "format_date",
    "range_by_days",
    "range_by_months",
    "round_to_seconds",
]


def parse_date(date: str) -> datetime.datetime:
    """
    Converts `ISO 8601`_ dates generated by the MediaWiki API into
    :py:class:`datetime.datetime` objects.

    This will return a time in what the wiki thinks is UTC. Plan
    accordingly for bad server configurations.

    .. _`ISO 8601`: http://en.wikipedia.org/wiki/ISO_8601

    :param date: string ISO 8601 date representation
    :returns: :py:class:`datetime.datetime` object
    """
    # MediaWiki API dates are always of the format
    #   YYYY-MM-DDTHH:MM:SSZ
    # (see $formats in wfTimestamp() in includes/GlobalFunctions.php)

    # strptime is slooow!
    # return datetime.datetime.strptime(date, '%Y-%m-%dT%H:%M:%SZ')
    return datetime.datetime(
        int(date[:4]),
        int(date[5:7]),
        int(date[8:10]),
        int(date[11:13]),
        int(date[14:16]),
        int(date[17:19]),
        tzinfo=datetime.UTC,
    )


def format_date(date: datetime.datetime) -> str:
    """
    An inverse function to :py:func:`parse_date`.

    :param datetime.datetime date: timestamp to format
    :returns: :py:class:`str` representation of the timestamp
    """
    assert date.tzinfo == datetime.UTC
    return date.strftime("%Y-%m-%dT%H:%M:%SZ")


def range_by_days(first: datetime.date | datetime.datetime, last: datetime.date | datetime.datetime) -> list[datetime.date]:
    """
    Generate a list of :py:class:`datetime.date` objects with consecutive items
    differing by 1 day.

    :param datetime.datetime first: the beginning of the range (inclusive)
    :param datetime.datetime last: the end of the range (inclusive)
    :returns: a list of :py:class:`datetime.date` objects
    """
    range_ = []
    first = datetime.date(first.year, first.month, first.day)
    last = datetime.date(last.year, last.month, last.day)
    while first <= last:
        range_.append(first)
        first += datetime.timedelta(days=1)
    return range_


def range_by_months(first: datetime.date | datetime.datetime, last: datetime.date | datetime.datetime) -> list[datetime.date]:
    """
    Generate a list of :py:class:`datetime.date` objects with consecutive items
    differing by 1 month.

    :param datetime.datetime first: the beginning of the range (inclusive)
    :param datetime.datetime last: the end of the range (inclusive)
    :returns: a list of :py:class:`datetime.date` objects
    """
    range_ = []
    first = datetime.date(first.year, first.month, 1)
    last = datetime.date(last.year, last.month, last.day)
    while first < last:
        range_.append(first)
        try:
            first = datetime.date(first.year, first.month + 1, 1)
        except ValueError:
            first = datetime.date(first.year + 1, 1, 1)
    range_.append(first)  # rightmost
    return range_


def round_to_seconds(dt: datetime.datetime) -> datetime.datetime:
    """
    Rounds the given :py:class:`datetime.datetime` object to the nearest whole
    second.
    """
    if dt.microsecond >= 500000:
        dt += datetime.timedelta(seconds=1, microseconds=-dt.microsecond)
    else:
        dt += datetime.timedelta(microseconds=-dt.microsecond)
    return dt
