import asyncio
from aiohttp import ClientSession, ClientConnectorError, ClientTimeout
from .creds import Creds, KEY_CLIENT_ID, KEY_CLIENT_SECRET, KEY_ACCESS_TOKEN, KEY_REFRESH_TOKEN, KEY_EXPIRES_IN
from ..exceptions import ensureKey, GoogleCredentialsExpired, CredRefreshGoogleError, CredRefreshMyError
from ..config import Config, Setting, VERSION
from yarl import URL
from ..time import Time
from ..logger import getLogger
from .driverequester import DriveRequester
from datetime import timedelta
from injector import singleton, inject


SCOPE = 'https://www.googleapis.com/auth/drive.file'

KEY_REDIRECT_URI = 'redirect_uri'
KEY_SCOPE = 'scope'
KEY_RESPONSE_TYPE = 'response_type'
KEY_INCLUDE_GRANTED_SCOPES = 'include_granted_scopes'
KEY_ACCESS_TYPE = 'access_type'
KEY_STATE = 'state'
KEY_PROMPT = 'prompt'
KEY_CODE = 'code'
KEY_GRANT_TYPE = 'grant_type'
KEY_VERSION = 'version'
KEY_CLIENT = 'client'

CRED_OBJECT_NAME = "credential token response"

logger = getLogger(__name__)


@singleton
class Exchanger():
    @inject
    def __init__(self,
                 time: Time,
                 session: ClientSession,
                 config: Config,
                 drive: DriveRequester,
                 client_id: str,
                 client_secret: str,
                 redirect: URL):
        self.time = time
        self.config = config
        self.session = session
        self.drive = drive
        self._client_id = client_id
        self._client_secret = client_secret
        self._redirect = redirect

    async def getAuthorizationUrl(self, state="") -> str:
        url = URL(self.config.get(Setting.DRIVE_AUTHORIZE_URL)).with_query({
            KEY_CLIENT_ID: self._client_id,
            KEY_SCOPE: SCOPE,
            KEY_RESPONSE_TYPE: 'code',
            KEY_INCLUDE_GRANTED_SCOPES: 'true',
            KEY_ACCESS_TYPE: "offline",
            KEY_STATE: state,
            KEY_REDIRECT_URI: str(self._redirect),
            KEY_PROMPT: "consent"
        })
        return str(url)

    async def exchange(self, code):
        data = {
            KEY_CLIENT_ID: self._client_id,
            KEY_CLIENT_SECRET: self._client_secret,
            KEY_CODE: code,
            KEY_REDIRECT_URI: str(self._redirect),
            KEY_GRANT_TYPE: 'authorization_code'
        }
        resp = None
        async with await self.drive.request("post", self.config.get(Setting.DRIVE_TOKEN_URL), data=data) as resp:
            return Creds.load(self.time, await resp.json(), id=self._client_id, secret=self._client_secret)

    async def refresh(self, creds: Creds):
        if creds.secret is not None:
            return await self._refresh_google(creds)
        else:
            return await self._refresh_default(creds)

    async def _refresh_google(self, creds: Creds):
        data = {
            KEY_CLIENT_ID: creds.id,
            KEY_CLIENT_SECRET: creds.secret,
            KEY_REFRESH_TOKEN: creds.refresh_token,
            KEY_GRANT_TYPE: 'refresh_token'
        }
        async with await self.drive.request("post", self.config.get(Setting.DRIVE_REFRESH_URL), data=data) as resp:
            data = await resp.json()
            return Creds(
                self.time,
                id=creds.id,
                secret=creds.secret,
                access_token=ensureKey(KEY_ACCESS_TOKEN, data, CRED_OBJECT_NAME),
                refresh_token=creds.refresh_token,
                expiration=self._get_expiration(data),
                original_expiration=creds.original_expiration)

    async def _refresh_default(self, creds: Creds):
        data = {
            KEY_CLIENT_ID: creds.id,
            KEY_REFRESH_TOKEN: creds.refresh_token,
        }
        token_paths = self.config.getTokenServers("/drive/refresh")
        last_error = None
        for url in token_paths:
            try:
                headers = {
                    'addon_version': VERSION,
                    'client': self.config.clientIdentifier()
                }
                async with self.session.post(str(url), headers=headers, json=data, timeout=ClientTimeout(total=self.config.get(Setting.EXCHANGER_TIMEOUT_SECONDS))) as resp:
                    if resp.status < 400:
                        return Creds.load(self.time, await resp.json(), original_expiration=creds.original_expiration)
                    elif resp.status == 503:
                        json = {}
                        try:
                            json = await resp.json()
                        except BaseException:
                            pass
                        if "error" in json:
                            if "invalid_grant" in json["error"]:
                                raise GoogleCredentialsExpired()
                            else:
                                # Record the error, but still try other hosts
                                last_error = CredRefreshGoogleError(json["error"])
                        else:
                            last_error = CredRefreshMyError("HTTP 503 from " + url.host)
                    elif resp.status == 401:
                        raise GoogleCredentialsExpired()
                    else:
                        try:
                            extra = (await resp.json())["error"]
                        except BaseException:
                            extra = ""

                        # this is likely due to misconfiguration
                        logger.warning("Got {0}:{1} from {2}, trying alternate server(s)...".format(resp.status, extra, url.host))
                        last_error = CredRefreshMyError("HTTP {} {}".format(resp.status, extra))
            except ClientConnectorError:
                logger.warning("Unable to reach " + str(url.host) + ", trying alternate server(s)...")
                last_error = "Couldn't communicate with " + url.host
            except asyncio.exceptions.TimeoutError:
                logger.warning("Timed out communicating with " + str(url.host) + ", trying alternate server(s)...")
                last_error = "Timed out communicating with " + url.host
        logger.error("Unable to refresh credentials with Google Drive")
        if isinstance(last_error, str):
            raise CredRefreshMyError(last_error)
        elif isinstance(last_error, Exception):
            raise last_error
        else:
            raise Exception("Unexpected error type: " + str(last_error))

    def refreshCredentials(self, refresh_token):
        return Creds(self.time, id=self._client_id, expiration=None, access_token=None, refresh_token=refresh_token, secret=self._client_secret)

    def _get_expiration(self, data):
        return self.time.now() + timedelta(seconds=int(ensureKey(KEY_EXPIRES_IN, data, CRED_OBJECT_NAME)))
