"""Provide CalendarData class."""
import zlib
from datetime import timedelta
from gzip import BadGzipFile, GzipFile
from logging import Logger
from threading import Lock
from urllib.error import ContentTooShortError, HTTPError, URLError
from urllib.request import (
    HTTPBasicAuthHandler,
    HTTPDigestAuthHandler,
    HTTPPasswordMgrWithDefaultRealm,
    build_opener,
    install_opener,
    urlopen,
)

from homeassistant.util.dt import now as hanow


class CalendarData:
    """CalendarData class.

    The CalendarData class is used to download and cache calendar data from a
    given URL.  Use the get method to retrieve the data after constructing your
    instance.
    """

    opener_lock = Lock()

    def __init__(
        self, logger: Logger, name: str, url: str, min_update_time: timedelta
    ):
        """Construct CalendarData object.

        :param logger: The logger for reporting problems
        :type logger: Logger
        :param name: The name of the calendar (used for reporting problems)
        :type name: str
        :param url: The URL of the calendar
        :type url: str
        :param min_update_time: The minimum time between downloading data from
            the URL when requested
        :type min_update_time: timedelta
        """
        self._calendar_data = None
        self._last_download = None
        self._min_update_time = min_update_time
        self._opener = None
        self.logger = logger
        self.name = name
        self.url = url

    def download_calendar(self) -> bool:
        """Download the calendar data.

        This only downloads data if self.min_update_time has passed since the
        last download.

        returns: True if data was downloaded, otherwise False.
        rtype: bool
        """
        now = hanow()
        if (
            self._calendar_data is None
            or self._last_download is None
            or (now - self._last_download) > self._min_update_time
        ):
            self._last_download = now
            self._calendar_data = None
            self.logger.debug(
                "%s: Downloading calendar data from: %s", self.name, self.url
            )
            self._download_data()
            return self._calendar_data is not None

        return False

    def get(self) -> str:
        """Get the calendar data that was downloaded.

        :return: The downloaded calendar data.
        :rtype: str
        """
        return self._calendar_data

    def set_headers(
        self,
        user_name: str,
        password: str,
        user_agent: str,
        accept_header: str,
    ):
        """Set a user agent, accept header, and/or user name and password.

        The user name and password will be set into an HTTPBasicAuthHandler an
        an HTTPDigestAuthHandler.  Both are attached to a new urlopener, so
        that HTTP Basic Auth and HTTP Digest Auth will be supported when
        opening the URL.

        If the user_agent parameter is not "", a User-agent header will be
        added to the urlopener.

        :param user_name: The user name
        :type user_name: str
        :param password: The password
        :type password: str
        :param user_agent: The User Agent string to use or ""
        :type user_agent: str
        :param accept_header: The accept header string to use or ""
        :type accept_header: str
        """
        if user_name != "" and password != "":
            passman = HTTPPasswordMgrWithDefaultRealm()
            passman.add_password(None, self.url, user_name, password)
            basic_auth_handler = HTTPBasicAuthHandler(passman)
            digest_auth_handler = HTTPDigestAuthHandler(passman)
            self._opener = build_opener(
                digest_auth_handler, basic_auth_handler
            )

        additional_headers = []
        if user_agent != "":
            additional_headers.append(("User-agent", user_agent))
        if accept_header != "":
            additional_headers.append(("Accept", accept_header))
        if len(additional_headers) > 0:
            if self._opener is None:
                self._opener = build_opener()
            self._opener.addheaders = additional_headers

    def _decode_data(self, conn):
        if (
            "Content-Encoding" in conn.headers
            and conn.headers["Content-Encoding"] == "gzip"
        ):
            reader = GzipFile(fileobj=conn)
        else:
            reader = conn
        try:
            return self._decode_stream(reader.read()).replace("\0", "")
        except zlib.error:
            self.logger.error(
                "%s: Failed to uncompress gzip data from url(%s): zlib",
                self.name,
                self.url,
            )
        except BadGzipFile as gzip_error:
            self.logger.error(
                "%s: Failed to uncompress gzip data from url(%s): %s",
                self.name,
                self.url,
                gzip_error.strerror,
            )
        return None

    def _decode_stream(self, strm):
        for encoding in "utf-8-sig", "utf-8", "utf-16":
            try:
                return strm.decode(encoding)
            except UnicodeDecodeError:
                continue
        return None

    def _download_data(self):
        """Download the calendar data."""
        try:
            with CalendarData.opener_lock:
                if self._opener is not None:
                    install_opener(self._opener)
                with urlopen(self._make_url()) as conn:
                    self._calendar_data = self._decode_data(conn)
        except HTTPError as http_error:
            self.logger.error(
                "%s: Failed to open url(%s): %s",
                self.name,
                self.url,
                http_error.reason,
            )
        except ContentTooShortError as content_too_short_error:
            self.logger.error(
                "%s: Could not download calendar data: %s",
                self.name,
                content_too_short_error.reason,
            )
        except URLError as url_error:
            self.logger.error(
                "%s: Failed to open url: %s", self.name, url_error.reason
            )
        except:  # pylint: disable=W0702
            self.logger.error(
                "%s: Failed to open url!", self.name, exc_info=True
            )

    def _make_url(self):
        now = hanow()
        return self.url.replace("{year}", f"{now.year:04}").replace(
            "{month}", f"{now.month:02}"
        )
