# ============================================================
# src/accountable_record/stage_070_resolution/graph.py
# ============================================================
"""Build structural resolution graphs from stage 070 locks.

WHY:
    The lock file records package entries and element entries. Each element
    says which versioned packages include it. This module derives the
    bidirectional package-to-elements / element-to-packages graph and records
    graph-level structural issues.

OBS:
    This layer derives graph facts only. It does not read files, emit
    diagnostics, mutate state, recompute digests, or compare against exports.
"""

from collections import Counter, defaultdict
from collections.abc import Iterable

from accountable_record.stage_070_resolution.model import (
    ElementsLock,
    ResolutionGraph,
)


def build_resolution_graph(lock: ElementsLock) -> ResolutionGraph:
    """Build a resolution graph from a parsed elements lock.

    Args:
        lock: Parsed elements lock.

    Returns:
        Derived graph model.
    """
    declared_packages = lock.package_versioned_ids()

    package_to_elements: dict[str, list[str]] = defaultdict(list)
    element_to_packages: dict[str, list[str]] = {}
    unknown_package_refs: set[str] = set()

    for element in lock.elements:
        element_to_packages[element.compact_id] = list(element.included_by)

        for package_ref in element.included_by:
            if package_ref not in declared_packages:
                unknown_package_refs.add(package_ref)
            package_to_elements[package_ref].append(element.compact_id)

    return ResolutionGraph(
        package_to_elements=_freeze_mapping(package_to_elements),
        element_to_packages=_freeze_mapping(element_to_packages),
        unknown_package_refs=tuple(sorted(unknown_package_refs)),
        duplicate_package_ids=_duplicates(
            record.compact_id for record in lock.packages
        ),
        duplicate_element_ids=_duplicates(
            record.compact_id for record in lock.elements
        ),
    )


def _freeze_mapping(mapping: dict[str, list[str]]) -> dict[str, tuple[str, ...]]:
    """Return a sorted immutable-value view of a string-list mapping."""
    return {
        key: tuple(sorted(values))
        for key, values in sorted(mapping.items(), key=lambda item: item[0])
    }


def _duplicates(values: Iterable[str]) -> tuple[str, ...]:
    """Return sorted duplicate string values from an iterable."""
    counter = Counter(values)
    return tuple(sorted(value for value, count in counter.items() if count > 1))
