#!/usr/bin/env python
# coding=utf-8

# Copyright [2019] [Apache Software Foundation]
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import print_function

import os
import pip
from distutils.version import LooseVersion
import sys
import subprocess
import click
import re
import os.path
import errno
import shutil
from .._compatibility import urlparse
import multiprocessing

__all__ = ['copy']


@click.group('pkg')
def cli():
    pass


@cli.command('pkg-showversion', help='Show the package version.')
@click.pass_context
def version(ctx):
    print(get_version(ctx.obj['package_path']))


@cli.command('pkg-showchanges', help='Show the package changelog.')
@click.pass_context
def log(ctx):
    os.system('less {}'.format(os.path.join(ctx.obj['base_path'], 'CHANGES.md')))


@cli.command('pkg-showinfo', help='Show information about the package.')
@click.pass_context
def info(ctx):
    version = get_version(ctx.obj['package_path'])
    repo = get_git_repository_url(ctx.obj['base_path'])
    branch = get_git_branch(ctx.obj['base_path'])
    commit = get_git_commit(ctx.obj['base_path'])
    tag = get_git_tag(ctx.obj['base_path'])
    tag_commit = get_git_commit(ctx.obj['base_path'], tag=tag)
    # tags = utils.get_git_tags(ctx.obj['base_path'])
    tagged = 'yes' if (tag[1:] == version) else 'no'
    clean = is_git_clean(ctx.obj['base_path'])
    status = 'clean' if clean else 'dirty'
    updated = ('' if (commit == tag_commit and clean) else
               '(dev)' if (not tag[1:] == version) else
               '(should be bumped)')
    pip = 'git+ssh://{repo}@{tag}#egg={pkg}'.format(
        repo=repo[:-4].replace(':', '/'), tag=tag,
        pkg=ctx.obj['package_name'])

    print('')
    print('package: {name}'.format(name=ctx.obj['package_name']))
    print('type:    {type_}'.format(type_=(ctx.obj['type'] or 'unknown')))
    print('version: {version} {updated}'.format(version=version,
                                                updated=updated))
    print('')
    print('branch:  {branch}'.format(branch=branch))
    print('status:  {status}'.format(status=status))
    print('commit:  {commit}'.format(commit=commit))
    print('repo:    {repo}'.format(repo=repo))
    print('')
    print('tagged:  {tagged}'.format(tagged=tagged))
    print('current: {tag} ({tag_commit})'.format(tag=tag,
                                                 tag_commit=tag_commit))
    print('pip url: {pip}'.format(pip=pip))
    print('')


@cli.command('pkg-updatedeps', help='Show information about the package.')
@click.pass_context
def deps(ctx):
    repos = get_repos_from_requirements(ctx.obj['base_path'])
    required_versions = get_tag_from_repo_url(repos)
    latest_versions = get_latest_tags_from_repos(repos)
    installed_pkgs = pip.get_installed_distributions()
    click.echo('')
    for repo in repos:
        status = 'outdated'
        required = required_versions[repo]
        latest = latest_versions[repo]
        try:
            repo_small = repo.split('@')[1]
            pkg_name = repo.split('egg=')[1]
        except IndexError:
            continue
        pkg_name_normalized = pkg_name.lower().replace('_', '-')
        installed = 'unknown'
        installed_list = [
            pkg.version
            for pkg in installed_pkgs
            if pkg.key in [pkg_name_normalized, pkg_name_normalized + '-lib']
        ]
        if installed_list:
            installed = 'v{}'.format(installed_list[0])

        if latest is None or installed is None:
            continue

        if LooseVersion(installed) > LooseVersion(latest):
            status = 'develop'
        elif LooseVersion(installed) < LooseVersion(required):
            status = 'up-to-date (old version installed)'
        elif required == latest:
            status = 'up-to-date'
        msg = '{pkg_name}: {status} (required: {required}, installed: {installed}, latest: {latest})'.format(
            repo=repo_small, pkg_name=pkg_name_normalized, status=status, required=required, installed=installed, latest=latest)
        if status == 'up-to-date' or (status == 'develop' and installed == required):
            color = 'green'
        elif status in ('develop', 'up-to-date (old version installed)') or installed == latest:
            color = 'yellow'
        else:
            color = 'red'
        click.echo(click.style(msg, fg=color))


@cli.command('pkg-bumpversion', help='Bump the package version.')
@click.argument('part', default='patch')
@click.option('--allow-dirty', is_flag=True,
              help='Allow dirty')
@click.option('--force', '-f', is_flag=True,
              help='Alias for --allow-dirty')
@click.option('--yes', '-y', is_flag=True,
              help='Answer yes to all prompts')
@click.pass_context
def bumpversion(ctx, part, allow_dirty, force, yes):
    args = [part]
    allow_dirty = allow_dirty or force

    is_clean = is_git_clean(ctx.obj['base_path'])
    if not is_clean and not allow_dirty:
        print('')
        print('ERROR: Git working directory is not clean.')
        print('')
        print('You can use --allow-dirty or --force if you know what '
              'you\'re doing.')
        exitcode = 1
    else:
        if allow_dirty:
            args.append('--allow-dirty')
        command = ['bumpversion'] + args

        old_version = get_version(ctx.obj['package_path'])
        exitcode = subprocess.call(command, cwd=ctx.obj['base_path'])
        new_version = get_version(ctx.obj['package_path'])

        if exitcode == 0:
            print('Bump version from {old} to {new}'.format(
                old=old_version, new=new_version))
        if yes or click.confirm('Do you want to edit CHANGES.md?'):
            click.edit(filename=os.path.join(ctx.obj['base_path'], 'CHANGES.md'))
    sys.exit(exitcode)


@cli.command('pkg-createtag', help='Create git tag using the package version.')
@click.pass_context
def tag(ctx):
    tag = 'v{}'.format(get_version(ctx.obj['package_path']))
    print('Creating git tag {}'.format(tag))
    command = ['git', 'tag', '-m', '"version {}"'.format(tag), tag]
    sys.exit(subprocess.call(command))


@cli.command('pkg-updatedeps', help='Update requirements.txt.')
@click.option('--install', '-i', is_flag=True)
@click.option('--install-all', '-a', is_flag=True)
@click.pass_context
def update(ctx, install, install_all):
    base_path = ctx.obj['base_path']
    repos = get_repos_from_requirements(base_path)
    required_versions = get_tag_from_repo_url(repos)
    latest_versions = get_latest_tags_from_repos(repos)
    installed_pkgs = pip.get_installed_distributions()
    install_list = ['-e .']
    click.echo('')
    for repo in repos:
        latest = latest_versions[repo]
        required = required_versions[repo]
        try:
            pkg_name = repo.split('egg=')[1]
        except IndexError:
            continue
        pkg_name_normalized = pkg_name.lower().replace('_', '-')
        installed = 'unknown'
        installed_list = [
            pkg.version
            for pkg in installed_pkgs
            if pkg.key in [pkg_name_normalized, pkg_name_normalized + '-lib']
        ]
        if installed_list:
            installed = 'v{}'.format(installed_list[0])

        if LooseVersion(required) < LooseVersion(latest):
            click.echo('Updating {} from {} to {}...'.format(pkg_name, required, latest))
            new_repo = update_repo_tag(repo, latest, path=base_path)
            if LooseVersion(installed) < LooseVersion(latest):
                install_list.append(new_repo)
        elif LooseVersion(installed) < LooseVersion(required):
            install_list.append(repo)
    if install_all:
        install = True
        install_list = ['-r requirements.txt']
    if install:
        for new_repo in install_list:
            new_repo = new_repo.strip()
            click.echo('')
            click.echo('Running `pip install -U {}` ...'.format(new_repo))
            command = ['pip', 'install', '-U'] + new_repo.split(' ')
            exitcode = subprocess.call(command, cwd=base_path)
            if exitcode == 0:
                click.echo('Done.')
            else:
                click.echo('Failed.')
                sys.exit(exitcode)


def copy(src, dest, ignore=('.git', '.pyc', '__pycache__')):
    try:
        shutil.copytree(src, dest, ignore=shutil.ignore_patterns(*ignore))
    except OSError as e:
        if e.errno == errno.ENOTDIR:
            shutil.copy(src, dest)
        else:
            print('Directory not copied. Error: %s' % e)


def get_version(path):
    """Return the project version from VERSION file."""

    with open(os.path.join(path, 'VERSION'), 'rb') as f:
        version = f.read().decode('ascii').strip()
    return version.strip()


def get_repos_from_requirements(path):
    if path is None:
        path = os.path.curdir
    with open(os.path.join(path, 'requirements.txt'), 'r') as fp:
        repos = [line.strip() for line in fp if 'git@' in line and not line.strip().startswith('#')]
    return repos


def get_tag_from_repo_url(repos):
    tags = {}
    for repo in repos:
        if '@' in repo:
            repo_parsed = urlparse(repo)
            repo_path = repo_parsed.path
            tags[repo] = repo_path.split('@')[1]
        else:
            tags[repo] = None
    return tags


def _clone(repo):
    return repo, git_clone(repo, checkout=False, depth=1)


def get_latest_tags_from_repos(repos):
    tags = {}
    if not repos:
        return tags

    pool = multiprocessing.Pool(len(repos))

    repos_ = pool.map(_clone, repos)
    for repo, path in repos_:
        if path:
            tag = get_git_tag(path)
        else:
            tag = None
        tags[repo] = tag
    return tags


def update_repo_tag(repo, tag, path=None):
    if path is None:
        path = os.path.curdir
    ret = ''
    content = ''
    with open(os.path.join(path, 'requirements.txt'), 'r') as fp:
        for line in fp:
            if repo in line:
                line = re.sub(r'@v[0-9]+\.[0-9]+\.[0-9]+', '@{}'.format(tag), line)
                ret += line
            content += line

    with open(os.path.join(path, 'requirements.txt'), 'w') as fp:
        fp.write(content)

    return ret


repo_re = re.compile(r':(\w+)\/(.*)\.git')


def git_clone(repo, dest=None, checkout=True, depth=None, branch=None, single_branch=False):
    if '#egg' in repo:
        repo_parsed = urlparse(repo)
        repo_path = repo_parsed.path
        if '@' in repo_path:
            repo_path = repo_path.split('@')[0]
        repo_path = repo_path.strip('/')
        repo_team, repo_name = tuple(repo_path.split('/'))
        repo = repo_parsed.netloc + ':' + repo_path
    else:
        repo_info = re.search(repo_re, repo)
        if not repo_info:
            return None
        repo_team = repo_info.group(1)
        repo_name = repo_info.group(2)
    if dest is None:
        path = os.path.join(os.path.expanduser('~'), '.marvin-python-toolbox', 'repos')
        dest = os.path.join(path, repo_team, repo_name)

    opts = ''
    if not checkout:
        opts += ' -n'
    if depth:
        opts += ' --depth ' + str(depth)
    if branch:
        opts += ' --branch ' + branch
    if single_branch:
        opts += ' --single-branch'

    if not os.path.exists(dest):
        os.makedirs(dest)
        command = 'git clone {opts} {repo} {dest}'.format(
            opts=opts, repo=repo, dest=dest)
        print(command)
        subprocess.Popen(command.split(), stdout=subprocess.PIPE).wait()

    opts = ''
    if depth:
        opts += ' --depth ' + str(depth)
    print('Fetching latest version from {} repository'.format(repo_name))
    try:
        subprocess.Popen(('git fetch --tags ' + opts).split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dest).wait()
    except OSError:
        print('Could not fetch tags from {}'.format(repo_name))
        dest = None

    return dest


def get_git_branch(path=None):
    if path is None:
        path = os.path.curdir
    command = 'git rev-parse --abbrev-ref HEAD'.split()
    branch = subprocess.Popen(command, stdout=subprocess.PIPE, cwd=path).stdout.read()
    return branch.strip().decode('utf-8')


def get_git_tag(path=None):
    if path is None:
        path = os.path.curdir
    command = 'git rev-list --tags --max-count=1'.split()
    commit = subprocess.Popen(command, stdout=subprocess.PIPE, cwd=path).stdout.read().decode('utf-8')
    command = 'git describe --tags {}'.format(commit).split()
    tag = subprocess.Popen(command, stdout=subprocess.PIPE, cwd=path).stdout.read().decode('utf-8')
    return tag.strip()


def get_git_commit(path=None, tag=None):
    if path is None:
        path = os.path.curdir
    if tag:
        command = 'git rev-list -n 1 {tag}'.format(tag=tag).split()
    else:
        command = 'git rev-parse HEAD'.split()
    commit = subprocess.Popen(command, stdout=subprocess.PIPE, cwd=path).stdout.read()
    return commit.strip().decode('utf-8')


def get_git_repository_url(path=None):
    if path is None:
        path = os.path.curdir
    command = 'git config --get remote.origin.url'.split()
    url = subprocess.Popen(command, stdout=subprocess.PIPE, cwd=path).stdout.read()
    return url.strip().decode('utf-8')


def get_git_tags(path=None):
    if path is None:
        path = os.path.curdir
    command = 'git tag'.split()
    tags = subprocess.Popen(command, stdout=subprocess.PIPE, cwd=path).stdout.read()
    return sorted(tags.strip().split('\n'), reverse=True)


def is_git_clean(path=None):
    if path is None:
        path = os.path.curdir
    command = 'git diff --quiet HEAD'.split()
    exit_code = subprocess.Popen(command, stdout=subprocess.PIPE, cwd=path).stdout.read()
    return exit_code
