# Standard library imports
import json
import os
import stat
import traceback
import typing
import urllib.parse
import uuid
import collections
from datetime import datetime
from os.path import exists, isdir, isfile, join

# Third party imports
import requests
import repo_cli
from binstar_client.utils.config import TOKEN_DIRS

from anaconda_navigator.utils.logs import logger

try:
    from urllib import quote_plus
except ImportError:
    from urllib.parse import quote_plus


Args = collections.namedtuple('Args', ['site'])


def inject_url_query(url: str, arguments: typing.Mapping[str, typing.Any]) -> str:
    """Replace query part in the `url`."""
    scheme: str
    netloc: str
    path: str
    params: str
    query: str
    fragment: str

    scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(url)
    query = urllib.parse.urlencode(arguments)
    return urllib.parse.urlunparse((scheme, netloc, path, params, query, fragment))


class TeamEditionAPI:
    """
    This is the interface for working with Anaconda Team Edition API.
    """
    DEFAULT_CONTENT_TYPE = {'Content-Type': 'application/json'}

    def __init__(self, base_url, config, verify_ssl=False):

        self._urls = {
            'repo': '{}/api/repo'.format(base_url),
            'login': '{}/api/auth/login'.format(base_url),
            'account': '{}/api/account'.format(base_url),
            'tokens': '{}/api/account/tokens'.format(base_url),
            'channels': '{}/api/channels'.format(base_url)
        }
        self._config = config
        self._verify_ssl = verify_ssl
        self.token = None
        self.domain = base_url

    def authenticate(self, username, password):
        """
        Login using direct grant and returns the JWT token.

        :param str username: The username to authenticate.
        :param str password: The password to authenticate.

        :return str: The dumped json string.
        """
        data = {'username': username, 'password': password}
        s = requests.Session()

        try:
            resp = s.post(
                self._urls['login'], data=json.dumps(data), headers=self.DEFAULT_CONTENT_TYPE, verify=self._verify_ssl
            )

        except requests.exceptions.SSLError:
            return json.dumps(
                {
                    'message':
                    'Your SSL certificate is self-signed or broken and cannot be validated. '
                    'Please change the certificate or set “ssl_verify:false” '
                    'in your .condarc configuration file.'
                }
            )

        except Exception:
            logger.error(
                'Exception happened during the login into Team Edition. Traceback: {}'.format(traceback.format_exc())
            )
            return json.dumps(
                {
                    'message':
                    'An unexpected error happened! Please see logs at {} and '
                    'contact your system administrator.'.format(logger.handlers[0].baseFilename)
                }
            )

        else:
            if resp.status_code != 200:
                return json.dumps(resp.json())

            token = resp.json()
            token['jwt_token'] = token['token']
            token['jwt_token_refresh'] = token['refresh_token']

            return json.dumps(token)

    def logout(self):
        self.remove_token()

    def _get_team_edition_api_url(self):
        return 'jwt_{}'.format(self._config.get('main', 'team_edition_api_url'))

    def load_token(self):
        """
        Loads the JWT token to be used in further requests to authenticate user.

        :return str:
        """
        for token_dir in TOKEN_DIRS:
            url = self._get_team_edition_api_url()

            tokenfile = join(token_dir, '%s.token' % quote_plus(url))
            _is_file = exists(tokenfile)

            if _is_file:
                with open(tokenfile) as fd:
                    token = fd.read().strip()

                if token:
                    return token
                else:
                    os.unlink(tokenfile)

        return "{}"

    def store_binstar_token(self, token):
        """
        Stores the binstar token required to work with Team Edition
        through 'native' conda interface.

        :param str token: The string as a token.
        """
        for token_dir in TOKEN_DIRS:
            if not isdir(token_dir):
                os.makedirs(token_dir)
            tokenfile = join(token_dir, '%s.token' % quote_plus(self._urls['repo']))

            if isfile(tokenfile):
                os.unlink(tokenfile)

            with open(tokenfile, 'w') as fd:
                fd.write(token)
            os.chmod(tokenfile, stat.S_IWRITE | stat.S_IREAD)

    def store_token(self, token):
        """
        Stores the passed JWT token locally to be used in further
        user authentication.

        :param str token: The json data dumped in the string.
        """
        for token_dir in TOKEN_DIRS:
            url = self._get_team_edition_api_url()

            if not isdir(token_dir):
                os.makedirs(token_dir)
            tokenfile = join(token_dir, '%s.token' % quote_plus(url))

            if isfile(tokenfile):
                os.unlink(tokenfile)

            with open(tokenfile, 'w') as fd:
                fd.write(token)
            os.chmod(tokenfile, stat.S_IWRITE | stat.S_IREAD)

    def remove_token(self):
        """
        Removes the existing JWT token from local space.
        """
        jwt_url = self._get_team_edition_api_url()
        url = self._get_team_edition_api_url().replace('jwt_', '')

        url_suffixes = [
            url,
            jwt_url,
            '{}/{}'.format(jwt_url, 'repo'),
            '{}/{}'.format(url, 'repo'),
            '{}/{}'.format(url, 'api'),
            '{}/{}/{}'.format(url, 'api', 'repo')
        ]

        for token_dir in TOKEN_DIRS:
            for url_suffix in url_suffixes:
                tokenfile = join(token_dir, '%s.token' % quote_plus(url_suffix))
                if isfile(tokenfile):
                    os.unlink(tokenfile)
        try:
            args = Args(None)
            repo_cli.utils.config.remove_token(args)
        except TypeError:
            pass

    def user(self, login=None):
        """
        Gets the user account info.

        :return dict[str, str]: The user data.
        """
        token = json.loads(self.load_token())

        s = requests.Session()
        resp = s.get(self._urls['account'], headers=self.DEFAULT_CONTENT_TYPE, cookies=token, verify=self._verify_ssl)

        data = resp.json()

        return {
            'login': data['username'],
        }

    def create_access_token(self, token):
        """
        Creates the access token (binstar) for the Team Edition API to provide
        access to channels listed in the repo.

        Returns the token and the token id.

        :param dict[str, str] token: The JWT token to be used for authentication.

        :return dict: The access token info.
        """
        request_data = {
            'name':
            'navigator-token-{}-{}'.format(datetime.now().strftime('%Y-%m-%d'), uuid.uuid4()),
            'expires_at':
            '{}-12-31'.format(datetime.now().year),
            'scopes': [
                'channel:view', 'channel:view-artifacts', 'subchannel:view', 'subchannel:view-artifacts',
                'artifact:view', 'artifact:download'
            ]
        }

        s = requests.Session()
        resp = s.post(
            self._urls['tokens'],
            headers=self.DEFAULT_CONTENT_TYPE,
            data=json.dumps(request_data),
            cookies=token,
            verify=self._verify_ssl
        )

        response_data = resp.json()
        self.store_binstar_token(response_data.get('token'))

        return response_data

    def remove_access_token(self, access_token_id):
        """
        Deletes the created access token (binstar) from the Team Edition database.

        :param str access_token_id: The token ID to be removed.
        """
        token = json.loads(self.load_token())

        s = requests.Session()
        s.delete(
            '{}/{}'.format(self._urls['tokens'], access_token_id),
            headers=self.DEFAULT_CONTENT_TYPE,
            cookies=token,
            verify=self._verify_ssl
        )

    def get_channels(self) -> typing.List[typing.Dict[str, typing.Any]]:
        """
        Returns all the channels which are accessible for the user.

        :rtype: list[dict[str, mixed]]
        :return: List of dictionaries with available channels data.
        """
        token: typing.Mapping[str, typing.Any] = json.loads(self.load_token())

        headers: typing.Mapping[str, typing.Any]
        if 'token' in token:
            headers = {'Authorization': 'Bearer {}'.format(token['token'])}
        else:
            headers = {'X-Auth': self.token}

        result: typing.List[typing.Dict[str, typing.Any]] = []

        step: int = 1000
        offset: int = 0
        total: typing.Optional[int] = None
        while (total is None) or (offset < total):
            url: str = inject_url_query(
                self._urls['channels'], {'offset': offset, 'limit': step, 'sort': 'name,updated_at'},
            )
            response: requests.Response = requests.get(url, headers=headers)
            response.raise_for_status()

            content: typing.Mapping[str, typing.Any] = response.json()
            if total is None:
                total = content['total_count']
            result.extend(content['items'])

            offset += step

        return result
