import argparse
import itertools
import logging
import os
import re
import sys
import tempfile
import urllib

import pygit2

from gitubuntu.git_repository import (
    GitUbuntuRepository,
    GitUbuntuRepositoryFetchError,
)
import gitubuntu.importer
import gitubuntu.source_information

MATCH_TAG_URI = re.compile(r'^Launchpad-URI:\s*(.*)$', flags=re.MULTILINE)
DOWNLOAD_MATCHES = {
    'changes': re.compile(r'_source\.changes$'),
    'dsc': re.compile(r'\.dsc$'),
}

def parse_args(subparsers=None, base_subparsers=None):
    kwargs = dict(description='Interact with the unapproved and new queues',
                  formatter_class=argparse.RawTextHelpFormatter,
                 )
    # The help for now will look weird with only one subcommand
    known_subcommands = {
        'sync': 'Import unapproved and new uploads',
        'clean': 'Delete all local queue tags',
         #'approve': 'Approve entries in the queue',
         #'reject': 'Reject entries in the queue',
    }

    if base_subparsers:
        kwargs['parents'] = base_subparsers
    if subparsers:
        parser = subparsers.add_parser('queue', **kwargs)
        parser.set_defaults(func=cli_main)
    else:
        parser = argparse.ArgumentParser(**kwargs)

    subsubparsers = parser.add_subparsers(dest='subsubcommand',
                        help='',
                        metavar='%s' % '|'.join(sorted(known_subcommands.keys())),
                       )
    subsubparsers.add_parser('clean', help=known_subcommands['clean'])
    sync_parser = subsubparsers.add_parser('sync', help=known_subcommands['sync'])
    sync_parser.add_argument(
        '--source',
        help="Override source package name on which to act",
    )
    sync_parser.add_argument(
        '--series',
        help="Override series' on which to act",
        action='append',
    )
    sync_parser.add_argument(
        '--parent',
        help="Override commit parent for new imports",
        default=True,
    )
    sync_parser.add_argument(
        '--orphan',
        help="Force orphan commit parent for new imports",
        dest='parent',
        action='store_const',
        const=None,
    )
    sync_parser.add_argument(
        '--no-trim',
        help="Do not remove old tags",
        dest='trim',
        action='store_false',
        default=True,
    )
    sync_parser.add_argument(
        '--new',
        help="Only consider the New queue",
        dest='statuses',
        action='store_const',
        const='New',
    )
    sync_parser.add_argument(
        '--unapproved',
        help="Only consider the Unapproved queue",
        dest='statuses',
        action='store_const',
        const='Unapproved',
    )
    sync_parser.add_argument(
        '--no-fetch',
        help="Do not fetch from the remote first",
        dest='fetch',
        action='store_false',
        default=True,
    )
    subsubparsers.required = True
    parser.add_argument('-d', '--directory', type=str,
                        help='Use git repository at specified location.',
                        default=os.path.abspath(os.getcwd())
                       )

    if not subparsers:
        return parser.parse_args()
    return 'queue - %s' % kwargs['description']

def cli_main(args):
    return main(
        args.directory,
        args.subsubcommand,
        args.fetch if args.subsubcommand == 'sync' else None,
        args.retries,
        args.retry_backoffs,
        args.source if args.subsubcommand == 'sync' else None,
        args.trim if args.subsubcommand == 'sync' else None,
        args.series if args.subsubcommand == 'sync' else None,
        args.parent if args.subsubcommand == 'sync' else None,
        args.statuses if args.subsubcommand == 'sync' else None,
    )

def _fetch_lp_queue_items(lp, series, package, statuses):
    '''Fetch unapproved and new entries from Launchpad

    Returns a dictionary of Launchpad upload objects keyed by persistent
    unique ID.
    '''
    statuses = statuses if statuses else ['Unapproved', 'New']
    s = lp.distributions['ubuntu'].getSeries(name_or_version=series)
    us = itertools.chain(*(
        s.getPackageUploads(
            pocket='Proposed',
            status=status,
            exact_match=True,
            name=package,
        ) for status in statuses
    ))
    lp_queue_items = {x.self_link: x for x in us}
    return lp_queue_items

def _fetch_queue_tag_names(repo, series=None):
    if series:
        return [r.name for r in
                repo.references_with_prefix('refs/tags/queue/%s/' % series)]
    else:
        return [r.name for r in
                repo.references_with_prefix('refs/tags/queue/')]

def _get_queue_tag_uri(tag):
    match = MATCH_TAG_URI.search(tag.message)
    assert match, "Tag URI not found"
    return match.group(1)

def _download_url(url, destdir):
    parsed_url = urllib.parse.urlparse(url)
    # Require secure URL
    if parsed_url.scheme != 'https':
        raise ValueError("URL scheme is not HTTPS: %s" % url)
    # Find and require a basename to store the file locally with a sensible
    # filename
    basename = parsed_url.path.split('/')[-1]
    if not basename:
        raise ValueError("Provided URL %s has no basename component" % url)
    with urllib.request.urlopen(url) as response:
        with open(os.path.join(destdir, basename), 'wb') as f:
            f.write(response.read())

def _get_upload_source_urls(upload):
    if upload.contains_source:
        return upload.sourceFileUrls()
    elif upload.contains_copy:
        return next(iter(upload.copy_source_archive.getPublishedSources(
            source_name=upload.package_name,
            version=upload.package_version,
            exact_match=True,
            order_by_date=True,
        ))).sourceFileUrls()
    else:
        raise RuntimeError("Cannot find source for %r" % upload)

def _commit_upload(repo, upload, parent_commit):
    with tempfile.TemporaryDirectory() as tmpdir:
        for url in _get_upload_source_urls(upload):
            _download_url(url, tmpdir)

        download_key = {}
        for filename in os.listdir(tmpdir):
            for file_type, regexp in DOWNLOAD_MATCHES.items():
                if regexp.search(filename):
                    download_key[file_type] = filename

        # We should have the dsc file at a minimum
        assert set(download_key.keys()) == {'dsc'}

        # Import the source package into a git tree object
        tree_hash = gitubuntu.importer.dsc_to_tree_hash(
            repo.raw_repo,
            os.path.join(tmpdir, download_key['dsc']),
        )

    # Ensure parents contains a string if specified
    parents = [pygit2.Oid(hex=str(parent_commit))] if parent_commit else []
    commit_hash = str(repo.commit_source_tree(
        tree=tree_hash,
        parents=parents,
        log_message='Queue import'.encode(),
    ))

    return commit_hash

def get_devel_head_ref(repo, series):
    for ref in [
        'refs/heads/importer/ubuntu/%s-devel' % series,
        'refs/remotes/pkg/ubuntu/%s-devel' % series,
    ]:
        try:
            return repo.raw_repo.lookup_reference(ref)
        except KeyError:
            pass
    raise KeyError(
        "Unable to find %s-devel branch in either local or remote "
        "importer branches."
    )

def sync(repo, lp, retries, retry_backoffs, source, trim, series_list, parent, statuses):
    if not series_list:
        series_list = [
            series.name
            for series
            in lp.distributions('ubuntu').series
            if series.active
        ]
    else:
        series_list = [s for l in [s.split(',') for s in series_list] for s in l]
    for series in series_list:
        logging.debug('Considering %s', series)
        series_parent = parent
        try:
            devel_head_ref = get_devel_head_ref(repo, series)
        except KeyError as e:
            if source:
                devel_srcpkg_name = source
            else:
                logging.warn(
                    "Series %s has no devel head pointer and no source "
                    "name specified; skipping",
                    series,
                )
                continue
            if series_parent is True:
                logging.warn(
                    "Series %s has no devel head pointer and no parent "
                    "specified; skipping",
                    series,
                )
                continue
        else:
            if series_parent is True:
                series_parent = devel_head_ref.target
            if source:
                devel_srcpkg_name = source
            else:
                devel_srcpkg_name = repo.get_changelog_srcpkg_from_treeish(
                    str(devel_head_ref.peel(pygit2.Tree).id)
                )
        assert series_parent is not True
        lp_queue_items = _fetch_lp_queue_items(
            lp, series, devel_srcpkg_name, statuses
        )
        all_tag_names = _fetch_queue_tag_names(repo, series)
        found_uris_in_repo = set()
        for tag_name in all_tag_names:
            logging.debug('Considering %s', tag_name)
            if not trim:
                continue
            tag_ref = repo.raw_repo.lookup_reference(tag_name)
            try:
                tag = tag_ref.peel(pygit2.Tag)
            except ValueError:
                if trim:
                    logging.info('%s is invalid; deleting' % tag_name)
                    tag_ref.delete()
                else:
                    logging.info('%s is invalid; not deleting as requested' % tag_name)
                continue
            tag_uri = _get_queue_tag_uri(tag)
            found_uris_in_repo.add(tag_uri)
            if tag_uri not in lp_queue_items:
                logging.info('%s is no longer in the queue; deleting' % tag_name)
                tag_ref.delete()
                found_uris_in_repo.remove(tag_uri)
                continue
            if lp_queue_items[tag_uri].status == 'Unapproved' and tag.peel(pygit2.Commit).parent_ids != [series_parent]:
                logging.info('%s is no longer based correctly; deleting' % tag_name)
                tag_ref.delete()
                found_uris_in_repo.remove(tag_uri)
                continue
        for uri, upload in lp_queue_items.items():
            if upload.status == 'Unapproved':
                tag_type = 'unapproved'
            elif upload.status == 'New':
                tag_type = 'new'
            else:
                raise RuntimeError('Unknown status for %s: %s' % (uri, upload.status))
            if uri in found_uris_in_repo:
                logging.debug('Skipping %s: already imported', uri)
                continue  # already imported
            logging.debug('Importing %s', uri)
            commit_hash = _commit_upload(repo, upload, series_parent)
            unique_id = repo.get_short_hash(commit_hash)
            repo.annotated_tag(
                'queue/%s/%s/%s' % (series, tag_type, unique_id),
                commit_hash,
                force=False,
                msg=('Launchpad-URI: %s' % uri),
            )

def clean(repo):
    for tag_name in _fetch_queue_tag_names(repo):
        tag_ref = repo.raw_repo.lookup_reference(tag_name)
        tag_ref.delete()

def main(
    directory,
    subsubcommand,
    fetch,
    retries,
    retry_backoffs,
    source,
    trim,
    series,
    parent,
    statuses,
):
    repo = GitUbuntuRepository(directory)

    lp = gitubuntu.source_information.launchpad_login()

    if subsubcommand == 'sync':
        if fetch:
            try:
                repo.fetch_base_remotes()
            except GitUbuntuRepositoryFetchError:
                logging.error('No objects found in remote pkg')
                return 1

        sync(
            repo,
            lp,
            retries,
            retry_backoffs,
            source,
            trim,
            series,
            parent,
            statuses,
        )
        # elif args.subsubcommand == 'approve':
        # elif args.subsubcommand == 'reject':
    elif subsubcommand == 'clean':
        clean(repo)

    return 0
