"""Shared FastAPI dependencies — the composition root for request-scoped wiring.

Concrete adapters are bound to ports here so routers and services stay
decoupled from infrastructure. Each request gets its own repositories/services
bound to the request-scoped DB session; stateless adapters (hasher, token
service) are process-cached.
"""

from collections.abc import AsyncIterator
from functools import lru_cache
from typing import Annotated

from fastapi import Depends, Query
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy.ext.asyncio import AsyncSession

from app.application.auth_service import AuthService
from app.application.metadata_service import MetadataEnrichmentService
from app.application.streaming_service import StreamingService
from app.application.subsonic_auth_service import SubsonicAuthService
from app.application.upload_service import UploadService
from app.application.user_service import UserService
from app.core.config import get_settings
from app.core.security import Argon2PasswordHasher, JwtTokenService, SubsonicPasswordCipher
from app.domain.entities import User
from app.domain.errors import AuthenticationError, PermissionDeniedError
from app.domain.ports import FileStorage, PasswordHasher, SubsonicCipher, TokenService
from app.infrastructure.db import get_sessionmaker
from app.infrastructure.db.repositories import (
    SqlAlchemyAlbumRepository,
    SqlAlchemyArtistRepository,
    SqlAlchemyHistoryRepository,
    SqlAlchemyLikeRepository,
    SqlAlchemyPlaylistRepository,
    SqlAlchemyRefreshTokenRepository,
    SqlAlchemyTrackRepository,
    SqlAlchemyUserRepository,
)
from app.infrastructure.metadata.acoustid import AcoustIdHttpClient
from app.infrastructure.metadata.fingerprint import FpcalcFingerprinter
from app.infrastructure.metadata.tags import MutagenTagReader
from app.infrastructure.sources.registry import SourceRegistry, build_source_registry
from app.infrastructure.storage.provider import get_file_storage
from app.workers.queue import enqueue_enrich


async def get_session() -> AsyncIterator[AsyncSession]:
    """Request-scoped DB session. Commits on success, rolls back on exception."""
    session = get_sessionmaker()()
    try:
        yield session
        await session.commit()
    except Exception:
        await session.rollback()
        raise
    finally:
        await session.close()


SessionDep = Annotated[AsyncSession, Depends(get_session)]


# -- stateless adapters (process-cached) ---------------------------------------
@lru_cache
def get_password_hasher() -> PasswordHasher:
    return Argon2PasswordHasher()


@lru_cache
def get_token_service() -> TokenService:
    return JwtTokenService(get_settings())


@lru_cache
def get_subsonic_cipher() -> SubsonicCipher:
    return SubsonicPasswordCipher(get_settings().subsonic_secret_key.get_secret_value())


@lru_cache
def get_source_registry() -> SourceRegistry:
    return build_source_registry(get_settings())


SourceRegistryDep = Annotated[SourceRegistry, Depends(get_source_registry)]


# -- request-scoped services ---------------------------------------------------
def get_auth_service(session: SessionDep) -> AuthService:
    return AuthService(
        users=SqlAlchemyUserRepository(session),
        refresh_tokens=SqlAlchemyRefreshTokenRepository(session),
        hasher=get_password_hasher(),
        tokens=get_token_service(),
    )


def get_user_service(session: SessionDep) -> UserService:
    return UserService(
        users=SqlAlchemyUserRepository(session),
        refresh_tokens=SqlAlchemyRefreshTokenRepository(session),
        hasher=get_password_hasher(),
    )


def get_subsonic_auth_service(session: SessionDep) -> SubsonicAuthService:
    return SubsonicAuthService(
        users=SqlAlchemyUserRepository(session),
        cipher=get_subsonic_cipher(),
    )


AuthServiceDep = Annotated[AuthService, Depends(get_auth_service)]
UserServiceDep = Annotated[UserService, Depends(get_user_service)]
SubsonicAuthServiceDep = Annotated[SubsonicAuthService, Depends(get_subsonic_auth_service)]


# -- file storage (process-cached) ---------------------------------------------
FileStorageDep = Annotated[FileStorage, Depends(get_file_storage)]


def get_upload_service(session: SessionDep, storage: FileStorageDep) -> UploadService:
    settings = get_settings()
    return UploadService(
        tracks=SqlAlchemyTrackRepository(session),
        artists=SqlAlchemyArtistRepository(session),
        storage=storage,
        tmp_dir=settings.upload_tmp_dir,
        enqueue_enrich=enqueue_enrich,
    )


def get_streaming_service(session: SessionDep, storage: FileStorageDep) -> StreamingService:
    return StreamingService(
        tracks=SqlAlchemyTrackRepository(session),
        storage=storage,
    )


def get_metadata_service(
    session: SessionDep, storage: FileStorageDep
) -> MetadataEnrichmentService:
    """Wires the §6.2 fingerprint/AcoustID adapters for read-only, inline use
    (the metadata editor's "find matches" — §A7). The full pipeline (incl.
    cover art) stays in the worker (`tasks/enrich_task.py`)."""
    settings = get_settings()
    api_key = settings.acoustid_api_key.get_secret_value() if settings.acoustid_api_key else None
    acoustid = AcoustIdHttpClient(
        api_key=api_key,
        user_agent=settings.musicbrainz_user_agent,
        api_url=settings.acoustid_api_url,
    )
    return MetadataEnrichmentService(
        tracks=SqlAlchemyTrackRepository(session),
        artists=SqlAlchemyArtistRepository(session),
        albums=SqlAlchemyAlbumRepository(session),
        storage=storage,
        tag_reader=MutagenTagReader(),
        fingerprinter=FpcalcFingerprinter(settings.fpcalc_path),
        acoustid=acoustid,
        acoustid_trust_score=settings.acoustid_trust_score,
    )


UploadServiceDep = Annotated[UploadService, Depends(get_upload_service)]
StreamingServiceDep = Annotated[StreamingService, Depends(get_streaming_service)]
MetadataServiceDep = Annotated[MetadataEnrichmentService, Depends(get_metadata_service)]


# -- library repository deps ---------------------------------------------------
def get_track_repository(session: SessionDep) -> SqlAlchemyTrackRepository:
    return SqlAlchemyTrackRepository(session)


def get_artist_repository(session: SessionDep) -> SqlAlchemyArtistRepository:
    return SqlAlchemyArtistRepository(session)


def get_album_repository(session: SessionDep) -> SqlAlchemyAlbumRepository:
    return SqlAlchemyAlbumRepository(session)


def get_playlist_repository(session: SessionDep) -> SqlAlchemyPlaylistRepository:
    return SqlAlchemyPlaylistRepository(session)


def get_like_repository(session: SessionDep) -> SqlAlchemyLikeRepository:
    return SqlAlchemyLikeRepository(session)


def get_history_repository(session: SessionDep) -> SqlAlchemyHistoryRepository:
    return SqlAlchemyHistoryRepository(session)


TrackRepoDep = Annotated[SqlAlchemyTrackRepository, Depends(get_track_repository)]
ArtistRepoDep = Annotated[SqlAlchemyArtistRepository, Depends(get_artist_repository)]
AlbumRepoDep = Annotated[SqlAlchemyAlbumRepository, Depends(get_album_repository)]
PlaylistRepoDep = Annotated[SqlAlchemyPlaylistRepository, Depends(get_playlist_repository)]
LikeRepoDep = Annotated[SqlAlchemyLikeRepository, Depends(get_like_repository)]
HistoryRepoDep = Annotated[SqlAlchemyHistoryRepository, Depends(get_history_repository)]


# -- current user / authorization ----------------------------------------------
# auto_error=False: we raise domain AuthenticationError (mapped to 401) so the
# error envelope stays consistent with the rest of the API.
_bearer = HTTPBearer(auto_error=False)
BearerDep = Annotated[HTTPAuthorizationCredentials | None, Depends(_bearer)]


async def get_current_user(credentials: BearerDep, auth: AuthServiceDep) -> User:
    if credentials is None:
        raise AuthenticationError("Missing bearer token.")
    return await auth.authenticate_access(credentials.credentials)


CurrentUser = Annotated[User, Depends(get_current_user)]


async def get_current_superuser(user: CurrentUser) -> User:
    if not user.is_superuser:
        raise PermissionDeniedError("Administrator privileges required.")
    return user


SuperUser = Annotated[User, Depends(get_current_superuser)]


async def get_streaming_user(
    auth: AuthServiceDep,
    credentials: BearerDep,
    token: str | None = None,
) -> User:
    """Authenticate a stream request.

    The browser ``<audio>`` element cannot send an ``Authorization`` header, so
    the access token is accepted as a ``?token=`` query param; native clients may
    still use a bearer header. Either way it's the same access token.
    """
    raw = token or (credentials.credentials if credentials else None)
    if not raw:
        raise AuthenticationError("Missing access token.")
    return await auth.authenticate_access(raw)


StreamUser = Annotated[User, Depends(get_streaming_user)]


# -- subsonic (/rest) authentication -------------------------------------------
# Subsonic puts credentials in the query string: u + (t & s) | p, plus c/v/f.
# The dep extracts them and delegates verification to the service; domain errors
# propagate to the rest-aware exception handler, which renders the Subsonic
# error envelope (HTTP 200). HTTPS is mandatory — the secret rides in the URL.
async def get_subsonic_user(
    service: SubsonicAuthServiceDep,
    u: Annotated[str | None, Query()] = None,
    t: Annotated[str | None, Query()] = None,
    s: Annotated[str | None, Query()] = None,
    p: Annotated[str | None, Query()] = None,
) -> User:
    return await service.authenticate(username=u, token=t, salt=s, password=p)


SubsonicUser = Annotated[User, Depends(get_subsonic_user)]


async def get_subsonic_format(f: Annotated[str | None, Query()] = None) -> str | None:
    """The requested response format (``f``): ``xml`` (default) or ``json``."""
    return f


SubsonicFormat = Annotated[str | None, Depends(get_subsonic_format)]
