"""
Support for the Fitbit API.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.fitbit/
"""
import os
import json
import logging
import datetime
import time

from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
from homeassistant.components.http import HomeAssistantView

_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ["fitbit==0.2.3"]
DEPENDENCIES = ["http"]

ICON = "mdi:walk"

_CONFIGURING = {}

# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=30)

FITBIT_AUTH_START = "/auth/fitbit"
FITBIT_AUTH_CALLBACK_PATH = "/auth/fitbit/callback"

DEFAULT_CONFIG = {
    "client_id": "CLIENT_ID_HERE",
    "client_secret": "CLIENT_SECRET_HERE"
}

FITBIT_CONFIG_FILE = "fitbit.conf"

FITBIT_RESOURCES_LIST = {
    "activities/activityCalories": "cal",
    "activities/calories": "cal",
    "activities/caloriesBMR": "cal",
    "activities/distance": "",
    "activities/elevation": "",
    "activities/floors": "floors",
    "activities/heart": "bpm",
    "activities/minutesFairlyActive": "minutes",
    "activities/minutesLightlyActive": "minutes",
    "activities/minutesSedentary": "minutes",
    "activities/minutesVeryActive": "minutes",
    "activities/steps": "steps",
    "activities/tracker/activityCalories": "cal",
    "activities/tracker/calories": "cal",
    "activities/tracker/distance": "",
    "activities/tracker/elevation": "",
    "activities/tracker/floors": "floors",
    "activities/tracker/minutesFairlyActive": "minutes",
    "activities/tracker/minutesLightlyActive": "minutes",
    "activities/tracker/minutesSedentary": "minutes",
    "activities/tracker/minutesVeryActive": "minutes",
    "activities/tracker/steps": "steps",
    "body/bmi": "BMI",
    "body/fat": "%",
    "sleep/awakeningsCount": "times awaken",
    "sleep/efficiency": "%",
    "sleep/minutesAfterWakeup": "minutes",
    "sleep/minutesAsleep": "minutes",
    "sleep/minutesAwake": "minutes",
    "sleep/minutesToFallAsleep": "minutes",
    "sleep/startTime": "start time",
    "sleep/timeInBed": "time in bed",
    "body/weight": ""
}

FITBIT_DEFAULT_RESOURCE_LIST = ["activities/steps"]

FITBIT_MEASUREMENTS = {
    "en_US": {
        "duration": "ms",
        "distance": "mi",
        "elevation": "ft",
        "height": "in",
        "weight": "lbs",
        "body": "in",
        "liquids": "fl. oz.",
        "blood glucose": "mg/dL",
    },
    "en_GB": {
        "duration": "milliseconds",
        "distance": "kilometers",
        "elevation": "meters",
        "height": "centimeters",
        "weight": "stone",
        "body": "centimeters",
        "liquids": "milliliters",
        "blood glucose": "mmol/L"
    },
    "metric": {
        "duration": "milliseconds",
        "distance": "kilometers",
        "elevation": "meters",
        "height": "centimeters",
        "weight": "kilograms",
        "body": "centimeters",
        "liquids": "milliliters",
        "blood glucose": "mmol/L"
    }
}


def config_from_file(filename, config=None):
    """Small configuration file management function."""
    if config:
        # We"re writing configuration
        try:
            with open(filename, "w") as fdesc:
                fdesc.write(json.dumps(config))
        except IOError as error:
            _LOGGER.error("Saving config file failed: %s", error)
            return False
        return config
    else:
        # We"re reading config
        if os.path.isfile(filename):
            try:
                with open(filename, "r") as fdesc:
                    return json.loads(fdesc.read())
            except IOError as error:
                _LOGGER.error("Reading config file failed: %s", error)
                # This won"t work yet
                return False
        else:
            return {}


def request_app_setup(hass, config, add_devices, config_path,
                      discovery_info=None):
    """Assist user with configuring the Fitbit dev application."""
    configurator = get_component("configurator")

    # pylint: disable=unused-argument
    def fitbit_configuration_callback(callback_data):
        """The actions to do when our configuration callback is called."""
        config_path = hass.config.path(FITBIT_CONFIG_FILE)
        if os.path.isfile(config_path):
            config_file = config_from_file(config_path)
            if config_file == DEFAULT_CONFIG:
                error_msg = ("You didn't correctly modify fitbit.conf",
                             " please try again")
                configurator.notify_errors(_CONFIGURING["fitbit"], error_msg)
            else:
                setup_platform(hass, config, add_devices, discovery_info)
        else:
            setup_platform(hass, config, add_devices, discovery_info)

    start_url = "{}{}".format(hass.config.api.base_url,
                              FITBIT_AUTH_CALLBACK_PATH)

    description = """Please create a Fitbit developer app at
                       https://dev.fitbit.com/apps/new.
                       For the OAuth 2.0 Application Type choose Personal.
                       Set the Callback URL to {}.
                       They will provide you a Client ID and secret.
                       These need to be saved into the file located at: {}.
                       Then come back here and hit the below button.
                       """.format(start_url, config_path)

    submit = "I have saved my Client ID and Client Secret into fitbit.conf."

    _CONFIGURING["fitbit"] = configurator.request_config(
        hass, "Fitbit", fitbit_configuration_callback,
        description=description, submit_caption=submit,
        description_image="/static/images/config_fitbit_app.png"
    )


def request_oauth_completion(hass):
    """Request user complete Fitbit OAuth2 flow."""
    configurator = get_component("configurator")
    if "fitbit" in _CONFIGURING:
        configurator.notify_errors(
            _CONFIGURING["fitbit"], "Failed to register, please try again.")

        return

    # pylint: disable=unused-argument
    def fitbit_configuration_callback(callback_data):
        """The actions to do when our configuration callback is called."""

    start_url = "{}{}".format(hass.config.api.base_url, FITBIT_AUTH_START)

    description = "Please authorize Fitbit by visiting {}".format(start_url)

    _CONFIGURING["fitbit"] = configurator.request_config(
        hass, "Fitbit", fitbit_configuration_callback,
        description=description,
        submit_caption="I have authorized Fitbit."
    )

# pylint: disable=too-many-locals


def setup_platform(hass, config, add_devices, discovery_info=None):
    """Set up the Fitbit sensor."""
    config_path = hass.config.path(FITBIT_CONFIG_FILE)
    if os.path.isfile(config_path):
        config_file = config_from_file(config_path)
        if config_file == DEFAULT_CONFIG:
            request_app_setup(hass, config, add_devices, config_path,
                              discovery_info=None)
            return False
    else:
        config_file = config_from_file(config_path, DEFAULT_CONFIG)
        request_app_setup(hass, config, add_devices, config_path,
                          discovery_info=None)
        return False

    if "fitbit" in _CONFIGURING:
        get_component("configurator").request_done(_CONFIGURING.pop("fitbit"))

    import fitbit

    access_token = config_file.get("access_token")
    refresh_token = config_file.get("refresh_token")
    if None not in (access_token, refresh_token):
        authd_client = fitbit.Fitbit(config_file.get("client_id"),
                                     config_file.get("client_secret"),
                                     access_token=access_token,
                                     refresh_token=refresh_token)

        if int(time.time()) - config_file.get("last_saved_at", 0) > 3600:
            authd_client.client.refresh_token()

        authd_client.system = authd_client.user_profile_get()["user"]["locale"]
        if authd_client.system != 'en_GB':
            if hass.config.units.is_metric:
                authd_client.system = "metric"
            else:
                authd_client.system = "en_US"

        dev = []
        for resource in config.get("monitored_resources",
                                   FITBIT_DEFAULT_RESOURCE_LIST):
            dev.append(FitbitSensor(authd_client, config_path, resource,
                                    hass.config.units.is_metric))
        add_devices(dev)

    else:
        oauth = fitbit.api.FitbitOauth2Client(config_file.get("client_id"),
                                              config_file.get("client_secret"))

        redirect_uri = "{}{}".format(hass.config.api.base_url,
                                     FITBIT_AUTH_CALLBACK_PATH)

        fitbit_auth_start_url, _ = oauth.authorize_token_url(
            redirect_uri=redirect_uri,
            scope=["activity", "heartrate", "nutrition", "profile",
                   "settings", "sleep", "weight"])

        hass.wsgi.register_redirect(FITBIT_AUTH_START, fitbit_auth_start_url)
        hass.wsgi.register_view(FitbitAuthCallbackView(hass, config,
                                                       add_devices, oauth))

        request_oauth_completion(hass)


class FitbitAuthCallbackView(HomeAssistantView):
    """Handle OAuth finish callback requests."""

    requires_auth = False
    url = '/auth/fitbit/callback'
    name = 'auth:fitbit:callback'

    def __init__(self, hass, config, add_devices, oauth):
        """Initialize the OAuth callback view."""
        super().__init__(hass)
        self.config = config
        self.add_devices = add_devices
        self.oauth = oauth

    def get(self, request):
        """Finish OAuth callback request."""
        from oauthlib.oauth2.rfc6749.errors import MismatchingStateError
        from oauthlib.oauth2.rfc6749.errors import MissingTokenError

        data = request.args

        response_message = """Fitbit has been successfully authorized!
        You can close this window now!"""

        if data.get("code") is not None:
            redirect_uri = "{}{}".format(self.hass.config.api.base_url,
                                         FITBIT_AUTH_CALLBACK_PATH)

            try:
                self.oauth.fetch_access_token(data.get("code"), redirect_uri)
            except MissingTokenError as error:
                _LOGGER.error("Missing token: %s", error)
                response_message = """Something went wrong when
                attempting authenticating with Fitbit. The error
                encountered was {}. Please try again!""".format(error)
            except MismatchingStateError as error:
                _LOGGER.error("Mismatched state, CSRF error: %s", error)
                response_message = """Something went wrong when
                attempting authenticating with Fitbit. The error
                encountered was {}. Please try again!""".format(error)
        else:
            _LOGGER.error("Unknown error when authing")
            response_message = """Something went wrong when
                attempting authenticating with Fitbit.
                An unknown error occurred. Please try again!
                """

        html_response = """<html><head><title>Fitbit Auth</title></head>
        <body><h1>{}</h1></body></html>""".format(response_message)

        config_contents = {
            "access_token": self.oauth.token["access_token"],
            "refresh_token": self.oauth.token["refresh_token"],
            "client_id": self.oauth.client_id,
            "client_secret": self.oauth.client_secret
        }
        if not config_from_file(self.hass.config.path(FITBIT_CONFIG_FILE),
                                config_contents):
            _LOGGER.error("failed to save config file")

        setup_platform(self.hass, self.config, self.add_devices)

        return html_response


# pylint: disable=too-few-public-methods
class FitbitSensor(Entity):
    """Implementation of a Fitbit sensor."""

    def __init__(self, client, config_path, resource_type, is_metric):
        """Initialize the Fitbit sensor."""
        self.client = client
        self.config_path = config_path
        self.resource_type = resource_type
        pretty_resource = self.resource_type.replace("activities/", "")
        pretty_resource = pretty_resource.replace("/", " ")
        pretty_resource = pretty_resource.title()
        if pretty_resource == "Body Bmi":
            pretty_resource = "BMI"
        self._name = pretty_resource
        unit_type = FITBIT_RESOURCES_LIST[self.resource_type]
        if unit_type == "":
            split_resource = self.resource_type.split("/")
            try:
                measurement_system = FITBIT_MEASUREMENTS[self.client.system]
            except KeyError:
                if is_metric:
                    measurement_system = FITBIT_MEASUREMENTS["metric"]
                else:
                    measurement_system = FITBIT_MEASUREMENTS["en_US"]
            unit_type = measurement_system[split_resource[-1]]
        self._unit_of_measurement = unit_type
        self._state = 0
        self.update()

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

    @property
    def state(self):
        """Return the state of the sensor."""
        return self._state

    @property
    def unit_of_measurement(self):
        """Return the unit of measurement of this entity, if any."""
        return self._unit_of_measurement

    @property
    def icon(self):
        """Icon to use in the frontend, if any."""
        return ICON

    # pylint: disable=too-many-branches
    @Throttle(MIN_TIME_BETWEEN_UPDATES)
    def update(self):
        """Get the latest data from the Fitbit API and update the states."""
        container = self.resource_type.replace("/", "-")
        response = self.client.time_series(self.resource_type, period="7d")
        self._state = response[container][-1].get("value")
        if self.resource_type == "activities/heart":
            self._state = response[container][-1].get("restingHeartRate")
        config_contents = {
            "access_token": self.client.client.token["access_token"],
            "refresh_token": self.client.client.token["refresh_token"],
            "client_id": self.client.client.client_id,
            "client_secret": self.client.client.client_secret,
            "last_saved_at": int(time.time())
        }
        if not config_from_file(self.config_path, config_contents):
            _LOGGER.error("failed to save config file")
