# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4; coding:utf-8 -*-
#
# Copyright (c) 2014 Gu1
# Licensed under the MIT license

"""
Hubic identity handling for Pyrax.
"""

import configparser
import os
import re
import time
import urllib.parse

import requests
from requests.compat import quote
from requests.compat import quote_plus

try:
    import pyrax
    from pyrax.base_identity import BaseIdentity
    from pyrax.base_identity import Service
    import pyrax.exceptions as exc
except ImportError as e:
    raise BackendException(f"""Hubic backend requires the pyrax library available from Rackspace.
Exception: {str(e)}""")

OAUTH_ENDPOINT = "https://api.hubic.com/oauth/"
API_ENDPOINT = "https://api.hubic.com/1.0/"
TOKENS_FILE = os.path.expanduser("~/.hubic_tokens")


class BearerTokenAuth(requests.auth.AuthBase):
    """
    Bearer token authentication for requests.
    """

    def __init__(self, token):
        """
        Initialize BearerTokenAuth.

        :param str token: The bearer token.
        """
        self.token = token

    def __call__(self, req):
        req.headers["Authorization"] = f"Bearer {self.token}"
        return req


class HubicIdentity(BaseIdentity):
    """
    Hubic-specific identity class for Pyrax.
    """

    def _get_auth_endpoint(self):
        return ""

    def set_credentials(
        self,
        email,
        password,
        client_id,
        client_secret,
        redirect_uri,
        authenticate=False,
    ):
        """
        Sets the username and password directly.
        """
        self._email = email
        self._password = password
        self._client_id = client_id
        self.tenant_id = client_id
        self._client_secret = client_secret
        self._redirect_uri = redirect_uri
        if authenticate:
            self.authenticate()

    def _read_credential_file(self, cfg):
        """
        Parses the credential file with Rackspace-specific labels.
        """
        self._email = cfg.get("hubic", "email")
        self._password = cfg.get("hubic", "password")
        self._client_id = cfg.get("hubic", "client_id")
        self.tenant_id = self._client_id
        self._client_secret = cfg.get("hubic", "client_secret")
        self._redirect_uri = cfg.get("hubic", "redirect_uri")

    def _parse_error(self, resp):
        if "location" not in resp.headers:
            return None
        query = urllib.parse.urlsplit(resp.headers["location"]).query
        qs = dict(urllib.parse.parse_qsl(query))
        return {"error": qs["error"], "error_description": qs["error_description"]}

    def _get_access_token(self, code):
        r = requests.post(
            f"{OAUTH_ENDPOINT}token/",
            data={
                "code": code,
                "redirect_uri": self._redirect_uri,
                "grant_type": "authorization_code",
            },
            auth=(self._client_id, self._client_secret),
        )
        if r.status_code != 200:
            try:
                err = r.json()
                err["code"] = r.status_code
            except Exception as e:
                err = {}

            raise exc.AuthenticationFailed(
                f"Unable to get oauth access token, wrong client_id or client_secret ? " f"({str(err)})"
            )

        oauth_token = r.json()

        config = configparser.ConfigParser()
        config.read(TOKENS_FILE)

        if not config.has_section("hubic"):
            config.add_section("hubic")

        if oauth_token["access_token"] is not None:
            config.set("hubic", "access_token", oauth_token["access_token"])
            with open(TOKENS_FILE, "wb") as configfile:
                config.write(configfile)
        else:
            raise exc.AuthenticationFailed(
                f"Unable to get oauth access token, wrong client_id or client_secret ? ({str(err)})"
            )

        if oauth_token["refresh_token"] is not None:
            config.set("hubic", "refresh_token", oauth_token["refresh_token"])
            with open(TOKENS_FILE, "wb") as configfile:
                config.write(configfile)
        else:
            raise exc.AuthenticationFailed("Unable to get the refresh token.")

        # removing username and password from .hubic_tokens
        if config.has_option("hubic", "email"):
            config.remove_option("hubic", "email")
            with open(TOKENS_FILE, "wb") as configfile:
                config.write(configfile)
            print("username has been removed from the .hubic_tokens file sent to the CE.")
        if config.has_option("hubic", "password"):
            config.remove_option("hubic", "password")
            with open(TOKENS_FILE, "wb") as configfile:
                config.write(configfile)
            print("password has been removed from the .hubic_tokens file sent to the CE.")

        return oauth_token

    def _refresh_access_token(self):
        config = configparser.ConfigParser()
        config.read(TOKENS_FILE)
        refresh_token = config.get("hubic", "refresh_token")

        if refresh_token is None:
            raise exc.AuthenticationFailed("refresh_token is null. Not acquiered before ?")

        success = False
        max_retries = 20
        retries = 0
        sleep_time = 30
        max_sleep_time = 3600

        while retries < max_retries and not success:
            r = requests.post(
                f"{OAUTH_ENDPOINT}token/",
                data={
                    "refresh_token": refresh_token,
                    "grant_type": "refresh_token",
                },
                auth=(self._client_id, self._client_secret),
            )
            if r.status_code != 200:
                if r.status_code == 509:
                    print("status_code 509: attempt #", retries, " failed")
                    retries += 1
                    time.sleep(sleep_time)
                    sleep_time = sleep_time * 2
                    if sleep_time > max_sleep_time:
                        sleep_time = max_sleep_time
                else:
                    try:
                        err = r.json()
                        err["code"] = r.status_code
                    except Exception as e:
                        err = {}

                    raise exc.AuthenticationFailed(
                        f"Unable to get oauth access token, wrong client_id or client_secret ? ({str(err)})"
                    )
            else:
                success = True

        if not success:
            raise exc.AuthenticationFailed(
                "All the attempts failed to get the refresh token: " "status_code = 509: Bandwidth Limit Exceeded"
            )

        oauth_token = r.json()

        if oauth_token["access_token"] is not None:
            return oauth_token
        else:
            raise exc.AuthenticationFailed("Unable to get oauth access token from json")

    def authenticate(self):
        config = configparser.ConfigParser()
        config.read(TOKENS_FILE)

        if config.has_option("hubic", "refresh_token"):
            oauth_token = self._refresh_access_token()
        else:
            r = requests.get(
                OAUTH_ENDPOINT
                + f"auth/?client_id={quote(self._client_id)}&redirect_uri={quote_plus(self._redirect_uri)}"
                f"&scope=credentials.r,account.r&response_type=code&state={pyrax.utils.random_ascii()}",
                allow_redirects=False,
            )
            if r.status_code != 200:
                raise exc.AuthenticationFailed(f"Incorrect/unauthorized client_id ({str(self._parse_error(r))})")

            try:
                from lxml import html as lxml_html
            except ImportError:
                lxml_html = None

            if lxml_html:
                oauth = lxml_html.document_fromstring(r.content).xpath('//input[@name="oauth"]')
                oauth = oauth[0].value if oauth else None
            else:
                oauth = re.search(
                    r'<input\s+[^>]*name=[\'"]?oauth[\'"]?\s+[^>]*value=[\'"]?(\d+)[\'"]?>',
                    r.content,
                )
                oauth = oauth.group(1) if oauth else None

            if not oauth:
                raise exc.AuthenticationFailed("Unable to get oauth_id from authorization page")

            if self._email is None or self._password is None:
                raise exc.AuthenticationFailed(
                    "Cannot retrieve email and/or password. " "Please run expresslane-hubic-setup.sh"
                )

            r = requests.post(
                f"{OAUTH_ENDPOINT}auth/",
                data={
                    "action": "accepted",
                    "oauth": oauth,
                    "login": self._email,
                    "user_pwd": self._password,
                    "account": "r",
                    "credentials": "r",
                },
                allow_redirects=False,
            )

            try:
                query = urllib.parse.urlsplit(r.headers["location"]).query
                code = dict(urllib.parse.parse_qsl(query))["code"]
            except Exception as e:
                raise exc.AuthenticationFailed("Unable to authorize client_id, " "invalid login/password ?")

            oauth_token = self._get_access_token(code)

        if oauth_token["token_type"].lower() != "bearer":
            raise exc.AuthenticationFailed("Unsupported access token type")

        r = requests.get(
            f"{API_ENDPOINT}account/credentials",
            auth=BearerTokenAuth(oauth_token["access_token"]),
        )

        swift_token = r.json()
        self.authenticated = True
        self.token = swift_token["token"]
        self.expires = swift_token["expires"]
        self.services["object_store"] = Service(
            self,
            {
                "name": "HubiC",
                "type": "cloudfiles",
                "endpoints": [{"public_url": swift_token["endpoint"]}],
            },
        )
        self.username = self.password = None
