import logging
import os
import subprocess
import sys

import pygit2

import gitubuntu.git_repository
import gitubuntu.spec as spec


class MultipleParentError(ValueError): pass
class BaseNotFoundError(RuntimeError): pass


def generate_single_parents(upload_tag):
    """Yield each successive parent of the upload tag provided

    But only as long as each parent is alone. As soon as a commit with multiple
    parents is spotted, an exception is raised.

    :param pygit2.Reference upload_tag: the upload tag from which to find
        parents.
    :raises MultipleParentError: if multiple parents for a commit are found.

    Yields a series of pygit2.Commit objects.
    """
    commit = upload_tag.peel(pygit2.Commit)
    while True:
        parents = commit.parents
        if not parents:
            return
        if len(parents) > 1:
            raise MultipleParentError()
        commit = parents[0]
        yield commit


def get_upload_tag_base(repo, upload_tag, commit_map):
    """Find the base import tag of an upload tag

    The base import tag is defined as the first import tag found as the upload
    tag's parents are successively examined.

    :param GitUbuntuRepository repo: the git repository
    :param pygit2.Reference upload_tag: the upload tag to examine
    :param dict(pygit2.Oid, pygit2.Reference): a mapping of commit hashes to
        their import tags, for all import tags that exist.
    :returns: the base import tag for the given upload tag
    :rtype: pygit2.Reference
    :raises BaseNotFoundError: if a base import tag cannot be found
    :raises MultipleParentError: if multiple parents for a commit are found
    """
    for parent in generate_single_parents(upload_tag):
        try:
            return commit_map[parent.id]
        except KeyError:
            pass
    raise BaseNotFoundError()


def export_upload_tag(repo, path, upload_tag, commit_map):
    """Export a single upload tag for later reconstruction

    If successful, two files will be created in the output directory: one with
    extension '.base', and one with extension '.pick'. The base name of these
    files is the version string from the upload tag. The '.base' file will
    contain a single LF-terminated string which is the version string from the
    import tag on which the upload tag patchset is based. The '.pick' file will
    contain the patchset described as a series of commit hash strings that can
    be picked in order, one on each line, as described by 'git rev-list
    --reverse'.

    Note that the upload and import tag names are already escaped according to
    dep14, so the output filenames will have their version parts escaped
    likewise.

    :param GitUbuntuRepository repo: the git repository.
    :param str path: the directory to export to.
    :param pygit2.Reference upload_tag: the upload tag to export.
    :param dict(pygit2.Oid, pygit2.Reference): a mapping of commit hashes to
        their import tags, for all import tags that exist.
    :raises BaseNotFoundError: if a base import tag cannot be found.
    :raises MultipleParentError: if multiple parents for a commit are found.
    :raises subprocess.CalledProcessError: if the underlying call to "git
        rev-list" fails.
    :returns: None.
    """
    import_tag = get_upload_tag_base(repo, upload_tag, commit_map)
    import_name = import_tag.name.split('/')[-1]
    export_name = upload_tag.name.split('/')[-1]

    with open(os.path.join(path, export_name) + '.base', 'w') as f:
        print(import_name, file=f)

    env = gitubuntu.git_repository._derive_git_cli_env(
        pygit2_repo=repo.raw_repo,
        initial_env=repo._initial_env,
        update_env=repo.env,
    )
    with open(os.path.join(path, export_name) + '.pick', 'w') as f:
        subprocess.check_call(
            [
                'git',
                'rev-list',
                '--reverse',
                '%s..%s' % (import_tag.name, upload_tag.name),
            ],
            env=env,
            stdout=f,
        )


def export_all(repo, path, namespace='importer'):
    """Export all upload tags for later reconstruction

    See the docstring of export_upload_tag() for the output format.

    :param GitUbuntuRepository repo: the git repository.
    :param str path: the directory to export to.
    :param str namespace: the namespace under which the import and upload tags
        can be found.
    :raises BaseNotFoundError: if a base import tag cannot be found.
    :raises MultipleParentError: if multiple parents for a commit are found.
    :returns: None.
    """
    import_tag_prefix = 'refs/tags/%s/import/' % namespace
    upload_tag_prefix = 'refs/tags/%s/upload/' % namespace

    commit_map = {}
    for ref in repo.references:
        if ref.name.startswith(import_tag_prefix):
            commit_map[ref.peel(pygit2.Commit).id] = ref

    for ref in repo.references:
        # only upload tags
        if not ref.name.startswith(upload_tag_prefix):
            continue

        try:
            export_upload_tag(repo, path, ref, commit_map)
        except MultipleParentError:
            logging.warning("Multiple parents exporting %s" % ref.name)
        except BaseNotFoundError:
            logging.warning("No base found exporting %s" % ref.name)


def import_single(repo, path, version, namespace='importer'):
    """Import rich history, creating a corresponding upload tag

    :param GitUbuntuRepository repo: the git repository.
    :param str path: the directory to import from.
    :param str version: the package version for which rich history should be
        imported.
    :param str namespace: the namespace under which the import and upload tags
        can be found.
    :raises FileNotFoundError: if rich history cannot be found for this
        version.
    :raises BaseNotFoundError: if a base import tag cannot be found.
    :raises subprocess.CalledProcessError: if any of the underlying calls to
        "git cherry-pick" or "git tag" fail.
    :returns: None.
    """
    version_name = gitubuntu.git_repository.git_dep14_tag(version)
    with open(os.path.join(path, version_name) + '.base', 'r') as f:
        base_version = f.read().strip()
    import_tag_name = 'refs/tags/%s/import/%s' % (namespace, base_version)

    # Ensure that the base import tag exists
    try:
        repo.raw_repo.lookup_reference(import_tag_name)
    except KeyError as e:
        raise BaseNotFoundError() from e

    with open(os.path.join(path, version_name) + '.pick', 'r') as pick:
        with repo.temporary_worktree(import_tag_name, 'rich-history-import'):
            env = {
                'GIT_COMMITTER_NAME': spec.SYNTHESIZED_COMMITTER_NAME,
                'GIT_COMMITTER_EMAIL': spec.SYNTHESIZED_COMMITTER_EMAIL,
            }
            for commit_string in pick:
                subprocess.check_call(
                    [
                        'git',
                        'cherry-pick',
                        '--ff',
                        '--allow-empty',
                        '--allow-empty-message',
                        commit_string.strip(),
                    ],
                    env=env,
                )
            upload_tag_name = '%s/upload/%s' % (namespace, version_name)
            subprocess.check_call(
                [
                    'git',
                    'tag',
                    '-a',
                    '-m', 'Previous upload tag automatically rebased',
                    upload_tag_name,
                ],
                env=env,
            )
