"""Dependency graph of installed Python packages.

Defines the ``DependencyGraph`` class, which builds a directed graph of
installed Python packages and their dependencies using
``importlib.metadata``. Provides methods to query which packages depend
on a given package, facilitating pyrig's multi-package discovery system.
"""

import importlib.metadata
import logging
from collections.abc import Generator

from pyrig.src.graph import DiGraph
from pyrig.src.string_ import (
    kebab_to_snake_case,
    package_req_name_split_pattern,
)

logger = logging.getLogger(__name__)


class DependencyGraph(DiGraph):
    """Directed graph of installed Python package dependencies.

    Nodes are package names, edges represent dependency relationships.
    Built automatically on first instantiation by scanning installed distributions.
    As a ``Singleton``, the graph is constructed once and shared across the
    application for the lifetime of the process.
    Central to pyrig's multi-package discovery system.
    """

    def __init__(self, root: str | None = None) -> None:
        """Initialize the dependency graph rooted at the given package.

        Only packages that depend on ``root`` (directly or transitively) are
        included in the graph after pruning.

        Args:
            root: Name of the root package to build the graph around.
        """
        super().__init__(root=self.normalize_package_name(root) if root else None)

    def build(self) -> None:
        """Build the graph from installed Python distributions."""
        logger.debug("Building dependency graph from installed distributions")
        for dist in importlib.metadata.distributions():
            name, deps = self.parse_name_and_deps(dist)
            if name:
                self.add_node(name)
                for dep in deps:
                    self.add_edge(name, dep)  # package → dependency
        logger.debug("Dependency graph built with %d packages", len(self.nodes()))

    def parse_name_and_deps(
        self, dist: importlib.metadata.Distribution
    ) -> tuple[str, Generator[str, None, None]]:
        """Extract package name and dependencies from a distribution.

        Uses the public ``importlib.metadata`` API (``dist.metadata`` and
        ``dist.requires``) to read the package name and dependency list.

        Args:
            dist: Distribution object to extract metadata from.

        Returns:
            Tuple of (normalized_name, list_of_normalized_dependency_names).
            Name is empty string if not found. Dependencies list may be empty.
        """
        raw_name = dist.name
        name = self.normalize_package_name(raw_name) if raw_name else ""

        deps = (self.parse_package_name_from_req(req) for req in (dist.requires or []))

        return name, deps

    def parse_package_name_from_req(self, req: str) -> str:
        """Extract package name from a requirement string.

        Uses ``pyrig.src.string_.package_req_name_split_pattern`` to split the
        requirement string at the first non-name character.

        Args:
            req: Requirement string (e.g., "requests>=2.0,<3.0").

        Returns:
            Normalized package name
        """
        # Split on the first non-alphanumeric character (except -, _, and .)
        # Uses module-level compiled pattern for performance
        dep = package_req_name_split_pattern().split(req.strip(), maxsplit=1)[0].strip()
        return self.normalize_package_name(dep)

    def normalize_package_name(self, name: str) -> str:
        """Normalize a package name (lowercase, hyphens → underscores).

        Args:
            name: Package name to normalize.

        Returns:
            Normalized package name.
        """
        return kebab_to_snake_case(name)
