#!/usr/bin/python3
# -*- coding: utf-8 -*-
# Copyright © 2023 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; version 2.1.
#
# 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Author: Zoltán Balogh <zbalogh@suse.com>

# Summary: Zypper subcommand to discover reverse dependencies of packages.
# Lists all packages that depend on a given package, optionally walking
# the full transitive dependency tree.

import argparse
import subprocess
import sys
import xml.etree.ElementTree as ET


def log_text(text, verbose, console):
    """Log progress text, overwriting the current line on a TTY."""
    if not verbose:
        return
    if console:
        sys.stderr.write(text + "\033[K\r")
        sys.stderr.flush()
    else:
        print(text, file=sys.stderr)


def requires_pkg(package):
    """Query zypper for packages that require the given package.

    Returns a list of (name, xml_line) tuples for each dependent solvable.
    Raises RuntimeError on zypper execution failure.
    """
    try:
        result = subprocess.run(
            ["zypper", "--no-refresh", "--xmlout", "se",
             "--match-exact", "--requires-pkg", package],
            capture_output=True)
    except OSError as e:
        raise RuntimeError(
            f"Could not execute zypper ({e.errno}): {e.strerror}")

    # zypper returns 104 when no results are found -- not an error
    if result.returncode not in (0, 104):
        stderr_msg = result.stderr.decode("utf-8", errors="replace").strip()
        raise RuntimeError(
            f"zypper exited with code {result.returncode}: {stderr_msg}")

    packages = []
    try:
        root = ET.fromstring(result.stdout)
    except ET.ParseError:
        return packages

    for solvable in root.iter("solvable"):
        name = solvable.get("name")
        if name:
            # Reconstruct the XML line for --detailed mode
            xml_line = ET.tostring(solvable, encoding="unicode").strip()
            packages.append((name, xml_line))
    return packages


def reverse_dependencies(package, seen, detailed, full_tree,
                         verbose, console, depth=0):
    """Recursively discover reverse dependencies.

    Returns a list of (name, xml_line) tuples for all (transitive)
    reverse dependents.
    """
    log_text(f"Package: {package} - {depth}", verbose, console)

    try:
        search_result = requires_pkg(package)
    except RuntimeError as e:
        log_text(str(e), verbose, console)
        sys.exit(1)

    log_text(f"{package} is required by: "
             f"{[name for name, _ in search_result]}", verbose, console)

    results = []
    for name, xml_line in search_result:
        if name not in seen:
            seen.add(name)
            results.append((name, xml_line))
            if full_tree:
                results.extend(
                    reverse_dependencies(name, seen, detailed, full_tree,
                                         verbose, console, depth + 1))
        else:
            log_text(f"Redundancy: {name}", verbose, console)

    return results


def main():
    parser = argparse.ArgumentParser(
        prog="zypper rdepends",
        description="List reverse dependencies of a package")
    parser.add_argument(
        "package",
        help="the package that the listed packages depend on")
    parser.add_argument(
        "-d", "--detailed", action="store_true",
        help="show status, name, kind, edition, arch, "
             "repository of the listed packages")
    parser.add_argument(
        "-f", "--full-tree", action="store_true",
        help="list all packages that indirectly depend on the given package")
    parser.add_argument(
        "-v", "--verbose", action="store_true",
        help="show progress information on stderr")
    args = parser.parse_args()

    console = sys.stderr.isatty()

    seen = set()
    results = reverse_dependencies(
        args.package, seen, args.detailed, args.full_tree,
        args.verbose, console)

    # Clear the progress line before printing results
    if args.verbose and console:
        sys.stderr.write("\033[K")
        sys.stderr.flush()

    for name, xml_line in sorted(results, key=lambda r: r[0]):
        print(xml_line if args.detailed else name)


if __name__ == "__main__":
    main()
