"""Build and check the source registry (stage 030 op).

WHY:
    Discovery produces a flat list of SourceRecords. The registry op turns that
    list into a SourceRegistry and enforces the one structural invariant that
    spans entries: identifier uniqueness. Keeping this separate from discovery
    mirrors the single-responsibility split the other stages use. Discover
    finds, registry indexes and keeps each op small.

OBS:
    The only judgment here is structural: do two records share an identifier.
    The registry does not rank, prefer, or interpret sources; AR.MULTIPLICITY
    forbids forcing resolution of competing records, so a duplicate is reported
    as a structural collision, not adjudicated.

OBS:
    A duplicate identifier raises StageInputDuplicate; the validation layer maps
    it to SOURCE_ID_DUPLICATE by type.
"""

from accountable_record.errors import StageInputDuplicate
from accountable_record.stage_030_sources.model import SourceRecord, SourceRegistry


def build_registry(records: list[SourceRecord]) -> SourceRegistry:
    """Build a SourceRegistry from discovered records.

    Args:
        records: Discovered source records, in discovery order.

    Returns:
        A :class:`SourceRegistry` preserving discovery order.

    Raises:
        StageInputDuplicate: Two records share one identifier. The error's
            location is the duplicated identifier.
    """
    seen: set[str] = set()
    for record in records:
        if record.source_id in seen:
            raise StageInputDuplicate(
                f"Source identifier {record.source_id!r} is declared more than once",
                location=record.source_id,
            )
        seen.add(record.source_id)

    return SourceRegistry(records=tuple(records))
