from dataclasses import dataclass, field
from pathlib import Path
from typing import Literal, Optional

RefRole = Literal["identity", "composition", "board", "location", "prop", "sheet"]
RefSource = Literal["shelf", "pool", "legacy", "sheet"]


@dataclass(frozen=True)
class RefAsset:
    path: Path
    role: RefRole
    subject: str
    kind: str
    is_hero: bool = False
    content_hash: Optional[str] = None
    source: RefSource = "shelf"
    view: Optional[str] = None   # turn-view qualifier: front|profile|back|threequarter; None for non-turn kinds


@dataclass(frozen=True)
class ReferenceBundle:
    """Ordered, role-tagged ref set for one shot."""
    assets: tuple[RefAsset, ...] = field(default_factory=tuple)

    def by_role(self, role: RefRole) -> tuple["RefAsset", ...]:
        return tuple(a for a in self.assets if a.role == role)

    def subjects(self) -> tuple[str, ...]:
        seen, out = set(), []
        for a in self.assets:
            if a.role == "identity" and a.subject not in seen:
                seen.add(a.subject)
                out.append(a.subject)
        return tuple(out)

    def hero_subjects(self) -> frozenset[str]:
        """Subjects with at least one USABLE hero/canonical identity ref."""
        return frozenset(
            a.subject for a in self.assets
            if a.role == "identity" and a.is_hero
        )

    def view_path(self, view: str) -> Optional[Path]:
        """Path of the first asset matching a named view; None when absent.

        'hero' → the first is_hero asset; otherwise match RefAsset.view, then RefAsset.kind
        (so 'fullbody' resolves via kind). Pure projection — callers apply their own
        fail-closed policy on a None return.
        """
        if view == "hero":
            return next((a.path for a in self.assets if a.is_hero), None)
        hit = next((a for a in self.assets if a.view == view), None)
        if hit is None:
            hit = next((a for a in self.assets if a.kind == view), None)
        return hit.path if hit is not None else None

    def paths(self) -> list[str]:
        return [str(a.path) for a in self.assets]

    def sheet(self) -> Optional[RefAsset]:
        """The composite sheet asset for this bundle, if one is attached; else None."""
        return next(
            (a for a in self.assets if a.kind == "sheet" or a.role == "sheet"), None
        )

    def canonical_ref_images(self, *, prefer_sheet: bool = True) -> tuple["RefAsset", ...]:
        """The canonical image set for a surface.

        When prefer_sheet and a sheet is present, return EXACTLY (sheet,) — a
        REPLACEMENT for the individual refs, so a downstream cap can never drop it.
        Otherwise the non-sheet images in declared order.
        """
        if prefer_sheet:
            s = self.sheet()
            if s is not None:
                return (s,)
        return tuple(a for a in self.assets if a.kind != "sheet")

    def fell_back(self) -> bool:
        """True when no sheet is attached — a sheet-preferring caller fell back to
        individuals (anti-masking signal: may be reading REC-76's broken base/pool path)."""
        return self.sheet() is None
