"""
Collection handling implementation.
"""

from datetime import datetime, timezone
import math
from typing import override

from phederation.collections.collections import CollectionManager
from phederation.models import APOrderedCollectionPage, dereference
from phederation.storage.base import StorageBackend
from phederation.utils import ObjectId
from phederation.utils.base import AccessType
from phederation.utils.exceptions import CollectionError, catch_exceptions


class CollectionManagerDB(CollectionManager):
    """Manages ActivityPub collections, by storing any collection in the database."""

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

    @override
    async def initialize(self):
        pass

    @catch_exceptions(CollectionError, "Failed in create_collection")
    @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.

        Args:
            collection_id: Collection id to create
            items: Initial collection items
            attributed_to: the actor or object this collection is attributed to.

        Returns:
            Collection ID
        """
        collection_id_check = await self.storage.collection.read(id=collection_id)
        if collection_id_check:
            self.logger.warning(f"Collection {collection_id} already exists")
            # raise CollectionError("Collection already exists")

        items = items or []

        total_items = len(items)
        n_pages = max(1, math.ceil(total_items / float(self.page_size)))
        items_split = [items[(k * self.page_size) : ((k + 1) * self.page_size)] for k in range(0, n_pages)]
        collections = [
            await self.create_new_page(
                collection_id=collection_id,
                page=page_number,
                n_pages=n_pages,
                attributed_to=attributed_to,
                total_items=len(items),
                items=items_split[page_number - 1],
            )
            for page_number in range(1, n_pages + 1)
        ]

        # create the general collection without the "page" id, linking to the first and last pages.
        # this collection does not have any items directly stored, but links to the pages.
        _ = await self.storage.collection.create(
            data=APOrderedCollectionPage(
                id=CollectionManager.collection_page(id=collection_id, page=None),
                attributed_to=attributed_to,
                total_items=len(items) if items else 0,
                ordered_items=None,
                published=datetime.now(timezone.utc),
                start_index=1,
                current=CollectionManager.collection_page(id=collection_id, page=1),
                first=CollectionManager.collection_page(id=collection_id, page=1),
                prev=None,
                next=None,
                last=CollectionManager.collection_page(id=collection_id, page=n_pages),
            )
        )

        # create all pages
        for collection in collections:
            _ = await self.storage.collection.create(data=collection)
        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.
        """
        collection = await self.storage.collection.read(id=collection_id)
        first = dereference(collection, "first")
        if not collection or not first:
            raise CollectionError(f"Collection not found: {collection_id}")
        collection_id = first

        current_page = collection_id
        for page_number in range(1, 100):  # TODO: do not search more than 100 pages for safety; make this configurable in settings.
            current_id = dereference(current_page, key="id")
            if not current_id:
                raise CollectionError(f"Page {page_number} in {collection_id} did not have an id")
            current_page = await self.storage.collection.read(id=current_id)
            if not current_page:
                raise CollectionError(f"Page {page_number} in {collection_id} not found")
            if not current_page.id:
                raise CollectionError(f"Page {page_number} in {collection_id} did not have an id")

            yield current_page

            if current_page.id == current_page.last:
                break
            current_page = getattr(current_page, "next", None)

    @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
        items_in_collection = await self.get_collection_items(collection_id=collection_id)
        return item in items_in_collection

    @catch_exceptions(CollectionError, "Failed in add_to_collection")
    @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

        # create the collection if it does not already exist
        page_number = 1
        collection_id_with_page = CollectionManager.collection_page(id=collection_id, page=page_number)
        collection_page_one = await self.storage.collection.read(id=collection_id_with_page)
        if collection_page_one is None:
            self.logger.info(f"Could not find collection {collection_id_with_page}, creating it.")
            _ = await self.create_collection(collection_id=collection_id, items=items)
            return

        # collect all current pages
        all_pages = [p async for p in self.pages(collection_id=collection_id)]

        # loop until we find a collection with fewer than the page_size number of items
        collection_to_increase = None
        for page in all_pages:
            if len(page.items or page.ordered_items or []) < self.page_size:
                collection_to_increase = page
                break

        # if no incomplete page exists, create a new one
        if not collection_to_increase:
            collection_to_increase = await self.create_new_page(
                collection_id=collection_id,
                page=len(all_pages) + 1,
                n_pages=len(all_pages) + 1,
                attributed_to=dereference(collection_page_one, "attributed_to"),
                total_items=collection_page_one.total_items or 0,
                items=None,
            )

            # fix last pointer
            previous_last = dereference(collection_page_one, "last")
            for page in all_pages:
                if page.id == previous_last:
                    page.next = collection_to_increase.id
                    collection_to_increase.prev = previous_last
                page.last = collection_to_increase.id

            all_pages.append(collection_to_increase)

        # fix total items
        previous_total_items = collection_page_one.total_items or 0
        for page in all_pages:
            page.total_items = previous_total_items + len(items)

        # now we can actually add the items and update all pages
        previous_items = [dereference(item, "id") for item in collection_to_increase.ordered_items or []]
        previous_items = [item for item in previous_items if item]
        collection_to_increase.ordered_items = previous_items + items

        for page in all_pages:
            if page.id:
                _ = await self.storage.collection.upsert(id=page.id, data=page)

    @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
        """
        # fail silently if the collection does not exist
        if not (await self.collection_exists(collection_id=collection_id)):
            return
        
        collection = await self.get_collection_page(collection_id=collection_id, page=1)

        # only use lists
        if isinstance(items, str):
            items = [items]
        if len(items) == 0:
            return

        # get all items of the collection, remove the list
        items_in_collection = await self.get_collection_items(collection_id=collection_id)
        items_after_removal = list(set(items_in_collection) - set(items))

        # delete collection from storage
        await self.delete_collection(collection_id=collection_id)

        # create collection from items after removal
        attributed_to = dereference(collection, "attributed_to")
        _ = await self.create_collection(collection_id=collection_id, items=items_after_removal, attributed_to=attributed_to)

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

        Args:
            collection_id (ObjectId): The collection to remove.
        """
        # fail silently if the collection does not exist
        collection = await self.collection_exists(collection_id=collection_id)
        if not collection:
            return
        async for page in self.pages(collection_id=collection_id):
            if page.id:
                _ = await self.storage.collection.delete(page.id)

        _ = await self.storage.collection.delete(collection_id)

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

        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.

        Returns:
            APOrderedCollectionPage | None: The collection if it exists, or None.
        """
        if page:
            collection_id = CollectionManager.collection_page(id=collection_id, page=page)
        return await self.storage.collection.count(collection_id=collection_id, access=access) > 0

    @catch_exceptions(CollectionError, "Failed in get_collection_page")
    @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
        """
        # iterate over pages until we reach the desired one
        current_page = collection_id
        for page_number in range(1, page + 1):
            current_id = dereference(current_page, key="id")
            if not current_id:
                raise CollectionError(f"Page {page_number} in {collection_id} did not have an id")
            current_page = await self.storage.collection.read(id=current_id)
            current_page = getattr(current_page, "next", None) or getattr(current_page, "first", None)

        if isinstance(current_page, str):
            result_page = await self.storage.collection.read(id=current_page)
        else:
            result_page = current_page

        if not result_page:
            self.logger.error(f"Page {page} of collection {collection_id} is None. Page not found")
            raise CollectionError("Page not found")

        if not isinstance(result_page, APOrderedCollectionPage):
            self.logger.error(f"Page {page} of collection {collection_id} is not of correct type (is type {type(result_page)}).")
            raise CollectionError("Page not found")

        return result_page

    @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.
        """
        # iterate over all pages, collecting items
        items: list[ObjectId] = []
        current_page = collection_id
        for page_number in range(1, 100):  # TODO: do not search more than 100 pages; make this configurable in settings.
            current_id = dereference(current_page, key="id")
            if not current_id:
                raise CollectionError(f"Page {page_number} in {collection_id} did not have an id")
            current_page = await self.storage.collection.read(id=current_id)
            if not current_page or not current_page.id:
                raise CollectionError(f"Page {page_number} in {collection_id} did not have an id")

            items_to_extend = current_page.items or current_page.ordered_items or []
            items.extend(items_to_extend)
            if current_page.id == current_page.last:
                break
            current_page = getattr(current_page, "next", None) or getattr(current_page, "first", None)
        return items
