#!/usr/bin/env python
import os
import re
import subprocess

import click


ROOT_DIR = os.path.dirname(
    os.path.dirname(os.path.abspath(__file__)),
)


@click.group()
def cli():
    """Command line tool for managing releases.

    To do a chalice release, run these commands::

        \b
        $ scripts/release bump-version --release-type patch
        $ NEXT_VERSION=$(scripts/release get-version)
        $ git commit -am "Bumping version to $NEXT_VERSION"
        $ scripts/release tag-release
        $ scripts/release build-release
        $ git push upstream master --tags
        $ twine upload dist/chalice-*

    """
    pass


@cli.command('bump-version')
@click.option('--release-type', type=click.Choice(['minor', 'patch']))
def bump_version(release_type):
    """Update necessary files with next version number."""
    next_version_number = get_next_version_number(release_type)
    print("%s release, bumping version to: %s" % (release_type.capitalize(),
                                                  next_version_number))
    for filename, replacer in get_files_to_change().items():
        print("Bumping version in %s" % filename)
        with open(filename, 'r') as f:
            contents = f.read()
            if callable(replacer):
                new_contents = replacer(next_version_number, contents)
            else:
                new_contents = _regex_based_version_bump(
                    next_version_number,
                    replacer,
                    contents)
            with open(filename, 'w') as f:
                f.write(new_contents)


@cli.command('build-release')
def build_release():
    """Build sdist/whl files."""
    original = os.getcwd()
    os.chdir(ROOT_DIR)
    try:
        subprocess.check_call(
            ['python', 'setup.py', 'sdist', 'bdist_wheel']
        )
    finally:
        os.chdir(original)


@cli.command('tag-release')
def tag_release():
    """Create a git tag based on the current version number."""
    # We're assuming that setup.py has already been updated
    # manually or using scripts/release/bump-version so the
    # current version in setup.py is the version number we should tag.
    version_number = get_current_version_number()
    click.echo("Tagging %s release" % version_number)
    subprocess.check_call(
        ['git', 'tag', '-a', version_number,
         '-m', 'Tagging %s release' % version_number],
    )


@cli.command('get-version')
def get_version():
    """Print the current version number in setup.py."""
    click.echo(get_current_version_number())


def get_files_to_change():
    # A mapping of all files that require version bumps.
    # You can either specify:
    # * Tuple[str, str] - regex to search, replacement string
    # * Callable[[str, str], str] - function to handle custom logic
    files_with_version_numbers = {
        'chalice/app.py': ("__version__ = '.*'", "__version__ = '{version}'"),
        'CHANGELOG.rst': update_changelog,
        'docs/source/conf.py': update_doc_conf,
        'setup.py': ("version='(.*)'", "version='{version}'"),
    }
    return files_with_version_numbers


def _regex_based_version_bump(next_version_number, replacer, contents):
    regex = replacer[0]
    replacement = replacer[1].format(version=next_version_number)
    new_contents = re.sub(regex, replacement, contents)
    return new_contents


def update_changelog(next_version_number, contents):
    underline = '=' * len(next_version_number)
    lines = contents.splitlines()
    # Ensure the file ends with a newline.
    lines.append('')
    i = lines.index('Next Release (TBD)')
    lines[i:i+2] = [next_version_number, underline]
    return '\n'.join(lines)


def update_doc_conf(next_version_number, contents):
    # For the docs the 'version' is only X.Y
    # and the release is X.Y.Z
    version = '.'.join(next_version_number.split('.')[:2])
    release = next_version_number
    new_contents = []
    for line in contents.splitlines():
        if line.startswith('version ='):
            new_contents.append("version = u'%s'" % version)
        elif line.startswith('release = '):
            new_contents.append("release = u'%s'" % release)
        else:
            new_contents.append(line)
    # Ensure the file ends with a newline.
    new_contents.append('')
    return '\n'.join(new_contents)


def get_next_version_number(release_type):
    # Returns a string like '1.0.0'.
    current = get_current_version_number()
    # Convert to a list of ints: [1, 0, 0].
    version_parts = list(int(i) for i in current.split('.'))
    # We've already validated that release_type is from a fixed
    # list of choices so we know it's going to be one of these.
    # We only support integer version parts, which shouldn't be
    # a problem now that we're post 1.0.
    if release_type == 'patch':
        version_parts[-1] += 1
    elif release_type == 'minor':
        version_parts[1] += 1
        version_parts[-1] = 0
    return '.'.join(str(i) for i in version_parts)


def get_current_version_number():
    # We can avoid executing setup.py because we know
    # specifically how the version is hardcoded in the setup.py file.
    # This won't work for the general case.
    regex = re.compile("version='(.*)',")
    with open(os.path.join(ROOT_DIR, 'setup.py')) as f:
        for line in f:
            match = regex.search(line)
            if match is not None:
                return match.groups()[0]
    raise RuntimeError("Could not find version number from setup.py")


def main():
    return cli()


if __name__ == '__main__':
    main()
