"""Support for Alexa skill auth."""
import asyncio
from datetime import timedelta
import json
import logging

import aiohttp
import async_timeout

from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, HTTP_OK
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
from homeassistant.util import dt

_LOGGER = logging.getLogger(__name__)

LWA_TOKEN_URI = "https://api.amazon.com/auth/o2/token"
LWA_HEADERS = {"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"}

PREEMPTIVE_REFRESH_TTL_IN_SECONDS = 300
STORAGE_KEY = "alexa_auth"
STORAGE_VERSION = 1
STORAGE_EXPIRE_TIME = "expire_time"
STORAGE_ACCESS_TOKEN = "access_token"
STORAGE_REFRESH_TOKEN = "refresh_token"


class Auth:
    """Handle authentication to send events to Alexa."""

    def __init__(self, hass, client_id, client_secret):
        """Initialize the Auth class."""
        self.hass = hass

        self.client_id = client_id
        self.client_secret = client_secret

        self._prefs = None
        self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)

        self._get_token_lock = asyncio.Lock()

    async def async_do_auth(self, accept_grant_code):
        """Do authentication with an AcceptGrant code."""
        # access token not retrieved yet for the first time, so this should
        # be an access token request

        lwa_params = {
            "grant_type": "authorization_code",
            "code": accept_grant_code,
            CONF_CLIENT_ID: self.client_id,
            CONF_CLIENT_SECRET: self.client_secret,
        }
        _LOGGER.debug(
            "Calling LWA to get the access token (first time), with: %s",
            json.dumps(lwa_params),
        )

        return await self._async_request_new_token(lwa_params)

    @callback
    def async_invalidate_access_token(self):
        """Invalidate access token."""
        self._prefs[STORAGE_ACCESS_TOKEN] = None

    async def async_get_access_token(self):
        """Perform access token or token refresh request."""
        async with self._get_token_lock:
            if self._prefs is None:
                await self.async_load_preferences()

            if self.is_token_valid():
                _LOGGER.debug("Token still valid, using it")
                return self._prefs[STORAGE_ACCESS_TOKEN]

            if self._prefs[STORAGE_REFRESH_TOKEN] is None:
                _LOGGER.debug("Token invalid and no refresh token available")
                return None

            lwa_params = {
                "grant_type": "refresh_token",
                "refresh_token": self._prefs[STORAGE_REFRESH_TOKEN],
                CONF_CLIENT_ID: self.client_id,
                CONF_CLIENT_SECRET: self.client_secret,
            }

            _LOGGER.debug("Calling LWA to refresh the access token")
            return await self._async_request_new_token(lwa_params)

    @callback
    def is_token_valid(self):
        """Check if a token is already loaded and if it is still valid."""
        if not self._prefs[STORAGE_ACCESS_TOKEN]:
            return False

        expire_time = dt.parse_datetime(self._prefs[STORAGE_EXPIRE_TIME])
        preemptive_expire_time = expire_time - timedelta(
            seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS
        )

        return dt.utcnow() < preemptive_expire_time

    async def _async_request_new_token(self, lwa_params):

        try:
            session = aiohttp_client.async_get_clientsession(self.hass)
            with async_timeout.timeout(10):
                response = await session.post(
                    LWA_TOKEN_URI,
                    headers=LWA_HEADERS,
                    data=lwa_params,
                    allow_redirects=True,
                )

        except (asyncio.TimeoutError, aiohttp.ClientError):
            _LOGGER.error("Timeout calling LWA to get auth token")
            return None

        _LOGGER.debug("LWA response header: %s", response.headers)
        _LOGGER.debug("LWA response status: %s", response.status)

        if response.status != HTTP_OK:
            _LOGGER.error("Error calling LWA to get auth token")
            return None

        response_json = await response.json()
        _LOGGER.debug("LWA response body  : %s", response_json)

        access_token = response_json["access_token"]
        refresh_token = response_json["refresh_token"]
        expires_in = response_json["expires_in"]
        expire_time = dt.utcnow() + timedelta(seconds=expires_in)

        await self._async_update_preferences(
            access_token, refresh_token, expire_time.isoformat()
        )

        return access_token

    async def async_load_preferences(self):
        """Load preferences with stored tokens."""
        self._prefs = await self._store.async_load()

        if self._prefs is None:
            self._prefs = {
                STORAGE_ACCESS_TOKEN: None,
                STORAGE_REFRESH_TOKEN: None,
                STORAGE_EXPIRE_TIME: None,
            }

    async def _async_update_preferences(self, access_token, refresh_token, expire_time):
        """Update user preferences."""
        if self._prefs is None:
            await self.async_load_preferences()

        if access_token is not None:
            self._prefs[STORAGE_ACCESS_TOKEN] = access_token
        if refresh_token is not None:
            self._prefs[STORAGE_REFRESH_TOKEN] = refresh_token
        if expire_time is not None:
            self._prefs[STORAGE_EXPIRE_TIME] = expire_time
        await self._store.async_save(self._prefs)
