#!/usr/bin/python3 -sP
# Copyright (c) 2016 Andrew Hills
#
# 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 sys
import logging
from argparse import ArgumentParser

from debian import changelog

import debrepo
from debrepo.composes import Compose, InvalidCompose
from debrepo.diff import diff_composes
from debrepo.utils import FMT
from debrepo.filters import ExcludeFilter, TransformFilter
from debrepo import repos, packages


def debrepodiff(args):
    logging_format = '%(levelname)s: %(message)s'
    if args.verbose > 1:
        logging_level = logging.DEBUG
    elif args.verbose > 0:
        logging_level = logging.INFO
    else:
        logging_level = logging.WARNING

    logging.basicConfig(format=logging_format, level=logging_level)

    # Build filters
    filters = []
    for repo_name in args.exclude_repo:
        filters.append(ExcludeFilter(
            match='exact', what=repos.Repo, data=repo_name))
    for dist_name in args.exclude_dist:
        filters.append(ExcludeFilter(
            match='exact', what=repos.Dist, data=dist_name))
    for component_name in args.exclude_comp:
        filters.append(ExcludeFilter(
            match='exact', what=repos.Component, data=component_name))
    for arch_name in args.exclude_arch:
        filters.append(ExcludeFilter(
            match='exact', what=repos.Arch, data=arch_name))
    for pkg_name in args.exclude_pkg:
        filters.append(ExcludeFilter(
            match='exact', what=packages.Package, data=pkg_name))
    for change_text in args.exclude_change:
        filters.append(ExcludeFilter(
            match='substring', what=changelog.ChangeBlock, data=change_text))
    if args.strip_dist_from_version:
        # After a dist is loaded, modify all its packages' version numbers
        # (packages don't know which dist they're in)
        def strip_dist_from_version(dist):
            for pkg in dist.packages:
                pkg.version = pkg.version.rstrip(dist.name)
        filters.append(TransformFilter(
            match=lambda self: len(self.packages) > 0, what=repos.Dist,
            transform=strip_dist_from_version))

    try:
        old_compose = Compose(args.old, strict=args.strict, filters=filters)
    except InvalidCompose as e:
        logging.error('Invalid old compose: %s', e.message)
        sys.exit(1)

    try:
        new_compose = Compose(args.new, strict=args.strict, filters=filters)
    except InvalidCompose as e:
        logging.error('Invalid new compose: %s', e.message)
        sys.exit(1)

    logging.info('Old: %s', old_compose)
    logging.info('New: %s', new_compose)

    diff = diff_composes(new_compose, old_compose)

    # If repo configuration changed, summarize it
    if diff.added:
        print(FMT.format(
            '{repoct|repository/repositories} new: {repos}',
            repoct=len(diff.added), repos=', '.join(diff.added)))
    if diff.removed:
        print(FMT.format(
            '{repoct|repository/repositories} removed: {repos}',
            repoct=len(diff.removed), repos=', '.join(diff.removed)))

    # Collect package updates from shared repos
    for repodiff in diff.compared:
        repo_name = repodiff.new_name
        print('%s repo changes:\n' % repo_name)

        structure_changes = []
        added_pkgs = []
        removed_pkgs = []
        updated_pkgs = []

        if repodiff.added:
            structure_changes.append(FMT.format(
                '{dists|*dist(s)} added to {repo} repo: {dists~, }',
                dists=repodiff.added, repo=repo_name))
        if repodiff.removed:
            structure_changes.append(FMT.format(
                '{dists|dist(s)} removed from {repo} repo: {dists~, }',
                dists=repodiff.removed, repo=repo_name))

        for distdiff in repodiff.compared:
            dist_name = distdiff.new_name

            if distdiff.added:
                structure_changes.append(FMT.format(
                    '{comps|component(s)} added to {repo}/{dist}: {comps~, }',
                    comps=distdiff.added, dist=dist_name, repo=repo_name))
            if distdiff.removed:
                structure_changes.append(FMT.format(
                    '{comps|component(s)} removed from {repo}/{dist}: '
                    '{comps~, }',
                    comps=distdiff.removed, dist=dist_name, repo=repo_name))

            for compdiff in distdiff.compared:
                comp_name = compdiff.new_name

                if compdiff.added:
                    structure_changes.append(FMT.format(
                        '{arches|architecture(s)} added to {repo}/{dist}/'
                        '{comp}: {arches~, }', arches=compdiff.added,
                        comp=comp_name, dist=dist_name, repo=repo_name))
                if compdiff.removed:
                    structure_changes.append(FMT.format(
                        '{arches|architecture(s)} removed from {repo}/{dist}/'
                        '{comp}: {arches~, }', arches=compdiff.removed,
                        comp=comp_name, dist=dist_name, repo=repo_name))

                for archdiff in compdiff.compared:
                    arch_name = archdiff.new_name

                    if archdiff.added:
                        added_pkgs.extend([pkg for pkg in archdiff.added])
                    if archdiff.removed:
                        removed_pkgs.extend([pkg for pkg in archdiff.removed])

                    # This funky list(.items()) syntax is for Python 2/3
                    for pkg_name, pkgdiff in list(archdiff.compared.items()):
                        update_line = '%s: %s -> %s' % (
                                pkg_name, pkgdiff.removed, pkgdiff.added)
                        if pkgdiff.compared:
                            # This is the same number of hyphens repodiff uses
                            update_line += '\n--------------------------\n'
                            update_line += '\n'.join(pkgdiff.compared)
                        # Avoid duplicates across arches
                        if update_line not in updated_pkgs:
                            updated_pkgs.append(update_line)
                        else:
                            logging.debug(
                                    'excluded duplicate changelog for %s',
                                    pkg_name)

        if added_pkgs:
            print('New packages: %s\n' % ', '.join(added_pkgs))

        if removed_pkgs:
            print('Removed packages: %s\n' % ', '.join(removed_pkgs))

        if updated_pkgs:
            print('Updated packages:\n\n%s\n' % '\n'.join(updated_pkgs))

        if structure_changes:
            print('Structural changes:\n%s', '\n'.join(
                [' - ' + change for change in structure_changes]))

        print(
            '''Summary:
Added Packages: %d
Removed Packages: %d
Modified Packages: %d
Other Changes: %d
''' % tuple(map(len, (
                added_pkgs, removed_pkgs, updated_pkgs, structure_changes))))

if __name__ == '__main__':
    parser = ArgumentParser(description='Compare two Debian composes')
    parser.add_argument(
            '-V', '--version', action='version',
            version='debrepo %s' % debrepo.__version__)
    parser.add_argument(
            '-v', '--verbose', action='count',
            help='verbosity (twice is more)')
    parser.add_argument(
            '-s', '--strict', action='store_true',
            help='stop parsing for malformed and extra data')
    parser.add_argument(
            '-o', '--old', required=True, help='path to old compose')
    parser.add_argument(
            '-n', '--new', required=True, help='path to new compose')
    # Filters
    parser.add_argument(
            '-R', '--exclude-repo',
            action='append', metavar='REPO', default=[],
            help='repo to exclude (may be specified multiple times)')
    parser.add_argument(
            '-D', '--exclude-dist',
            action='append', metavar='DIST', default=[],
            help='dist to exclude (may be specified multiple times)')
    parser.add_argument(
            '-C', '--exclude-comp',
            action='append', metavar='COMP', default=[],
            help='component to exclude (may be specified multiple times)')
    parser.add_argument(
            '-A', '--exclude-arch',
            action='append', metavar='ARCH', default=[],
            help='arch to exclude (may be specified multiple times)')
    parser.add_argument(
            '-P', '--exclude-pkg',
            action='append', metavar='PKG', default=[],
            help='package to exclude (may be specified multiple times)')
    parser.add_argument(
            '--exclude-change', action='append', metavar='STR', default=[],
            help='exclude changelog entries containing STR')
    parser.add_argument(
            '--strip-dist-from-version', action='store_true',
            help='remove dist from package version string')
    args = parser.parse_args()
    debrepodiff(args)
