#!/usr/bin/python3


from __future__ import annotations

import json
import logging
from collections.abc import Callable
from pathlib import Path
from typing import Any, Optional

from slpkg.config import config_load
from slpkg.tree import collect_transitive_names as _ct_names
from slpkg.tree import render_tree as _render_tree_fn
from slpkg.utilities import Utilities

logger = logging.getLogger(__name__)


class Why:  # pylint: disable=[R0902,R0903]
    """Show which installed packages depend on the given package(s)."""

    def __init__(self, packages: list[str], options: Optional[dict[str, Any]] = None) -> None:
        self.packages = packages
        self.deps_log_file: Path = config_load.deps_log_file
        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.quiet: bool = (options or {}).get("option_quiet", False)
        self.transitive: bool = (options or {}).get("option_transitive", False)
        self.option_for_pkg_version: bool = (options or {}).get("option_pkg_version", False)
        self.unicode: bool = config_load.unicode
        self.utils = Utilities()

    def _read_deps_log(self) -> dict[str, list[str]]:
        """Read deps.log, returning an empty dict if missing or invalid."""
        if not self.deps_log_file.is_file():
            logger.info("deps.log not found: %s", self.deps_log_file)
            return {}
        try:
            data = json.loads(self.deps_log_file.read_text(encoding="utf-8"))
            if not isinstance(data, dict):
                logger.warning("deps.log is not a JSON object: %s", self.deps_log_file)
                return {}
            return data
        except (json.JSONDecodeError, OSError) as e:
            logger.error("Failed to read deps.log '%s': %s", self.deps_log_file, e)
            return {}

    def _find_transitive(  # pylint: disable=[R0913,R0917]
        self,
        package: str,
        deps_log: dict[str, list[str]],
        installed: set[str],
        depth: int = 0,
        visited: Optional[frozenset[str]] = None,
    ) -> list[tuple[int, str]]:
        """Return (depth, name) pairs for the full ancestor tree of package.

        Used by quiet mode. Alphabetically sorted at each level, cycle-safe.

        Returns:
            list[tuple[int, str]]: (depth, name) pairs.
        """
        if visited is None:
            visited = frozenset()
        visited = visited | {package}

        direct_parents = sorted(
            key for key, deps in deps_log.items() if package in deps and key in installed and key not in visited
        )

        result: list[tuple[int, str]] = []
        for parent in direct_parents:
            result.append((depth, parent))
            result.extend(self._find_transitive(parent, deps_log, installed, depth + 1, visited))
        return result

    def _build_tree(
        self,
        package: str,
        deps_log: dict[str, list[str]],
        installed: set[str],
        visited: Optional[frozenset[str]] = None,
    ) -> dict[str, Any]:
        """Recursively build the ancestor tree as a nested dict.

        Args:
            package: The package whose ancestors to find.
            deps_log: The deps.log data.
            installed: Set of currently installed package names.
            visited: Already-visited packages — prevents cycles.

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

        parents = sorted(
            key for key, deps in deps_log.items() if package in deps and key in installed and key not in visited
        )

        return {parent: self._build_tree(parent, deps_log, installed, visited) for parent in parents}

    def _render_tree(
        self,
        tree: dict[str, Any],
        direct_parents: set[str],
        prefix: str = "",
        depth: int = 0,
    ) -> list[str]:
        """Render a tree dict as formatted lines with connectors.

        Delegates to the shared tree.render_tree() utility, passing this
        instance's terminal settings.
        """
        return _render_tree_fn(
            tree,
            direct_parents,
            prefix,
            depth,
            unicode=self.unicode,
            grey=self.grey,
            endc=self.endc,
        )

    @staticmethod
    def _collect_transitive_names(tree: dict[str, Any], _depth: int = 0) -> set[str]:
        """Return the set of all unique package names at depth > 0 in the tree."""
        return _ct_names(tree, _depth)

    def _pkg_version(self, name: str, all_installed: dict[str, str]) -> str:
        """Return the installed version of name, or '' if unknown."""
        filename = all_installed.get(name, "")
        return self.utils.split_package(filename).get("version", "") if filename else ""

    def _resolve_packages(self, installed: set[str]) -> None:
        """Expand the '*' wildcard in self.packages to all installed packages."""
        if "*" in self.packages:
            self.packages = [p for p in self.packages if p != "*"] + sorted(installed)

    def _make_label_fn(self, all_installed: dict[str, str]) -> Callable[[str], str]:
        """Return a function that formats a package name with its installed version."""
        if self.option_for_pkg_version:
            def label_fn(name: str) -> str:
                ver = self._pkg_version(name, all_installed)
                return f"{name} {self.yellow}{ver}{self.endc}" if ver else name
        else:
            def label_fn(name: str) -> str:
                return name
        return label_fn

    def _view_quiet(self, deps_log: dict[str, list[str]], installed: set[str]) -> None:
        """Print results in quiet mode (one package name per line)."""
        for package in self.packages:
            if self.transitive:
                seen: set[str] = set()
                for _, name in self._find_transitive(package, deps_log, installed):
                    if name not in seen:
                        seen.add(name)
                        print(name)
            else:
                needed_by = sorted(key for key, deps in deps_log.items() if package in deps and key in installed)
                for parent in needed_by:
                    print(parent)
        logger.info("Why view completed (quiet) for packages: %s", ", ".join(self.packages))

    def _view_package_header(self, package: str, all_installed: dict[str, str]) -> None:
        """Print the package header line with optional installed version."""
        pkg_str = f"{self.bold}{self.cyan}{package}{self.endc}"
        if self.option_for_pkg_version:
            ver = self._pkg_version(package, all_installed)
            if ver:
                pkg_str += f" {self.yellow}{ver}{self.endc}"
        print(f"{pkg_str}:")

    def _view_package_transitive(
        self,
        package: str,
        deps_log: dict[str, list[str]],
        installed: set[str],
        label_fn: Callable[[str], str],
    ) -> None:
        """Print the transitive ancestor tree for a single package."""
        direct_parents = {key for key, deps in deps_log.items() if package in deps and key in installed}
        tree = self._build_tree(package, deps_log, installed)

        if not tree:
            print(f"  {self.grey}Not tracked as a dependency of any installed package.{self.endc}")
            return

        for line in _render_tree_fn(
            tree,
            direct_parents,
            unicode=self.unicode,
            grey=self.grey,
            endc=self.endc,
            label_fn=label_fn if self.option_for_pkg_version else None,
        ):
            print(f"  {line}")

        n_direct = len(direct_parents)
        n_transitive = len(self._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 _view_package_flat(
        self,
        package: str,
        deps_log: dict[str, list[str]],
        installed: set[str],
        label_fn: Callable[[str], str],
    ) -> None:
        """Print the flat (non-transitive) list of dependents for a single package."""
        needed_by = sorted(key for key, deps in deps_log.items() if package in deps and key in installed)
        if not needed_by:
            print(f"  {self.grey}Not tracked as a dependency of any installed package.{self.endc}")
            return

        for parent in needed_by:
            print(f"  {label_fn(parent)}")
        count = len(needed_by)
        noun = "package" if count == 1 else "packages"
        print(f"\n  Found {self.cyan}{count}{self.endc} installed {noun} require {self.bold}{package}{self.endc}.")

    def _view_package(  # pylint: disable=[R0913,R0917]
        self,
        package: str,
        deps_log: dict[str, list[str]],
        installed: set[str],
        all_installed: dict[str, str],
        label_fn: Callable[[str], str],
    ) -> None:
        """Print the full output block for a single package."""
        self._view_package_header(package, all_installed)

        if not deps_log:
            print(f"  {self.grey}Not tracked as a dependency of any installed package.{self.endc}")
            print()
            return

        if self.transitive:
            self._view_package_transitive(package, deps_log, installed, label_fn)
        else:
            self._view_package_flat(package, deps_log, installed, label_fn)

        print()

    def view(self) -> None:
        """Print which installed packages require each given package."""
        deps_log = self._read_deps_log()
        all_installed = self.utils.all_installed()
        installed = set(all_installed.keys())

        self._resolve_packages(installed)
        label_fn = self._make_label_fn(all_installed)

        if self.quiet:
            self._view_quiet(deps_log, installed)
            return

        header = "Dependency lookup (transitive):" if self.transitive else "Dependency lookup:"
        print(f"{self.bold}{header}{self.endc}")

        for package in self.packages:
            self._view_package(package, deps_log, installed, all_installed, label_fn)

        print(f"{self.grey}{self.deps_log_file}{self.endc}")
        logger.info("Why view completed for packages: %s", ", ".join(self.packages))
