"""evohomeclient provides a client for the oiginal Evohome API.

Further information at: https://evohome-client.readthedocs.io
"""
import codecs
import json
import logging
import sys
import time

import requests

try:
    import http.client as http_client
except ImportError:
    # Python 2
    import httplib as http_client

# Are we are dealing with Python 2 or 3
_VER = sys.version_info
#: Python 2.x?
IS_PY2 = _VER[0] == 2
#: Python 3.x?
IS_PY3 = _VER[0] == 3

logging.basicConfig()
_LOGGER = logging.getLogger(__name__)


class EvohomeClient(object):  # pylint: disable=useless-object-inheritance
    """Provide a client to access the Honeywell Evohome system."""

    # pylint: disable=too-many-instance-attributes,too-many-arguments

    def __init__(
        self,
        username,
        password,
        debug=False,
        user_data=None,
        hostname="https://tccna.honeywell.com",
    ):
        """Take the username and password for the service.

        If user_data is given then this will be used to try and reduce the number of
        calls to the authentication service which is known to be rate limited.
        """
        if debug is True:
            _LOGGER.setLevel(logging.DEBUG)
            _LOGGER.debug("Debug mode is explicitly enabled.")

            requests_logger = logging.getLogger("requests.packages.urllib3")
            requests_logger.setLevel(logging.DEBUG)
            requests_logger.propagate = True

            http_client.HTTPConnection.debuglevel = 1
        else:
            _LOGGER.debug(
                "Debug mode is not explicitly enabled (but may be enabled elsewhere)."
            )

        self.username = username
        self.password = password

        self.user_data = user_data
        self.hostname = hostname

        self.full_data = None
        self.gateway_data = None
        self.reader = codecs.getdecoder("utf-8")

        self.location_id = ""
        self.devices = {}
        self.named_devices = {}
        self.postdata = {}
        self.headers = {}

    def _convert(self, content):
        return json.loads(self.reader(content)[0])

    def _populate_full_data(self, force_refresh=False):
        if self.full_data is None or force_refresh:
            self._populate_user_info()

            user_id = self.user_data["userInfo"]["userID"]
            session_id = self.user_data["sessionId"]

            url = (
                self.hostname + "/WebAPI/api/locations?userId=%s&allData=True" % user_id
            )
            self.headers["sessionId"] = session_id

            response = self._do_request("get", url, json.dumps(self.postdata))

            self.full_data = self._convert(response.content)[0]

            self.location_id = self.full_data["locationID"]

            self.devices = {}
            self.named_devices = {}

            for device in self.full_data["devices"]:
                self.devices[device["deviceID"]] = device
                self.named_devices[device["name"]] = device

    def _populate_user_info(self):
        if self.user_data is None:
            url = self.hostname + "/WebAPI/api/Session"
            self.postdata = {
                "Username": self.username,
                "Password": self.password,
                "ApplicationId": "91db1612-73fd-4500-91b2-e63b069b185c",
            }
            self.headers = {"content-type": "application/json"}

            response = self._do_request(
                "post", url, data=json.dumps(self.postdata), retry=False
            )

            self.user_data = self._convert(response.content)

        return self.user_data

    def temperatures(self, force_refresh=False):
        """Retrieve the current details for each zone."""
        self._populate_full_data(force_refresh)

        for device in self.full_data["devices"]:
            set_point = 0
            status = ""
            if "heatSetpoint" in device["thermostat"]["changeableValues"]:
                set_point = float(
                    device["thermostat"]["changeableValues"]["heatSetpoint"]["value"]
                )
                status = device["thermostat"]["changeableValues"]["heatSetpoint"][
                    "status"
                ]

            else:
                status = device["thermostat"]["changeableValues"]["status"]
            yield {
                "thermostat": device["thermostatModelType"],
                "id": device["deviceID"],
                "name": device["name"],
                "temp": float(device["thermostat"]["indoorTemperature"]),
                "setpoint": set_point,
                "status": status,
                "mode": device["thermostat"]["changeableValues"]["mode"],
            }

    def get_modes(self, zone):
        """Return the set of modes the device can be assigned."""
        self._populate_full_data()
        device = self._get_device(zone)
        return device["thermostat"]["allowedModes"]

    def _get_device(self, zone):
        if isinstance(zone, str) or (
            IS_PY2 and isinstance(zone, basestring)  # noqa: F821
        ):
            device = self.named_devices[zone]
        else:
            device = self.devices[zone]
        return device

    def _get_task_status(self, task_id):
        self._populate_full_data()
        url = self.hostname + "/WebAPI/api/commTasks?commTaskId=%s" % task_id

        response = self._do_request("get", url)

        return self._convert(response.content)["state"]

    def _get_task_id(self, response):
        ret = self._convert(response.content)

        if isinstance(ret, list):
            task_id = ret[0]["id"]
        else:
            task_id = ret["id"]
        return task_id

    def _do_request(self, method, url, data=None, retry=True):
        if method == "get":
            func = requests.get
        elif method == "put":
            func = requests.put
        elif method == "post":
            func = requests.post

        response = func(url, data=data, headers=self.headers)

        # catch 401/unauthorized since we may retry
        if (
            response.status_code == requests.codes.unauthorized and retry
        ):  # pylint: disable=no-member
            # Attempt to refresh sessionId if it has expired
            if "code" in response.text:  # don't use response.json() here!
                if response.json()[0]["code"] == "Unauthorized":
                    _LOGGER.debug("Session expired, re-authenticating...")
                    # Get a new sessionId
                    self.user_data = None
                    self._populate_user_info()
                    # Set headers with new sessionId
                    session_id = self.user_data["sessionId"]
                    self.headers["sessionId"] = session_id
                    _LOGGER.debug("sessionId = %s", session_id)

                    response = func(url, data=data, headers=self.headers)

        # display error message if the vendor provided one
        if response.status_code != requests.codes.ok:  # pylint: disable=no-member
            if "code" in response.text:  # don't use response.json()!
                message = (
                    "HTTP Status = "
                    + str(response.status_code)
                    + ", Response = "
                    + response.text
                )
                raise requests.HTTPError(message)

        response.raise_for_status()

        return response

    def _set_status(self, status, until=None):
        self._populate_full_data()
        url = (
            self.hostname
            + "/WebAPI/api/evoTouchSystems?locationId=%s" % self.location_id
        )

        if until is None:
            data = {"QuickAction": status, "QuickActionNextTime": None}
        else:
            data = {
                "QuickAction": status,
                "QuickActionNextTime": until.strftime("%Y-%m-%dT%H:%M:%SZ"),
            }

        response = self._do_request("put", url, json.dumps(data))

        task_id = self._get_task_id(response)

        while self._get_task_status(task_id) != "Succeeded":
            time.sleep(1)

    def set_status_normal(self):
        """Set the system to normal operation."""
        self._set_status("Auto")

    def set_status_custom(self, until=None):
        """Set the system to the custom programme."""
        self._set_status("Custom", until)

    def set_status_eco(self, until=None):
        """Set the system to the eco mode."""
        self._set_status("AutoWithEco", until)

    def set_status_away(self, until=None):
        """Set the system to the away mode."""
        self._set_status("Away", until)

    def set_status_dayoff(self, until=None):
        """Set the system to the day off mode."""
        self._set_status("DayOff", until)

    def set_status_heatingoff(self, until=None):
        """Set the system to the heating off mode."""
        self._set_status("HeatingOff", until)

    def _get_device_id(self, zone):
        device = self._get_device(zone)
        return device["deviceID"]

    def _set_heat_setpoint(self, zone, data):
        self._populate_full_data()

        device_id = self._get_device_id(zone)

        url = (
            self.hostname
            + "/WebAPI/api/devices/%s/thermostat/changeableValues/heatSetpoint"
            % device_id
        )

        response = self._do_request("put", url, json.dumps(data))

        task_id = self._get_task_id(response)

        while self._get_task_status(task_id) != "Succeeded":
            time.sleep(1)

    def set_temperature(self, zone, temperature, until=None):
        """Set the temperature of the given zone."""
        if until is None:
            data = {"Value": temperature, "Status": "Hold", "NextTime": None}
        else:
            data = {
                "Value": temperature,
                "Status": "Temporary",
                "NextTime": until.strftime("%Y-%m-%dT%H:%M:%SZ"),
            }

        self._set_heat_setpoint(zone, data)

    def cancel_temp_override(self, zone):
        """Remove an existing temperature override."""
        data = {"Value": None, "Status": "Scheduled", "NextTime": None}
        self._set_heat_setpoint(zone, data)

    def _get_dhw_zone(self):
        for device in self.full_data["devices"]:
            if device["thermostatModelType"] == "DOMESTIC_HOT_WATER":
                return device["deviceID"]

    def _set_dhw(self, status="Scheduled", mode=None, next_time=None):
        """Set DHW to On, Off or Auto, either indefinitely, or until a set time."""
        data = {
            "Status": status,
            "Mode": mode,
            "NextTime": next_time,
            "SpecialModes": None,
            "HeatSetpoint": None,
            "CoolSetpoint": None,
        }

        self._populate_full_data()
        dhw_zone = self._get_dhw_zone()
        if dhw_zone is None:
            raise Exception("No DHW zone reported from API")
        url = (
            self.hostname
            + "/WebAPI/api/devices/%s/thermostat/changeableValues" % dhw_zone
        )

        response = self._do_request("put", url, json.dumps(data))

        task_id = self._get_task_id(response)

        while self._get_task_status(task_id) != "Succeeded":
            time.sleep(1)

    def set_dhw_on(self, until=None):
        """Set DHW to on, either indefinitely, or until a specified time.

        When On, the DHW controller will work to keep its target temperature at/above
        its target temperature.  After the specified time, it will revert to its
        scheduled behaviour.
        """
        time_until = None if until is None else until.strftime("%Y-%m-%dT%H:%M:%SZ")

        self._set_dhw(status="Hold", mode="DHWOn", next_time=time_until)

    def set_dhw_off(self, until=None):
        """Set DHW to on, either indefinitely, or until a specified time.

        When Off, the DHW controller will ignore its target temperature. After the
        specified time, it will revert to its scheduled behaviour.
        """
        time_until = None if until is None else until.strftime("%Y-%m-%dT%H:%M:%SZ")

        self._set_dhw(status="Hold", mode="DHWOff", next_time=time_until)

    def set_dhw_auto(self):
        """Set DHW to On or Off, according to its schedule."""
        self._set_dhw(status="Scheduled")