"""DMS asset service layer.

Wraps the platform ``simorgh.apps.storage.services`` functions with DMS-specific
context (``app_context="dms"``), chunk management, and domain validation.

Public surface:
  * ``upload_asset``         — single-request direct upload → FileMetadata
  * ``initiate_session``     — create a resumable upload session
  * ``receive_chunk``        — save a chunk to provider + increment counter
  * ``complete_session``     — assemble chunks → FileMetadata, clean up temp data
"""

from __future__ import annotations

import io
from typing import BinaryIO

from simorgh.apps.dms.assets.constants import APP_CONTEXT, CHUNK_PATH_PREFIX
from simorgh.apps.dms.common.exceptions import ChunkError, UploadSessionError
from simorgh.apps.storage.models import FileMetadata, UploadSession, UploadSessionStatus
from simorgh.apps.storage.providers import StorageError, get_default_provider
from simorgh.apps.storage.services import (
    complete_upload_session as _platform_complete,
    create_upload_session as _platform_create_session,
    receive_chunk as _platform_receive_chunk,
    store_file as _platform_store_file,
)


def upload_asset(
    *,
    filename: str,
    content: BinaryIO,
    content_type: str = "",
    tenant_id: int | None = None,
    organization_node_id: int | None = None,
    uploaded_by_id: int | None = None,
    folder_id: str | None = None,
) -> FileMetadata:
    """Perform a single-request direct upload and return the created FileMetadata.

    Delegates to ``storage.services.store_file`` with ``app_context="dms"``.
    """
    return _platform_store_file(
        filename=filename,
        content=content,
        content_type=content_type,
        tenant_id=tenant_id,
        organization_node_id=organization_node_id,
        uploaded_by_id=uploaded_by_id,
        folder_id=folder_id,
        app_context=APP_CONTEXT,
    )


def initiate_session(
    *,
    filename: str,
    content_type: str = "",
    total_size_bytes: int,
    total_chunks: int = 1,
    expected_checksum_sha256: str = "",
    tenant_id: int | None = None,
    organization_node_id: int | None = None,
    uploaded_by_id: int | None = None,
    folder_id: str | None = None,
) -> UploadSession:
    """Create and return a new resumable upload session tagged as DMS."""
    return _platform_create_session(
        filename=filename,
        content_type=content_type,
        total_size_bytes=total_size_bytes,
        total_chunks=total_chunks,
        expected_checksum_sha256=expected_checksum_sha256,
        tenant_id=tenant_id,
        organization_node_id=organization_node_id,
        uploaded_by_id=uploaded_by_id,
        folder_id=folder_id,
        app_context=APP_CONTEXT,
    )


def receive_chunk(
    session: UploadSession,
    chunk_index: int,
    chunk_data: BinaryIO,
) -> UploadSession:
    """Persist a chunk to the storage provider and increment the session counter.

    Chunks are stored at:
      ``sessions/{session.public_id}/chunk_{chunk_index:05d}``

    The platform ``receive_chunk`` is called afterwards to update the counter
    and transition the session status to UPLOADING.

    Raises ``UploadSessionError`` if the session is in a terminal state.
    Raises ``ChunkError`` if the provider write fails.
    """
    if session.is_expired:
        raise UploadSessionError(
            f"Upload session {session.public_id} has expired."
        )
    if session.status in (UploadSessionStatus.COMPLETED, UploadSessionStatus.FAILED):
        raise UploadSessionError(
            f"Upload session {session.public_id} is in terminal state '{session.status}'."
        )

    provider = get_default_provider()
    chunk_path = f"{CHUNK_PATH_PREFIX}/{session.public_id}/chunk_{chunk_index:05d}"
    try:
        provider.save(chunk_path, chunk_data)
    except StorageError as exc:
        raise ChunkError(
            f"Failed to store chunk {chunk_index} for session {session.public_id}."
        ) from exc

    return _platform_receive_chunk(session)


def complete_session(session: UploadSession) -> FileMetadata:
    """Assemble all chunks, create a FileMetadata record, and clean up temp data.

    Steps:
    1. Read each chunk from the provider in index order.
    2. Concatenate into a BytesIO stream.
    3. Delegate to ``storage.services.complete_upload_session``.
    4. Best-effort delete of temporary chunk files.

    Raises ``ChunkError`` if any expected chunk is missing.
    Raises ``UploadSessionError`` if the session state is invalid.
    """
    if session.is_expired:
        raise UploadSessionError(
            f"Upload session {session.public_id} has expired."
        )
    if session.status in (UploadSessionStatus.COMPLETED, UploadSessionStatus.FAILED):
        raise UploadSessionError(
            f"Upload session {session.public_id} is already in terminal state '{session.status}'."
        )
    if not session.all_chunks_received:
        raise UploadSessionError(
            f"Upload session {session.public_id}: expected {session.total_chunks} chunks, "
            f"received {session.received_chunks}. Cannot complete."
        )

    provider = get_default_provider()
    assembled = io.BytesIO()

    for i in range(session.total_chunks):
        chunk_path = f"{CHUNK_PATH_PREFIX}/{session.public_id}/chunk_{i:05d}"
        try:
            with provider.open(chunk_path) as chunk_fh:
                assembled.write(chunk_fh.read())
        except (StorageError, OSError, FileNotFoundError) as exc:
            raise ChunkError(
                f"Chunk {i} missing for session {session.public_id}."
            ) from exc

    assembled.seek(0)

    file_asset = _platform_complete(session, assembled_content=assembled)

    # Best-effort cleanup of temporary chunk files.
    for i in range(session.total_chunks):
        chunk_path = f"{CHUNK_PATH_PREFIX}/{session.public_id}/chunk_{i:05d}"
        try:
            provider.delete(chunk_path)
        except Exception:  # noqa: BLE001
            pass

    return file_asset
