"""
Collection handling implementation.
"""

from abc import ABC, abstractmethod
from collections.abc import AsyncGenerator
from datetime import datetime, timezone
from logging import Logger
from typing import Any
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse

from phederation.models import APOrderedCollectionPage
from phederation.storage.base import StorageBackend
from phederation.utils import ObjectId
from phederation.utils.base import AccessType, urljoin
from phederation.utils.exceptions import CollectionError, catch_exceptions
from phederation.utils.logging import configure_logger


class CollectionManager(ABC):
    """Manages ActivityPub collections."""

    def __init__(self, storage: StorageBackend, page_size: int = 20, access_type: AccessType = AccessType.PUBLIC):
        self.storage: StorageBackend = storage
        self.page_size: int = page_size
        self.logger: Logger = configure_logger(__name__, prefix=self.storage.settings.federation.logging_prefix)
        self.access_type: AccessType = access_type

    @staticmethod
    def collection_id(id: ObjectId, name: str, page: int | None = None) -> ObjectId:
        """Creates a resolvable id for a collection with given name."""
        url = urljoin(id, name)
        if not page:
            return url
        else:
            return f"{url}?page={page}"

    @staticmethod
    def collection_page(id: ObjectId, page: int | None = None) -> ObjectId:
        """Creates a resolvable id for a collection page with given collection id."""
        if page is None:
            return id
        else:
            # set the page element of the URL to the given integer
            url_parts = list(urlparse(id))
            query = dict(parse_qsl(url_parts[4]))
            query.update({"page": str(page)})
            url_parts[4] = urlencode(query)
            return urlunparse(url_parts)

    @abstractmethod
    async def initialize(self):
        pass

    async def create_new_page(
        self,
        collection_id: ObjectId,
        page: int,
        n_pages: int,
        total_items: int,
        attributed_to: ObjectId | None = None,
        items: list[ObjectId] | None = None,
    ) -> APOrderedCollectionPage:
        if items is None or len(items) == 0:
            items = None
        return APOrderedCollectionPage(
            id=CollectionManager.collection_page(id=collection_id, page=page),
            attributed_to=attributed_to,
            total_items=total_items,
            part_of=collection_id,
            ordered_items=items,
            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=(CollectionManager.collection_page(id=collection_id, page=page - 1) if page - 1 > 0 else None),
            next=(CollectionManager.collection_page(id=collection_id, page=page + 1) if page + 1 <= n_pages else None),
            last=CollectionManager.collection_page(id=collection_id, page=n_pages),
        )

    @catch_exceptions(CollectionError, "Failed in create_collection")
    @abstractmethod
    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. This actor always has access to all items in the collection.

        Returns:
            Collection ID
        """
        pass

    @abstractmethod
    def pages(
        self, collection_id: ObjectId, attributed_to: ObjectId | None = None, access: AccessType = AccessType.PUBLIC
    ) -> AsyncGenerator[APOrderedCollectionPage, Any]:
        """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.
        """
        pass

    @abstractmethod
    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.
        """
        pass

    @catch_exceptions(CollectionError, "Failed in add_to_collection")
    @abstractmethod
    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
            access: AccessType: the accessibility level of the items to add. This can be independent of the access type of the collection, but not less restrictive.
                               If the access type of the colleciton is PRIVATE, adding PUBLIC elements will not make them accessible.
            add_only_once (bool): if True, will not add any item id more than once. Default: False.
        """
        pass

    @abstractmethod
    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
            access: AccessType: the access type used for removal. Only elements with accesss type like this or more accessible can be removed.
        """
        pass

    @abstractmethod
    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.
        """
        pass

    @abstractmethod
    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.
        """
        pass

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

    @abstractmethod
    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
            access (AccessType): only retrieve items with this access type (or more permissible). Default: PUBLIC.

        Returns:
            list[ObjectId]: All item ids.
        """
        pass
