#!/usr/bin/python3
#
# Copyright (C) 2011, Stefano Rivera <stefanor@ubuntu.com>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

# pylint: disable=invalid-name
# pylint: enable=invalid-name

import argparse
import sys

from distro_info import DistroDataOutdated

from ubuntutools import getLogger
from ubuntutools.misc import codename_to_distribution, system_distribution, vendor_to_distroinfo
from ubuntutools.rdepends import RDependsException, query_rdepends

Logger = getLogger()

DEFAULT_MAX_DEPTH = 10  # We want avoid any infinite loop...


def main():
    system_distro_info = vendor_to_distroinfo(system_distribution())()
    try:
        default_release = system_distro_info.devel()
    except DistroDataOutdated as e:
        Logger.warning(e)
        default_release = "unstable"

    description = (
        "List reverse-dependencies of package. "
        "If the package name is prefixed with src: then the "
        "reverse-dependencies of all the binary packages that "
        "the specified source package builds will be listed."
    )

    parser = argparse.ArgumentParser(description=description)
    parser.add_argument(
        "-r",
        "--release",
        default=default_release,
        help="Query dependencies in RELEASE. Default: %(default)s",
    )
    parser.add_argument(
        "-R",
        "--without-recommends",
        action="store_false",
        dest="recommends",
        help="Only consider Depends relationships, not Recommends",
    )
    parser.add_argument(
        "-s", "--with-suggests", action="store_true", help="Also consider Suggests relationships"
    )
    parser.add_argument(
        "-b",
        "--build-depends",
        action="store_true",
        help="Query build dependencies (synonym for --arch=source)",
    )
    parser.add_argument(
        "-a", "--arch", default="any", help="Query dependencies in ARCH. Default: any"
    )
    parser.add_argument(
        "-c",
        "--component",
        action="append",
        help="Only consider reverse-dependencies in COMPONENT. "
        "Can be specified multiple times. Default: all",
    )
    parser.add_argument(
        "-l", "--list", action="store_true", help="Display a simple, machine-readable list"
    )
    parser.add_argument(
        "-u",
        "--service-url",
        metavar="URL",
        dest="server",
        default=None,
        help="Reverse Dependencies webservice URL. Default: UbuntuWire",
    )
    parser.add_argument(
        "-x",
        "--recursive",
        action="store_true",
        help="Consider to find reverse dependencies recursively.",
    )
    parser.add_argument(
        "-d",
        "--recursive-depth",
        type=int,
        default=DEFAULT_MAX_DEPTH,
        help="If recusive, you can specify the depth.",
    )
    parser.add_argument("package")

    options = parser.parse_args()

    opts = {}
    if options.server is not None:
        opts["server"] = options.server

    # Convert unstable/testing aliases to codenames:
    distribution = codename_to_distribution(options.release)
    if not distribution:
        parser.error(f"Unknown release codename {options.release}")
    distro_info = vendor_to_distroinfo(distribution)()
    try:
        options.release = distro_info.codename(options.release, default=options.release)
    except DistroDataOutdated:
        # We already logged a warning
        pass

    if options.build_depends:
        options.arch = "source"

    if options.arch == "source":
        fields = [
            "Reverse-Build-Depends",
            "Reverse-Build-Depends-Indep",
            "Reverse-Build-Depends-Arch",
            "Reverse-Testsuite-Triggers",
        ]
    else:
        fields = ["Reverse-Depends"]
        if options.recommends:
            fields.append("Reverse-Recommends")
        if options.with_suggests:
            fields.append("Reverse-Suggests")

    def build_results(package, result, fields, component, recursive):
        try:
            data = query_rdepends(package, options.release, options.arch, **opts)
        except RDependsException as e:
            Logger.error(str(e))
            sys.exit(1)
        if not data:
            return

        if fields:
            data = {k: v for k, v in data.items() if k in fields}
        if component:
            data = {
                k: [rdep for rdep in v if rdep["Component"] in component] for k, v in data.items()
            }
            data = {k: v for k, v in data.items() if v}

        result[package] = data

        if recursive > 0:
            for rdeps in result[package].values():
                for rdep in rdeps:
                    build_results(rdep["Package"], result, fields, component, recursive - 1)

    result = {}
    build_results(
        options.package,
        result,
        fields,
        options.component,
        options.recursive and options.recursive_depth or 0,
    )

    if options.list:
        display_consise(result)
    else:
        display_verbose(options.package, result)


def display_verbose(package, values):
    if not values:
        Logger.info("No reverse dependencies found")
        return

    def log_package(values, package, arch, dependency, offset=0):
        line = f"{'  ' * offset}* {package}"
        if all_archs and set(arch) != all_archs:
            line += f" [{' '.join(sorted(arch))}]"
        if dependency:
            if len(line) < 30:
                line += " " * (30 - len(line))
                line += f"  (for {dependency})"
        Logger.info(line)
        data = values.get(package)
        if data:
            offset = offset + 1
            for rdeps in data.values():
                for rdep in rdeps:
                    log_package(
                        values,
                        rdep["Package"],
                        rdep.get("Architectures", all_archs),
                        rdep.get("Dependency"),
                        offset,
                    )

    all_archs = set()
    # This isn't accurate, but we make up for it by displaying what we found
    for data in values.values():
        for rdeps in data.values():
            for rdep in rdeps:
                if "Architectures" in rdep:
                    all_archs.update(rdep["Architectures"])

    for field, rdeps in values[package].items():
        Logger.info("%s", field)
        Logger.info("%s", "=" * len(field))
        rdeps.sort(key=lambda x: x["Package"])
        for rdep in rdeps:
            log_package(
                values,
                rdep["Package"],
                rdep.get("Architectures", all_archs),
                rdep.get("Dependency"),
            )
        Logger.info("")

    if all_archs:
        Logger.info(
            "Packages without architectures listed are reverse-dependencies in: %s",
            ", ".join(sorted(list(all_archs))),
        )


def display_consise(values):
    result = set()
    for data in values.values():
        for rdeps in data.values():
            for rdep in rdeps:
                result.add(rdep["Package"])

    Logger.info("\n".join(sorted(list(result))))


if __name__ == "__main__":
    main()
