
from chitai.services import dependencies as deps
from chitai.database import models as m
from chitai.services.author import AuthorService
from chitai.services.filters.author import AuthorLibraryFilter
from chitai.services.filters.publisher import PublisherLibraryFilter
from chitai.services.filters.tags import TagLibraryFilter
from chitai.services.opds.models import Entry, Link, LinkTypes, LinkRelations
from chitai.services.opds.opds import create_acquisition_feed, create_navigation_feed, create_library_navigation_feed, create_collection_navigation_feed, create_pagination_links, create_search_link, get_opensearch_document
from chitai.services import BookService, ShelfService, LibraryService
from chitai.services.publisher import PublisherService
from chitai.services.tag import TagService
from litestar import Controller, Request, Response, get
from litestar.response import File
from litestar.di import Provide
from litestar.exceptions import HTTPException
from advanced_alchemy.filters import  CollectionFilter, LimitOffset, OrderBy
from chitai.middleware.basic_auth import basic_auth_mw
from urllib.parse import urlencode
from typing import Annotated
from litestar.params import Dependency
from advanced_alchemy.service import FilterTypeT



class OpdsController(Controller):
    """ Controller for managing OPDS endpoints """

    middleware=[basic_auth_mw]

    dependencies = {
        "user": Provide(deps.provide_user_via_basic_auth),
        "library_service": Provide(deps.provide_library_service),
        "library": Provide(deps.get_library_by_id),
        "books_service": Provide(deps.provide_book_service),
        "shelf_service": Provide(deps.provide_shelf_service),
        "tag_service": Provide(deps.provide_tag_service),
        "author_service": Provide(deps.provide_author_service),
        "publisher_service": Provide(deps.provide_publisher_service),
        "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,
        }
    )

    path = "/opds"

    @get()
    async def get_root_feed(self, library_service: LibraryService) -> Response:

        libraries = await library_service.list()

        entries = [
            Entry(
                id=f"/opds/library/{lib.id}",
                title=lib.name,
                link=[
                    Link(
                        title=lib.name,
                        href=f"/opds/library/{lib.id}",
                        rel=LinkRelations.SUBSECTION,
                        type=LinkTypes.NAVIGATION
                    )
                ]
            ) for lib in libraries
        ]

        feed = create_navigation_feed(
                id="/opds",
                title="Root",
                self_url="/opds",
                links=[  
                    Link(
                        rel="search",
                        href="/opds/opensearch",
                        type="application/opensearchdescription+xml",
                        title="Search books",
                    )
                ],
                entries=entries
            )
        
        return Response(
            feed,
            media_type="application/xml"
        )
    
    @get("/acquisition")
    async def get_acquisition_feed(
        self,
        request: Request,
        feed_id: str,
        feed_title: str,
        books_service: BookService,
        book_filters: Annotated[list[FilterTypeT], Dependency(skip_validation=True)] = [],
        filters: Annotated[list[FilterTypeT], Dependency(skip_validation=True)] = []
    ) -> Response:
        
        all_filters = [*filters, *book_filters]
        books, total = await books_service.list_and_count(*all_filters)
        
        limit, offset = extract_limit_offset(all_filters)

        links = []
        
        # Create pagination links if it is a paginated feed
        if request.query_params.get('paginated'):
            pagination = create_pagination_links(
                request=request,
                total=total,
                limit=limit,
                offset=offset,
                feed_title=feed_title,
                link_type=LinkTypes.ACQUISITION
            )
 
            links.extend([link for link in [pagination.next_link, pagination.prev_link] if link])
        
        # Add search link if this is a searchable feed
        if request.query_params.get('search'):
            links.append(create_search_link(request))
        
        # Create self URL
        self_url = f"{request.url.path}?{urlencode(list(request.query_params.items()), doseq=True)}"
        
        feed = create_acquisition_feed(
            id=feed_id,
            title=feed_title,
            url=self_url,
            books=books,
            links=links,
        )
        
        return Response(feed, media_type="application/xml")


    @get("/opensearch")
    async def opensearch(self, user: m.User, request: Request) -> Response:
            
        return Response(
            get_opensearch_document(
                base_url=f'/opds/search?{urlencode(list(request.query_params.items()), doseq=True)}&'
            ),
            media_type="application/xml"
        )
    
    @get("/library/{library_id:int}")
    async def get_library_feed(self, library: m.Library) -> Response:

        feed = create_library_navigation_feed(library)

        return Response(feed, media_type="application/xml")
    
    
    @get("/library/{library_id:int}/{collection_type:str}")
    async def get_library_collection_feed(
        self,
        collection_type: str,
        library: m.Library,
        user: m.User,
        author_service: AuthorService,
        shelf_service: ShelfService,
        tag_service: TagService,
        publisher_service: PublisherService,
        request: Request,
        filters: Annotated[list[FilterTypeT], Dependency(skip_validation=True)] = []
    ) -> Response:
        
        service_map = {
            'shelves': (shelf_service, lambda: shelf_service.list_and_count(
                *filters,
                CollectionFilter("library_id", values=[library.id]),
                OrderBy("name", "asc"),
                m.BookList.user_id == user.id
            )),
            'tags': (tag_service, lambda: tag_service.list_and_count(
                *filters,
                TagLibraryFilter(libraries=[library.id]),
                OrderBy("name", "asc"),
                uniquify=True,
            )),
            'authors': (author_service, lambda: author_service.list_and_count(
                *filters,
                AuthorLibraryFilter(libraries=[library.id]),
                OrderBy("name", "asc"),
                uniquify=True
            )),
            'publishers': (publisher_service, lambda: publisher_service.list_and_count(
                *filters,
                PublisherLibraryFilter(libraries=[library.id]),
                OrderBy("name", "asc"),
                uniquify=True
            ))
        }
    
        if collection_type not in service_map:
            raise HTTPException(status_code=404, detail="Collection type not found")
        
        _, fetch_items = service_map[collection_type]
        items, total = await fetch_items()
        links = []

        # Create pagination links if it is a paginated feed
        limit, offset = extract_limit_offset(filters)

        if request.query_params.get('paginated'):
            pagination = create_pagination_links(
                request=request,
                total=total,
                limit=limit,
                offset=offset,
                feed_title=collection_type,
                link_type=LinkTypes.ACQUISITION
            )
 
            links.extend([link for link in [pagination.next_link, pagination.prev_link] if link])
        
        feed = create_collection_navigation_feed(library, collection_type, items, links)
        return Response(feed, media_type="application/xml")
        

    
    @get("/search")
    async def search_books(
        self, books_service: BookService,
        request: Request,
        book_filters: Annotated[
            list[FilterTypeT], Dependency(skip_validation=True)
        ] = [],
        filters: Annotated[list[FilterTypeT], Dependency(skip_validation=True)] = []
    ) -> Response:
        
        filters = [*filters, *book_filters]

        books, total = await books_service.list_and_count(
            *filters
        )

        limit, offset = extract_limit_offset(filters)
        
        # Create pagination links
        pagination = create_pagination_links(
            request=request,
            total=total,
            limit=limit,
            offset=offset,
            feed_title="Search Results",
            link_type=LinkTypes.ACQUISITION
        )

        links = [link for link in [pagination.next_link, pagination.prev_link] if link]

        catalog_xml = create_acquisition_feed(
            id=f"/opds/search?q=q",
            title="Search results",
            url=f"/opds/search?q=q",
            books=books,
            links=links
        )

        return Response(catalog_xml, media_type="application/xml")
    
    @get(path="download/{book_id:int}/{file_id:int}")
    async def get_file(
        self, book_id: int, file_id: int, books_service: BookService
    ) -> File:
        
        return await books_service.get_file(book_id, file_id)


def extract_limit_offset(filters: list[FilterTypeT]) -> tuple[int, int]:
    """Extract page size and offset from filters"""
    limit_offset_filter = next(
        (f for f in filters if isinstance(f, LimitOffset)),
        None
    )
    
    if limit_offset_filter:
        return limit_offset_filter.limit, limit_offset_filter.offset
    
    raise ValueError("LimitOffset filter not found")