import calendar
import datetime
from dateutil import tz

from typing import List, Optional
from typing_extensions import Final


class Time:
    """
    Python's time stuff sucks, so this provides a sane interface to getting
    standard unix timestamps at UTC timezone given various parameters.
    """

    SECONDS_IN_MINUTE: Final[int] = 60
    SECONDS_IN_HOUR: Final[int] = 3600
    SECONDS_IN_DAY: Final[int] = 86400
    SECONDS_IN_WEEK: Final[int] = 604800

    @staticmethod
    def now() -> int:
        """
        Returns the current unix timestamp in the UTC timezone.
        """
        return calendar.timegm(datetime.datetime.utcnow().timetuple())

    @staticmethod
    def end_of_today() -> int:
        """
        Returns the unix timestamp for the end of today in UTC timezone.
        """
        now = datetime.datetime.utcnow().date()
        beginning_of_day = datetime.datetime(
            now.year,
            now.month,
            now.day,
            tzinfo=tz.tzutc()
        )
        end_of_day = beginning_of_day + datetime.timedelta(days=1)
        return calendar.timegm(end_of_day.timetuple())

    @staticmethod
    def beginning_of_today() -> int:
        """
        Returns the unix timestamp for the beginning of today in UTC timezone.
        """
        now = datetime.datetime.utcnow().date()
        beginning_of_day = datetime.datetime(
            now.year,
            now.month,
            now.day,
            tzinfo=tz.tzutc()
        )
        return calendar.timegm(beginning_of_day.timetuple())

    @staticmethod
    def end_of_this_week() -> int:
        """
        Returns the unix timestamp for the end of this week in UTC timezone.
        """
        now = datetime.datetime.utcnow().date()
        this_week = now - datetime.timedelta(days=now.timetuple().tm_wday)
        next_week = this_week + datetime.timedelta(days=7)
        return calendar.timegm(next_week.timetuple())

    @staticmethod
    def beginning_of_this_week() -> int:
        """
        Returns the unix timestamp for the beginning of this week in UTC timezone.
        """
        now = datetime.datetime.utcnow().date()
        this_week = now - datetime.timedelta(days=now.timetuple().tm_wday)
        return calendar.timegm(this_week.timetuple())

    @staticmethod
    def end_of_this_month() -> int:
        """
        Returns the unix timestamp for the end of this month in UTC timezone.
        """
        now = datetime.datetime.utcnow().date()
        return Time.timestamp_from_date(now.year, now.month + 1, 1)

    @staticmethod
    def beginning_of_this_month() -> int:
        """
        Returns the unix timestamp for the beginning of this month in UTC timezone.
        """
        now = datetime.datetime.utcnow().date()
        this_month = datetime.date(now.year, now.month, 1)
        return calendar.timegm(this_month.timetuple())

    @staticmethod
    def todays_date() -> List[int]:
        """
        Returns a [year, month, day] list representing today's date.
        """
        now = datetime.datetime.utcnow().date()
        return [now.year, now.month, now.day]

    @staticmethod
    def yesterdays_date() -> List[int]:
        """
        Returns a [year, month, day] list representing yesterday's date.
        """
        now = datetime.datetime.utcnow().date()
        yesterday = now - datetime.timedelta(days=1)
        return [yesterday.year, yesterday.month, yesterday.day]

    @staticmethod
    def week_in_days_since_epoch(timestamp: Optional[int]=None) -> int:
        """
        Returns the day number of the beginning of this week, where day zero is
        the unix epoch at UTC timezone. So if we were one week in, this would return
        7. If a timestamp is provided, returns the same value from that reverence
        point instead of now.
        """
        if timestamp is None:
            date = datetime.datetime.utcnow().date()
        else:
            date = datetime.datetime.utcfromtimestamp(timestamp).date()
        week = date - datetime.timedelta(days=date.timetuple().tm_wday)
        return (week - datetime.date(1970, 1, 1)).days

    @staticmethod
    def days_into_year(timestamp: Optional[int]=None) -> List[int]:
        """
        Returns a [year, days] list representing the current year, and number
        of days into the current year. If a timestamp is provided, returns the
        same value from that reverence point instead of now.
        """
        if timestamp is None:
            date = datetime.datetime.utcnow().date().timetuple()
        else:
            date = datetime.datetime.utcfromtimestamp(timestamp).date().timetuple()
        return [date.tm_year, date.tm_yday]

    @staticmethod
    def days_into_week(timestamp: Optional[int]=None) -> int:
        """
        Returns an integer representing the number of days into the current week
        we are, with 0 = monday, 1 = tuesday, etc. If a timestamp is provided,
        returns the same value from that reverence point instead of now.
        """
        if timestamp is None:
            date = datetime.datetime.utcnow().date().timetuple()
        else:
            date = datetime.datetime.utcfromtimestamp(timestamp).date().timetuple()
        return date.tm_wday

    @staticmethod
    def timestamp_from_date(year: int, month: int=1, day: int=1) -> int:
        """
        Given a date (either a year, year/month, or year/month/day), returns
        the unix timestamp from UTC of that date. Supports out of bounds
        indexing on month.
        """
        while month < 1:
            year = year - 1
            month = month + 12
        while month > 12:
            year = year + 1
            month = month - 12

        date = datetime.datetime(
            year,
            month,
            day,
            tzinfo=tz.tzutc()
        )
        return calendar.timegm(date.timetuple())

    @staticmethod
    def date_from_timestamp(timestamp: int) -> List[int]:
        """
        Returns a [year, month, day] given a UTC unix timestamp.
        """
        date = datetime.datetime.utcfromtimestamp(timestamp).date()
        return [date.year, date.month, date.day]

    @staticmethod
    def format(timestamp: int, formatstr: str) -> str:
        """
        Returns a unix timestamp based at UTC timezone formatted as a string.
        """
        return datetime.datetime.utcfromtimestamp(timestamp).strftime(formatstr)
