#!/usr/bin/python3


from __future__ import annotations

import logging
from collections.abc import Callable, Generator
from typing import Any, Optional

from slpkg.config import config_load
from slpkg.tree import collect_transitive_names, render_tree
from slpkg.utilities import Utilities

logger = logging.getLogger(__name__)


class Reverse:  # pylint: disable=[R0902]
    """Prints the packages that depend on (reverse command)."""

    def __init__(self, data: dict[str, dict[str, str]], packages: list[str], options: dict[str, bool]) -> None:
        logger.debug("Initializing Reverse module with %d packages and options: %s", len(packages), options)
        self.data = data
        self.packages = packages
        self.options = options

        self.bold = config_load.bold
        self.grey = config_load.grey
        self.yellow = config_load.yellow
        self.cyan = config_load.cyan
        self.endc = config_load.endc
        self.unicode = config_load.unicode

        self.utils = Utilities()

        self.option_for_full_reverse: bool = options.get("option_full_reverse", False)
        self.option_for_pkg_version: bool = options.get("option_pkg_version", False)
        self.option_quiet: bool = options.get("option_quiet", False)
        logger.debug(
            "Reverse module initialized. Full reverse option: %s, Package version option: %s",
            self.option_for_full_reverse,
            self.option_for_pkg_version,
        )

    def _make_label_fn(self) -> Callable[[str], str] | None:
        """Return a label function that appends the repo version to a package name.

        Returns:
            Callable[[str], str] | None: Label function, or None if version display is disabled.
        """
        if not self.option_for_pkg_version:
            return None

        def get_label(name: str) -> str:
            ver = self.set_the_package_version(name)
            return f"{name} {self.yellow}{ver}{self.endc}" if ver else name

        return get_label

    def _print_quiet(self, package: str) -> None:
        """Print reverse-dependency names for quiet mode (no decoration).

        Args:
            package (str): Package whose reverse dependencies to list.
        """
        if self.option_for_full_reverse:
            tree = self._build_reverse_tree(package)
            for name in sorted(set(tree.keys()) | collect_transitive_names(tree)):
                print(name)
        else:
            for name in sorted(dict(self.find_requires(package))):
                print(name)

    def _print_full_reverse(self, package: str, label_fn: Any) -> None:
        """Print the recursive reverse-dependency tree for a package.

        Args:
            package (str): Package whose full reverse tree to render.
            label_fn: Optional callable to build a display label for each node.
        """
        tree = self._build_reverse_tree(package)
        if not tree:
            print(f"  {self.grey}No reverse dependencies.{self.endc}")
            return

        direct = set(tree.keys())
        for line in render_tree(
            tree,
            direct_parents=direct,
            unicode=self.unicode,
            grey=self.grey,
            endc=self.endc,
            label_fn=label_fn,
        ):
            print(f"  {line}")

        n_direct = len(direct)
        n_transitive = len(collect_transitive_names(tree))
        parts = [f"{n_direct} direct"]
        if n_transitive:
            parts.append(f"{n_transitive} transitive")
        print(f"\n  {self.grey}{', '.join(parts)}.{self.endc}")

    def _print_flat_reverse(self, package: str, label_fn: Any) -> None:
        """Print a flat (depth-0) reverse-dependency list for a package.

        Args:
            package (str): Package whose direct reverse dependencies to render.
            label_fn: Optional callable to build a display label for each node.
        """
        dependees: dict[str, list[str]] = dict(self.find_requires(package))
        if not dependees:
            print(f"  {self.grey}No reverse dependencies.{self.endc}")
            return

        flat_tree: dict[str, Any] = {name: {} for name in sorted(dependees)}
        for line in render_tree(
            flat_tree,
            unicode=self.unicode,
            grey=self.grey,
            endc=self.endc,
            label_fn=label_fn,
        ):
            print(f"  {line}")

        n = len(dependees)
        noun = "reverse dependency" if n == 1 else "reverse dependencies"
        print(f"\n  {self.grey}Found {n} {noun} for {package}.{self.endc}")

    def find(self) -> None:
        """Find and display packages that depend on the specified ones."""
        logger.info("Starting to find packages that depend on the specified ones.")
        self.packages = self.utils.apply_package_pattern(self.data, self.packages)
        label_fn = self._make_label_fn()

        for package in self.packages:
            logger.info("Processing package: %s", package)

            if self.option_quiet:
                self._print_quiet(package)
                continue

            # Header line: package name (+ version if requested).
            pkg_str = f"{self.bold}{self.cyan}{package}{self.endc}"
            if self.option_for_pkg_version:
                ver = self.set_the_package_version(package)
                if ver:
                    pkg_str += f" {self.yellow}{ver}{self.endc}"
            print(f"\n{pkg_str}:")

            if self.option_for_full_reverse:
                self._print_full_reverse(package, label_fn)
            else:
                self._print_flat_reverse(package, label_fn)

        logger.info("Finished finding dependees for all packages.")

    def set_the_package_version(self, package: str) -> str:
        """Return the repository version for package, or '' if unknown.

        Args:
            package (str): Package name.
        """
        logger.debug("Setting package version for: %s", package)
        package_version = self.data.get(package, {}).get("version", "")
        logger.debug("Version found for '%s': %s", package, package_version)
        return package_version

    def find_requires(self, package: str) -> Generator[tuple[str, list[str]], None, None]:
        """Yield (name, requires_list) for every package that lists package in its requires.

        Args:
            package (str): Package name.

        Yields:
            Generator: (name, requires_list) tuples.
        """
        logger.debug("Searching for packages that depend on '%s'.", package)
        for name, data in self.data.items():
            if "requires" in data and isinstance(data["requires"], list):
                if package in data["requires"]:
                    logger.debug("Found '%s' depends on '%s'.", name, package)
                    yield name, data["requires"]
            else:
                logger.debug("Package '%s' has no valid 'requires'. Skipping.", name)
        logger.debug("Finished searching for packages depending on '%s'.", package)

    def _build_reverse_tree(
        self,
        package: str,
        visited: Optional[frozenset[str]] = None,
    ) -> dict[str, Any]:
        """Recursively build the reverse-dependency tree as a nested dict.

        Args:
            package: The package whose reverse-dependees to find.
            visited: Already-visited packages — prevents cycles.

        Returns:
            dict[str, Any]: Nested dict {dependee: {their_dependees: {...}}, ...}.
        """
        if visited is None:
            visited = frozenset()
        visited = visited | {package}

        dependees = sorted(
            name
            for name, data in self.data.items()
            if "requires" in data
            and isinstance(data["requires"], list)
            and package in data["requires"]
            and name not in visited
        )
        return {dep: self._build_reverse_tree(dep, visited) for dep in dependees}
