import os
# compatibility with Python >= 2.7.13
try:
    import urllib2
except ImportError:
    import urllib.request as urllib2

import json
import errno
import ssl
import shlex
import codecs
from distutils.version import StrictVersion
from sys import version_info

# compatibility with Python >= 2.7.13
try:
    from urlparse import urljoin
except ImportError:
    from urllib.parse import urljoin

from ansible.errors import AnsibleError
import ansible.utils
try:
    from ansible.plugins.lookup import LookupBase
except ImportError:
    # ansible-1.9.x
    class LookupBase(object):
        def __init__(self, basedir=None, runner=None, **kwargs):
            self.runner = runner
            self.basedir = basedir or (self.runner.basedir
                                       if self.runner
                                       else None)

        def get_basedir(self, variables):
            return self.basedir

USE_CACHE = os.environ.get(
    "ANSIBLE_HASHICORP_VAULT_USE_CACHE", "yes").lower() in ("yes", "1", "true")
DISABLE_VAULT_CAHOSTVERIFY = "no"
VAULT_CACHE = {}


class LookupModule(LookupBase):

    def run(self, terms, inject=None, variables=None, **kwargs):
        # Ansible variables are passed via "variables" in ansible 2.x, "inject" in 1.9.x

        basedir = self.get_basedir(variables)

        if hasattr(ansible.utils, 'listify_lookup_plugin_terms'):
            # ansible-1.9.x
            terms = ansible.utils.listify_lookup_plugin_terms(terms, basedir, inject)

        term_split = terms[0].split(' ', 1)
        key = term_split[0]

        # the environment variable takes precendence over the Ansible variable.
        cafile = os.getenv('VAULT_CACERT') or (variables or inject).get('vault_cacert')
        capath = os.getenv('VAULT_CAPATH') or (variables or inject).get('vault_capath')
        cahostverify = (os.getenv('VAULT_CAHOSTVERIFY') or
                        (variables or inject).get('vault_cahostverify') or 'yes') != DISABLE_VAULT_CAHOSTVERIFY
        skipverify = ((os.getenv('VAULT_SKIP_VERIFY') in ['1', 'true', 'True', 't']) or
                      (variables or inject).get('vault_skip_verify'))
        self._verify_python_version(key, cafile, capath, cahostverify)

        try:
            parameters = term_split[1]
            parameters = shlex.split(parameters)

            parameter_bag = {}
            for parameter in parameters:
                parameter_split = parameter.split('=', 1)

                parameter_key = parameter_split[0]
                parameter_value = parameter_split[1]
                parameter_bag[parameter_key] = parameter_value

            data = json.dumps(parameter_bag)
        except Exception:
            data = None

        try:
            field = terms[1]
        except IndexError:
            field = None

        # the environment variable takes precendence over the Ansible variable.
        url = os.getenv('VAULT_ADDR') or (variables or inject).get('vault_addr')
        if not url:
            raise AnsibleError('Vault address not set. Specify with'
                               ' VAULT_ADDR environment variable or vault_addr Ansible variable')

        # Support for Approle backend
        approle_role_id = os.getenv('ANSIBLE_HASHICORP_VAULT_ROLE_ID')
        approle_secret_id = os.getenv('ANSIBLE_HASHICORP_VAULT_SECRET_ID')
        approle_role_path = os.getenv('ANSIBLE_HASHICORP_VAULT_ROLE_PATH', 'v1/auth/approle/login')

        # first check if an approle token is already cached
        vault_token = VAULT_CACHE.get('ANSIBLE_HASHICORP_VAULT_APPROLE_TOKEN', None)

        # if approle role-id and secret-id are set, use approle to get a token
        # and if caching is activated, the token will be stored in the cache
        if not vault_token and approle_role_id and approle_secret_id:
            vault_token = self._fetch_approle_token(
                cafile, capath, approle_role_id, approle_secret_id, approle_role_path, url, cahostverify, skipverify)
            if vault_token and USE_CACHE:
                VAULT_CACHE['ANSIBLE_HASHICORP_VAULT_APPROLE_TOKEN'] = vault_token

        # the environment variable takes precedence over the file-based token.
        # intentionally do *not* support setting this via an Ansible variable,
        # so as not to encourage bad security practices.
        github_token = os.getenv('VAULT_GITHUB_API_TOKEN')
        if not vault_token:
            vault_token = os.getenv('VAULT_TOKEN')
        if not vault_token and not github_token:
            token_path = os.path.join(os.getenv('HOME'), '.vault-token')
            try:
                with open(token_path) as token_file:
                    vault_token = token_file.read().strip()
            except IOError as err:
                if err.errno != errno.ENOENT:
                    raise AnsibleError('Error occurred when opening ' + token_path + ': ' + err.strerror)
        if not github_token and not vault_token:
            raise AnsibleError('Vault or GitHub authentication token missing. Specify with'
                               ' VAULT_TOKEN/VAULT_GITHUB_API_TOKEN environment variable or in $HOME/.vault-token '
                               '(Current $HOME value is ' + os.getenv('HOME') + ')')

        if USE_CACHE and key in VAULT_CACHE:
            result = VAULT_CACHE[key]
        else:
            if not vault_token:
                token_result = self._fetch_github_token(cafile, capath, github_token, url, cahostverify, skipverify)
                vault_token = token_result['auth']['client_token']
            result = self._fetch_secret(cafile, capath, data, key, vault_token, url, cahostverify, skipverify)
            if USE_CACHE:
                VAULT_CACHE[key] = result

        if type(result) is dict:
            if field is not None:
                return [result['data'][field]]
            elif 'data' in result:
                return [result['data']]
        return [result]

    def _fetch_approle_token(self, cafile, capath, role_id, secret_id,
                             approle_role_path, url, cahostverify, skipverify):
        request_url = urljoin(url, approle_role_path)
        req_params = {
            'role_id': role_id,
            'secret_id': secret_id
        }
        result = self._fetch_client_token(cafile, capath, request_url, req_params, cahostverify, skipverify)
        token = result['auth']['client_token']
        return token

    def _fetch_github_token(self, cafile, capath, github_token, url, cahostverify, skipverify):
        request_url = urljoin(url, "v1/auth/github/login")
        req_params = {}
        req_params['token'] = github_token
        result = self._fetch_client_token(cafile, capath, request_url, req_params, cahostverify, skipverify)
        return result

    def _fetch_client_token(self, cafile, capath, url, data, cahostverify, skipverify):
        try:
            context = None
            if cafile or capath:
                context = ssl.create_default_context(cafile=cafile, capath=capath)
                context.check_hostname = cahostverify
            elif skipverify:
                context = ssl._create_unverified_context()
            data = data.encode('utf-8') if data else None
            req = urllib2.Request(url, json.dumps(data))
            req.add_header('Content-Type', 'application/json')
            response = urllib2.urlopen(req, context=context) if context else urllib2.urlopen(req)
        except Exception as ex:
            if hasattr(ex, 'code') and ex.code in [301, 302, 303, 307]:
                return self._fetch_client_token(cafile, capath, ex.headers.dict['location'], data, cahostverify,
                                                skipverify)
            else:
                raise AnsibleError('Unable to retrieve personal token from vault: %s' % (ex))
        reader = codecs.getreader("utf-8")
        result = json.load(reader(response))
        return result

    def _fetch_secret(self, cafile, capath, data, key, vault_token, url, cahostverify, skipverify):
        try:
            context = None
            if cafile or capath:
                context = ssl.create_default_context(cafile=cafile, capath=capath)
                context.check_hostname = cahostverify
            elif skipverify:
                context = ssl._create_unverified_context()
            request_url = urljoin(url, "v1/%s" % (key))
            data = data.encode('utf-8') if data else None
            req = urllib2.Request(request_url, data)
            req.add_header('X-Vault-Token', vault_token)
            req.add_header('Content-Type', 'application/json')
            response = urllib2.urlopen(req, context=context) if context else urllib2.urlopen(req)
        except Exception as ex:
            if hasattr(ex, 'code') and ex.code in [301, 302, 303, 307]:
                return self._fetch_secret(cafile, capath, data, key, vault_token, ex.headers.dict['location'],
                                          cahostverify, skipverify)
            else:
                raise AnsibleError('Unable to read %s from vault: %s' % (key, ex))
        reader = codecs.getreader("utf-8")
        body = reader(response)
        if response.headers.get('Content-Type') == 'application/json':
            body = json.load(body)
        return body

    def _verify_python_version(self, key, cafile, capath, cahostverify):
        python_version_cur = ".".join([str(version_info.major),
                                       str(version_info.minor),
                                       str(version_info.micro)])
        python_version_min = "2.7.9"
        if StrictVersion(python_version_cur) < StrictVersion(python_version_min):
            if cafile or capath:
                raise AnsibleError('Unable to read %s from vault:'
                                   ' Using Python %s, and vault lookup plugin requires at least %s'
                                   ' to use an SSL context (VAULT_CACERT or VAULT_CAPATH)'
                                   % (key, python_version_cur, python_version_min))
            elif cahostverify:
                raise AnsibleError('Unable to read %s from vault:'
                                   ' Using Python %s, and vault lookup plugin requires at least %s'
                                   ' to verify Vault certificate. (set VAULT_CAHOSTVERIFY to \'%s\''
                                   ' to disable certificate verification.)'
                                   % (key, python_version_cur, python_version_min, DISABLE_VAULT_CAHOSTVERIFY))