"""Immutable domain objects representing filesystem state.

This module defines :class:`TrackManifest` and
:class:`TrackManifestCollection`, which are the output of the
:class:`~scanner.scanner.FileScanner`.

These objects contain only filesystem information and must never
depend on SQLAlchemy or any database logic.
"""

from __future__ import annotations

from collections.abc import Iterator
from dataclasses import dataclass


@dataclass(frozen=True)
class TrackManifest:
    """Immutable record of a single audio file on disk.

    Attributes:
        relative_path: Path relative to the music directory root.
        original_filename: The file's name without directory components.
        file_size: File size in bytes.
        sha256: SHA-256 hash of the file contents (hex digest).
    """

    relative_path: str
    original_filename: str
    file_size: int
    sha256: str


@dataclass
class TrackManifestCollection:
    """Collection of :class:`TrackManifest` objects.

    Provides lookups by relative path and SHA-256 hash, and exposes
    duplicate detection results.

    Attributes:
        manifests: Tuple of all manifests in the collection.
    """

    manifests: tuple[TrackManifest, ...]

    def __init__(self, manifests: list[TrackManifest] | None = None) -> None:
        """Initialize the collection.

        Args:
            manifests: Optional initial list of manifests.  The list
                is converted to a tuple to ensure immutability.
        """
        self.manifests = tuple(manifests) if manifests is not None else ()

    def __len__(self) -> int:
        """Return the number of manifests in the collection."""
        return len(self.manifests)

    def __iter__(self) -> Iterator[TrackManifest]:
        """Iterate over manifests in the collection."""
        return iter(self.manifests)

    @property
    def paths(self) -> set[str]:
        """Return a set of all relative paths in the collection."""
        return {m.relative_path for m in self.manifests}

    def find_by_path(self, relative_path: str) -> TrackManifest | None:
        """Find a manifest by its relative path.

        Args:
            relative_path: The relative path to search for.

        Returns:
            The matching :class:`TrackManifest`, or ``None`` if not found.
        """
        for m in self.manifests:
            if m.relative_path == relative_path:
                return m
        return None

    def find_by_sha256(self, sha256: str) -> list[TrackManifest]:
        """Find all manifests matching a SHA-256 hash.

        Args:
            sha256: The SHA-256 hash to search for.

        Returns:
            A list of all manifests with the given hash.  Multiple
            results indicate duplicate files.
        """
        return [m for m in self.manifests if m.sha256 == sha256]

    def get_duplicates(self) -> dict[str, list[TrackManifest]]:
        """Return a mapping of SHA-256 hashes to duplicate manifests.

        Only hashes that appear more than once are included.

        Returns:
            A dict where keys are SHA-256 hashes and values are lists
            of manifests sharing that hash.  Hashes with a single
            manifest are excluded.
        """
        groups: dict[str, list[TrackManifest]] = {}
        for m in self.manifests:
            groups.setdefault(m.sha256, []).append(m)
        return {h: manifests for h, manifests in groups.items() if len(manifests) > 1}
