"""
actors.py
This module defines the Actor types for ActivityPub.
These actors are based on the ActivityStreams vocabulary and ActivityPub protocol.

For more information on Actors in ActivityPub, see:
https://www.w3.org/TR/activitypub/#actors
"""

from enum import Enum
from typing import Any, ClassVar, TypedDict, override

from pydantic import Field, HttpUrl, field_validator
from pydantic.config import ConfigDict
from pydantic_core import Url

from phederation.utils.base import ObjectId, APDateTime
from phederation.utils.exceptions import InvalidURLError

from .errors import BaseError
from .keys import APPublicKey
from .links import RelativeLink, RelativeLinkTemplate
from .objects import APObject, dereference, dereference_or_raise


class ErrorMessageActorUnauthorized(BaseError):
    message: str = "Unauthorized."
    description: str = "The actor is not authorized to perform this action."


class ActivityDict(TypedDict):
    actor: str
    object: dict[str, Any]
    published: str
    type: str
    context: str


class ActorType(Enum):
    """Actor types, subset of ObjectType."""

    ACTOR = "Actor"
    ACCOUNT = "Account"
    PROFILE = "Profile"
    USER = "User"

    PERSON = "Person"
    GROUP = "Group"
    ORGANIZATION = "Organization"
    SERVICE = "Service"
    APPLICATION = "Application"

    @override
    def __str__(self):
        return str(self.value)


class APAccount(APObject):
    type: str = ActorType.ACCOUNT.value
    username: str = Field(..., description="Username of the actor associated with this local account")
    fullname: str | None = Field(default=None, description="Actual name of the actor associated with this local account")
    email: str | None = Field(default=None, description="Email address, if available")
    created_at: APDateTime | None = Field(default=None, description="Created time")
    updated_at: APDateTime | None = Field(default=None, description="Last update time")

    model_config: ClassVar[ConfigDict] = ConfigDict(
        json_schema_extra={
            "examples": [
                {
                    "id": "https://example.org/accounts/exampleuser",
                    "type": "Account",
                    "name": "exampleuser",
                    "fullname": "FirstnameExample LastnameExample",
                    "username": "exampleuser",
                    "email": "email@example.org",
                    "created_at": "2022-01-13 00:10:20.470276",
                }
            ]
        }
    )


class APActor(APObject):
    """
    Base Actor type as defined in ActivityPub.

    Specification: https://www.w3.org/TR/activitypub/#actor-objects
    Usage: https://www.w3.org/TR/activitypub/#actor-objects
    """

    type: str = ActorType.ACTOR.value
    inbox: ObjectId = Field(..., description="Primary inbox URL")
    outbox: ObjectId = Field(..., description="Primary outbox URL")
    shared_inbox: ObjectId | None = Field(default=None, description="Set if the actor instance has a shared inbox URL")
    following: None | ObjectId = Field(default=None, description="Following collection")
    followers: None | ObjectId = Field(default=None, description="Followers collection")
    liked: None | ObjectId = Field(default=None, description="Liked collection")
    blocks: None | ObjectId = Field(default=None, description="Collection for blocked objects/actors")
    objects: None | ObjectId = Field(default=None, description="Collection for objects created by the actor")
    collections: None | ObjectId = Field(default=None, description="Collection for collections created by the actor")
    streams: None | list[ObjectId] = Field(default=None, description="Additional streams")
    endpoints: None | dict[str, ObjectId] = Field(default=None, description="Additional endpoints")
    public_key: None | APPublicKey | str | list[str] | list[APPublicKey] = Field(default=None, description="Public key information, can be a list")
    local: bool = Field(default=True, description="If the actor is local to this instance.")
    discoverable: bool = Field(default=True, description="If this actor can be discovered in the /users route.")
    memorial: bool = Field(default=False, description="If this is a memorial of an actor.")

    # below are fields that can be updated. All fields above depend on the instance and the actor_id
    preferred_username: None | str = Field(default=None, description="Preferred username")
    autoaccept: bool = Field(default=True, description="If the actor automatically accepts follow requests.")

    @field_validator("inbox", "outbox", "following", "followers", "liked")
    @classmethod
    def validate_urls(cls, v: str | Url) -> ObjectId:
        try:
            return str(HttpUrl(v))
        except ValueError:
            raise InvalidURLError(f"Invalid URL: {v}")

    @override
    def serialize(self, include_context: bool = True) -> dict[str, Any]:
        """Serialize APActor object.
        This override of the super class is necessary to ensure the public key id is stored properly.

        Args:
            include_context (bool, optional): if the JSON-LD context should be included in the json dict. Defaults to True.

        Returns:
            dict[str, Any]: JSON-LD object for this actor.
        """
        actor_dict = super().serialize(include_context=include_context)
        key_ids = self.public_key_as_ids()
        if key_ids:
            actor_dict["key_id"] = key_ids
        return actor_dict

    def public_key_as_ids(self):
        public_key_ids = None
        if isinstance(self.public_key, str):
            public_key_ids = self.public_key
        if isinstance(self.public_key, APPublicKey):
            public_key_ids = dereference(self.public_key, "id")
        if isinstance(self.public_key, list):
            public_key_ids = [dereference_or_raise(key, "id") for key in self.public_key]
        return public_key_ids

    def add_public_key(self, key: ObjectId | APPublicKey):
        key_list: list[ObjectId] = []
        if isinstance(self.public_key, str) or isinstance(self.public_key, APPublicKey):
            key_list = [dereference_or_raise(self.public_key, "id"), dereference_or_raise(key, "id")]
        if isinstance(self.public_key, list):
            key_list = [dereference_or_raise(key, "id")] + [dereference_or_raise(key, "id") for key in self.public_key]
        self.public_key = key_list

    model_config: ClassVar[ConfigDict] = ConfigDict(
        json_schema_extra={
            "examples": [
                {
                    "id": "https://example.org/users/exampleuser",
                    "type": "Person",
                    "name": "exampleuser",
                    "summary": "A test account for ActivityPub.",
                    "inbox": "http://example.org/users/exampleuser/inbox",
                    "outbox": "http://example.org/users/exampleuser/outbox",
                    "following": "http://example.org/users/exampleuser/following",
                    "followers": "http://example.org/users/exampleuser/followers",
                    "preferredUsername": "exampleuser",
                    "publicKey": {
                        "id": "http://example.org/keys/af7af0c4-fd87-4f8d-8f40-be130f464e4b",
                        "type": "Key",
                        "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nDFGSDFJGSFJSDJFASDJASDQ121231+1231231SDFSFDGDFGD3453QARASD\n-----END PUBLIC KEY-----\n",
                    },
                    "links": [
                        {
                            "rel": "self",
                            "type": "application/activity+json",
                            "href": "http://example.org/users/exampleuser",
                        },
                        {
                            "rel": "http://ostatus.org/schema/1.0/subscribe",
                            "template": "http://example.org/authorize_interaction?uri={uri}",
                        },
                    ],
                    "discoverable": True,
                    "memorial": False,
                    "username": "exampleuser",
                    "domain": "example.org",
                    "autoaccept": "True",
                    "shared_inbox": "http://example.org/inbox",
                    "local": "True",
                    "created_at": "2022-01-13 00:10:20.470276",
                    "updated_at": "None",
                    "last_fetched_at": "None",
                }
            ]
        }
    )


class APPerson(APActor):
    """
    Represents a Person actor in ActivityPub.

    A Person is an individual user account.

    Properties:
        type (Literal["Person"]): Always set to "Person".

    Inherits all other properties from APActor.

    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person
    """

    type: str = Field(default=ActorType.PERSON.value, description="Indicates that this object represents a person")


class APGroup(APActor):
    """
    Represents a Group actor in ActivityPub.

    A Group represents a formal or informal collective of Actors.

    Attributes:
        type (str): Always set to "Group".

    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-group
    """

    type: str = Field(default=ActorType.GROUP.value, description="Indicates that this object represents a group")


class APOrganization(APActor):
    """
    Represents an Organization actor in ActivityPub.

    An Organization is a kind of Actor representing a formal or informal organization.

    Attributes:
        type (str): Always set to "Organization".

    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-organization
    """

    type: str = Field(
        default=ActorType.ORGANIZATION.value,
        description="Indicates that this object represents an organization",
    )


class APApplication(APActor):
    """
    Represents an Application actor in ActivityPub.

    An Application represents a software application.

    Attributes:
        type (str): Always set to "Application".

    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application
    """

    type: str = Field(
        default=ActorType.APPLICATION.value,
        description="Indicates that this object represents an application",
    )


class APService(APActor):
    """
    Represents a Service actor in ActivityPub.

    A Service is a kind of Actor that represents a service of any kind.

    Attributes:
        type (str): Always set to "Service".

    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service
    """

    type: str = Field(default=ActorType.SERVICE.value, description="Indicates that this object represents a service")


class APUser(APActor):
    """Schema for a user on this instance."""

    type: str = Field(default=ActorType.USER.value, description="Indicates that this object represents a user")
    links: list[RelativeLink | RelativeLinkTemplate]
