# All Rights Reserved.
#
#    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.

"""Show the changes that will be included in the release.
"""

import argparse
import atexit
import json
import logging
import os
import os.path
import shutil
import subprocess
import sys
import tempfile

import requests

from openstack_releases import gitutils
from openstack_releases import yamlutils

LOG = logging.getLogger(__name__)


def git_log(workdir, repo, git_range, extra_args=[]):
    results = {
        'range': git_range,
        'logs': []
    }
    cmd = ['git', 'log', '--no-color']
    cmd.extend(extra_args)
    if isinstance(git_range, str):
        cmd.append(git_range)
    elif isinstance(git_range, tuple):
        cmd.append('{start}..{end}'.format(
            start=git_range[0], end=git_range[1]))
    else:
        cmd.extend(git_range)
    LOG.debug('\n' + ' '.join(cmd) + '\n')
    output = subprocess.check_output(cmd, cwd=os.path.join(workdir, repo))
    results['logs'] = [el for el in output.decode('utf-8').split("\n") if el]
    LOG.debug(results)
    return results


def filter_results(content,
                   ignore_no_results=False,
                   ignore_errors=False,
                   ignore_not_yet_released=False,
                   ignore_all=False):
    filtered = []
    for repo in content:
        removed = False
        if ignore_all and \
            (repo['error'] or
             repo['not_yet_released'] or
             not repo['commits'] or
             not repo['commits'].get('logs', None)):
            removed = True
            LOG.debug("ignoring all")
        elif ignore_errors and repo['error']:
            removed = True
            LOG.debug("ignoring errors")
        elif ignore_not_yet_released and repo['not_yet_released']:
            removed = True
            LOG.debug("ignoring not yet released")
        elif ignore_no_results and (not repo.get('commits', None) or
                                    not repo.get('commits', None).get(
                                        'logs', None)):
            LOG.debug("ignoring no results")
            removed = True
        if not removed:
            filtered.append(repo)
        else:
            LOG.debug('repo {} will be ignored'.format(repo['repo']))
            LOG.debug(repo)
    return filtered


def generate_output(content, output_format='std',
                    ignore_no_results=False,
                    ignore_errors=False,
                    ignore_not_yet_released=False,
                    ignore_all=False):
    filtered = filter_results(content, ignore_no_results, ignore_errors,
                              ignore_not_yet_released, ignore_all)
    if output_format == 'json':
        return json.dumps(filtered, indent=4)
    if output_format == 'yaml':
        return yamlutils.dumps(filtered)
    if output_format == 'std':
        out = []
        for repo in filtered:
            out.append(
                '\033[1m\033[91m[ Unreleased changes in '
                '{rep} ({br}) ]\033[0m'.format(rep=repo['repo'],
                                               br=repo['branch'])
            )

            if repo['not_yet_released'] or repo['error']:
                out.append(repo['msg'])
                continue

            range_msg = 'Changes between {start} and {end}'.format(
                start=repo['commits']['range'][0],
                end=repo['commits']['range'][1])
            out.append(range_msg)
            if repo['commits'].get('logs', None):
                out.append("\n".join(repo['commits']['logs']))
            out.append('')
        return '\n'.join(out)


def main():
    if not sys.stdout.encoding:
        # Wrap sys.stdout with a writer that knows how to handle
        # encoding Unicode data.
        import codecs
        wrapped_stdout = codecs.getwriter('UTF-8')(sys.stdout)
        sys.stdout = wrapped_stdout

    parser = argparse.ArgumentParser()
    parser.add_argument(
        '--no-cleanup',
        dest='cleanup',
        default=True,
        action='store_false',
        help='do not remove temporary files',
    )
    parser.add_argument(
        "-v",
        "--verbosity",
        action="count",
        help="increase output verbosity",
        default=0)
    parser.add_argument(
        "--ignore-no-results",
        action='store_true',
        default=False,
        help="Ignore projects without difference between the HEAD and "
             "the retrieved previous tag. "
             "They will be ignored in the command output.")
    parser.add_argument(
        "--ignore-errors",
        action='store_true',
        default=False,
        help="Ignore projects in error (repos not found).")
    parser.add_argument(
        "--ignore-not-yet-released",
        action='store_true',
        default=False,
        help="Ignore projects not yet released (previous tag not found).")
    parser.add_argument(
        "-I",
        "--ignore-all",
        action='store_true',
        default=False,
        help="Ignore projects without difference between the HEAD and "
             "previous tag, projects not yet released, projects in error. "
             "Similar to call command with "
             "`--ignore-no-results --ignore-errors --ignore-not-yet-released`"
             " They will be ignored in the command output.")
    parser.add_argument(
        "-f",
        "--format",
        choices=['std', 'json', 'yaml'],
        default='std',
        help="Output format")
    parser.add_argument(
        'branch',
        help=('Branch to analyze'),
    )
    parser.add_argument(
        'repos',
        nargs='*',
        help=('Repos to analyze, '
              'repo should be e.g. openstack/glance'),
    )
    args = parser.parse_args()

    log_level = logging.ERROR
    if args.verbosity >= 3:
        log_level = logging.DEBUG
    elif args.verbosity >= 2:
        log_level = logging.INFO
    elif args.verbosity >= 1:
        log_level = logging.WARNING

    # Set up logging, including making some loggers quiet.
    logging.basicConfig(
        format='%(levelname)7s: %(message)s',
        stream=sys.stdout,
        level=log_level,
    )
    logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)

    workdir = tempfile.mkdtemp(prefix='releases-')
    LOG.debug('creating temporary files in %s' % workdir)

    def cleanup_workdir():
        if args.cleanup:
            shutil.rmtree(workdir, True)
        else:
            LOG.info('not cleaning up %s' % workdir)
    atexit.register(cleanup_workdir)

    # Remove any inherited PAGER environment variable to avoid
    # blocking the output waiting for input.
    os.environ['PAGER'] = ''
    output = []

    for repo in args.repos:
        repo = f'openstack/{repo}' if 'openstack/' not in repo else repo
        current = {
            'repo': repo,
            'branch': args.branch,
            'commits': None,
            'error': False,
            'not_yet_released': False,
            'msg': ''
        }
        url = 'https://opendev.org/{}'.format(repo)
        res = requests.get(url)
        if res.status_code == 404:
            current.update({'error': True})
            current.update(
                {'msg': "fatal: repository '{}' not found".format(url)})
            output.append(current)
            continue

        # Start by checking out master, always. We need the repo
        # checked out before we can tell if the stable branch
        # really exists.
        gitutils.clone_repo(
            workdir,
            repo,
            branch='master',
        )

        # Set some git configuration values to allow us to perform
        # local operations like tagging.
        gitutils.ensure_basic_git_config(
            workdir, repo,
            {'user.email': 'openstack-infra@lists.openstack.org',
             'user.name': 'OpenStack Proposal Bot'},
        )

        # Determine which branch we should actually be looking
        # at. Assume any series for which there is no stable
        # branch will be on 'master'.
        branch = args.branch.replace('stable/', '')
        if gitutils.stable_branch_exists(workdir, repo, branch):
            branch = 'stable/' + branch
        else:
            branch = 'master'

        if branch != 'master':
            # Check out the repo again to the right branch if we
            # didn't get it the first time.
            gitutils.clone_repo(
                workdir,
                repo,
                branch=branch,
            )

        # look at the previous tag for the parent of the commit
        # getting the new release
        previous_tag = gitutils.get_latest_tag(workdir, repo, always=False)

        if not previous_tag:
            current.update({'not_yet_released': True})
            current.update(
                {'msg': '{} has not yet been released'.format(repo)})
            output.append(current)
            continue

        start_range = previous_tag
        head_sha = gitutils.get_head(workdir, repo)

        if not start_range:
            current.update({'not_yet_released': True})
            current.update(
                {'msg': '{} has not yet been released'.format(repo)})
            output.append(current)
            continue

        commits = git_log(workdir, repo, (start_range, head_sha),
                          extra_args=[
                              '--no-color',
                              '--no-merges',
                              '--graph',
                              '--format=%h %ci %s'])

        current.update({'commits': commits})
        output.append(current)

    LOG.debug(output)

    out = generate_output(output, output_format=args.format,
                          ignore_no_results=args.ignore_no_results,
                          ignore_errors=args.ignore_errors,
                          ignore_not_yet_released=args.ignore_not_yet_released,
                          ignore_all=args.ignore_all)
    print("".join(out))
    return 0