#!/usr/bin/env python3
#
# Copyright (C) 2017 Chris Lamb <lamby@debian.org>
#
# 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 3 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, see <http://www.gnu.org/licenses/>.

import argparse
import bz2
import collections
import json
import logging
import os
import sys
import time

import apt
import requests

try:
    from xdg.BaseDirectory import xdg_cache_home
except ImportError:
    print("This script requires the xdg python3 module.", file=sys.stderr)
    print("Please install the python3-xdg Debian package in order to use"
          "this utility.", file=sys.stderr)
    sys.exit(1)


class ReproducibleCheck:
    HELP = """
        Reports on the reproducible status of installed packages.
        For more details please see <https://reproducible-builds.org>.
    """

    NAME = os.path.basename(__file__)
    VERSION = 1

    STATUS_URL = 'https://tests.reproducible-builds.org/debian/' \
        'reproducible-tracker.json.bz2'

    CACHE = os.path.join(xdg_cache_home, NAME, os.path.basename(STATUS_URL))
    CACHE_AGE_SECONDS = 86400

    @classmethod
    def parse(cls):
        parser = argparse.ArgumentParser(description=cls.HELP)

        parser.add_argument(
            '-d',
            '--debug',
            help="show debugging messages",
            default=False,
            action='store_true',
        )

        parser.add_argument(
            '-r',
            '--raw',
            help="print unreproducible binary packages only (for dd-list -i)",
            default=False,
            action='store_true',
        )

        parser.add_argument(
            '--version',
            help="print version and exit",
            default=False,
            action='store_true',
        )

        return cls(parser.parse_args())

    def __init__(self, args):
        self.args = args

        logging.basicConfig(
            format='%(asctime).19s %(levelname).1s: %(message)s',
            level=logging.DEBUG if args.debug else logging.INFO,
        )

        self.log = logging.getLogger()

    def main(self):
        if self.args.version:
            print("{} version {}".format(self.NAME, self.VERSION))
            return 0

        self.update_cache()

        data = self.get_data()
        installed = self.get_installed_packages()
        unreproducible = {x: y for x, y in installed.items() if x in data}

        if self.args.raw:
            self.output_raw(unreproducible, installed)
        else:
            self.output_by_source(unreproducible, installed)

        return 0

    def update_cache(self):
        self.log.debug("Checking cache file %s ...", self.CACHE)

        try:
            if os.path.getmtime(self.CACHE) >= \
                    time.time() - self.CACHE_AGE_SECONDS:
                self.log.debug("Cache is up to date")
                return
        except OSError:
            pass

        self.log.info("Updating cache...")

        response = requests.get(self.STATUS_URL)

        os.makedirs(os.path.dirname(self.CACHE), exist_ok=True)

        with open(self.CACHE, 'wb+') as f:
            f.write(response.content)

    def get_data(self):
        self.log.debug("Loading data from cache %s", self.CACHE)

        with bz2.open(self.CACHE) as f:
            return {
                (x['package'], y['architecture'], x['version'])
                for x in json.loads(f.read().decode('utf-8'))
                for y in x['architecture_details']
                if y['status'] == 'unreproducible'
            }

    @staticmethod
    def get_installed_packages():
        result = collections.defaultdict(list)

        for x in apt.Cache():
            for y in x.versions:
                if not y.is_installed:
                    continue

                key = (y.source_name, y.architecture, y.version)
                result[key].append(x.shortname)

        return result

    @staticmethod
    def output_by_source(unreproducible, installed):
        num_installed = sum(len(x) for x in installed.keys())
        num_unreproducible = sum(len(x) for x in unreproducible.keys())
        default_architecture = apt.apt_pkg.config.find('APT::Architecture')

        for key, binaries in sorted(unreproducible.items()):
            source, architecture, version = key

            binaries_fmt = '({}) '.format(', '.join(binaries)) \
                if binaries != [source] else ''

            print("{}{} ({}) is unreproducible {}".format(
                source,
                '/{}'.format(architecture)
                if architecture != default_architecture else '',
                version,
                binaries_fmt,
            ), end='')
            print("<https://tests.reproducible-builds.org/debian/{}>".format(
                source,
            ))

        msg = "{}/{} ({:.2f}%) of installed binary packages are unreproducible."
        print(msg.format(
            num_unreproducible,
            num_installed,
            100. * num_unreproducible / num_installed,
        ))

    @staticmethod
    def output_raw(unreproducible, installed):  # pylint: disable=unused-argument
        for x in sorted(x for xs in unreproducible.values() for x in set(xs)):
            print(x)


if __name__ == '__main__':
    try:
        sys.exit(ReproducibleCheck.parse().main())
    except (KeyboardInterrupt, BrokenPipeError):
        sys.exit(1)
