#!/usr/bin/env python

# Sync a suite with a Seed list.
# Copyright (C) 2004, 2005, 2009, 2010, 2011, 2012  Canonical Ltd.
# Author: James Troup <james.troup@canonical.com>

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

# XXX - add indication if all of the binaries of a source packages are
#       listed for promotion at once
#       i.e. to allow 'change-override -S' usage

from __future__ import print_function

__metaclass__ = type

import atexit
from collections import defaultdict, OrderedDict
import copy
import csv
import gzip
try:
    from html import escape
except ImportError:
    from cgi import escape
import json
from optparse import OptionParser
import os
import shutil
import sys
import tempfile
from textwrap import dedent
import time
try:
    from urllib.parse import quote_plus
except ImportError:
    from urllib import quote_plus

import apt_pkg
from launchpadlib.launchpad import Launchpad

from charts import make_chart, make_chart_header


tempdir = None

archive_source = {}
archive_binary = {}

current_source = {}
current_binary = {}

germinate_source = {}
germinate_binary = {}

seed_source = defaultdict(set)
seed_binary = defaultdict(set)


class MIRLink:
    def __init__(self, id, status, title, assignee):
        self.id = id
        self.status = status
        self.title = title
        self.assignee = assignee

    def __str__(self):
        if self.status not in ('Fix Committed', 'Fix Released') and self.assignee:
            s = "MIR: #%d (%s for %s)" % (self.id, self.status,
                                          self.assignee.display_name)
        else:
            s = "MIR: #%d (%s)" % (self.id, self.status)
        # no need to repeat the standard title
        if not self.title.startswith("[MIR]"):
            s += " %s" % self.title
        return s

    def html(self):
        h = 'MIR: <a href="https://launchpad.net/bugs/%d">#%d</a> (%s)' % (
            self.id, self.id, escape(self.status))
        # no need to repeat the standard title
        if not self.title.startswith("[MIR]"):
            h += " %s" % escape(self.title)
        return h


def ensure_tempdir():
    global tempdir
    if not tempdir:
        tempdir = tempfile.mkdtemp(prefix='component-mismatches')
        atexit.register(shutil.rmtree, tempdir)


def decompress_open(tagfile):
    ensure_tempdir()
    decompressed = tempfile.mktemp(dir=tempdir)
    fin = gzip.GzipFile(filename=tagfile)
    with open(decompressed, 'wb') as fout:
        fout.write(fin.read())
    return open(decompressed, 'r')


def read_current_source(options):
    for suite in options.suites:
        for component in options.all_components:
            sources_path = "%s/dists/%s/%s/source/Sources.gz" % (
                options.archive_dir, suite, component)
            for section in apt_pkg.TagFile(decompress_open(sources_path)):
                if 'Package' in section and 'Version' in section:
                    (pkg, version) = (section['Package'], section['Version'])
                    if pkg not in archive_source:
                        archive_source[pkg] = (version, component)
                    else:
                        if apt_pkg.version_compare(
                                archive_source[pkg][0], version) < 0:
                            archive_source[pkg] = (
                                version, component.split("/")[0])

    for pkg, (version, component) in archive_source.items():
        if component in options.components:
            current_source[pkg] = (version, component)


def read_current_binary(options):
    components_with_di = []
    for component in options.all_components:
        components_with_di.append(component)
        components_with_di.append('%s/debian-installer' % component)
    for suite in options.suites:
        for component in components_with_di:
            for arch in [
                    "i386", "amd64", "armhf", "arm64", "ppc64el",
                    "s390x"]:
                binaries_path = "%s/dists/%s/%s/binary-%s/Packages.gz" % (
                    options.archive_dir, suite, component, arch)
                for section in apt_pkg.TagFile(decompress_open(binaries_path)):
                    if 'Package' in section and 'Version' in section:
                        (pkg, version) = (section['Package'],
                                          section['Version'])
                        if 'source' in section:
                            src = section['Source'].split(" ", 1)[0]
                        else:
                            src = section['Package']
                        if pkg not in archive_binary:
                            archive_binary[pkg] = (
                                version, component.split("/")[0], src)
                        else:
                            if apt_pkg.version_compare(
                                    archive_binary[pkg][0], version) < 0:
                                archive_binary[pkg] = (version, component, src)

    for pkg, (version, component, src) in archive_binary.items():
        if component in options.components:
            current_binary[pkg] = (version, component, src)


def read_germinate(options):
    for flavour in reversed(options.flavours.split(",")):
        # List of seeds
        seeds = ["all"]
        try:
            filename = "%s/structure_%s_%s_i386" % (
                options.germinate_path, flavour, options.suite)
            with open(filename) as structure:
                for line in structure:
                    if not line or line.startswith('#') or ':' not in line:
                        continue
                    seeds.append(line.split(':')[0])
        except IOError:
            continue
        # ideally supported+build-depends too, but Launchpad's
        # cron.germinate doesn't save this

        for arch in ["i386", "amd64", "armhf", "arm64", "ppc64el",
                     "s390x"]:
            for seed in seeds:
                filename = "%s/%s_%s_%s_%s" % (
                    options.germinate_path, seed, flavour, options.suite, arch)
                with open(filename) as f:
                    for line in f:
                        # Skip header and footer
                        if (line[0] == "-" or line.startswith("Package") or
                                line[0] == " "):
                            continue
                        # Skip empty lines
                        line = line.strip()
                        if not line:
                            continue
                        pkg, source, why = [word.strip()
                                            for word in line.split('|')][:3]
                        if seed == "all":
                            germinate_binary[pkg] = (
                                source, why, flavour, arch)
                            germinate_source[source] = (flavour, arch)
                        else:
                            seed_binary[seed].add(pkg)
                            seed_source[seed].add(source)


def is_included_binary(options, pkg):
    if options.include:
        for seed in options.include.split(","):
            if seed in seed_binary and pkg in seed_binary[seed]:
                return True
        return False
    return True


def is_excluded_binary(options, pkg):
    if options.exclude:
        seeds = set(seed_binary) - set(options.exclude.split(","))
        for seed in seeds:
            if seed in seed_binary and pkg in seed_binary[seed]:
                return False
        for seed in options.exclude.split(","):
            if seed in seed_binary and pkg in seed_binary[seed]:
                return True
    return False


def is_included_source(options, pkg):
    if options.include:
        for seed in options.include.split(","):
            if seed in seed_source and pkg in seed_source[seed]:
                return True
        return False
    return True


def is_excluded_source(options, pkg):
    if options.exclude:
        seeds = set(seed_source) - set(options.exclude.split(","))
        for seed in seeds:
            if seed in seed_source and pkg in seed_source[seed]:
                return False
        for seed in options.exclude.split(","):
            if seed in seed_source and pkg in seed_source[seed]:
                return True
    return False


def get_source(binary):
    return current_binary[binary][2]


def find_signer(options, source):
    # look at the source package publishing history for the most recent
    # package_signer, a copy from debian won't have a package signer
    series = options.distro.getSeries(name_or_version=options.suite)
    publications = options.archive.getPublishedSources(
        distro_series=series, source_name=source,
        exact_match=True)
    if not publications:
        return('no publications found', '')
    sorted_pubs = sorted([(ps.date_published, ps)
                          for ps in publications
                          if ps.date_published is not None], reverse=True)
    for pub in sorted_pubs:
        if pub[1].package_signer:
            signer = pub[1].package_signer.name
            web_link = pub[1].package_signer.web_link
            return(signer, web_link)
        else:
            signer = ''
            web_link = ''
    return (signer, web_link)


def do_reverse(options, source, binaries, why_d):
    global signers
    try:
        signers.keys()
    except NameError:
        signers = {}
    output = []
    depend = {}
    recommend = {}
    build_depend = {}
    for binary in binaries:
        why = why_d[source][binary]
        if why.find("Build-Depend") != -1:
            why = why.replace("(Build-Depend)", "").strip()
            build_depend[why] = ""
        elif why.find("Recommends") != -1:
            why = why.replace("(Recommends)", "").strip()
            recommend[why] = ""
        else:
            depend[why] = ""

    def do_category(map, category):
        keys = []
        for k in map:
            if k.startswith('Rescued from '):
                pkg = k.replace('Rescued from ', '')
            else:
                pkg = k
            # seed names have spaces in them
            if ' ' not in pkg:
                try:
                    source = get_source(pkg)
                except KeyError:
                    source = pkg
                    pass
                if source not in signers:
                    signer, web_link = find_signer(options, source)
                    if signer and web_link:
                        signers[source] = (signer, web_link)
            if k in current_binary:
                keys.append('%s (%s)' % (k, current_binary[k][1].upper()))
            elif k in current_source:
                keys.append('%s (%s)' % (k, current_source[k][1].upper()))
            else:
                keys.append(k)
        keys.sort()
        if keys:
            return ["[Reverse-%s: %s]" % (category, ", ".join(keys))]
        else:
            return []

    output.extend(do_category(depend, 'Depends'))
    output.extend(do_category(recommend, 'Recommends'))
    output.extend(do_category(build_depend, 'Build-Depends'))

    return output


def do_dot(why, fd, mir_bugs, suite):
    # write dot graph for given why dictionary

    written_nodes = set()

    fd.write(
        'digraph "component-mismatches: movements to main/restricted" {\n')
    for s, binwhy in why.iteritems():
        for binary, why in binwhy.iteritems():
            # ignore binaries from this source, and "rescued"
            if why in binwhy or why.startswith('Rescued'):
                continue

            if "(Recommends)" in why:
                relation = " R "
                color = "gray"
                why = why.replace(" (Recommends)", "")
            elif "Build-Depend" in why:
                relation = " B"
                color = "blue"
                why = why.replace(" (Build-Depend)", "")
            else:
                relation = ""
                color = "black"

            try:
                why = get_source(why)
            except KeyError:
                # happens for sources which are in universe, or seeds
                try:
                    why = germinate_binary[why][0]
                except:
                    pass

            # helper function to write a node
            def write_node(name):
                # ensure to only write it once
                if name in written_nodes:
                    return name
                written_nodes.add(name)

                fd.write('  "%s" [label="%s" style="filled" tooltip="%s"' %
                         (name, name, ', '.join(package_team_mapping[name])))

                mirs = mir_bugs.get(name, [])
                approved_mirs = [
                    id for id, status, title, assignee in mirs
                    if status in ('Fix Committed', 'Fix Released')]

                url = None
                if name.endswith(' seed'):
                    fc = "green"
                elif name in current_source:
                    fc = "lightgreen"
                    url = ("https://launchpad.net/ubuntu/+source/%s" %
                           quote_plus(name))
                elif approved_mirs:
                    fc = "yellow"
                    url = "https://launchpad.net/bugs/%i" % approved_mirs[0]
                elif mirs:
                    if mirs[0][1] == 'Incomplete':
                        fc = "darkkhaki"
                    else:
                        fc = "darksalmon"
                    url = "https://launchpad.net/bugs/%i" % mirs[0][0]
                else:
                    fc = "white"
                    # Need to use &amp; otherwise the svg will have a syntax error
                    url = ("https://launchpad.net/ubuntu/+source/%s/+filebug?"
                           "field.title=%s&amp;field.status=Incomplete"
                           "&amp;field.tags=%s" %
                           (quote_plus(name), quote_plus("[MIR] %s" % name),
                            quote_plus(suite)))
                fd.write(' fillcolor="%s"' % fc)
                if url:
                    fd.write(' URL="%s"' % url)
                fd.write("]\n")
                return name

            s_node = write_node(s)
            why_node = write_node(why)

            # generate relation
            fd.write('  "%s" -> "%s" [label="%s" color="%s" '
                     'fontcolor="%s"]\n' %
                     (why_node, s_node, relation, color, color))

    # add legend
    fd.write("""
 {
    rank="source"
    NodeLegend[shape=none, margin=0, label=<
    <table border="0" cellborder="1" cellspacing="0" cellpadding="4">
      <tr><td>Nodes</td></tr>
      <tr><td bgcolor="green">seed</td></tr>
      <tr><td bgcolor="lightgreen">in main/restricted </td></tr>
      <tr><td bgcolor="yellow">approved MIR (clickable)</td></tr>
      <tr><td bgcolor="darksalmon">unapproved MIR (clickable)</td></tr>
      <tr><td bgcolor="darkkhaki">Incomplete/stub MIR (clickable)</td></tr>
      <tr><td bgcolor="white">No MIR (click to file one)</td></tr>
    </table>
   >];

    EdgeLegend[shape=none, margin=0, label=<
    <table border="0" cellborder="1" cellspacing="0" cellpadding="4">
      <tr><td>Edges</td></tr>
      <tr><td>Depends:</td></tr>
      <tr><td><font color="gray">Recommends:</font></td></tr>
      <tr><td><font color="blue">Build-Depends: </font></td></tr>
    </table>
   >];
  }
}
""")


def filter_source(component, sources):
    return [
        s for s in sources
        if s in archive_source and archive_source[s][1] == component]


def filter_binary(component, binaries):
    return [
        b for b in binaries
        if b in archive_binary and archive_binary[b][1] == component]


package_team_mapping = defaultdict(set)


def get_teams(options, source):
    global package_team_mapping

    if os.path.exists(options.package_team_mapping):
        with open(options.package_team_mapping) as ptm_file:
            for team, packages in json.load(ptm_file).items():
                if team == "unsubscribed":
                    continue
                for package in packages:
                    package_team_mapping[package].add(team)

    if source in package_team_mapping:
        for team in package_team_mapping[source]:
            yield team
    elif package_team_mapping:
        yield "unsubscribed"


def print_section_text(options, header, body,
                       source_and_binary=False, binary_only=False):
    if body:
        print(" %s" % header)
        print(" %s" % ("-" * len(header)))
        print()
        for entry in body:
            line = entry[0]
            source = line[0]
            binaries = " ".join(line[1:])
            if source_and_binary:
                print(" o %s: %s" % (source, binaries))
            elif binary_only:
                indent_right = 75 - len(binaries) - len(source) - 2
                print(" o %s%s{%s}" % (binaries, " " * indent_right, source))
            else:
                print(" o %s" % source)
            for line in entry[1:]:
                print("   %s" % line)
            if len(entry) != 1:
                print()
        if len(body[-1]) == 1:
            print()
        print("=" * 70)
        print()


def print_section_html(options, header, body,
                       source_and_binary=False, binary_only=False):
    if body:
        def print_html(*args, **kwargs):
            print(*args, file=options.html_output, **kwargs)

        def source_link(source):
            return (
                '<a href="https://launchpad.net/ubuntu/+source/%s">%s</a>' % (
                    escape(source, quote=True), escape(source)))

        print_html("<h2>%s</h2>" % escape(header))
        print_html("<table>")
        for entry in body:
            line = entry[0]
            source = line[0]
            binaries = " ".join(line[1:])
            if source_and_binary:
                print_html(
                    '<tr><th colspan="2">%s: %s' % (
                        source_link(source), escape(binaries)))
            elif binary_only:
                print_html('<tr><th>%s</th>' % escape(binaries), end="")
                print_html(
                    "<th><small>%s</small></th></tr>" % source_link(source))
            else:
                print_html(
                    '<tr><th colspan="2">%s</th></tr>' % source_link(source))
            for line in entry[1:]:
                if isinstance(line, MIRLink):
                    line = line.html()
                else:
                    for item in line.strip('[]').split(' '):
                        if item.strip(',') in signers:
                            comma = ''
                            if item.endswith(','):
                                comma = ','
                                pkg = item.strip(',')
                            else:
                                pkg = item
                            # neither of these will help fix the issue
                            if signers[pkg][0] in ['ps-jenkins',
                                                   'ci-train-bot']:
                                continue
                            line = line.replace(item, '%s (Uploader: %s)%s' %
                                                (pkg, signers[pkg][0], comma))
                    line = escape(line)
                print_html(
                    '<tr><td colspan="2"><span class="note">%s'
                    '</span></td></tr>' % line)
        print_html("</table>")


def do_output(options,
              orig_source_add, orig_source_remove, binary_add, binary_remove,
              mir_bugs):
    results = {}
    results["time"] = int(options.time * 1000)

    global package_team_mapping
    package_team_mapping = defaultdict(set)
    if os.path.exists(options.package_team_mapping):
        with open(options.package_team_mapping) as ptm_file:
            for team, packages in json.load(ptm_file).items():
                if team == "unsubscribed":
                    continue
                for package in packages:
                    package_team_mapping[package].add(team)

    if options.html_output is not None:
        print(dedent("""\
            <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
             "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
            <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
            <head>
              <meta http-equiv="Content-Type"
                    content="text/html; charset=utf-8" />
              <title>Component mismatches for %s</title>
              <style type="text/css">
                body { background: #CCCCB0; color: black; }
                a { text-decoration: none; }
                table { border-collapse: collapse; border-style: none none;
                        margin-bottom: 3ex; empty-cells: show; }
                table th { text-align: left;
                           border-style: groove none none none;
                           border-width: 3px; padding-right: 10px;
                           font-weight: normal; }
                table td { vertical-align: top; text-align: left;
                           border-style: none none;
                           border-width: 1px; padding-right: 10px; }
                .note { margin-left: 3ex; }
              </style>
              %s
            </head>
            <body>
            <h1>Component mismatches for %s</h1>
            """) % (escape(options.suite), make_chart_header(),
                    escape(options.suite)), file=options.html_output)

    # Additions

    binary_only = defaultdict(dict)
    both = defaultdict(dict)

    source_add = copy.copy(orig_source_add)
    source_remove = copy.copy(orig_source_remove)

    for pkg in binary_add:
        (source, why, flavour, arch) = binary_add[pkg]
        if source not in orig_source_add:
            binary_only[source][pkg] = why
        else:
            both[source][pkg] = why
            if source in source_add:
                source_add.remove(source)

    all_output = OrderedDict()
    results["source promotions"] = 0
    results["binary promotions"] = 0
    for component in options.components:
        if component == "main":
            counterpart = "universe"
        elif component == "restricted":
            counterpart = "multiverse"
        else:
            continue

        output = []
        for source in filter_source(counterpart, sorted(both)):
            binaries = sorted(both[source])
            entry = [[source] + binaries]

            for (id, status, title, assignee) in mir_bugs.get(source, []):
                entry.append(MIRLink(id, status, title, assignee))

            entry.extend(do_reverse(options, source, binaries, both))
            output.append(entry)

        all_output["Source and binary movements to %s" % component] = {
            "output": output,
            "source_and_binary": True,
            }
        results["source promotions"] += len(output)

        output = []
        for source in sorted(binary_only):
            binaries = filter_binary(counterpart, sorted(binary_only[source]))

            if binaries:
                entry = [[source] + binaries]
                entry.extend(do_reverse(options, source, binaries,
                             binary_only))
                output.append(entry)

        all_output["Binary only movements to %s" % component] = {
            "output": output,
            "binary_only": True,
            }
        results["binary promotions"] += len(output)

        output = []
        for source in filter_source(counterpart, sorted(source_add)):
            output.append([[source]])

        all_output["Source only movements to %s" % component] = {
            "output": output,
            }
        results["source promotions"] += len(output)

    if options.dot:
        with open(options.dot, 'w') as f:
            do_dot(both, f, mir_bugs, options.suite)

    # Removals

    binary_only = defaultdict(dict)
    both = defaultdict(dict)
    for pkg in binary_remove:
        source = get_source(pkg)
        if source not in orig_source_remove:
            binary_only[source][pkg] = ""
        else:
            both[source][pkg] = ""
            if source in source_remove:
                source_remove.remove(source)

    results["source demotions"] = 0
    results["binary demotions"] = 0
    for component in options.components:
        if component == "main":
            counterpart = "universe"
        elif component == "restricted":
            counterpart = "multiverse"
        else:
            continue

        output = []
        for source in filter_source(component, sorted(both)):
            binaries = sorted(both[source])
            output.append([[source] + binaries])

        all_output["Source and binary movements to %s" % counterpart] = {
            "output": output,
            "source_and_binary": True,
            }
        results["source demotions"] += len(output)

        output = []
        for source in sorted(binary_only):
            binaries = filter_binary(component, sorted(binary_only[source]))

            if binaries:
                output.append([[source] + binaries])

        all_output["Binary only movements to %s" % counterpart] = {
            "output": output,
            "binary_only": True,
            }
        results["binary demotions"] += len(output)

        output = []
        for source in filter_source(component, sorted(source_remove)):
            output.append([[source]])

        all_output["Source only movements to %s" % counterpart] = {
            "output": output,
            }
        results["source demotions"] += len(output)

    for title, output_spec in all_output.items():
        source_and_binary = output_spec.get("source_and_binary", False)
        binary_only = output_spec.get("binary_only", False)
        print_section_text(
            options, title, output_spec["output"],
            source_and_binary=source_and_binary, binary_only=binary_only)
        if options.html_output is not None and package_team_mapping:
            by_team = defaultdict(list)
            for entry in output_spec["output"]:
                source = entry[0][0]
                for team in package_team_mapping[source]:
                    by_team[team].append(entry)
                if not package_team_mapping[source]:
                    by_team["unsubscribed"].append(entry)
            for team, entries in sorted(by_team.items()):
                print_section_html(
                    options, "%s (%s)" % (title, team), entries,
                    source_and_binary=source_and_binary,
                    binary_only=binary_only)

    if options.html_output is not None:
        print("<h2>Over time</h2>", file=options.html_output)
        print(
            make_chart("component-mismatches.csv", [
                "source promotions", "binary promotions",
                "source demotions", "binary demotions",
                ]),
            file=options.html_output)
        print(
            "<p><small>Generated: %s</small></p>" % escape(options.timestamp),
            file=options.html_output)
        print("</body></html>", file=options.html_output)

    return results


def do_source_diff(options):
    removed = []
    added = []
    removed = list(set(current_source).difference(set(germinate_source)))
    for pkg in germinate_source:
        if (pkg not in current_source and
                is_included_source(options, pkg) and
                not is_excluded_source(options, pkg)):
            added.append(pkg)
    removed.sort()
    added.sort()
    return (added, removed)


def do_binary_diff(options):
    removed = []
    added = {}
    removed = list(set(current_binary).difference(set(germinate_binary)))
    for pkg in germinate_binary:
        if (pkg not in current_binary and
                is_included_binary(options, pkg) and
                not is_excluded_binary(options, pkg)):
            added[pkg] = germinate_binary[pkg]
    removed.sort()
    return (added, removed)


def get_mir_bugs(options, sources):
    '''Return MIR bug information for a set of source packages.

    Return a map source -> [(id, status, title, assignee), ...]
    '''
    result = defaultdict(list)
    mir_team = options.launchpad.people['ubuntu-mir']
    bug_statuses = ("New", "Incomplete", "Won't Fix", "Confirmed", "Triaged",
                    "In Progress", "Fix Committed", "Fix Released")
    for source in sources:
        tasks = options.distro.getSourcePackage(name=source).searchTasks(
            bug_subscriber=mir_team, status=bug_statuses)
        for task in tasks:
            result[source].append((task.bug.id, task.status, task.bug.title,
                                   task.assignee))

    return result


def main():
    apt_pkg.init()

    parser = OptionParser(description='Sync a suite with a Seed list.')
    parser.add_option(
        "-l", "--launchpad", dest="launchpad_instance", default="production")
    parser.add_option('-o', '--output-file', help='output to this file')
    parser.add_option('--html-output-file', help='output HTML to this file')
    parser.add_option(
        '--csv-file', help='record CSV time series data in this file')
    parser.add_option(
        '--package-team-mapping',
        default=os.path.expanduser('~/public_html/package-team-mapping.json'),
        help='path to package-team-mapping.json')
    parser.add_option('-s', '--suite', help='check this suite')
    parser.add_option('-f', '--flavours', default='ubuntu',
                      help='check these flavours (comma-separated)')
    parser.add_option('-i', '--include', help='include these seeds')
    parser.add_option('-e', '--exclude', help='exclude these seeds')
    parser.add_option('-d', '--dot',
                      help='generate main promotion graph suitable for dot')
    parser.add_option(
        '--germinate-path',
        default=os.path.expanduser('~/mirror/ubuntu-germinate/'),
        help='read Germinate output from this directory')
    parser.add_option(
        '--archive-dir',
        default=os.path.expanduser('~/mirror/ubuntu/'),
        help='use Ubuntu archive located in this directory')
    options, args = parser.parse_args()

    options.launchpad = Launchpad.login_anonymously(
        'component-mismatches', options.launchpad_instance)
    options.distro = options.launchpad.distributions['ubuntu']
    options.archive = options.distro.getArchive(name='primary')

    options.component = "main,restricted"
    options.components = options.component.split(',')
    options.all_components = ["main", "restricted", "universe", "multiverse"]

    if options.suite is None:
        options.suite = options.distro.current_series.name

    # Considering all the packages to have a full installable suite. So:
    # -security = release + -security
    # -updates = release + -updates + -security
    # -proposed = release + updates + security + proposed
    if "-" in options.suite:
        options.suite, options.pocket = options.suite.split("-")
        options.suites = [options.suite]
        if options.pocket in ["updates", "security", "proposed"]:
            options.suites.append("%s-security" % options.suite)
        if options.pocket in ["updates", "proposed"]:
            options.suites.append("%s-updates" % options.suite)
        if options.pocket in ["proposed"]:
            options.suites.append("%s-proposed" % options.suite)
    else:
        options.suites = [options.suite]

    if options.output_file is not None:
        sys.stdout = open('%s.new' % options.output_file, 'w')
    if options.html_output_file is not None:
        options.html_output = open('%s.new' % options.html_output_file, 'w')
    else:
        options.html_output = None

    options.time = time.time()
    options.timestamp = time.strftime(
        '%a %b %e %H:%M:%S %Z %Y', time.gmtime(options.time))
    print('Generated: %s' % options.timestamp)
    print()

    read_germinate(options)
    read_current_source(options)
    read_current_binary(options)
    source_add, source_remove = do_source_diff(options)
    binary_add, binary_remove = do_binary_diff(options)
    mir_bugs = get_mir_bugs(options, source_add)
    results = do_output(
        options, source_add, source_remove, binary_add, binary_remove,
        mir_bugs)

    if options.html_output_file is not None:
        options.html_output.close()
        os.rename(
            '%s.new' % options.html_output_file, options.html_output_file)
    if options.output_file is not None:
        sys.stdout.close()
        os.rename('%s.new' % options.output_file, options.output_file)
    if options.csv_file is not None:
        if sys.version < "3":
            open_mode = "ab"
            open_kwargs = {}
        else:
            open_mode = "a"
            open_kwargs = {"newline": ""}
        csv_is_new = not os.path.exists(options.csv_file)
        with open(options.csv_file, open_mode, **open_kwargs) as csv_file:
            # Field names deliberately hardcoded; any changes require
            # manually rewriting the output file.
            fieldnames = [
                "time",
                "source promotions",
                "binary promotions",
                "source demotions",
                "binary demotions",
                ]
            csv_writer = csv.DictWriter(csv_file, fieldnames)
            if csv_is_new:
                csv_writer.writeheader()
            csv_writer.writerow(results)


if __name__ == '__main__':
    main()
