# src/chitai/controllers/book.py

# TODO: Make the endpoints more consistent (i.e sometimes book_id is a path parameter, other times it a query param)

# Standard library
from typing import Annotated

# Third-party libraries
from litestar import Controller, get, post, put, patch, delete
from litestar.di import Provide
from litestar.params import Dependency, Body
from litestar.enums import RequestEncodingType
from litestar.response import File, Stream
from litestar.exceptions import HTTPException
from litestar.status_codes import HTTP_400_BAD_REQUEST
from litestar.datastructures import UploadFile
from advanced_alchemy.service.pagination import OffsetPagination
from advanced_alchemy.filters import CollectionFilter
from advanced_alchemy.service import FilterTypeT

# Local imports
from chitai.services import dependencies as deps
from chitai import schemas as s
from chitai.database import models as m
from chitai.services import BookService, BookProgressService


class BookController(Controller):
    """Controller for managing book-related operations."""

    path = "/books"
    dependencies = {
        "books_service": Provide(deps.provide_book_service),
        "progress_service": Provide(deps.provide_progress_service),
        "library_service": Provide(deps.provide_library_service),
        "library": Provide(deps.get_library_by_id),
        "authors_filter": Provide(deps.provide_authors_filter, sync_to_thread=False),
        "publishers_filter": Provide(
            deps.provide_publishers_filter, sync_to_thread=False
        ),
        "tags_filter": Provide(deps.provide_tags_filter, sync_to_thread=False),
        "bookshelves_filter": Provide(
            deps.provide_bookshelves_filter, sync_to_thread=False
        ),
        "progress_filter": Provide(deps.provide_progress_filter, sync_to_thread=False),
        "libraries_filter": Provide(
            deps.provide_libraries_filter, sync_to_thread=False
        ),
        "book_filters": Provide(deps.provide_book_filters, sync_to_thread=False),
    } | deps.create_book_filter_dependencies(
        {
            "id_filter": int,
            "pagination_type": "limit_offset",
            "sort_field": "title",
            "search": ["title"],
            "search_ignore_case": True,
        }
    )

    @post(request_max_body_size=None)
    async def create_book(
        self,
        books_service: BookService,
        library: m.Library,
        data: Annotated[s.BookCreate, Body(media_type=RequestEncodingType.MULTI_PART)],
    ) -> s.BookRead:
        """
        Create a new book with metadata and files.

        Accepts book metadata and associated files, processes them through the
        book service, and returns the created book details.

        Path Parameters:
            library_id: The ID of the library the book belongs to.

        Request Body:
            data: Book creation data including metadata and files.

        Injected Dependencies:
            books_service: The book service for database operations.
            library: The library the book belongs to.

        Returns:
            The created book as a BookRead schema.

        """

        result = await books_service.create_book(data, library)
        book = await books_service.get(result.id)
        return books_service.to_schema(book, schema_type=s.BookRead)

    @post("fromFiles", request_max_body_size=None)
    async def create_books_without_metadata(
        self,
        books_service: BookService,
        library: m.Library,
        data: Annotated[
            s.BooksCreateFromFiles, Body(media_type=RequestEncodingType.MULTI_PART)
        ],
    ) -> OffsetPagination[s.BookRead]:
        """
        Create multiple books from uploaded files.

        Groups files by directory and creates separate books for each group.
        Metadata is automatically extracted from the files.

        Request Body:
            data: Container with list of uploaded files.

        Injected Dependencies:
            books_service: The book service for database operations.
            library: The library the books belong to.

        Returns:
            Paginated list of created books.
        """
        try:
            results = await books_service.create_many_from_files(data, library)
        except ValueError:
            raise HTTPException(
                status_code=HTTP_400_BAD_REQUEST, detail="Must upload at least one file"
            )

        books = await books_service.list(
            CollectionFilter("id", [result.id for result in results])
        )
        return books_service.to_schema(books, schema_type=s.BookRead)

    @get(path="/{book_id:int}")
    async def get_book_by_id(
        self, book_id: int, books_service: BookService, current_user: m.User
    ) -> s.BookRead:
        """
        Retrieve book details by ID.

        Path Parameters:
            book_id: The ID of the book to retrieve.

        Injected Dependencies:
            books_service: The book service for database operations.
            current_user: The authenticated user making the request.

        Returns:
            The book details as a BookRead schema.
        """
        book = await books_service.get(book_id)
        return books_service.to_schema(book, schema_type=s.BookRead)

    @get()
    async def list_books(
        self,
        books_service: BookService,
        book_filters: Annotated[
            list[FilterTypeT], Dependency(skip_validation=True)
        ] = [],
        filters: Annotated[list[FilterTypeT], Dependency(skip_validation=True)] = [],
    ) -> OffsetPagination[s.BookRead]:
        """
        List books with filtering, pagination, and search.

        Supports filtering by library, tags, authors, shelves, and publishers.

        Query Parameters:
            library_id: The library to list books from.
            tags: Optional list of tag IDs to filter by.
            authors: Optional list of author IDs to filter by.
            shelves: Optional list of bookshelf IDs to filter by.
            publishers: Optional list of publisher IDs to filter by.
            id: Injected filter. Filter by book ID.
            sort_field: Injected filter. Field to sort by (default: title).
            search: Injected filter. Search on title field.
            search_ignore_case: Injected filter. Case-insensitive search.

        Injected Dependencies:
            books_service: The book service for database operations.
            current_user: The authenticated user making the request.
            filters: Pre-constructed filters from dependencies.

        Returns:
            Paginated list of books matching the criteria.
        """

        books, total = await books_service.list_and_count(*filters, *book_filters)
        return books_service.to_schema(books, total, filters, schema_type=s.BookRead)

    @patch(path="{book_id:int}")
    async def update_book_metadata(
        self,
        book_id: int,
        data: Annotated[
            s.BookMetadataUpdate, Body(media_type=RequestEncodingType.JSON)
        ],
        books_service: BookService,
        library: m.Library,
    ) -> s.BookRead:
        """
        Update a book's metadata and optionally reorganize files if necessary.

        Allows editing book metadata such as title, author, and series information.
        If metadata changes affect the file organization path, files are
        automatically reorganized.

        Path Parameters:
            book_id: The ID of the book to update.

        Request Body:
            data: Updated metadata for the book.

        Injected Dependencies:
            books_service: The book service for database operations.
            library: The library containing the book.

        Returns:
            The updated book as a BookRead schema.
        """
        await books_service.update_book(book_id, data, library)
        book = await books_service.get(book_id)
        return books_service.to_schema(book, schema_type=s.BookRead)

    @put(path="{book_id:int}/cover")
    async def update_cover(
        self,
        book_id: int,
        data: Annotated[UploadFile, Body(media_type=RequestEncodingType.MULTI_PART)],
        books_service: BookService,
        library: m.Library,
    ) -> s.BookRead:
        """
        Update's a book's cover image.

        Allows uploading a new cover image that replaces the current cover.

        Path Parameters:
            book_id: The ID of the book to update.

        Request Body:
            cover_image: The new cover image for the book.

        Injected Dependencies:
            books_service: The book service for database operations.
            library: The library containing the book.

        Returns:
            The updated book as a BookRead schema.
        """

        await books_service.update_book(book_id, {"cover_image": data}, library)
        updated_book = await books_service.get(book_id)
        return books_service.to_schema(updated_book, schema_type=s.BookRead)

    @get(path="download/{book_id:int}/{file_id:int}")
    async def get_file(
        self, book_id: int, file_id: int, books_service: BookService
    ) -> File:
        """
        Download a single file from a book.

        Path Parameters:
            book_id: The ID of the book containing the file.
            file_id: The ID of the file to download.

        Injected Dependencies:
            books_service: The book service for database operations.

        Returns:
            The file ready for download.
        """
        return await books_service.get_file(book_id, file_id)

    @get(path="download")
    async def get_files(
        self, book_ids: list[int], library_id: int, books_service: BookService
    ) -> Stream:
        """
        Download multiple books as a single ZIP file.

        Compresses all files from specified books into a single archive for download.

        Query Parameters:
            book_ids: List of book IDs to include in the download.
            library_id: The library containing the books.

        Injected Dependencies:
            books_service: The book service for database operations.

        Returns:
            A stream of the compressed ZIP file.
        """
        return Stream(
            books_service.get_files(book_ids, library_id),
            headers={
                "Content-Disposition": 'attachment; filename="download.zip"',
                "Content-Type": "application/zip",
            },
        )

    @post(path="/{book_id:int}/files", request_max_body_size=None)
    async def add_book_files(
        self,
        book_id: int,
        data: Annotated[
            list[UploadFile], Body(media_type=RequestEncodingType.MULTI_PART)
        ],
        library: m.Library,
        books_service: BookService,
    ) -> s.BookRead:
        """
        Add files to an existing book.

        Path Parameters:
            book_id: The ID of the book to modify

        Request Body:
            files: The files to add to the book

        Injected Dependencies:
            library: The library containing the book.
            books_service: The book service for database operations.

        Returns:
            The modified book

        """

        await books_service.add_files(book_id, data, library)
        book = await books_service.get(book_id)
        return books_service.to_schema(book, schema_type=s.BookRead)

    @delete(path="{book_id:int}/files")
    async def delete_book_files(
        self,
        library: m.Library,
        books_service: BookService,
        book_id: int,
        file_ids: list[int],
        delete_files: bool = False,
    ) -> None:
        """
        Delete specific files from a book.

        Removes the specified files from both the database and optionally
        remove them from the filesystem.

        Path Parameters:
            book_id: The ID of the book.

        Query Parameters:
            file_ids: List of file IDs to delete.
            delete_files: If True, also delete files from the filesystem.

        Injected Dependencies:
            books_service: The book service for database operations.
            library: The library containing the book.
        """

        if len(file_ids) == 0:
            raise HTTPException(
                status_code=HTTP_400_BAD_REQUEST,
                detail="At least one file to delete is required",
            )

        await books_service.remove_files(book_id, file_ids, delete_files, library)

    @delete(path="/")
    async def delete_books(
        self,
        books_service: BookService,
        library: m.Library,
        book_ids: list[int],
        delete_files: bool = False,
    ) -> None:
        """
        Delete multiple books by ID.

        Removes books from the database and optionally deletes associated files
        from the filesystem.

        Query Parameters:
            book_ids: List of book IDs to delete.
            delete_files: If True, also delete associated files from the filesystem.
            library_id: The library the given books belong to.

        Injected Dependencies:
            books_service: The book service for database operations.
            library: The library containing the books.

        """

        await books_service.delete_books(book_ids, library, delete_files=delete_files)

    @post(path="/progress/{book_id:int}")
    async def update_progress(
        self,
        book_id: int,
        current_user: m.User,
        data: s.BookProgressCreate,
        progress_service: BookProgressService,
    ) -> None:
        """
        Update or create reading progress for a book.

        Records the user's progress on a book, creating or updating the progress entry.

        Path Parameters:
            book_id: The ID of the book.

        Request Body:
            data: Progress data to update.

        Injected Dependencies:
            current_user: The authenticated user.
            progress_service: The progress service for database operations.

        """
        progress = await progress_service.get_one_or_none(
            m.BookProgress.user_id == current_user.id, m.BookProgress.book_id == book_id
        )
        dict_data = data.model_dump() | {"user_id": current_user.id, "book_id": book_id}
        await progress_service.upsert(dict_data, progress.id if progress else None)

    @post(path="progress")
    async def set_book_progress_batch(
        self,
        book_ids: list[int],
        data: Annotated[s.BookProgressCreate, Body()],
        current_user: m.User,
        progress_service: BookProgressService,
    ) -> None:
        """
        Marks all the user progress on the books matching the IDs as completed.

        Query Parameters:
            book_ids: List of book IDs to mark as completed.

        Injected Dependencies:
            current_user: The authenticated user.
            progress_service: The progress service for database operations.
        """

        # TODO Optimize this by performing a batch query and batch upsert
        for book_id in book_ids:
            existing_progress = await progress_service.get_one_or_none(
                m.BookProgress.user_id == current_user.id,
                m.BookProgress.book_id == book_id,
            )

            progress_data = data.model_dump() | {
                "user_id": current_user.id,
                "book_id": book_id,
            }

            await progress_service.upsert(
                progress_data, existing_progress.id if existing_progress else None
            )
