#!/usr/bin/env python
# We're using optparse because we need to support 2.6
# which doesn't have argparse.  Given that argparse is
# a dependency that eventually gets installed, we could
# try to bootstrap, but using optparse is just easier.

import optparse
import os
import platform
import shutil
import subprocess
import sys
import tarfile
import tempfile
from contextlib import contextmanager

PACKAGES_DIR = os.path.join(
    os.path.dirname(os.path.abspath(__file__)), 'packages'
)
INSTALL_DIR = os.path.expanduser(os.path.join('~', '.local', 'lib', 'aws'))
GTE_PY37 = sys.version_info[:2] >= (3, 7)
UNSUPPORTED_PYTHON = (
    (2, 6),
    (3, 3),
    (3, 4),
    (3, 5),
)
INSTALL_ARGS = (
    '--no-binary :all: --no-build-isolation --no-cache-dir --no-index '
)


class BadRCError(Exception):
    pass


class MultipleBundlesError(Exception):
    pass


class PythonDeprecationWarning(Warning):
    """
    Python version being used is scheduled to become unsupported
    in an future release. See warning for specifics.
    """

    pass


def _build_deprecations():
    py_27_params = {
        'date': 'July 15, 2021',
        'blog_link': 'https://aws.amazon.com/blogs/developer/announcing-end-'
        'of-support-for-python-2-7-in-aws-sdk-for-python-and-'
        'aws-cli-v1/',
    }
    return {(2, 7): py_27_params}


DEPRECATED_PYTHON = _build_deprecations()


@contextmanager
def cd(dirname):
    original = os.getcwd()
    os.chdir(dirname)
    try:
        yield
    finally:
        os.chdir(original)


def run(cmd):
    sys.stdout.write("Running cmd: %s\n" % cmd)
    p = subprocess.Popen(
        cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
    )
    stdout, stderr = p.communicate()
    if p.returncode != 0:
        output = (stdout + stderr).decode("utf-8")
        raise BadRCError(f"Bad rc ({p.returncode}) for cmd '{cmd}': {output}")
    return stdout


def bin_path():
    """
    Get the system's binary path, either `bin` on reasonable
    systems or `Scripts` on Windows.
    """
    path = 'bin'

    if platform.system() == 'Windows':
        path = 'Scripts'

    return path


def create_install_structure(working_dir, install_dir):
    if not os.path.isdir(install_dir):
        os.makedirs(install_dir)
    create_virtualenv(location=install_dir, working_dir=working_dir)


def _create_virtualenv_internal(location, working_dir):
    # On py3 we use the built in venv to create our virtualenv.
    # There's a bug with sys.executable on external virtualenv
    # that causes installation failures.
    run('%s -m venv %s' % (sys.executable, location))


def _create_virtualenv_external(location, working_dir):
    # working_dir is used (generally somewhere in /tmp) so that we
    # don't modify the install/packages directories.
    with cd(PACKAGES_DIR):
        venv = _get_venv_package_tarball('.')
        compressed = tarfile.open(venv)
        compressed.extractall(path=working_dir)
        compressed.close()
    with cd(working_dir):
        # We know that virtualenv is the only dir in this directory
        # so we can listdir()[0] it.
        with cd(os.listdir('.')[0]):
            run(
                ('%s virtualenv.py --no-download ' '--python %s %s')
                % (sys.executable, sys.executable, location)
            )


def _get_package_tarball(package_dir, package_prefix):
    package_filenames = sorted(
        [p for p in os.listdir(package_dir) if p.startswith(package_prefix)]
    )
    return package_filenames[-1]


def _get_venv_package_tarball(package_dir):
    return _get_package_tarball(package_dir, 'virtualenv')


def create_working_dir():
    d = tempfile.mkdtemp()
    return d


def pip_install_packages(install_dir):
    cli_tarball = [
        p for p in os.listdir(PACKAGES_DIR) if p.startswith('awscli')
    ]
    if len(cli_tarball) != 1:
        message = (
            "Multiple versions of the CLI were found in %s. Please clear "
            "out this directory before proceeding."
        )
        raise MultipleBundlesError(message % PACKAGES_DIR)
    cli_tarball = cli_tarball[0]
    pip_script = os.path.join(install_dir, bin_path(), 'pip')

    setup_requires_dir = os.path.join(PACKAGES_DIR, 'setup')
    with cd(setup_requires_dir):
        _install_setup_deps(pip_script, '.')

    with cd(PACKAGES_DIR):
        run(
            '%s install %s --find-links file://%s %s'
            % (pip_script, INSTALL_ARGS, PACKAGES_DIR, cli_tarball)
        )


def _install_setup_deps(pip_script, setup_package_dir):
    # Some packages declare `setup_requires`, which is a list of dependencies
    # to be used at setup time. These need to be installed before anything
    # else, and pip doesn't manage them.  We have to manage this ourselves
    # so for now we're explicitly installing the one setup_requires package
    # we need.  This comes from python-dateutils.
    setuptools_scm_tarball = _get_package_tarball(
        setup_package_dir, 'setuptools_scm'
    )
    run(
        '%s install --no-binary :all: --no-cache-dir --no-index '
        '--find-links file://%s %s'
        % (pip_script, setup_package_dir, setuptools_scm_tarball)
    )
    wheel_tarball = _get_package_tarball(setup_package_dir, 'wheel')
    run(
        '%s install --no-binary :all: --no-cache-dir --no-index '
        '--find-links file://%s %s'
        % (pip_script, setup_package_dir, wheel_tarball)
    )


def create_symlink(real_location, symlink_name):
    if os.path.isfile(symlink_name):
        print("Symlink already exists: %s" % symlink_name)
        print("Removing symlink.")
        os.remove(symlink_name)
    symlink_dir_name = os.path.dirname(symlink_name)
    if not os.path.isdir(symlink_dir_name):
        os.makedirs(symlink_dir_name)
    os.symlink(real_location, symlink_name)
    return True


def main():
    parser = optparse.OptionParser()
    parser.add_option(
        '-i',
        '--install-dir',
        help="The location to install "
        "the AWS CLI.  The default value is ~/.local/lib/aws",
        default=INSTALL_DIR,
    )
    parser.add_option(
        '-b',
        '--bin-location',
        help="If this argument is "
        "provided, then a symlink will be created at this "
        "location that points to the aws executable. "
        "This argument is useful if you want to put the aws "
        "executable somewhere already on your path, e.g. "
        "-b /usr/local/bin/aws.  This is an optional argument. "
        "If you do not provide this argument you will have to "
        "add INSTALL_DIR/bin to your PATH.",
    )
    py_version = sys.version_info[:2]
    if py_version in UNSUPPORTED_PYTHON:
        unsupported_python_msg = (
            "Unsupported Python version detected: Python %s.%s\n"
            "To continue using this installer you must use Python 2.7, "
            "Python 3.6 or later.\n"
            "For more information see the following blog post:\n"
            "https://aws.amazon.com/blogs/developer/deprecation-of-python-2-6"
            "-and-python-3-3-in-botocore-boto3-and-the-aws-cli/"
        )
        print(unsupported_python_msg % py_version)
        sys.exit(1)

    if py_version in DEPRECATED_PYTHON:
        params = DEPRECATED_PYTHON[py_version]
        deprecated_python_msg = (
            "Deprecated Python version detected: Python {}.{}\n"
            "Starting {}, the AWS CLI will no longer support "
            "this version of Python. To continue receiving service updates, "
            "bug fixes, and security updates please upgrade to Python 3.6 or "
            "later. More information can be found here: {}"
        ).format(
            py_version[0], py_version[1], params['date'], params['blog_link']
        )
        print(deprecated_python_msg, file=sys.stderr)

    opts = parser.parse_args()[0]
    working_dir = create_working_dir()
    try:
        create_install_structure(working_dir, opts.install_dir)
        pip_install_packages(opts.install_dir)
        real_location = os.path.join(opts.install_dir, bin_path(), 'aws')
        if opts.bin_location and create_symlink(
            real_location, opts.bin_location
        ):
            print("You can now run: %s --version" % opts.bin_location)
        else:
            print("You can now run: %s --version" % real_location)
    finally:
        shutil.rmtree(working_dir)


if GTE_PY37:
    create_virtualenv = _create_virtualenv_internal
else:
    create_virtualenv = _create_virtualenv_external


if __name__ == '__main__':
    main()
