#!/usr/bin/env python3
# encoding: utf-8
"""
release.py

Created by Thomas Mangin on 2011-01-24.
Copyright (c) 2009-2017 Exa Networks. All rights reserved.
"""

import os
import sys
import fileinput

import glob
import shutil
import zipapp
import argparse


CHANGELOG = 'doc/CHANGELOG.rst'


class Path:
    root = os.path.join(os.getcwd(), os.path.dirname(sys.argv[0]))
    changelog = os.path.join(root, CHANGELOG)
    lib_exa = os.path.join(root, 'src/exabgp')
    version = os.path.join(root, 'src/exabgp/version.py')
    pyproject = os.path.join(root, 'pyproject.toml')
    sbin = os.path.join(root, 'sbin/exabgp')
    debian = os.path.join(root, 'debian/changelog')
    egg = os.path.join(root, 'src/exabgp.egg-info')
    build_exabgp = os.path.join(root, 'build/src/exabgp')
    build_root = os.path.join(root, 'build')

    @classmethod
    def remove_egg(cls, *args):
        from shutil import rmtree

        print('removing left-over egg')
        if os.path.exists(cls.egg):
            rmtree(cls.egg)
        if os.path.exists(cls.build_exabgp):
            rmtree(cls.build_root)
        return 0


class Version:
    JSON = '5.0.0'
    TEXT = '5.0.0'

    template = """\
import os
import sys


def get_zipapp():
    return os.path.abspath(os.path.sep.join(__file__.split(os.path.sep)[:-2]))


def get_root():
    if os.path.isfile(get_zipapp()):
        return get_zipapp()
    return os.path.abspath(os.path.sep.join(__file__.split(os.path.sep)[:-1]))


commit = '%s'
release = '%s'
json = '%s'
text = '%s'
version = os.environ.get('exabgp_version', release)

# Do not change the first line as it is parsed by scripts

if sys.version_info.major < 3:
    sys.exit('exabgp requires python3.6 or later')
if sys.version_info.major == 3 and sys.version_info.minor < 6:
    sys.exit('exabgp requires python3.6 or later')

if __name__ == '__main__':
    sys.stdout.write(version)
"""

    @staticmethod
    def current():
        sys.path.append(Path.lib_exa)
        from version import version as release

        # transitional fix
        if '-' in release:
            release = release.split('-')[0]

        return release

    @staticmethod
    def changelog():
        with open(Path.changelog) as f:
            f.readline()
            for line in f:
                if line.lower().startswith('version '):
                    return line.split()[1].rstrip().rstrip(':').strip()
        return ''

    @staticmethod
    def set(tag, commit):
        if Command.dryrun:
            print('  [dry-run] would write src/exabgp/version.py')
            return True
        with open(Path.version, 'w') as f:
            f.write(Version.template % (commit, tag, Version.JSON, Version.TEXT))
        return True

    @staticmethod
    def latest(tags):
        valid = [[int(_) for _ in tag.split('.')] for tag in tags if Version.valid(tag)]
        return '.'.join(str(_) for _ in sorted(valid)[-1])

    @staticmethod
    def valid(tag):
        parts = tag.split('.')
        return len(parts) == 3 and parts[0].isdigit() and parts[1].isdigit() and parts[2].isdigit()

    @staticmethod
    def candidates(tag):
        latest = [int(_) for _ in tag.split('.')]
        return [
            '.'.join([str(_) for _ in (latest[0], latest[1], latest[2] + 1)]),
            '.'.join([str(_) for _ in (latest[0], latest[1] + 1, 0)]),
            '.'.join([str(_) for _ in (latest[0] + 1, 0, 0)]),
        ]


class Debian:
    template = """\
exabgp (%s-0) unstable; urgency=low

  * Latest ExaBGP release.

 -- Vincent Bernat <bernat@debian.org>  %s

"""

    @staticmethod
    def set(version):
        if Command.dryrun:
            print('  [dry-run] would write debian/changelog')
            return
        from email.utils import formatdate

        with open(Path.debian, 'w') as w:
            w.write(Debian.template % (version, formatdate()))
        print('updated debian/changelog')


class Command:
    dryrun = 'dry-run' if os.environ.get('DRY', os.environ.get('DRYRUN', os.environ.get('DRY_RUN', False))) else ''

    @staticmethod
    def run(cmd):
        print('>', cmd)
        return Git.dryrun or os.system(cmd)  # noqa: S605 - release tool, not user input


class Git(Command):
    @staticmethod
    def commit(comment):
        return Git.run('git commit -a -m "%s"' % comment)

    @staticmethod
    def push(tag='', repo=''):
        command = 'git push'
        if repo:
            command += ' %s' % repo
        if tag:
            command += ' %s' % tag
        return Git.run(command)

    @staticmethod
    def head_commit():
        return os.popen('git rev-parse --short HEAD').read().strip()

    @staticmethod
    def tags():
        return os.popen('git tag').read().strip().split('\n')

    @staticmethod
    def tag(release):
        return Git.run('git tag -s -a %s -m "release %s"' % (release, release))

    @staticmethod
    def pending():
        allowed = (
            'src/exabgp/version.py',
            # 'debian/changelog',
            'README.md',
            'doc/README.rst',
            'pyproject.toml',
            'sbin/exabgp',
            'uv.lock',
        )
        commit = None
        for line in os.popen('git status --porcelain').read().split('\n'):
            if not line.strip():
                continue
            status = line[:2]
            filepath = line[3:].strip()
            if status.strip() in ('M', 'MM', 'AM'):
                if any(filepath.endswith(a) for a in allowed):
                    if commit is not False:
                        commit = True
                else:
                    return False
            elif status.strip() in ('R', 'RM'):
                return False
        return commit

    @staticmethod
    def has_remote(name):
        return name in os.popen('git remote').read().strip().split('\n')


#
# Check that that there is no version inconsistancy before any pypi action
#


def update_file_version(filepath, current, release):
    """Replace current version with release version in a file"""
    if Command.dryrun:
        print('  [dry-run] would update %s' % filepath)
        return
    with fileinput.FileInput(filepath, inplace=True) as replacer:
        for line in replacer:
            print(line.replace(current, release), end='')


def release_github(args):
    print()
    print('updating Github')

    current = Version.current()
    print('current version is %s (from version.py)' % current)

    release = Version.changelog()
    print('release version is %s (from %s)' % (release, CHANGELOG))

    if not Version.valid(release):
        print('invalid new version in %s' % CHANGELOG)
        return 1

    tags = Git.tags()
    if release in tags:
        print(f'version {release} was already released/used')
        return 1

    candidates = Version.candidates(Version.latest(tags))
    if release not in candidates:
        print('valid versions are:', ', '.join(candidates))
        print('this release is not one of the candidates, aborting')
        return 1

    print('updating src/exabgp/version.py')
    Version.set(release, Git.head_commit())
    # print('updating debian/changelog')
    # Debian.set(release)

    print('updating ', end='')
    for path in ('README.md', 'doc/README.rst', 'pyproject.toml', 'sbin/exabgp'):
        print(path + ' ', end='')
        update_file_version(path, current, release)
    print()

    print('updating uv.lock')
    if not Command.dryrun:
        import subprocess

        subprocess.run(['uv', 'lock'], stderr=subprocess.DEVNULL)

    print('checking if we need to commit a version.py change')
    status = Git.pending()
    if status is None:
        print('all is already set for release')
    elif status is False:
        print('more than one file is modified and need updating, aborting')
        return 1
    else:
        if Git.commit('updating version to %s' % release):
            print('could not commit version change (%s)' % release)
            return 1
        print('version was updated')

    print('tagging the new version')
    if Git.tag(release):
        print('could not tag version (%s)' % release)
        return 1

    print('pushing the new tag')
    if Git.push(tag=release, repo='origin'):
        print('could not push release tag to origin')
        return 1

    if Git.push(repo='origin'):
        print('could not push release version to origin')
        return 1

    if Git.has_remote('upstream'):
        if Git.push(tag=release, repo='upstream'):
            print('could not push release tag to upstream')
            return 1

        if Git.push(repo='upstream'):
            print('could not push release version to upstream')
            return 1
    else:
        print('no upstream remote, skipping')

    return 0


def release_pypi(args):
    test = args.test

    print()
    print('updating PyPI')

    Path.remove_egg()

    if Command.run('python3 setup.py sdist bdist_wheel'):
        print('could not generate egg')
        return 1

    # keyring used to save credential
    # https://pypi.org/project/twine/

    release = Version.latest(Git.tags())

    server = ''
    if test:
        server = '--repository-url https://test.pypi.org/legacy/'

    if Command.run('twine upload %s dist/exabgp-%s.tar.gz dist/exabgp-%s-py3-none-any.whl' % (server, release, release)):
        print('could not upload with twine')
        return 1

    print('all done.')
    return 0


def remove_cache():
    for name in glob.glob('./src/*/__pycache__'):
        shutil.rmtree(name)
    for name in glob.glob('./src/*/*/__pycache__'):
        shutil.rmtree(name)

    for name in glob.glob('./src/*/*.pyc'):
        os.remove(name)
    for name in glob.glob('./src/*/*/*.pyc'):
        os.remove(name)


def generate_binary(args):
    target = args.target
    debug = args.debug

    if sys.version_info.minor < 5:
        sys.exit('zipapp is only available with python 3.5+')

    FOLDER = os.path.dirname(os.path.realpath(__file__))
    IMPORT = os.path.abspath(os.path.join(FOLDER, 'src'))

    if not os.path.exists(IMPORT):
        sys.exit(f'could not import "{IMPORT}"')
    sys.path = [IMPORT]

    params = {
        'source': 'src',
        'target': target,
        'interpreter': '/usr/bin/env python3',
        'main': 'exabgp.application.main:main',
    }

    if debug:
        params['interpreter'] = (
            '/usr/bin/env EXABGP_DEBUG_PDB=1 EXABGP_DEBUG_CONFIGURATION=1 EXABGP_DEBUG_ROUTE=1 python3'
        )
    if sys.version_info.minor >= 7:
        params['compressed'] = not debug

    # here = os.path.dirname(os.path.realpath(__file__))
    # data = os.path.abspath(os.path.join(here, 'data'))

    zipapp.create_archive(**params)


def show_version(args):
    if args.version == 'current':
        print(Version.current())
    if args.version == 'release':
        print(Version.changelog())


def main():
    # To late for it .. but here as a reminder that it can be done
    os.environ['PYTHONDONTWRITEBYTECODE'] = ''

    if os.environ.get('SCRUTINIZER', '') == 'true':
        sys.exit(0)

    if len(sys.argv) > 1 and sys.argv[1] == 'debian':
        release = Version.changelog()
        Debian.set(release)
        sys.exit(0)

    parser = argparse.ArgumentParser(description='exabgp release tool')
    root = parser.add_subparsers()

    binary = root.add_parser('binary', help='release an exabgp binary')
    binary.add_argument('target', help='name of the binary to create', default='./vyos')
    binary.add_argument('-d', '--debug', help='run python with pdb', action='store_true')
    binary.set_defaults(func=generate_binary)

    pypi = root.add_parser('pypi', help='create egg/wheel')
    pypi.add_argument('-t', '--test', help='only test', action='store_true')
    pypi.set_defaults(func=release_pypi)

    github = root.add_parser('github', help='tag a new version on github, and update pypi')
    github.set_defaults(func=release_github)

    cleanup = root.add_parser('cleanup', help='delete left-over file from release')
    cleanup.set_defaults(func=Path.remove_egg)

    show = root.add_parser('show', help='show exabgp version')
    show.add_argument('version', help='which version', choices=['current', 'release'])
    show.set_defaults(func=show_version)

    args = parser.parse_args()
    if 'func' not in dir(args):
        sys.exit(1)
    args.func(args)
    sys.exit(0)


if __name__ == '__main__':
    main()
