"""
Collection handling implementation.
"""

import asyncio
import datetime
import math
from typing import override

from phederation.collections.collections import CollectionManager
from phederation.models import dereference
from phederation.models.collections import APOrderedCollectionPage
from phederation.models.objects import APCollectionItem
from phederation.storage.base import StorageBackend
from phederation.utils import ObjectId
from phederation.utils.base import AccessType


class CollectionManagerNoDB(CollectionManager):
    """Manages ActivityPub collections, without storing any collection in the database (instead using pagination from StorageBackend)."""

    def __init__(self, storage: StorageBackend, page_size: int = 20):
        super(CollectionManagerNoDB, self).__init__(storage=storage, page_size=page_size)

    @override
    async def initialize(self):
        pass

    @override
    async def create_collection(
        self,
        collection_id: ObjectId,
        items: list[ObjectId] | None = None,
        attributed_to: ObjectId | None = None,
        access: AccessType = AccessType.PUBLIC,
    ) -> ObjectId:
        """
        Create a new collection.
        In the nodb setting, this method does nothing if no items are set - a collection does not exist without items.

        Args:
            collection_id: Collection id to create
            items: Initial collection items
            attributed_to: the actor or object this collection is attributed to.
            access (AccessType): access type of the individual items added. Default: PUBLIC

        Returns:
            Collection ID
        """
        if items:
            await self.add_to_collection(collection_id=collection_id, items=items, access=access)
        return collection_id

    @override
    async def pages(self, collection_id: ObjectId, attributed_to: ObjectId | None = None, access: AccessType = AccessType.PUBLIC):
        """Iterate over all pages of the collection.

        Args:
            collection_id (ObjectId): id of the collection. Can include a page, but will always start with the first item.

        Raises:
            CollectionError: If a part of the collection cannot be found.

        Yields:
            APOrderedCollectionPage: pages of the collection, one by one.
        """
        total_items: int = await self.storage.collection_items.count(collection_id=collection_id, access=access)
        n_pages: int = max(1, math.ceil(total_items / float(self.storage.settings.storage.page_size)))
        page_number = 1
        async for page_items in self.storage.paginate(collection_id=collection_id, access=access):
            yield await self.create_new_page(
                collection_id=collection_id,
                page=page_number,
                n_pages=n_pages,
                total_items=total_items,
                attributed_to=attributed_to,
                items=[i.stored_id for i in page_items] if page_items else [],
            )
            page_number += 1

    @override
    async def contains(self, collection_id: ObjectId, item: ObjectId | None, access: AccessType = AccessType.PUBLIC) -> bool:
        """Check if the collection contains the given item.

        Args:
            collection_id (ObjectId): id of the collection
            item (ObjectId | None): item to check. Always returns False if item is None.

        Returns:
            bool: True if contained, False otherwise.
        """
        if not item:
            return False
        content = await self.storage.collection_items.select(collection_id=collection_id, stored_ids=[item], access=access)
        return len(content) > 0

    @override
    async def add_to_collection(
        self, collection_id: ObjectId, items: ObjectId | list[ObjectId], access: AccessType = AccessType.PUBLIC, add_only_once: bool = False
    ) -> None:
        """
        Add items to collection.

        Args:
            collection_id: Collection ID
            items: Item(s) to add
        """
        if isinstance(items, str):
            items = [items]
        if len(items) == 0:
            return
        collection_items = [
            APCollectionItem(
                collection_id=collection_id,
                stored_id=item,
                visibility=access.value,
                created_at=datetime.datetime.now(datetime.timezone.utc),
                id=f"{collection_id}_{item}" if add_only_once else None,
            )
            for item in items
        ]
        if len(collection_items) == 1:
            _ = await self.storage.collection_items.create(collection_items[0], raise_if_exists=False)
        else:
            _ = await self.storage.collection_items.create_many(collection_items)

    @override
    async def remove_from_collection(self, collection_id: ObjectId, items: ObjectId | list[ObjectId], access: AccessType = AccessType.PUBLIC) -> None:
        """
        Remove items from collection.
        This deletes the entire collection, including all pages, and reforms it - meaning it is not very efficient.

        Args:
            collection_id: Collection ID
            items: Item(s) to remove
        """
        # only use lists
        if isinstance(items, str):
            items = [items]
        if len(items) == 0:
            return

        # get all accessible items of the collection, remove the list
        item_ids = await self.storage.collection_items.select(collection_id=collection_id, stored_ids=items, access=access)
        delete_tasks = [self.storage.collection_items.delete(id=item.id) for item in item_ids if item.id]
        _ = await asyncio.gather(*delete_tasks)

    @override
    async def delete_collection(self, collection_id: ObjectId) -> None:
        """Deletes the entire collection (with AccessType.PRIVATE), including all pages, from storage.
        Items within the collection are not affected.

        Args:
            collection_id (ObjectId): The collection to remove.
        """
        items = await self.get_collection_items(collection_id=collection_id, access=AccessType.PRIVATE)
        await self.remove_from_collection(collection_id=collection_id, items=items, access=AccessType.PRIVATE)

    @override
    async def collection_exists(self, collection_id: ObjectId, page: int | None = None, access: AccessType = AccessType.PUBLIC) -> bool:
        """Check if the collection exists.

        This will check how many items are accessible by access in the collection.
        If none are there, it will return False.

        Args:
            collection_id (ObjectId): id of the collection to check.
            page (int | None, optional): Optional, only check if the given page exists. Defaults to None.
            access: which items to count

        Returns:
            bool: True if collection has more than zero items, or False.
        """
        n_items = await self.storage.collection_items.count(collection_id=collection_id, access=access)
        return n_items > 0

    @override
    async def get_collection_page(
        self, collection_id: ObjectId, attributed_to: ObjectId | None = None, page: int = 1, access: AccessType = AccessType.PUBLIC
    ) -> APOrderedCollectionPage:
        """
        Get collection page.

        Args:
            collection_id: Collection ID
            page: Page number

        Returns:
            Collection page
        """
        total_items: int = await self.storage.collection_items.count(collection_id=collection_id, access=access)
        n_pages: int = max(1, math.ceil(total_items / float(self.storage.settings.storage.page_size)))
        page_items = await self.storage.get_collection_page_items(collection_id=collection_id, page=page, access=access)
        return await self.create_new_page(
            collection_id=collection_id,
            page=page,
            n_pages=n_pages,
            total_items=total_items,
            attributed_to=attributed_to,
            items=[i.stored_id for i in page_items] if page_items else [],
        )

    @override
    async def get_collection_items(self, collection_id: ObjectId, access: AccessType = AccessType.PUBLIC) -> list[ObjectId]:
        """Returns all item ids of all pages in the entire collection, not paginated.

        Args:
            collection_id (ObjectId): Id of the collection

        Returns:
            list[ObjectId]: All item ids.
        """
        result: list[ObjectId] = []
        async for page in self.pages(collection_id=collection_id, attributed_to=None, access=access):
            items = page.items or page.ordered_items or []
            dereferenced = [dereference(item, "id") for item in items]
            result.extend([id for id in dereferenced if id])
        return result
