from dataclasses import dataclass, field
from datetime import datetime, timezone
import enum
from typing import Annotated, Any, override
from urllib.parse import urlparse

import httpx
from pydantic import (
    AfterValidator,
    BaseModel,
    Field,
    HttpUrl,
    PlainSerializer,
    ValidationError,
)

from phederation.utils.serialization import ActivityPubBase


def serialize_datetime(datetime: datetime | None):
    if not datetime:
        return "null"
    return datetime.astimezone(tz=timezone.utc).isoformat()


APDateTime = Annotated[
    datetime,
    PlainSerializer(serialize_datetime, return_type=str),
]


class AccessType(enum.Enum):
    """Represents the type of access on and above which the object is accessible."""

    # everybody is allowed to access
    PUBLIC = "public"
    # Only follower of attributed_to actor are allowed to access
    # TODO: implement this
    FOLLOWER = "follower"
    # Only the attributed_to actor is allowed to access
    PRIVATE = "private"

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

    @staticmethod
    def to_number(_type: "AccessType"):
        if _type == AccessType.PUBLIC:
            return 0
        elif _type == AccessType.FOLLOWER:
            return 1
        else:
            return 2

    @staticmethod
    def minimum_security(t1: "AccessType", t2: "AccessType") -> "AccessType":
        """Computes the minimum access level between the two arguments.

        This makes the result THE LEAST secure.

        Examples:
            - minimum(PRIVATE, PUBLIC) == PUBLIC
            - minimum(FOLLOWER, PRIVATE) == FOLLOWER

        Args:
            t1 (AccessType): Access 1
            t2 (AccessType): Access 2

        Returns:
            AccessType: the least secure access of the two.
        """
        count1 = AccessType.to_number(t1)
        count2 = AccessType.to_number(t2)
        return t1 if count1 < count2 else t2

    @staticmethod
    def maximum_security(t1: "AccessType", t2: "AccessType") -> "AccessType":
        """Computes the maximum access level between the two arguments.

        This makes the result THE MOST secure.

        Examples:
            - minimum(PRIVATE, PUBLIC) == PRIVATE
            - minimum(FOLLOWER, PRIVATE) == PRIVATE

        Args:
            t1 (AccessType): Access 1
            t2 (AccessType): Access 2

        Returns:
            AccessType: the most secure access of the two.
        """
        count1 = AccessType.to_number(t1)
        count2 = AccessType.to_number(t2)
        return t1 if count1 > count2 else t2

    @staticmethod
    def from_visibility(visibility: str):
        for _type in [AccessType.PUBLIC, AccessType.FOLLOWER, AccessType.PRIVATE]:
            if visibility.lower() == _type.value.lower():
                return _type
        raise ValueError(f"Visibility '{visibility}' cannot be converted to AccessType")

    def is_accessible(self, by_access_type: "AccessType") -> bool:
        """Checks if the "self" access level is accessible by the provided access level.

        Example 1: "self=PRIVATE" and "by_access_type=FOLLOWER" will return False, because "PRIVATE" is more restrictive than "FOLLOWER".
        Example 2: "self=PUBLIC" and "by_access_type=FOLLOWER" will return True.

        Args:
            access_type (AccessType): Access level to check against.

        Returns:
            bool: True if the SELF access level is LESS OR AS restrictive than the by_access_type level.
        """
        if self == AccessType.PUBLIC:
            return True
        if self == AccessType.FOLLOWER and (by_access_type == AccessType.PRIVATE or by_access_type == AccessType.FOLLOWER):
            return True
        if self == AccessType.PRIVATE and by_access_type == AccessType.PRIVATE:
            return True
        return False


PUBLIC_URLS: list[str] = ["public", "Public", "as:Public", "https://www.w3.org/ns/activitystreams#Public"]


def validate_object_id(url: "None | ObjectId | HttpUrl | str | dict[str, Any] | ActivityPubBaseWithId") -> "ObjectId":
    if isinstance(url, ActivityPubBaseWithId):
        object_id = str(url.id)
    elif isinstance(url, dict):
        if "id" in url:
            object_id = url.get("id", None)
    if isinstance(url, str) or isinstance(url, HttpUrl):
        if url in PUBLIC_URLS:
            url = PUBLIC_URLS[-1]
        object_id = str(HttpUrl(url))
        return object_id
    raise ValueError(f"Could not resolve id in object '{url}'.")


"""
Represents an id of an APObject.
Basically this is just a string that is a resolveable, unique URI.
The main use case in this code base is to be able to resolve many different versions of this URI, e.g.
"just a string", "a dictionary with the id as a key", or even just "https://*object_id*/inbox".
"""
ObjectId = Annotated[str, AfterValidator(validate_object_id)]


class ActivityPubBaseWithId(ActivityPubBase):
    """Generic base class for ActivityPub classes, including an id of type ObjectId and a visibility field."""

    id: None | ObjectId = Field(default=None, description="Unique identifier")
    visibility: str = Field(default=AccessType.PUBLIC.value, description="Visibility of the object, useful for access management")


@dataclass
class RateLimit:
    """Rate limit settings for a single domain."""

    requests: int
    """ Limit of requests per period that are allowed from a given domain. """

    period: int
    """ Period (in seconds) for how long the number of requests are answered, until the number is reset. """


class InstanceEndpoints(BaseModel):
    shared_inbox: ObjectId | None = Field(default=None, description="Link to the shared inbox of the instance, if exposed")
    instance_actor_id: ObjectId | None = Field(default=None, description="Id of the instance actor, if exposed")
    resolve: ObjectId | None = Field(default=None, description="The /resolve endpoint for multiple objects, if exposed")


class NodeInfoUsageUsers(BaseModel):
    total: int = 0
    activeHalfyear: int = 0
    activeMonth: int = 0


class NodeInfoUsage(BaseModel):
    users: NodeInfoUsageUsers = field(default_factory=lambda: NodeInfoUsageUsers())
    localPosts: int = 0
    localComments: int = 0


class NodeInfo(ActivityPubBaseWithId):
    """NodeInfo data, containing statistics about the instance. Shared across the network."""

    type: str = "NodeInfo"
    software: dict[str, str] = field(default_factory=lambda: {})
    protocols: list[str] = field(default_factory=lambda: [])
    services: dict[str, list[str]] = field(default_factory=lambda: {})
    usage: NodeInfoUsage = field(default_factory=lambda: NodeInfoUsage())
    open_registrations: bool = False
    metadata: dict[str, Any] = field(default_factory=lambda: {})
    version: str = "2.1"  # the NodeInfo version that is supported
    updated_at: APDateTime = field(default_factory=lambda: datetime.now(timezone.utc))


class ObjectType(enum.Enum):
    """Object types."""

    OBJECT = "Object"

    NOTE = "Note"
    ARTICLE = "Article"
    DOCUMENT = "Document"
    PAGE = "Page"
    IMAGE = "Image"
    VIDEO = "Video"
    AUDIO = "Audio"
    TOMBSTONE = "Tombstone"

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

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

    COLLECTION = "Collection"
    ORDERED_COLLECTION = "OrderedCollection"
    COLLECTION_PAGE = "CollectionPage"
    ORDERED_COLLECTION_PAGE = "OrderedCollectionPage"
    COLLECTION_ITEM = "CollectionItem"

    EVENT = "Event"
    PLACE = "Place"
    LINK = "Link"
    MENTION = "Mention"
    RELATIONSHIP = "Relationship"

    KEY = "Key"
    DATA_INTEGRITY_PROOF = "DataIntegrityProof"
    NODE_INFO = "NodeInfo"

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


class CollectionType(enum.Enum):
    """Collection types."""

    COLLECTION = "Collection"
    ORDERED_COLLECTION = "OrderedCollection"
    COLLECTION_PAGE = "CollectionPage"
    ORDERED_COLLECTION_PAGE = "OrderedCollectionPage"
    COLLECTION_ITEM = "CollectionItem"

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


class ActivityType(enum.Enum):
    """Activity types."""

    CREATE = "Create"
    FOLLOW = "Follow"
    LIKE = "Like"
    ANNOUNCE = "Announce"
    DELETE = "Delete"
    UPDATE = "Update"
    UNDO = "Undo"
    ACCEPT = "Accept"
    REJECT = "Reject"
    ADD = "Add"
    REMOVE = "Remove"
    BLOCK = "Block"

    MIGRATE = "Migrate"
    MAINTENANCE = "Maintenance"
    ACTIVITY_IN_QUEUE = "ActivityInQueue"

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


class UrlType(str, enum.Enum):
    Users = "users"
    Actors = "actors"
    Accounts = "accounts"
    Activities = "activities"
    ActivitiesIncoming = "activities_incoming"
    ActivitiesOutgoing = "activities_outgoing"
    Objects = "objects"
    Blocks = "blocks"
    Follows = "follows"
    Keys = "keys"
    Media = "media"
    Collections = "collections"
    CollectionItems = "collection_items"
    MessageQueue = "queue"
    Proofs = "proofs"
    NodeInfo = "nodeinfo"
    Maintenance = "maintenance"
    Migration = "migration"
    Instance = "instance"
    Resolve = "resolve"
    Unknown = ""

    @staticmethod
    def from_object_type(obj_type: str) -> "UrlType":
        if obj_type == "Key":
            return UrlType.Keys
        if obj_type == "Actor" or obj_type == "User":
            return UrlType.Users
        if obj_type == "DataIntegrityProof":
            return UrlType.Proofs
        if ActivityType.__contains__(obj_type):
            return UrlType.Activities
        if ObjectType.__contains__(obj_type):
            return UrlType.Objects
        return UrlType.Unknown
    
    @staticmethod
    def from_local_url(url: str) -> "UrlType":
        path = urlparse(url).path.split("/")
        path = [p for p in path if len(p) > 0]
        try:
            url_type = UrlType(path[0])
        except Exception:
            return UrlType.Unknown
        return url_type


def urljoin(*args: str) -> str:
    """
    Joins given arguments into an url. Trailing but not leading slashes are stripped for each argument.
    """
    args_clean = [a for a in args if len(str(a).strip("/")) > 0]

    return "/".join(map(lambda x: str(x).strip("/"), args_clean))


def assemble_id_url(type: UrlType, base_url: ObjectId, primary: str | None = None) -> ObjectId:
    if not primary:
        return urljoin(base_url, type.value)
    else:
        return urljoin(base_url, type.value, primary)


def actor_id_from_username(base_url: ObjectId, username: str) -> ObjectId:
    """
    Standardized way to get an actor id from a domain and username.
    """
    return assemble_id_url(type=UrlType.Users, base_url=base_url, primary=username)


def username_from_actor_id(actor_id: ObjectId):
    path_split = urlparse(actor_id).path.split("/")
    if len(path_split) < 3:
        raise ValidationError(f"Actor_id {actor_id} cannot be split properly to obtain username")
    username = path_split[2]
    return username


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


async def async_request(url: str, headers: dict[str, str], timeout: int):
    async with httpx.AsyncClient() as client:
        return await client.get(str(url), headers=headers, timeout=timeout)


@dataclass
class ResolverResult:
    """Result of resolving a given object or url."""

    content: str | dict[str, str] | None = None
    """Message returned by the service, if any"""

    status_code: None | int = None
    """Status code returned by the resolved Url, or None if there was a non-HttpStatus related error."""

    error_message: None | str = None
    """Error message for a failed resolve."""

    def get(self, key: str):
        if self.content and isinstance(self.content, dict) and key in self.content:
            return self.content[key]
