import logging
from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path
from uuid import UUID

from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import selectinload
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession

from ..errors import AppError
from ..errors import ErrorCategory
from ..models import Document
from .base import ServiceBase

logger = logging.getLogger(__name__)


class DocumentError(AppError):
    """Raised when document operations fail."""


class MissingImageError(DocumentError):
    """Raised when a required image is missing."""


class UploadError(DocumentError):
    """Raised when uploaded content cannot be persisted safely."""


class DocumentAlreadyExistsError(DocumentError):
    """Raised when a document with the same filename already exists in the database."""


@dataclass(frozen=True)
class UploadJobResult:
    """Summary of created upload records."""

    document_id: UUID
    job_id: UUID
    stored_path: Path
    original_filename: str


class DocumentService(ServiceBase):
    """Thin service class for managing documents in the database."""

    #
    # CRUD Operations
    #

    async def create_document(
        self,
        document: Document,
        *,
        session: AsyncSession | None = None,
    ) -> Document:
        """Create a new document in the database."""
        async with self._session_scope(session) as _session:
            _session.add(document)
            try:
                await self._finalize(session=_session, caller_session=session, refresh=(document,))
            except IntegrityError as exc:
                raise DocumentAlreadyExistsError(
                    f"Document with id {document.id} already exists",
                    category=ErrorCategory.VALIDATION,
                    suggestion="Rename the file and try again.",
                ) from exc
            return document

    async def read_document(self, document_id: UUID, *, session: AsyncSession | None = None) -> Document:
        """Read an existing document from the database.

        The selectinload option is used to eagerly load related jobs for the document.
        """
        async with self._session_scope(session) as _session:
            document = await _session.get(
                Document,
                document_id,
                options=(selectinload(Document.jobs),),  # pyright: ignore[reportArgumentType]
            )
        if document is None:
            raise DocumentError(
                f"Document with id {document_id} not found",
                category=ErrorCategory.NOT_FOUND,
                suggestion="Re-upload the source document and retry.",
            )
        elif not Path(document.file_path).exists():
            raise MissingImageError(
                f"Document with id {document_id} is missing its image file in {self.settings.upload_dir:!s}",
                category=ErrorCategory.NOT_FOUND,
                suggestion="Re-upload the source document and retry.",
            )
        return document

    async def update_document(self, document: Document, *, session: AsyncSession | None = None) -> Document:
        """Update an existing document in the database."""
        async with self._session_scope(session) as _session:
            merged = await _session.merge(document)
            await self._finalize(session=_session, caller_session=session, refresh=(merged,))
            return merged

    async def delete_document(self, document: Document, *, session: AsyncSession | None = None) -> None:
        """Delete a document from the database."""
        async with self._session_scope(session) as _session:
            await _session.delete(document)
            await self._finalize(session=_session, caller_session=session)

    # Query Operations

    async def query_documents(
        self, *, filename: str | None = None, session: AsyncSession | None = None
    ) -> Sequence[Document]:
        """Query documents from the database based on provided filters."""
        async with self._session_scope(session) as _session:
            query = select(Document)
            if filename is not None:
                query = query.where(Document.filename == filename)
            result = await _session.exec(query)
            return result.all()

    async def list_documents(self, *, session: AsyncSession | None = None) -> Sequence[Document]:
        """List all documents in the database."""
        async with self._session_scope(session) as _session:
            result = await _session.exec(select(Document))
            return result.all()
