"""File scanner for AI MusiMuse.

This module implements :class:`FileScanner`, which recursively
discovers supported audio files in a music directory, calculates
SHA-256 hashes using streamed reads, and produces a
:class:`~scanner.manifest.TrackManifestCollection`.

The Scanner must not access SQLite or import SQLAlchemy.
"""

from __future__ import annotations

import hashlib
from pathlib import Path

from core.formats import SUPPORTED_EXTENSIONS
from log.logger import get_logger
from scanner.manifest import TrackManifest, TrackManifestCollection

_CHUNK_SIZE: int = 65536

_logger = get_logger(__name__)


def calculate_sha256(file_path: Path) -> str:
    """Calculate the SHA-256 hash of a file using streamed reads.

    The file is read in chunks to avoid loading the entire file
    into memory.

    Args:
        file_path: Path to the file to hash.

    Returns:
        The SHA-256 hash as a hex digest string.
    """
    hasher = hashlib.sha256()
    with open(file_path, "rb") as f:
        while True:
            chunk = f.read(_CHUNK_SIZE)
            if not chunk:
                break
            hasher.update(chunk)
    return hasher.hexdigest()


class FileScanner:
    """Recursively scans a music directory for supported audio files.

    Attributes:
        music_directory: Root directory to scan.
    """

    def __init__(self, music_directory: Path) -> None:
        """Initialize the scanner.

        Args:
            music_directory: Root directory to scan for audio files.
        """
        self._music_directory = music_directory

    @property
    def music_directory(self) -> Path:
        """Return the configured music directory."""
        return self._music_directory

    def scan(self) -> TrackManifestCollection:
        """Scan the music directory and return a manifest collection.

        Walks the directory tree recursively, filters by supported
        extensions, calculates SHA-256 hashes, and builds a
        :class:`TrackManifestCollection`.

        Returns:
            A :class:`TrackManifestCollection` containing one
            :class:`TrackManifest` per supported audio file found.
        """
        _logger.info("scan.started", directory=str(self._music_directory))

        manifests: list[TrackManifest] = []

        if not self._music_directory.exists():
            _logger.warning(
                "scan.directory_missing", directory=str(self._music_directory)
            )
            return TrackManifestCollection(manifests)

        for file_path in sorted(self._music_directory.rglob("*")):
            if not file_path.is_file():
                continue
            if file_path.suffix.lower() not in SUPPORTED_EXTENSIONS:
                continue

            relative_path = file_path.relative_to(self._music_directory).as_posix()
            sha256 = calculate_sha256(file_path)
            file_size = file_path.stat().st_size

            manifests.append(
                TrackManifest(
                    relative_path=relative_path,
                    original_filename=file_path.name,
                    file_size=file_size,
                    sha256=sha256,
                )
            )

        collection = TrackManifestCollection(manifests)

        duplicates = collection.get_duplicates()
        if duplicates:
            for hash_val, dup_manifests in duplicates.items():
                paths = [m.relative_path for m in dup_manifests]
                _logger.warning(
                    "duplicate.detected",
                    sha256=hash_val,
                    paths=paths,
                )

        _logger.info(
            "scan.finished",
            files_found=len(manifests),
            duplicates=len(duplicates),
        )

        return collection
