import datetime
import pytz
import pandas as pd 


PANDAS_TIMESTAMP_TYPE = pd._libs.tslibs.timestamps.Timestamp


def list_pytz_timezones():
    """
    Lists all the timezones that we can interact with in pytz.

    :returns: pytz_timezones: list: List of all pytz timezones.
    """
    pytz_timezones = pytz.all_timezones
    return pytz_timezones


def check_timezone_is_defined_in_pytz(focus_timezone: str):
    """
    Checks whether a time zone is defined or not in pytz.

    :param: focus_timezone: str: Timezone to check. 
    """
    pytz_timezones = list_pytz_timezones()
    if focus_timezone not in pytz_timezones:
        log_message = f"You asked for using the time zone '{focus_timezone}' that is not defined in pytz. "\
        f"Please choose a time zone in '{pytz_timezones}'"
        raise ValueError(log_message)
    pass


# Timezones switch:
def switch_datetime_date_timezone_to_utc(datetime_date: datetime.datetime):
    """
    Switches the timezone of a datetime date to 'UTC'. 
        DISCLAIMER: this is not a date conversion. For this you should instead use 'convert_datetime_date_in_timezone'.

    :param: datetime_date:  datetime.datetime: The datetime date with a timezone to switch to UTC.
    :returns: datetime_date_with_utc_timezone: datetime.datetime: datetime date in 'UTC'.
    """
    datetime_date_with_utc_timezone = datetime_date.replace(tzinfo=datetime.timezone.utc)
    return datetime_date_with_utc_timezone


def switch_pandas_timestamp_timezone_to_utc(pandas_timestamp: PANDAS_TIMESTAMP_TYPE):
    """
    Switches the timezone of a pandas timestamp to 'UTC'.
        DISCLAIMER: this is not a date conversion. For this you should instead use 'convert_pandas_timestamp_in_timezone'.

    :param: pandas_timestamp:  pandas._libs.tslibs.timestamps.Timestamp: The pandas timestam date to switch to UTC.
    :returns: pandas_timestamp_with_utc_timezone: pandas._libs.tslibs.timestamps.Timestamp: pandas timestamp in 'UTC'.
    """
    pandas_timestamp_with_utc_timezone = pandas_timestamp.tz_localize("UTC")
    return pandas_timestamp_with_utc_timezone


# Timezones dates conversions:
def convert_datetime_date_in_timezone(datetime_date: datetime.datetime, target_timezone: str="UTC"):
    """
    Converts a datetime date in another timezone.

    :param: datetime_date: datetime.datetime: The datetime date to convert in another timezone.
    :param: target_timezone: str: Timezone where to convert the date.
        Supported timezones are the ones listed in pytz using 'pytz.all_timezones'.
    :returns: datetime_date_in_timezone: datetime.datetime: 'datetime_date' converted in 'target_timezone'.
    """
    check_timezone_is_defined_in_pytz(target_timezone)
    datetime_date_in_timezone = datetime_date.astimezone(pytz.timezone(target_timezone))
    return datetime_date_in_timezone


def convert_pandas_timestamp_in_timezone(pandas_timestamp: PANDAS_TIMESTAMP_TYPE, target_timezone: str="UTC"):
    """
    Converts a pandas timestamp in another timezone.

    :param: pandas_timestamp: pandas._libs.tslibs.timestamps.Timestamp: The pandas_timestamp to convert in another timezone.
    :param: target_timezone: str: Timezone where to convert the date.
        Supported timezones are the ones listed in pytz using 'pytz.all_timezones'.
    :returns: pandas_timestamp_in_timezone: pandas._libs.tslibs.timestamps.Timestamp: 'pandas_timestamp' converted
        in 'target_timezone'.
    """
    check_timezone_is_defined_in_pytz(target_timezone)
    pandas_timestamp_timezone = pandas_timestamp.tz
    if pandas_timestamp_timezone is None:
        log_message = f"Current timestamp is '{pandas_timestamp}' and has no timezone set. "\
            "You have set one timezone with method 'tz_localize' prior to convert the pandas timestamp "\
            "(see the function 'switch_pandas_timestamp_timezone_to_utc' as an example) "
        raise ValueError(log_message)
    pandas_timestamp_in_timezone = pandas_timestamp.tz_convert(pytz.timezone(target_timezone))
    return pandas_timestamp_in_timezone


# Timezones time offsets
def get_datetime_date_time_offset_seconds(datetime_date: datetime.datetime):
    """
    Computes the time offset of a datetime date, in seconds.

    :param: datetime_date: datetime.datetime: The datetime date on which to compute the time offset.
    :returns: time_offset_seconds: float: The datetime date time offset.
    """
    datetime_date_without_time_offset = switch_datetime_date_timezone_to_utc(datetime_date)
    time_delta = datetime_date_without_time_offset - datetime_date.astimezone(pytz.timezone('UTC'))
    time_offset_seconds = time_delta.total_seconds()
    return time_offset_seconds


def get_pandas_timestamp_time_offset_seconds(pandas_timestamp: PANDAS_TIMESTAMP_TYPE):
    """
    Computes the time offset of a pandas timestamp, in seconds.

    :param: pandas_timestamp: pandas._libs.tslibs.timestamps.Timestamp: The pandas timestamp on which to compute the time offset.
    :returns: time_offset_seconds: float: The pandas timestamp time offset.
    """
    time_delta = pandas_timestamp.tz_localize(None).tz_localize("UTC") - pandas_timestamp.tz_convert("UTC")
    time_offset_seconds = time_delta.total_seconds()
    return time_offset_seconds


def get_timezones_offsets_dataframe():
    """
    Retrieves a dataframe that contains the time offsets associated with each of the timezones available in pytz.

    :returns: timezones_offsets_dataframe: pandas.core.frame.DataFrame: Dataframe containing the time offsets
        associated with each of the timezones available in pytz.
    """
    now = datetime.datetime.now()
    pytz_timezones = pytz.all_timezones
    timezones_data = []
    for timezone_label in pytz_timezones:
        timezone_label_splitted = timezone_label.split("/")
        timezone_area = timezone_label_splitted[0]
        if len(timezone_label_splitted)>1:
            timezone_focus = timezone_label_splitted[1]
        else:
            timezone_focus = timezone_area
        
        now_in_timezone = convert_datetime_date_in_timezone(now, focus_timezone=timezone_label)
        time_offset_seconds = get_datetime_date_time_offset_seconds(now_in_timezone)
        time_offset_hours = round(time_offset_seconds / 3600)
        timezones_data.append({"timezone_label": timezone_label,
                               "timezone_area": timezone_area,
                               "timezone_focus": timezone_focus,
                               "time_offset_hours": time_offset_hours})
        
    timezones_offsets_dataframe = pd.DataFrame(timezones_data)\
    .sort_values(by=["time_offset_hours", "timezone_area", "timezone_focus"]).reset_index(drop=True)
    return timezones_offsets_dataframe


def get_timezones_matching_with_time_offset(time_offset: int):
    """
    Retrieves a dataframe that contains information about all pytz timezones that have a specific time offset.
    
    :param: time_offset: int: The time offset on which to filter the results.
        DISCLAIMER: It must have values contained in [-12; 14].
    :returns: timezones_matching_with_time_offset: pandas.core.frame.DataFrame: Dataframe containing information about 
        the time zones that have an offset of 'time_offset'.
    """
    time_offset_is_below_minimum = (time_offset < -12)
    time_offset_is_above_maximum = (time_offset > 14)
    if time_offset_is_below_minimum or time_offset_is_above_maximum:
        log_message = f"The selected time offset has value '{time_offset}'  that is "\
        f"{'below' if time_offset_is_below_minimum else 'above'} {-12 if time_offset_is_below_minimum else 14}: "\
        "please select a value contained in [-12; 14]." 
        raise ValueError(log_message)
    timezones_offsets_dataframe = get_timezones_offsets_dataframe()
    timezones_matching_with_time_offset =\
    timezones_offsets_dataframe[timezones_offsets_dataframe["time_offset_hours"]==time_offset]
    return timezones_matching_with_time_offset