"""Library to handle connection with Tibber API."""
import asyncio
import datetime as dt
import logging

import aiohttp
import async_timeout
import pytz
from dateutil.parser import parse
from graphql_subscription_manager import SubscriptionManager

from .const import RESOLUTION_HOURLY, __version__

DEFAULT_TIMEOUT = 15
DEMO_TOKEN = "476c477d8a039529478ebd690d35ddd80e3308ffc49b59c65b142321aee963a4"
API_ENDPOINT = "https://api.tibber.com/v1-beta/gql"
SUB_ENDPOINT = "wss://api.tibber.com/v1-beta/gql/subscriptions"

_LOGGER = logging.getLogger(__name__)


class Tibber:
    """Class to communicate with the Tibber api."""

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

    def __init__(
        self,
        access_token=DEMO_TOKEN,
        timeout=DEFAULT_TIMEOUT,
        websession=None,
        time_zone=None,
    ):
        """Initialize the Tibber connection."""
        if websession is None:
            self.websession = aiohttp.ClientSession(
                headers={aiohttp.hdrs.USER_AGENT: f"pyTibber/{__version__}"}
            )
        else:
            self.websession = websession
        self._timeout = timeout
        self._access_token = access_token
        self.time_zone = time_zone or pytz.utc
        self._name = None
        self._user_id = None
        self._home_ids = []
        self._all_home_ids = []
        self._homes = {}
        self.sub_manager = None
        self.user_agent = "pyTibber"

    async def close_connection(self):
        """Close the Tibber connection."""
        await self.websession.close()

    async def rt_connect(self):
        """Start subscription manager for real time data."""
        if self.sub_manager is not None:
            return
        self.sub_manager = SubscriptionManager(
            f"token={self._access_token}", SUB_ENDPOINT
        )
        self.sub_manager.start()

    async def rt_disconnect(self):
        """Stop subscription manager."""
        if self.sub_manager is None:
            return
        await self.sub_manager.stop()

    async def execute(self, document, variable_values=None):
        """Execute gql."""
        res = await self._execute(document, variable_values)
        if res is None:
            return None
        return res.get("data")

    async def _execute(self, document, variable_values=None, retry=2):
        """Execute gql."""
        payload = {"query": document, "variables": variable_values or {}}

        post_args = {
            "headers": {"Authorization": "Bearer " + self._access_token},
            "data": payload,
        }
        try:
            user_agent = self.websession._default_headers.get(  # pylint: disable=protected-access
                aiohttp.hdrs.USER_AGENT, ""
            )  # will be fixed by aiohttp 4.0
            if "pyTibber" not in user_agent:
                post_args["headers"][
                    aiohttp.hdrs.USER_AGENT
                ] = f"{user_agent} {self.user_agent}/{__version__}"
        except Exception:  # pylint: disable=broad-except
            pass

        try:
            async with async_timeout.timeout(self._timeout):
                resp = await self.websession.post(API_ENDPOINT, **post_args)
            if resp.status != 200:
                _LOGGER.error("Error connecting to Tibber, resp code: %s", resp.status)
                return None
            result = await resp.json()
        except aiohttp.ClientError as err:
            if retry > 0:
                return await self._execute(document, variable_values, retry - 1)
            _LOGGER.error("Error connecting to Tibber: %s ", err, exc_info=True)
            raise
        except asyncio.TimeoutError:
            _LOGGER.error("Timed out when connecting to Tibber")
            raise
        errors = result.get("errors")
        if errors:
            _LOGGER.error("Received non-compatible response %s", errors)
        return result

    async def update_info(self, *_):
        """Update home info async."""
        query = """
        {
          viewer {
            name
            userId
            homes {
              id
              subscriptions {
                status
              }
            }
          }
        }
        """

        res = await self._execute(query)
        if res is None:
            return
        errors = res.get("errors", [])
        if errors:
            msg = errors[0].get("message", "failed to login")
            _LOGGER.error(msg)
            raise InvalidLogin(msg)

        data = res.get("data")
        if not data:
            return
        viewer = data.get("viewer")
        if not viewer:
            return
        self._name = viewer.get("name")
        self._user_id = viewer.get("userId")

        homes = viewer.get("homes", [])
        self._home_ids = []
        for _home in homes:
            home_id = _home.get("id")
            self._all_home_ids += [home_id]
            subs = _home.get("subscriptions")
            if not subs:
                continue
            status = subs[0].get("status", "ended").lower()
            if not home_id or status != "running":
                continue
            self._home_ids += [home_id]

    @property
    def user_id(self):
        """Return user id of user."""
        return self._user_id

    @property
    def name(self):
        """Return name of user."""
        return self._name

    @property
    def home_ids(self):
        """Return list of home ids."""
        return self.get_home_ids(only_active=True)

    def get_home_ids(self, only_active=True):
        """Return list of home ids."""
        if only_active:
            return self._home_ids
        return self._all_home_ids

    def get_homes(self, only_active=True):
        """Return list of Tibber homes."""
        return [self.get_home(home_id) for home_id in self.get_home_ids(only_active)]

    def get_home(self, home_id):
        """Return an instance of TibberHome for given home id."""
        if home_id not in self._all_home_ids:
            _LOGGER.error("Could not find any Tibber home with id: %s", home_id)
            return None
        if home_id not in self._homes:
            self._homes[home_id] = TibberHome(home_id, self)
        return self._homes[home_id]

    async def send_notification(self, title, message):
        """Send notification."""
        # pylint: disable=consider-using-f-string)
        query = """
        mutation{{
          sendPushNotification(input: {{
            title: "{}",
            message: "{}",
          }}){{
            successful
            pushedToNumberOfDevices
          }}
        }}
        """.format(
            title,
            message,
        )

        res = await self.execute(query)
        if not res:
            return False
        noti = res.get("sendPushNotification", {})
        successful = noti.get("successful", False)
        pushed_to_number_of_devices = noti.get("pushedToNumberOfDevices", 0)
        _LOGGER.debug(
            "send_notification: status %s, send to %s devices",
            successful,
            pushed_to_number_of_devices,
        )
        return successful

    async def fetch_conumption_data_active_homes(self):
        """Fetch consumption data for active homes."""
        tasks = []
        for home in self.get_homes(only_active=True):
            tasks.append(home.fetch_conumption_data())
        await asyncio.gather(*tasks)


class TibberHome:
    """Instance of Tibber home."""

    # pylint: disable=too-many-instance-attributes, too-many-public-methods

    def __init__(self, home_id, tibber_control):
        """Initialize the Tibber home class."""
        self._tibber_control = tibber_control
        self._home_id = home_id
        self._current_price_total = None
        self._current_price_info = {}
        self._price_info = {}
        self._level_info = {}
        self.info = {}
        self._subscription_id = None
        self._data = None
        self.last_data_timestamp = None

        self.month_cons = None
        self.month_cost = None
        self.peak_hour = None
        self.peak_hour_time = None
        self.last_cons_data_timestamp = None
        self.hourly_consumption_data = []

    async def fetch_conumption_data(self):
        """Update consumption info async."""
        # pylint: disable=consider-using-f-string)
        now = dt.datetime.utcnow().astimezone(dt.timezone.utc)
        if self.last_cons_data_timestamp is not None and (
            now - self.last_cons_data_timestamp
        ) < dt.timedelta(hours=24):
            return
        n_hours = now.hour + now.day * 24

        consumption = await self.get_historic_data(
            n_hours, resolution=RESOLUTION_HOURLY
        )

        if not consumption:
            _LOGGER.error("Could not find consumption info.")
            return consumption
        now = dt.datetime.now()
        _month_cons = 0
        _month_cost = 0
        _month_hour_max_month_hour_cons = 0
        _month_hour_max_month_hour = None
        for node in consumption:
            _time = parse(node["from"])
            if _time.month != now.month or _time.year != now.year:
                continue
            if node.get("consumption") is None:
                continue

            if (
                self.last_cons_data_timestamp is None
                or _time + dt.timedelta(hours=1) > self.last_cons_data_timestamp
            ):
                self.last_cons_data_timestamp = _time + dt.timedelta(hours=1)
            if node["consumption"] > _month_hour_max_month_hour_cons:
                _month_hour_max_month_hour_cons = node["consumption"]
                _month_hour_max_month_hour = _time
            _month_cons += node["consumption"]

            if node.get("cost") is not None:
                _month_cost += node["cost"]

        self.month_cons = round(_month_cons, 2)
        self.month_cost = round(_month_cost, 2)
        self.peak_hour = round(_month_hour_max_month_hour_cons, 2)
        self.peak_hour_time = _month_hour_max_month_hour
        self.hourly_consumption_data = consumption

    async def update_info(self):
        """Update current price info async."""
        # pylint: disable=consider-using-f-string)
        query = (
            """
        {
          viewer {
            home(id: "%s") {
              appNickname
              features {
                  realTimeConsumptionEnabled
                }
              currentSubscription {
                status
              }
              address {
                address1
                address2
                address3
                city
                postalCode
                country
                latitude
                longitude
              }
              meteringPointData {
                consumptionEan
                energyTaxType
                estimatedAnnualConsumption
                gridCompany
                productionEan
                vatType
              }
              owner {
                name
                isCompany
                language
                contactInfo {
                  email
                  mobile
                }
              }
              timeZone
              subscriptions {
                id
                status
                validFrom
                validTo
                statusReason
              }
             currentSubscription {
                    priceInfo {
                      current {
                        currency
                      }
                    }
                  }
                }
              }
            }
        """
            % self._home_id
        )

        self.info = await self._tibber_control.execute(query)

    async def update_info_and_price_info(self):
        """Update current price info async."""
        # pylint: disable=consider-using-f-string)
        query = (
            """
        {
          viewer {
            home(id: "%s") {
              currentSubscription {
                priceInfo {
                  current {
                    energy
                    tax
                    total
                    startsAt
                    level
                  }
                  today {
                    total
                    startsAt
                    level
                  }
                  tomorrow {
                    total
                    startsAt
                    level
                  }
                }
              }
              appNickname
              features {
                realTimeConsumptionEnabled
              }
              currentSubscription {
                status
              }
              address {
                address1
                address2
                address3
                city
                postalCode
                country
                latitude
                longitude
              }
              meteringPointData {
                consumptionEan
                energyTaxType
                estimatedAnnualConsumption
                gridCompany
                productionEan
                vatType
              }
              owner {
                name
                isCompany
                language
                contactInfo {
                  email
                  mobile
                }
              }
              timeZone
              subscriptions {
                id
                status
                validFrom
                validTo
                statusReason
              }
              currentSubscription {
                priceInfo {
                  current {
                    currency
                  }
                }
              }
            }
          }
        }

        """
            % self._home_id
        )

        self.info = await self._tibber_control.execute(query)
        self._process_price_info(self.info)

    async def update_current_price_info(self):
        """Update current price info async."""
        # pylint: disable=consider-using-f-string)
        query = (
            """
        {
          viewer {
            home(id: "%s") {
              currentSubscription {
                priceInfo {
                  current {
                    energy
                    tax
                    total
                    startsAt
                  }
                }
              }
            }
          }
        }
        """
            % self.home_id
        )
        price_info_temp = await self._tibber_control.execute(query)
        if not price_info_temp:
            _LOGGER.error("Could not find current price info.")
            return
        try:
            home = price_info_temp["viewer"]["home"]
            current_subscription = home["currentSubscription"]
            price_info = current_subscription["priceInfo"]["current"]
        except (KeyError, TypeError):
            _LOGGER.error("Could not find current price info.")
            return
        if price_info:
            self._current_price_info = price_info

    async def update_price_info(self):
        """Update price info async."""
        # pylint: disable=consider-using-f-string)
        query = (
            """
        {
          viewer {
            home(id: "%s") {
              currentSubscription {
                priceInfo {
                  current {
                    energy
                    tax
                    total
                    startsAt
                    level
                  }
                  today {
                    total
                    startsAt
                    level
                  }
                  tomorrow {
                    total
                    startsAt
                    level
                  }
                }
              }
            }
          }
        }
        """
            % self.home_id
        )
        price_info = await self._tibber_control.execute(query)
        self._process_price_info(price_info)

    def _process_price_info(self, price_info):
        if not price_info:
            _LOGGER.error("Could not find price info.")
            return
        self._price_info = {}
        self._level_info = {}
        for key in ["current", "today", "tomorrow"]:
            try:
                home = price_info["viewer"]["home"]
                current_subscription = home["currentSubscription"]
                price_info_k = current_subscription["priceInfo"][key]
            except (KeyError, TypeError):
                _LOGGER.error("Could not find price info for %s.", key)
                continue
            if key == "current":
                self._current_price_info = price_info_k
                continue
            for data in price_info_k:
                self._price_info[data.get("startsAt")] = data.get("total")
                self._level_info[data.get("startsAt")] = data.get("level")

    @property
    def current_price_total(self):
        """Get current price total."""
        if not self._current_price_info:
            return None
        return self._current_price_info.get("total")

    @property
    def current_price_info(self):
        """Get current price info."""
        return self._current_price_info

    @property
    def price_total(self):
        """Get dictionary with price total, key is date-time."""
        return self._price_info

    @property
    def price_level(self):
        """Get dictionary with price level, key is date-time."""
        return self._level_info

    @property
    def home_id(self):
        """Return home id."""
        return self._home_id

    @property
    def has_active_subscription(self):
        """Return home id."""
        try:
            sub = self.info["viewer"]["home"]["currentSubscription"]["status"]
        except (KeyError, TypeError):
            return False
        return sub in ["running", "awaiting market", "awaiting time restriction"]

    @property
    def has_real_time_consumption(self):
        """Return home id."""
        try:
            return self.info["viewer"]["home"]["features"]["realTimeConsumptionEnabled"]
        except (KeyError, TypeError):
            return False

    @property
    def address1(self):
        """Return the home adress1."""
        try:
            return self.info["viewer"]["home"]["address"]["address1"]
        except (KeyError, TypeError):
            _LOGGER.error("Could not find address1.")
        return ""

    @property
    def consumption_unit(self):
        """Return the consumption."""
        return "kWh"

    @property
    def currency(self):
        """Return the currency."""
        try:
            current_subscription = self.info["viewer"]["home"]["currentSubscription"]
            return current_subscription["priceInfo"]["current"]["currency"]
        except (KeyError, TypeError, IndexError):
            _LOGGER.error("Could not find currency.")
        return ""

    @property
    def country(self):
        """Return the country."""
        try:
            return self.info["viewer"]["home"]["address"]["country"]
        except (KeyError, TypeError):
            _LOGGER.error("Could not find country.")
        return ""

    @property
    def price_unit(self):
        """Return the price unit."""
        currency = self.currency
        consumption_unit = self.consumption_unit
        if not currency or not consumption_unit:
            _LOGGER.error("Could not find price_unit.")
            return " "
        return currency + "/" + consumption_unit

    async def rt_subscribe(self, async_callback):
        """Connect to Tibber and subscribe to Tibber rt subscription."""
        if self._subscription_id is not None:
            _LOGGER.error("Already subscribed.")
            return
        await self._tibber_control.rt_connect()
        # pylint: disable=consider-using-f-string)
        document = (
            """
            subscription{
              liveMeasurement(homeId:"%s"){
                accumulatedConsumption
                accumulatedConsumptionLastHour
                accumulatedCost
                accumulatedProduction
                accumulatedProductionLastHour
                accumulatedReward
                averagePower
                currency
                currentL1
                currentL2
                currentL3
                lastMeterConsumption
                lastMeterProduction
                maxPower
                minPower
                power
                powerFactor
                powerProduction
                powerReactive
                signalStrength
                timestamp
                voltagePhase1
                voltagePhase2
                voltagePhase3
            }
           }
        """
            % self.home_id
        )

        self._subscription_id = await self._tibber_control.sub_manager.subscribe(
            document, async_callback
        )

    async def rt_unsubscribe(self):
        """Unsubscribe to Tibber rt subscription."""
        if self._subscription_id is None:
            _LOGGER.error("Not subscribed.")
            return
        await self._tibber_control.sub_manager.unsubscribe(self._subscription_id)

    @property
    def rt_subscription_running(self):
        """Is real time subscription running."""
        return (
            self._tibber_control.sub_manager is not None
            and self._tibber_control.sub_manager.is_running
        )

    async def get_historic_data(self, n_data, resolution=RESOLUTION_HOURLY):
        """Get historic data."""
        # pylint: disable=consider-using-f-string)
        query = """
                {{
                  viewer {{
                    home(id: "{}") {{
                      consumption(resolution: {}, last: {}) {{
                        nodes {{
                          from
                          totalCost
                          cost
                          consumption
                        }}
                      }}
                    }}
                  }}
                }}
          """.format(
            self.home_id,
            resolution,
            n_data,
        )
        data = await self._tibber_control.execute(query)
        if not data:
            _LOGGER.error("Could not find the data.")
            return
        data = data["viewer"]["home"]["consumption"]
        if data is None:
            self._data = []
            return
        self._data = data["nodes"]
        return self._data

    def current_price_data(self):
        """get current price."""
        now = dt.datetime.now(self._tibber_control.time_zone)
        res = None, None, None
        for key, price_total in self.price_total.items():
            price_time = parse(key).astimezone(self._tibber_control.time_zone)
            time_diff = (now - price_time).total_seconds() / 60
            if not self.last_data_timestamp or price_time > self.last_data_timestamp:
                self.last_data_timestamp = price_time
            if 0 <= time_diff < 60:
                res = round(price_total, 3), self.price_level[key], price_time
        return res

    def current_attributes(self):
        """get current attributes."""
        # pylint: disable=too-many-locals
        max_price = 0
        min_price = 10000
        sum_price = 0
        off_peak_1 = 0
        peak = 0
        off_peak_2 = 0
        num1 = 0
        num0 = 0
        num2 = 0
        num = 0
        now = dt.datetime.now(self._tibber_control.time_zone)
        for key, price_total in self.price_total.items():
            price_time = parse(key).astimezone(self._tibber_control.time_zone)
            price_total = round(price_total, 3)
            if now.date() == price_time.date():
                max_price = max(max_price, price_total)
                min_price = min(min_price, price_total)
                if price_time.hour < 8:
                    off_peak_1 += price_total
                    num1 += 1
                elif price_time.hour < 20:
                    peak += price_total
                    num0 += 1
                else:
                    off_peak_2 += price_total
                    num2 += 1
                num += 1
                sum_price += price_total

        attr = {}
        attr["max_price"] = max_price
        attr["avg_price"] = round(sum_price / num, 3) if num > 0 else 0
        attr["min_price"] = min_price
        attr["off_peak_1"] = round(off_peak_1 / num1, 3) if num1 > 0 else 0
        attr["peak"] = round(peak / num0, 3) if num0 > 0 else 0
        attr["off_peak_2"] = round(off_peak_2 / num2, 3) if num2 > 0 else 0
        grid_company = self.info["viewer"]["home"]["meteringPointData"].get(
            "gridCompany", ""
        )
        if grid_company and "glitre" in grid_company.lower():
            now = now.astimezone(pytz.timezone("Europe/Oslo"))
            if now.month >= 10 or now.month <= 3:
                grid_price = 44.99 / 100
            else:
                grid_price = 43.11 / 100
            if now.hour >= 22 or now.hour < 6:
                grid_price -= 12 / 100
            attr["grid_price"] = round(grid_price, 3)

        return attr


class InvalidLogin(Exception):
    """Invalid login exception."""
