"""UploadService — handles user file uploads."""

import contextlib
import hashlib
import os
import tempfile
import uuid
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from pathlib import Path
from typing import Protocol

import anyio

from app.domain.entities.user import User
from app.domain.ports import ArtistRepository, FileStorage, TrackRepository

EnrichEnqueuer = Callable[[uuid.UUID], Awaitable[None]]


class UploadFileProtocol(Protocol):
    filename: str | None

    async def read(self, size: int = -1) -> bytes: ...


@dataclass(frozen=True)
class UploadResult:
    track_id: uuid.UUID
    title: str
    already_exists: bool


async def _stream_to_temp(upload: UploadFileProtocol, dest: Path) -> tuple[str, int]:
    h = hashlib.sha256()
    size = 0
    async with await anyio.open_file(dest, "wb") as out:
        while True:
            chunk = await upload.read(65536)
            if not chunk:
                break
            h.update(chunk)
            await out.write(chunk)
            size += len(chunk)
    return h.hexdigest(), size


class UploadService:
    def __init__(
        self,
        tracks: TrackRepository,
        artists: ArtistRepository,
        storage: FileStorage,
        tmp_dir: Path | None = None,
        enqueue_enrich: EnrichEnqueuer | None = None,
    ) -> None:
        self._tracks = tracks
        self._artists = artists
        self._storage = storage
        self._tmp_dir = tmp_dir
        self._enqueue_enrich = enqueue_enrich

    async def handle_upload(
        self,
        *,
        upload: UploadFileProtocol,
        user: User,
    ) -> UploadResult:
        filename = upload.filename or "unknown"
        ext = Path(filename).suffix.lower().lstrip(".") or "bin"
        title = Path(filename).stem or "Unknown"

        fd, tmp_str = tempfile.mkstemp(
            suffix=f".{ext}",
            dir=str(self._tmp_dir) if self._tmp_dir else None,
        )
        tmp_path = Path(tmp_str)
        try:
            os.close(fd)
            sha256_hex, file_size = await _stream_to_temp(upload, tmp_path)

            existing = await self._tracks.get_by_source("upload", sha256_hex)
            if existing is not None:
                return UploadResult(
                    track_id=existing.id,
                    title=existing.title,
                    already_exists=True,
                )

            track_id = uuid.uuid4()
            key = f"tracks/{str(track_id)[:2]}/{track_id}.{ext}"

            await self._storage.save_file(key, tmp_path)
            try:
                artist = await self._artists.get_or_create("Unknown Artist")
                track = await self._tracks.add(
                    id=track_id,
                    title=title,
                    artist_id=artist.id,
                    storage_uri=key,
                    file_format=ext,
                    file_size=file_size,
                    source="upload",
                    source_id=sha256_hex,
                    metadata_status="pending",
                    added_by=user.id,
                )
            except Exception:
                with contextlib.suppress(Exception):
                    await self._storage.delete(key)
                raise

            if self._enqueue_enrich is not None:
                await self._enqueue_enrich(track.id)

            return UploadResult(
                track_id=track.id,
                title=track.title,
                already_exists=False,
            )
        finally:
            await anyio.Path(tmp_path).unlink(missing_ok=True)
