import logging
from datetime import datetime

try:
    import _winreg as winreg
except ImportError:
    import winreg

try:
    import zoneinfo  # pragma: no cover
except ImportError:
    from backports import zoneinfo  # pragma: no cover

from tzlocal import utils
from tzlocal.windows_tz import win_tz

_cache_tz = None
_cache_tz_name = None

log = logging.getLogger("tzlocal")


def valuestodict(key):
    """Convert a registry key's values to a dictionary."""
    result = {}
    size = winreg.QueryInfoKey(key)[1]
    for i in range(size):
        data = winreg.EnumValue(key, i)
        result[data[0]] = data[1]
    return result


def _get_dst_info(tz):
    # Find the offset for when it doesn't have DST:
    dst_offset = std_offset = None
    has_dst = False
    year = datetime.now().year
    for dt in (datetime(year, 1, 1), datetime(year, 6, 1)):
        if tz.dst(dt).total_seconds() == 0.0:
            # OK, no DST during winter, get this offset
            std_offset = tz.utcoffset(dt).total_seconds()
        else:
            has_dst = True

    return has_dst, std_offset, dst_offset


def _get_localzone_name():
    # Windows is special. It has unique time zone names (in several
    # meanings of the word) available, but unfortunately, they can be
    # translated to the language of the operating system, so we need to
    # do a backwards lookup, by going through all time zones and see which
    # one matches.
    tzenv = utils._tz_name_from_env()
    if tzenv:
        return tzenv

    log.debug("Looking up time zone info from registry")
    handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)

    TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
    localtz = winreg.OpenKey(handle, TZLOCALKEYNAME)
    keyvalues = valuestodict(localtz)
    localtz.Close()

    if "TimeZoneKeyName" in keyvalues:
        # Windows 7 and later

        # For some reason this returns a string with loads of NUL bytes at
        # least on some systems. I don't know if this is a bug somewhere, I
        # just work around it.
        tzkeyname = keyvalues["TimeZoneKeyName"].split("\x00", 1)[0]
    else:
        # Don't support XP any longer
        raise LookupError("Can not find Windows timezone configuration")

    timezone = win_tz.get(tzkeyname)
    if timezone is None:
        # Nope, that didn't work. Try adding "Standard Time",
        # it seems to work a lot of times:
        timezone = win_tz.get(tzkeyname + " Standard Time")

    # Return what we have.
    if timezone is None:
        raise zoneinfo.ZoneInfoNotFoundError(tzkeyname)

    if keyvalues.get("DynamicDaylightTimeDisabled", 0) == 1:
        # DST is disabled, so don't return the timezone name,
        # instead return Etc/GMT+offset

        tz = zoneinfo.ZoneInfo(timezone)
        has_dst, std_offset, dst_offset = _get_dst_info(tz)
        if not has_dst:
            # The DST is turned off in the windows configuration,
            # but this timezone doesn't have DST so it doesn't matter
            return timezone

        if std_offset is None:
            raise zoneinfo.ZoneInfoNotFoundError(
                f"{tzkeyname} claims to not have a non-DST time!?"
            )

        if std_offset % 3600:
            # I can't convert this to an hourly offset
            raise zoneinfo.ZoneInfoNotFoundError(
                f"tzlocal can't support disabling DST in the {timezone} zone."
            )

        # This has whole hours as offset, return it as Etc/GMT
        return f"Etc/GMT{-std_offset//3600:+.0f}"

    return timezone


def get_localzone_name() -> str:
    """Get the zoneinfo timezone name that matches the Windows-configured timezone."""
    global _cache_tz_name
    if _cache_tz_name is None:
        _cache_tz_name = _get_localzone_name()

    return _cache_tz_name


def get_localzone() -> zoneinfo.ZoneInfo:
    """Returns the zoneinfo-based tzinfo object that matches the Windows-configured timezone."""

    global _cache_tz
    if _cache_tz is None:
        _cache_tz = zoneinfo.ZoneInfo(get_localzone_name())

    if not utils._tz_name_from_env():
        # If the timezone does NOT come from a TZ environment variable,
        # verify that it's correct. If it's from the environment,
        # we accept it, this is so you can run tests with different timezones.
        utils.assert_tz_offset(_cache_tz, error=False)

    return _cache_tz


def reload_localzone() -> zoneinfo.ZoneInfo:
    """Reload the cached localzone. You need to call this if the timezone has changed."""
    global _cache_tz
    global _cache_tz_name
    _cache_tz_name = _get_localzone_name()
    _cache_tz = zoneinfo.ZoneInfo(_cache_tz_name)
    utils.assert_tz_offset(_cache_tz, error=False)
    return _cache_tz
