"""
Provides access to the models stored in the database, via the server.
"""
import datetime
import logging
from typing import NamedTuple, Sequence
from urllib.parse import urljoin

import requests


LOG = logging.getLogger(__name__)

SERVER_URL = "http://127.0.0.1:5000"
DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f"


class ServerStatus:
    """ Provides helper classes to check whether the server is up. """

    @classmethod
    def is_server_running(cls) -> bool:
        try:
            req = requests.get(urljoin(SERVER_URL, "ping"))

            if req.status_code == 200:
                return True, req.content
            return False, req.content

        except requests.ConnectionError as ex:
            return False, ex

    @classmethod
    def unsettled_consumptions(cls) -> dict:
        req = requests.get(urljoin(SERVER_URL, "status"))

        data = req.json()

        if data["unsettled"]["amount"]:
            data["unsettled"]["first"] = datetime.datetime.strptime(
                data["unsettled"]["first"], DATETIME_FORMAT
            )
            data["unsettled"]["last"] = datetime.datetime.strptime(
                data["unsettled"]["last"], DATETIME_FORMAT
            )

        return data


class Person(NamedTuple):
    """ Represents a Person, as retrieved from the database. """

    name: str
    active: bool = True
    person_id: int = None
    consumptions: dict = {}

    def add_consumption(self, type_id: str) -> bool:
        """ Register a consumption for this Person. """
        req = requests.post(
            urljoin(SERVER_URL, f"people/{self.person_id}/add_consumption/{type_id}")
        )
        try:
            data = req.json()

            if "error" in data:
                LOG.error(
                    "Could not add consumption for %s (%s): %s",
                    self.person_id,
                    req.status_code,
                    data,
                )
                return False

            self.consumptions.update(data["person"]["consumptions"])

            return Consumption.from_dict(data["consumption"])
        except ValueError:
            LOG.error(
                "Did not get JSON on adding Consumption (%s): %s",
                req.status_code,
                req.content,
            )
            return False

    def create(self) -> "Person":
        """ Create a new Person from the current attributes. As tuples are
        immutable, a new Person with the correct id is returned. """
        req = requests.post(
            urljoin(SERVER_URL, "people"),
            json={"person": {"name": self.name, "active": True}},
        )

        try:
            data = req.json()
        except ValueError:
            LOG.error(
                "Did not get JSON on adding Person (%s): %s",
                req.status_code,
                req.content,
            )
            return None

        if "error" in data or req.status_code != 201:
            LOG.error("Could not create Person (%s): %s", req.status_code, data)
            return None

        return Person.from_dict(data["person"])

    def set_active(self, new_state=True) -> "Person":
        req = requests.patch(
            urljoin(SERVER_URL, f"people/{self.person_id}"),
            json={"person": {"active": new_state}},
        )

        try:
            data = req.json()
        except ValueError:
            LOG.error(
                "Did not get JSON on updating Person (%s): %s",
                req.status_code,
                req.content,
            )
            return None

        if "error" in data or req.status_code != 200:
            LOG.error("Could not update Person (%s): %s", req.status_code, data)
            return None

        return Person.from_dict(data["person"])

    @classmethod
    def get(cls, person_id: int) -> "Person":
        """ Retrieve a Person by id. """
        req = requests.get(urljoin(SERVER_URL, f"/people/{person_id}"))

        try:
            data = req.json()

            if "error" in data:
                LOG.warning(
                    "Could not get person %s (%s): %s", person_id, req.status_code, data
                )
                return None

            return Person.from_dict(data["person"])

        except ValueError:
            LOG.error(
                "Did not get JSON from server on getting Person (%s): %s",
                req.status_code,
                req.content,
            )
            return None

    @classmethod
    def get_all(cls, active=None) -> ["Person"]:
        """ Get all active People. """
        params = {}
        if active is not None:
            params["active"] = int(active)

        req = requests.get(urljoin(SERVER_URL, "/people"), params=params)

        try:
            data = req.json()

            if "error" in data:
                LOG.warning("Could not get people (%s): %s", req.status_code, data)

            return [Person.from_dict(item) for item in data["people"]]

        except ValueError:
            LOG.error(
                "Did not get JSON from server on getting People (%s): %s",
                req.status_code,
                req.content,
            )
            return None

    @classmethod
    def from_dict(cls, data: dict) -> "Person":
        """ Reconstruct a Person object from a dict. """
        return Person(
            name=data["name"],
            active=data["active"],
            person_id=data["person_id"],
            consumptions=data["consumptions"],
        )


class Export(NamedTuple):
    created_at: datetime.datetime
    settlement_ids: Sequence[int]
    export_id: int
    settlements: Sequence["Settlement"] = []

    @classmethod
    def from_dict(cls, data: dict) -> "Export":
        """ Reconstruct an Export from a dict. """
        return cls(
            export_id=data["export_id"],
            created_at=datetime.datetime.strptime(data["created_at"], DATETIME_FORMAT),
            settlement_ids=data["settlement_ids"],
            settlements=data.get("settlements", []),
        )

    @classmethod
    def get_all(cls) -> ["Export"]:
        """ Get a list of all existing Exports. """
        req = requests.get(urljoin(SERVER_URL, "exports"))

        try:
            data = req.json()
        except ValueError:
            LOG.error(
                "Did not get JSON on listing Exports (%s): %s",
                req.status_code,
                req.content,
            )
            return None

        if "error" in data or req.status_code != 200:
            LOG.error("Could not list Exports (%s): %s", req.status_code, data)
            return None

        return [cls.from_dict(e) for e in data["exports"]]

    @classmethod
    def get(cls, export_id: int) -> "Export":
        """ Retrieve one Export. """
        req = requests.get(urljoin(SERVER_URL, f"exports/{export_id}"))

        try:
            data = req.json()
        except ValueError:
            LOG.error(
                "Did not get JSON on getting Export (%s): %s",
                req.status_code,
                req.content,
            )
            return None

        if "error" in data or req.status_code != 200:
            LOG.error("Could not get Export (%s): %s", req.status_code, data)
            return None

        data["export"]["settlements"] = data["settlements"]

        return cls.from_dict(data["export"])

    @classmethod
    def create(cls) -> "Export":
        """ Create a new Export, containing all un-exported Settlements. """
        req = requests.post(urljoin(SERVER_URL, "exports"))

        try:
            data = req.json()
        except ValueError:
            LOG.error(
                "Did not get JSON on adding Export (%s): %s",
                req.status_code,
                req.content,
            )
            return None

        if "error" in data or req.status_code != 201:
            LOG.error("Could not create Export (%s): %s", req.status_code, data)
            return None

        data["export"]["settlements"] = data["settlements"]

        return cls.from_dict(data["export"])


class ConsumptionType(NamedTuple):
    """ Represents a stored ConsumptionType. """

    name: str
    consumption_type_id: int = None
    icon: str = None

    def create(self) -> "ConsumptionType":
        """ Create a new ConsumptionType from the current attributes. As tuples
        are immutable, a new ConsumptionType with the correct id is returned.
        """
        req = requests.post(
            urljoin(SERVER_URL, "consumption_types"),
            json={"consumption_type": {"name": self.name, "icon": self.icon}},
        )

        try:
            data = req.json()
        except ValueError:
            LOG.error(
                "Did not get JSON on adding ConsumptionType (%s): %s",
                req.status_code,
                req.content,
            )
            return None

        if "error" in data or req.status_code != 201:
            LOG.error(
                "Could not create ConsumptionType (%s): %s", req.status_code, data
            )
            return None

        return ConsumptionType.from_dict(data["consumption_type"])

    @classmethod
    def get(cls, consumption_type_id: int) -> "ConsumptionType":
        """ Retrieve a ConsumptionType by id. """
        req = requests.get(
            urljoin(SERVER_URL, f"/consumption_types/{consumption_type_id}")
        )

        try:
            data = req.json()

            if "error" in data:
                LOG.warning(
                    "Could not get consumption type %s (%s): %s",
                    consumption_type_id,
                    req.status_code,
                    data,
                )
                return None

            return cls.from_dict(data["consumption_type"])

        except ValueError:
            LOG.error(
                "Did not get JSON from server on getting consumption type (%s): %s",
                req.status_code,
                req.content,
            )
            return None

    @classmethod
    def get_all(cls) -> ["ConsumptionType"]:
        """ Get all active ConsumptionTypes. """
        req = requests.get(urljoin(SERVER_URL, "/consumption_types"))

        try:
            data = req.json()

            if "error" in data:
                LOG.warning(
                    "Could not get consumption types (%s): %s", req.status_code, data
                )

            return [cls.from_dict(item) for item in data["consumption_types"]]

        except ValueError:
            LOG.error(
                "Did not get JSON from server on getting ConsumptionTypes (%s): %s",
                req.status_code,
                req.content,
            )
            return None

    @classmethod
    def from_dict(cls, data: dict) -> "ConsumptionType":
        """ Reconstruct a ConsumptionType from a dict. """
        return cls(
            name=data["name"],
            consumption_type_id=data["consumption_type_id"],
            icon=data.get("icon"),
        )


class Consumption(NamedTuple):
    """ Represents a stored Consumption. """

    consumption_id: int
    person_id: int
    consumption_type_id: int
    created_at: datetime.datetime
    reversed: bool = False
    settlement_id: int = None

    @classmethod
    def from_dict(cls, data: dict) -> "Consumption":
        """ Reconstruct a Consumption from a dict. """
        return cls(
            consumption_id=data["consumption_id"],
            person_id=data["person_id"],
            consumption_type_id=data["consumption_type_id"],
            settlement_id=data["settlement_id"],
            created_at=datetime.datetime.strptime(data["created_at"], DATETIME_FORMAT),
            reversed=data["reversed"],
        )

    def reverse(self) -> "Consumption":
        """ Reverse this consumption. """
        req = requests.delete(
            urljoin(SERVER_URL, f"/consumptions/{self.consumption_id}")
        )

        try:
            data = req.json()

            if "error" in data:
                LOG.error(
                    "Could not reverse consumption %s (%s): %s",
                    self.consumption_id,
                    req.status_code,
                    data,
                )
                return False

            return Consumption.from_dict(data["consumption"])

        except ValueError:
            LOG.error(
                "Did not get JSON on reversing Consumption (%s): %s",
                req.status_code,
                req.content,
            )
            return False


class Settlement(NamedTuple):
    """ Represents a stored Settlement. """

    settlement_id: int
    name: str
    consumption_summary: dict
    count_info: dict = {}

    @classmethod
    def from_dict(cls, data: dict) -> "Settlement":
        return Settlement(
            settlement_id=data["settlement_id"],
            name=data["name"],
            consumption_summary=data["consumption_summary"],
            count_info=data.get("count_info", {}),
        )

    @classmethod
    def create(cls, name: str) -> "Settlement":
        req = requests.post(
            urljoin(SERVER_URL, "/settlements"), json={"settlement": {"name": name}}
        )

        return cls.from_dict(req.json()["settlement"])

    @classmethod
    def get(cls, settlement_id: int) -> "Settlement":
        req = requests.get(urljoin(SERVER_URL, f"/settlements/{settlement_id}"))

        try:
            data = req.json()
        except ValueError:
            LOG.error(
                "Did not get JSON on retrieving Settlement (%s): %s",
                req.status_code,
                req.content,
            )
            return None

        if "error" in data or req.status_code != 200:
            LOG.error("Could not get Export (%s): %s", req.status_code, data)
            return None

        data["settlement"]["count_info"] = data["count_info"]

        return cls.from_dict(data["settlement"])