from datetime import datetime, timezone
from logging import Logger

from phederation.collections.collections import CollectionManager
from phederation.models import APAccount, APActor, APCreate, ActorType
from phederation.models.collections import ValidCollection
from phederation.storage import StorageBackend
from phederation.utils import (
    ObjectId,
    UrlType,
    actor_id_from_username,
    collection_id_from_name,
    configure_logger,
)
from phederation.utils.base import AccessType, ObjectType, urljoin
from phederation.utils.exceptions import HandlerError, UserError
from phederation.utils.settings import PhedSettings

from .key_management import KeyManager


class ActorManager:

    def __init__(self, settings: PhedSettings, storage: StorageBackend, key_manager: KeyManager, collections: CollectionManager) -> None:
        self.storage: StorageBackend = storage
        self.key_manager: KeyManager = key_manager
        self.collections: CollectionManager = collections
        self.settings: PhedSettings = settings
        self.logger: Logger = configure_logger(__name__, prefix=self.settings.federation.logging_prefix)

    async def actor_from_storage(self, id: ObjectId):
        """Attempt to read an APActor object with the given actor id from storage.

        Note that access levels are disregarded here, it is a direct read from storage.

        Args:
            id (ObjectId): Actor id from the actor to read.

        Returns:
            APActor: An object, or None if not found.
        """
        return await self.storage.actor.read(id)

    async def _create_actor_collections(
        self,
        username: str,
        actor_id: ObjectId,
        actor_type: ActorType | ObjectType = ActorType.PERSON,
        autoaccept: bool = True,
        discoverable: bool = True,
    ) -> APActor:
        actor: APActor | None = await self.storage.actor.read(id=actor_id)

        if actor is None:
            # create the keys for the actor
            active_key = await self.key_manager.generate_key_pair(actor_id=actor_id)

            # create all collections
            inbox = collection_id_from_name(actor_id, name=ValidCollection.Inbox.value)
            outbox = collection_id_from_name(actor_id, name=ValidCollection.Outbox.value)
            followers = collection_id_from_name(actor_id, name=ValidCollection.Followers.value)
            following = collection_id_from_name(actor_id, name=ValidCollection.Following.value)
            blocks = collection_id_from_name(actor_id, name=ValidCollection.Blocks.value)
            liked = collection_id_from_name(actor_id, name=ValidCollection.Liked.value)
            objects = collection_id_from_name(actor_id, name=ValidCollection.Objects.value)
            lists = collection_id_from_name(actor_id, name=ValidCollection.Collections.value)

            endpoints: dict[str, ObjectId] | None = {}
            endpoints["id"] = actor_id + "#endpoints"
            if "media" in self.settings.federation.endpoints:
                endpoints["uploadMedia"] = urljoin(actor_id, "endpoints", "uploadMedia")
            if ValidCollection.Objects.value in self.settings.federation.endpoints:
                endpoints[ValidCollection.Objects.value] = objects
            if ValidCollection.Collections.value in self.settings.federation.endpoints:
                endpoints[ValidCollection.Collections.value] = lists
            if len(endpoints.keys()) == 0:
                endpoints = None

            if "sharedInbox" in self.settings.federation.endpoints:
                shared_inbox_url = urljoin(self.settings.domain.base_url, "/inbox")
            else:
                shared_inbox_url = None

            # TODO: make collection visibility selectable by actor. Right now, all is PUBLIC

            # generate APActor
            actor_obj = APActor(
                type=str(actor_type),
                id=actor_id,
                autoaccept=autoaccept,
                preferred_username=username,
                inbox=await self.collections.create_collection(collection_id=inbox, attributed_to=actor_id),
                outbox=await self.collections.create_collection(collection_id=outbox, attributed_to=actor_id),
                followers=await self.collections.create_collection(collection_id=followers, attributed_to=actor_id),
                following=await self.collections.create_collection(collection_id=following, attributed_to=actor_id),
                blocks=await self.collections.create_collection(collection_id=blocks, attributed_to=actor_id),
                liked=await self.collections.create_collection(collection_id=liked, attributed_to=actor_id),
                objects=await self.collections.create_collection(collection_id=objects, attributed_to=actor_id),
                collections=await self.collections.create_collection(collection_id=lists, attributed_to=actor_id),
                shared_inbox=shared_inbox_url,
                endpoints=endpoints,
                public_key=active_key.key_id,
                visibility=AccessType.PUBLIC.value,  # Important! Actors are always public (for webfinger), but may not be "discoverable".
                discoverable=discoverable,
            )

            _ = await self.storage.actor.create(data=actor_obj, raise_if_exists=True)
            actor = await self.storage.actor.read(id=actor_id)

            # also store in users collection. Make them private if not discoverable.
            collection_users = collection_id_from_name(id=self.settings.domain.base_url, name=UrlType.Users.value)
            await self.collections.add_to_collection(
                collection_id=collection_users, items=actor_id, access=AccessType.PUBLIC if actor_obj.discoverable else AccessType.PRIVATE
            )
        if not actor:
            raise ValueError("Actor could not be created.")
        return actor

    async def create_actor(self, activity: APCreate) -> APActor | None:
        """Tries to create an actor on the local instance, including a separate "account" for them."""
        if not activity.object:
            raise HandlerError("Activity object is None")
        if not isinstance(activity.object, APActor):
            raise HandlerError(f"Activity object type is not APActor (is {type(activity.object)})")
        actor: APActor = activity.object
        username = actor.preferred_username if actor.preferred_username else "<username>"
        actor_type = actor.type
        autoaccept = actor.autoaccept

        actor_id = actor_id_from_username(base_url=self.settings.domain.base_url, username=username)

        actor_types_to_create = [ActorType.PERSON, ActorType.GROUP, ActorType.SERVICE, ActorType.APPLICATION, ActorType.ORGANIZATION]
        if ActorType(actor_type) not in actor_types_to_create:
            raise HandlerError(f"Actor type must be in {actor_types_to_create}.")

        actor_storage = await self.storage.actor.read(id=actor_id)
        if actor_storage:
            self.logger.info(f"Tried to create actor '{actor_id}' but they already exist.")
            return None
        actor_created = await self._create_actor_collections(
            username=username, actor_id=actor_id, actor_type=ActorType(actor_type), autoaccept=autoaccept, discoverable=actor.discoverable
        )

        # create the account for the new user if it is a Person.
        if ActorType(actor_type) == ActorType.PERSON:
            account = APAccount(
                id=actor_id,
                username=username,
                fullname=actor.name,
                email=None,  # TODO: deal with submitting email for new accounts
                created_at=datetime.now(timezone.utc),
                updated_at=None,
                visibility=actor.visibility,
            )
            _ = await self.storage.account.create(data=account, raise_if_exists=False)

        self.logger.info(f"Created new user on the instance, including account: {actor_id}.")
        return actor_created

    async def create_actor_from_username(self, username: str):
        base_url = self.settings.domain.base_url
        create_actor_activity = APCreate(
            object=APActor(
                preferred_username=username,
                autoaccept=self.settings.federation.auto_accept_follows_default,
                type=ActorType.PERSON.value,
                inbox=urljoin(base_url, UrlType.Users.value, username, ValidCollection.Inbox.value),
                outbox=urljoin(base_url, UrlType.Users.value, username, ValidCollection.Outbox.value),
                visibility=AccessType.PUBLIC.value,
                discoverable=True,
            ),
            actor=actor_id_from_username(base_url=self.settings.domain.base_url, username=username),
        )
        _ = await self.create_actor(create_actor_activity)

    async def update_actor(self, update: APActor, actor_id: ObjectId):
        """Update an actor on this instance.
        If the actor is already in storage, updates their mutable information.

        Mutable fields for actors
         - preferred_username
         - autoaccept
        
        Args:
            update (APActor): The information for the update.
            actor_id (ObjectId): The id of the actor to update.

        Returns:
            APActor: the information of the updated actor, with the original .name and .id fields.
        
        Raises:
            UserError:  If the actor does not already exist in storage, or if the actor_id is not local to the instance (i.e. the actor is a remote actor).
        """
        if not self.settings.domain.is_local(actor_id):
            raise UserError(f"Actor {actor_id} is not a local actor")
        actor_original = await self.actor_from_storage(id=actor_id)
        if not actor_original:
            raise UserError(f"Actor {actor_id} does not exist in storage")
        
        # update mutable fields
        actor_original.preferred_username = update.preferred_username
        actor_original.autoaccept = update.autoaccept
        _ = await self.storage.actor.upsert(id=actor_id, data=actor_original)
        return actor_original

    async def delete_actor(self, actor_id: ObjectId):
        """Removes the actor with the given id.

        Removed
         - actor in storage
         - account of the actor in storage, if it exists
         - actor id from users collection

        Args:
            actor_id (ObjectId): _description_
        """
        actor_deleted = await self.storage.actor.delete(id=actor_id)

        if actor_deleted:
            _ = await self.storage.account.delete(id=actor_id)
            users_collection_id = collection_id_from_name(id=self.settings.domain.base_url, name=UrlType.Users.value)
            await self.collections.remove_from_collection(users_collection_id, items=actor_id, access=AccessType.PRIVATE)

    async def deactivate_actor(self, actor_id: ObjectId):
        """Deactivates the given actor (if they are an actor in storage).

        Args:
            actor_id (ObjectId): Id of the actor in storage.

        Returns:
            bool: True if the actor could be found, otherwise False.
        """
        actor = await self.storage.actor.read(id=actor_id)
        if not actor:
            return False
        actor.memorial = True
        actor.discoverable = False
        _ = await self.storage.actor.upsert(id=actor_id, data=actor)
        return True
