"""
Module for managing timezone on Windows systems.
"""
import logging
from datetime import datetime

from salt.exceptions import CommandExecutionError

try:
    import pytz

    HAS_PYTZ = True
except ImportError:
    HAS_PYTZ = False

log = logging.getLogger(__name__)

# Define the module's virtual name
__virtualname__ = "timezone"


class TzMapper:
    def __init__(self, unix_to_win):
        self.win_to_unix = {k.lower(): v for k, v in unix_to_win.items()}
        self.unix_to_win = {v.lower(): k for k, v in unix_to_win.items()}

    def add(self, k, v):
        self.unix_to_win[k.lower()] = v
        self.win_to_unix[v.lower()] = k

    def remove(self, k):
        self.win_to_unix.pop(self.unix_to_win.pop(k.lower()).lower())

    def get_win(self, key, default=None):
        return self.unix_to_win.get(key.lower(), default)

    def get_unix(self, key, default=None):
        return self.win_to_unix.get(key.lower(), default)

    def list_win(self):
        return sorted(self.unix_to_win.values())

    def list_unix(self):
        return sorted(self.win_to_unix.values())


mapper = TzMapper(
    {
        "AUS Central Standard Time": "Australia/Darwin",
        "AUS Eastern Standard Time": "Australia/Sydney",
        "Afghanistan Standard Time": "Asia/Kabul",
        "Alaskan Standard Time": "America/Anchorage",
        "Aleutian Standard Time": "America/Adak",
        "Altai Standard Time": "Asia/Barnaul",
        "Arab Standard Time": "Asia/Riyadh",
        "Arabian Standard Time": "Asia/Dubai",
        "Arabic Standard Time": "Asia/Baghdad",
        "Argentina Standard Time": "America/Buenos_Aires",
        "Astrakhan Standard Time": "Europe/Astrakhan",
        "Atlantic Standard Time": "America/Halifax",
        "Aus Central W. Standard Time": "Australia/Eucla",
        "Azerbaijan Standard Time": "Asia/Baku",
        "Azores Standard Time": "Atlantic/Azores",
        "Bahia Standard Time": "America/Bahia",
        "Bangladesh Standard Time": "Asia/Dhaka",
        "Belarus Standard Time": "Europe/Minsk",
        "Bougainville Standard Time": "Pacific/Bougainville",
        "Canada Central Standard Time": "America/Regina",
        "Cape Verde Standard Time": "Atlantic/Cape_Verde",
        "Caucasus Standard Time": "Asia/Yerevan",
        "Cen. Australia Standard Time": "Australia/Adelaide",
        "Central America Standard Time": "America/Guatemala",
        "Central Asia Standard Time": "Asia/Almaty",
        "Central Brazilian Standard Time": "America/Cuiaba",
        "Central Europe Standard Time": "Europe/Budapest",
        "Central European Standard Time": "Europe/Warsaw",
        "Central Pacific Standard Time": "Pacific/Guadalcanal",
        "Central Standard Time": "America/Chicago",
        "Central Standard Time (Mexico)": "America/Mexico_City",
        "Chatham Islands Standard Time": "Pacific/Chatham",
        "China Standard Time": "Asia/Shanghai",
        "Cuba Standard Time": "America/Havana",
        "Dateline Standard Time": "Etc/GMT+12",
        "E. Africa Standard Time": "Africa/Nairobi",
        "E. Australia Standard Time": "Australia/Brisbane",
        "E. Europe Standard Time": "Europe/Chisinau",
        "E. South America Standard Time": "America/Sao_Paulo",
        "Easter Island Standard Time": "Pacific/Easter",
        "Eastern Standard Time": "America/New_York",
        "Eastern Standard Time (Mexico)": "America/Cancun",
        "Egypt Standard Time": "Africa/Cairo",
        "Ekaterinburg Standard Time": "Asia/Yekaterinburg",
        "FLE Standard Time": "Europe/Kiev",
        "Fiji Standard Time": "Pacific/Fiji",
        "GMT Standard Time": "Europe/London",
        "GTB Standard Time": "Europe/Bucharest",
        "Georgian Standard Time": "Asia/Tbilisi",
        "Greenland Standard Time": "America/Godthab",
        "Greenwich Standard Time": "Atlantic/Reykjavik",
        "Haiti Standard Time": "America/Port-au-Prince",
        "Hawaiian Standard Time": "Pacific/Honolulu",
        "India Standard Time": "Asia/Calcutta",
        "Iran Standard Time": "Asia/Tehran",
        "Israel Standard Time": "Asia/Jerusalem",
        "Jordan Standard Time": "Asia/Amman",
        "Kaliningrad Standard Time": "Europe/Kaliningrad",
        "Korea Standard Time": "Asia/Seoul",
        "Libya Standard Time": "Africa/Tripoli",
        "Line Islands Standard Time": "Pacific/Kiritimati",
        "Lord Howe Standard Time": "Australia/Lord_Howe",
        "Magadan Standard Time": "Asia/Magadan",
        "Magallanes Standard Time": "America/Punta_Arenas",
        "Marquesas Standard Time": "Pacific/Marquesas",
        "Mauritius Standard Time": "Indian/Mauritius",
        "Middle East Standard Time": "Asia/Beirut",
        "Montevideo Standard Time": "America/Montevideo",
        "Morocco Standard Time": "Africa/Casablanca",
        "Mountain Standard Time": "America/Denver",
        "Mountain Standard Time (Mexico)": "America/Chihuahua",
        "Myanmar Standard Time": "Asia/Rangoon",
        "N. Central Asia Standard Time": "Asia/Novosibirsk",
        "Namibia Standard Time": "Africa/Windhoek",
        "Nepal Standard Time": "Asia/Katmandu",
        "New Zealand Standard Time": "Pacific/Auckland",
        "Newfoundland Standard Time": "America/St_Johns",
        "Norfolk Standard Time": "Pacific/Norfolk",
        "North Asia East Standard Time": "Asia/Irkutsk",
        "North Asia Standard Time": "Asia/Krasnoyarsk",
        "North Korea Standard Time": "Asia/Pyongyang",
        "Omsk Standard Time": "Asia/Omsk",
        "Pacific SA Standard Time": "America/Santiago",
        "Pacific Standard Time": "America/Los_Angeles",
        "Pacific Standard Time (Mexico)": "America/Tijuana",
        "Pakistan Standard Time": "Asia/Karachi",
        "Paraguay Standard Time": "America/Asuncion",
        "Romance Standard Time": "Europe/Paris",
        "Russia Time Zone 10": "Asia/Srednekolymsk",
        "Russia Time Zone 11": "Asia/Kamchatka",
        "Russia Time Zone 3": "Europe/Samara",
        "Russian Standard Time": "Europe/Moscow",
        "SA Eastern Standard Time": "America/Cayenne",
        "SA Pacific Standard Time": "America/Bogota",
        "SA Western Standard Time": "America/La_Paz",
        "SE Asia Standard Time": "Asia/Bangkok",
        "Saint Pierre Standard Time": "America/Miquelon",
        "Sakhalin Standard Time": "Asia/Sakhalin",
        "Samoa Standard Time": "Pacific/Apia",
        "Saratov Standard Time": "Europe/Saratov",
        "Singapore Standard Time": "Asia/Singapore",
        "South Africa Standard Time": "Africa/Johannesburg",
        "Sri Lanka Standard Time": "Asia/Colombo",
        "Syria Standard Time": "Asia/Damascus",
        "Taipei Standard Time": "Asia/Taipei",
        "Tasmania Standard Time": "Australia/Hobart",
        "Tocantins Standard Time": "America/Araguaina",
        "Tokyo Standard Time": "Asia/Tokyo",
        "Tomsk Standard Time": "Asia/Tomsk",
        "Tonga Standard Time": "Pacific/Tongatapu",
        "Transbaikal Standard Time": "Asia/Chita",
        "Turkey Standard Time": "Europe/Istanbul",
        "Turks And Caicos Standard Time": "America/Grand_Turk",
        "US Eastern Standard Time": "America/Indianapolis",
        "US Mountain Standard Time": "America/Phoenix",
        "UTC": "Etc/GMT",
        "UTC+12": "Etc/GMT-12",
        "UTC+13": "Etc/GMT-13",
        "UTC-02": "Etc/GMT+2",
        "UTC-08": "Etc/GMT+8",
        "UTC-09": "Etc/GMT+9",
        "UTC-11": "Etc/GMT+11",
        "Ulaanbaatar Standard Time": "Asia/Ulaanbaatar",
        "Venezuela Standard Time": "America/Caracas",
        "Vladivostok Standard Time": "Asia/Vladivostok",
        "W. Australia Standard Time": "Australia/Perth",
        "W. Central Africa Standard Time": "Africa/Lagos",
        "W. Europe Standard Time": "Europe/Berlin",
        "W. Mongolia Standard Time": "Asia/Hovd",
        "West Asia Standard Time": "Asia/Tashkent",
        "West Bank Standard Time": "Asia/Hebron",
        "West Pacific Standard Time": "Pacific/Port_Moresby",
        "Yakutsk Standard Time": "Asia/Yakutsk",
    }
)


def __virtual__():
    """
    Only load on windows
    """
    if not __utils__["platform.is_windows"]():
        return False, "Module win_timezone: Not on Windows client"
    if not HAS_PYTZ:
        return False, "Module win_timezone: pytz not found"
    if not __utils__["path.which"]("tzutil"):
        return False, "Module win_timezone: tzutil not found"
    return __virtualname__


def get_zone():
    """
    Get current timezone (i.e. America/Denver)

    Returns:
        str: Timezone in unix format

    Raises:
        CommandExecutionError: If timezone could not be gathered

    CLI Example:

    .. code-block:: bash

        salt '*' timezone.get_zone
    """
    cmd = ["tzutil", "/g"]
    res = __salt__["cmd.run_all"](cmd, python_shell=False)
    if res["retcode"] or not res["stdout"]:
        raise CommandExecutionError(
            "tzutil encountered an error getting timezone", info=res
        )
    tz = res["stdout"].lower()
    if tz.endswith("_dstoff"):
        tz = tz[:-7]

    return mapper.get_unix(tz, "Unknown")


def get_offset():
    """
    Get current numeric timezone offset from UTC (i.e. -0700)

    Returns:
        str: Offset from UTC

    CLI Example:

    .. code-block:: bash

        salt '*' timezone.get_offset
    """
    # http://craigglennie.com/programming/python/2013/07/21/working-with-timezones-using-Python-and-pytz-localize-vs-normalize/
    tz_object = pytz.timezone(get_zone())
    utc_time = pytz.utc.localize(datetime.utcnow())
    loc_time = utc_time.astimezone(tz_object)
    norm_time = tz_object.normalize(loc_time)
    return norm_time.strftime("%z")


def get_zonecode():
    """
    Get current timezone (i.e. PST, MDT, etc)

    Returns:
        str: An abbreviated timezone code

    CLI Example:

    .. code-block:: bash

        salt '*' timezone.get_zonecode
    """
    tz_object = pytz.timezone(get_zone())
    loc_time = tz_object.localize(datetime.utcnow())
    return loc_time.tzname()


def set_zone(timezone):
    """
    Sets the timezone using the tzutil.

    Args:
        timezone (str): A valid timezone

    Returns:
        bool: ``True`` if successful, otherwise ``False``

    Raises:
        CommandExecutionError: If invalid timezone is passed

    CLI Example:

    .. code-block:: bash

        salt '*' timezone.set_zone 'America/Denver'
    """
    # if it's one of the key's just use it
    if timezone.lower() in mapper.win_to_unix:
        win_zone = timezone

    elif timezone.lower() in mapper.unix_to_win:
        # if it's one of the values, use the key
        win_zone = mapper.get_win(timezone)

    else:
        # Raise error because it's neither key nor value
        raise CommandExecutionError("Invalid timezone passed: {}".format(timezone))

    # Set the value
    cmd = ["tzutil", "/s", win_zone]
    res = __salt__["cmd.run_all"](cmd, python_shell=False)
    if res["retcode"]:
        raise CommandExecutionError(
            "tzutil encountered an error setting timezone: {}".format(timezone),
            info=res,
        )
    return zone_compare(timezone)


def zone_compare(timezone):
    """
    Compares the given timezone with the machine timezone. Mostly useful for
    running state checks.

    Args:
        timezone (str):
            The timezone to compare. This can be in Windows or Unix format. Can
            be any of the values returned by the ``timezone.list`` function

    Returns:
        bool: ``True`` if they match, otherwise ``False``

    Example:

    .. code-block:: bash

        salt '*' timezone.zone_compare 'America/Denver'
    """
    # if it's one of the key's just use it
    if timezone.lower() in mapper.win_to_unix:
        check_zone = timezone

    elif timezone.lower() in mapper.unix_to_win:
        # if it's one of the values, use the key
        check_zone = mapper.get_win(timezone)

    else:
        # Raise error because it's neither key nor value
        raise CommandExecutionError("Invalid timezone passed: {}".format(timezone))

    return get_zone() == mapper.get_unix(check_zone, "Unknown")


def list(unix_style=True):
    """
    Return a list of Timezones that this module supports. These can be in either
    Unix or Windows format.

    .. versionadded:: 2018.3.3

    Args:
        unix_style (bool):
            ``True`` returns Unix-style timezones. ``False`` returns
            Windows-style timezones. Default is ``True``

    Returns:
        list: A list of supported timezones

    CLI Example:

    .. code-block:: bash

        # Unix-style timezones
        salt '*' timezone.list

        # Windows-style timezones
        salt '*' timezone.list unix_style=False
    """
    if unix_style:
        return mapper.list_unix()
    else:
        return mapper.list_win()


def get_hwclock():
    """
    Get current hardware clock setting (UTC or localtime)

    .. note::
        The hardware clock is always local time on Windows so this will always
        return "localtime"

    CLI Example:

    .. code-block:: bash

        salt '*' timezone.get_hwclock
    """
    # The hardware clock is always localtime on Windows
    return "localtime"


def set_hwclock(clock):
    """
    Sets the hardware clock to be either UTC or localtime

    .. note::
        The hardware clock is always local time on Windows so this will always
        return ``False``

    CLI Example:

    .. code-block:: bash

        salt '*' timezone.set_hwclock UTC
    """
    # The hardware clock is always localtime on Windows
    return False
