#!/usr/bin/env python
"""Script to create self contained install.

The goal of this script is simple:

  * Create a self contained install of the CLI that
  has requires no external resources during installation.

It does this by using all the normal python tooling
(virtualenv, pip) but provides a simple, easy to use
interface for those not familiar with the python
ecosystem.

"""

import os
import shutil
import subprocess
import sys
import tempfile
import zipfile
from contextlib import contextmanager

EXTRA_RUNTIME_DEPS = [
    # Use an up to date virtualenv/pip/setuptools on > 2.6.
    ("virtualenv", "16.7.8"),
]
BUILDTIME_DEPS = [
    ("setuptools-scm", "3.3.3"),
    ("wheel", "0.33.6"),
]
PIP_DOWNLOAD_ARGS = "--no-binary :all:"
# The constraints file is used to lock the version of dateutils needed
# to be 2.8.0 until we can drop py26/py33 support.  This lets us put
# botocore's version range on dateutils to be <3.0.
CONSTRAINTS_FILE = os.path.join(
    os.path.dirname(os.path.abspath(__file__)), "assets", "constraints-bundled.txt"
)


class BadRCError(Exception):
    pass


@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()
    rc = p.wait()
    if p.returncode != 0:
        raise BadRCError("Bad rc (%s) for cmd '%s': %s" % (rc, cmd, stderr + stdout))
    return stdout


def create_scratch_dir():
    # This creates the dir where all the bundling occurs.
    # First we need a top level dir.
    dirname = tempfile.mkdtemp(prefix="bundle")
    # Then we need to create a dir where all the packages
    # will come from.
    os.mkdir(os.path.join(dirname, "packages"))
    os.mkdir(os.path.join(dirname, "packages", "setup"))
    return dirname


def download_package_tarballs(dirname, packages):
    with cd(dirname):
        for package, package_version in packages:
            run(
                "%s -m pip download %s==%s %s"
                % (sys.executable, package, package_version, PIP_DOWNLOAD_ARGS)
            )


def download_cli_deps(scratch_dir):
    cfnlint_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    with cd(scratch_dir):
        run(
            "pip download -c %s %s %s"
            % (CONSTRAINTS_FILE, PIP_DOWNLOAD_ARGS, cfnlint_dir)
        )


def _remove_cli_zip(scratch_dir):
    clidir = [f for f in os.listdir(scratch_dir) if f.startswith("cfn-lint")]
    assert len(clidir) == 1
    os.remove(os.path.join(scratch_dir, clidir[0]))


def add_cli_sdist(scratch_dir):
    cfnlint_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    if os.path.exists(os.path.join(cfnlint_dir, "dist")):
        shutil.rmtree(os.path.join(cfnlint_dir, "dist"))
    with cd(cfnlint_dir):
        run("%s setup.py sdist" % sys.executable)
        filename = os.listdir("dist")[0]
        shutil.move(os.path.join("dist", filename), os.path.join(scratch_dir, filename))


def create_bootstrap_script(scratch_dir):
    install_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "install")
    shutil.copy(install_script, os.path.join(scratch_dir, "install"))


def zip_dir(scratch_dir):
    basename = "cfn-lint-bundle.zip"
    dirname, tmpdir = os.path.split(scratch_dir)
    final_dir_name = os.path.join(dirname, "cfn-lint-bundle")
    if os.path.isdir(final_dir_name):
        shutil.rmtree(final_dir_name)
    shutil.move(scratch_dir, final_dir_name)
    with cd(dirname):
        with zipfile.ZipFile(basename, "w", zipfile.ZIP_DEFLATED) as zipped:
            for root, dirnames, filenames in os.walk("cfn-lint-bundle"):
                for filename in filenames:
                    zipped.write(os.path.join(root, filename))
    return os.path.join(dirname, basename)


def verify_preconditions():
    # The pip version looks like:
    # 'pip 1.4.1 from ....'
    pip_version = run("%s -m pip --version" % sys.executable).strip().split()[1]
    # Virtualenv version just has the version string: '1.14.5\n'
    virtualenv_version = run("%s -m virtualenv --version" % sys.executable).strip()
    _min_version_required("9.0.1", pip_version, "pip")
    _min_version_required("15.1.0", virtualenv_version, "virtualenv")


def _min_version_required(min_version, actual_version, name):
    # precondition: min_version is major.minor.patch
    #               actual_version is major.minor.patch
    min_split = min_version.split(".")
    actual_split = actual_version.decode("utf-8").split(".")
    for min_version_part, actual_version_part in zip(min_split, actual_split):
        if int(actual_version_part) >= int(min_version_part):
            return
    raise ValueError(
        "%s requires at least version %s, but version %s was "
        "found." % (name, min_version, actual_version)
    )


def main():
    verify_preconditions()
    scratch_dir = create_scratch_dir()
    package_dir = os.path.join(scratch_dir, "packages")
    print("Bundle dir at: %s" % scratch_dir)
    download_package_tarballs(
        package_dir,
        packages=EXTRA_RUNTIME_DEPS,
    )

    # Some packages require setup time dependencies, and so we will need to
    # manually install them. We isolate them to a particular directory so we
    # can run the install before the things they're dependent on. We have to do
    # this because pip won't actually find them since it doesn't handle build
    # dependencies.
    setup_dir = os.path.join(package_dir, "setup")
    download_package_tarballs(
        setup_dir,
        packages=BUILDTIME_DEPS,
    )
    download_cli_deps(package_dir)
    add_cli_sdist(package_dir)
    create_bootstrap_script(scratch_dir)
    zip_filename = zip_dir(scratch_dir)
    print("Zipped bundle installer is at: %s" % zip_filename)


if __name__ == "__main__":
    main()
