"""
objects.py
This module defines the core object types for ActivityPub.
These objects are based on the ActivityStreams vocabulary.

For more information, see:
https://www.w3.org/TR/activitystreams-vocabulary/
"""

import enum
from typing import Any, ClassVar, cast, override

from pydantic import Field, HttpUrl, field_validator, model_validator
from pydantic.alias_generators import to_snake
from pydantic.config import ConfigDict

from phederation.models.proofs import DataIntegrityProof
from phederation.utils.base import (
    APDateTime,
    AccessType,
    ActivityPubBaseWithId,
    ObjectId,
    ObjectType,
)
from phederation.utils.exceptions import ValidationError as APValidationError
from phederation.utils.serialization import ActivityPubBase

from .links import APLink


class DocumentType(enum.Enum):
    """Document types, a subset of Object types that can be resolved to APDocument."""

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

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


class APCollectionItem(ActivityPubBaseWithId):
    """Generic base class for storing ids of activity pub objects, together with access level, created date for paginatin, and object id."""

    type: str = ObjectType.COLLECTION_ITEM.value
    stored_id: ObjectId = Field(
        ...,
        description="Stored ObjectId. Different from the id property, because that one is the primary key and will lead to duplicates in the collections.",
    )
    collection_id: ObjectId = Field(..., description="Object identifier of the associated collection")
    visibility: str = Field(default=AccessType.PUBLIC.value, description="Access level (public, followers, private)")
    created_at: APDateTime | None = Field(default=None, description="Creation date")


def dereference(data: None | str | ObjectId | dict[str, Any] | ActivityPubBase, key: str) -> ObjectId | None:
    """Tries to get a URL/string property from the given key in the data dictionary.

    Args:
        data (dict[str, Any]): The data dictionary, possibly containing the given key.
        key (str): Key to search the URL for. The data[key] may be another json object, or an URL already.
    """
    if not data:
        return None
    data_dict: dict[str, Any]
    if isinstance(data, str) or isinstance(data, HttpUrl):
        data = {"id": str(data)}
    if isinstance(data, ActivityPubBase):
        snake_key = to_snake(key)
        data_dict = {key: getattr(data, snake_key, None)}
    else:
        data_dict = data
    if key not in data_dict:
        return None
    value = cast(str | dict[str, Any] | ActivityPubBaseWithId, data_dict[key])
    if isinstance(value, str) or isinstance(value, HttpUrl):
        return str(value)
    if isinstance(value, dict):
        value_id: str | None = value.get("id", None)
        if value_id is not None:
            return value_id
    if isinstance(value, ActivityPubBaseWithId):
        return getattr(value, "id", None)
    return None


def dereference_or_raise(data: None | str | ObjectId | dict[str, Any] | ActivityPubBase, key: str) -> ObjectId:
    value = dereference(data=data, key=key)
    if value is None:
        raise APValidationError(f"Key '{key}' not found in data {data}")
    return value


class APObject(ActivityPubBaseWithId):
    """Base class for all ActivityPub objects.

    According to ActivityStreams 2.0 Core Types: https://www.w3.org/TR/activitystreams-core/#object
    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#object
    Usage: https://www.w3.org/TR/activitypub/#object
    """

    type: str = Field(default="Object", description="The type of the object")
    attachment: None | list["str | APObject"] = Field(default=None, description="Files attached to the object")
    attributed_to: "str | None | APObject" = Field(default=None, description="Entity attributed to this object")
    content: str | None = Field(default=None, description="The content of the object")
    hash: str | None = Field(default=None, description="SHA256-Hash of the content. OPTIONAL")
    context: "None | str | APObject" = Field(default=None, description="Context of the object")
    name: str | None = Field(default=None, description="The name/title of the object")
    end_time: APDateTime | None = Field(default=None, description="End time of the object")
    generator: "None | str | APObject" = Field(default=None, description="Application that generated the object")
    icon: "None | str | APObject" = Field(default=None, description="Icon representing the object")
    image: "None | str | APObject" = Field(default=None, description="Image representing the object")
    in_reply_to: "None | str | APObject" = Field(default=None, description="Object this is in reply to")
    location: "None | str | APObject" = Field(default=None, description="Location associated with the object")
    preview: "None | str | APObject" = Field(default=None, description="Preview of the object")
    published: APDateTime | None = Field(default=None, description="Publication date")
    replies: "str | None | APObject" = Field(default=None, description="Replies to this object")
    start_time: APDateTime | None = Field(default=None, description="Start time of the object")
    summary: str | None = Field(default=None, description="Summary of the object")
    tag: None | list["str | APObject | APLink"] = Field(default=None, description="Tags associated with the object")
    updated: APDateTime | None = Field(default=None, description="Last update time")
    url: list[ObjectId] | None = Field(default=None, description="URL of the object")
    audience: list[ObjectId] | None = Field(default=None, description="Intended audience")
    to: list[ObjectId] | None = Field(default=None, description="Primary recipients")
    bto: list[ObjectId] | None = Field(default=None, description="Private primary recipients")
    cc: list[ObjectId] | None = Field(default=None, description="Secondary recipients")
    bcc: list[ObjectId] | None = Field(default=None, description="Private secondary recipients")
    media_type: str | None = Field(default=None, description="MIME type")
    visibility: str = Field(default="public", description="The visibility to actors other than the object's creator")
    proof: DataIntegrityProof | list[DataIntegrityProof] | None = Field(
        default=None, description="OPTIONAL Data integrity proof (or list of proofs) of the object."
    )

    likes: "None | str | APObject" = Field(default=None, description="Likes collection")
    shares: "None | str | APObject" = Field(default=None, description="Shares collection")

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

    def clear_recipients(self):
        self.to = None
        self.cc = None
        self.bto = None
        self.bcc = None
        self.audience = None

    async def get_recipients(self) -> list[ObjectId]:
        """Get the list of actors in the activity recipient fields (to,cc,bto,bcc,audience).
        Does *not* resolve the inboxes of these recipients, and also does *not* resolve collections (like followers etc.).

        Returns:
            list[ObjectId]: List of actor_id's for all recipients found.
        """
        recipients: list[ObjectId] = []
        recipients += self.to or []
        recipients += self.cc or []
        recipients += self.bto or []
        recipients += self.bcc or []
        recipients += self.audience or []

        recipients = list(set(recipients))
        return recipients

    model_config: ClassVar[ConfigDict] = ConfigDict(
        json_schema_extra={
            "examples": [
                {
                    "id": "https://example.org/id-of-the-object-unused",
                    "type": "Object",
                    "attributedTo": "https://example.org/id-of-an-actor",
                    "content": "The content of the object described in text.",
                    "summary": "A summary of the object.",
                }
            ]
        },
    )


class APEvent(APObject):
    """Event object as defined in ActivityStreams 2.0.
    An Event represents any kind of event that can occur, such as a concert, a meeting, or a conference.

    Validation:
        - end_time must be after start_time if both are provided
        - Inherits all validation from APObject

    https://www.w3.org/TR/activitystreams-vocabulary/#event
    """

    type: str = Field(default="Event", description="Indicates that this object represents an event")
    start_time: APDateTime | None = Field(default=None, description="The start time of the event")
    end_time: APDateTime | None = Field(default=None, description="The end time of the event")

    @model_validator(mode="after")
    def validate_end_time(self) -> "APEvent":
        """Validate that end_time is after start_time if both are provided."""
        if self.start_time and self.end_time:
            if self.end_time <= self.start_time:
                raise ValueError("end_time must be after start_time")
        return self


class APPlace(APObject):
    """Place object as defined in ActivityStreams 2.0.

    A Place represents a physical or logical location. It can be used to represent
    a geographic location, a venue, or any other kind of place.

    Validation:
        - latitude must be between -90 and 90
        - longitude must be between -180 and 180
        - units must be one of: 'cm', 'feet', 'inches', 'km', 'm', 'miles'
        - Inherits all validation from APObject

    https://www.w3.org/TR/activitystreams-vocabulary/#place
    """

    type: str = Field(default="Place", description="Indicates that this object represents a place")
    accuracy: float | None = Field(default=None, description="The accuracy of the coordinates (in meters)")
    altitude: float | None = Field(default=None, description="The altitude of the place (in meters)")
    latitude: float | None = Field(default=None, ge=-90, le=90, description="The latitude of the place (-90 to 90)")
    longitude: float | None = Field(default=None, ge=-180, le=180, description="The longitude of the place (-180 to 180)")
    radius: float | None = Field(default=None, description="The radius of uncertainty")
    units: str | None = Field(default=None, description="The units for the radius (e.g., 'cm', 'feet', 'km')")

    @field_validator("units")
    def validate_units(cls, v: str):
        valid_units = ["cm", "feet", "inches", "km", "m", "miles"]
        if v and v not in valid_units:
            raise ValueError(f"Invalid unit: {v}. Must be one of {', '.join(valid_units)}")
        return v

    @field_validator("latitude")
    def validate_latitude(cls, l: float):
        if l < -90 or l > 90:
            raise ValueError(f"Invalid latitude: {l}. Must be in the range of (-90, 90)")
        return l

    @field_validator("longitude")
    def validate_longitude(cls, l: float):
        if l < -180 or l > 180:
            raise ValueError(f"Invalid longitude: {l}. Must be in the range of (-180, 180)")
        return l


class APProfile(APObject):
    """
    Represents a Profile object.

    https://www.w3.org/TR/activitystreams-vocabulary/#profile
    """

    type: str = "Profile"
    describes: str | APObject


class APRelationship(APObject):
    """
    Represents a Relationship between objects.

    https://www.w3.org/TR/activitystreams-vocabulary/#relationship
    """

    type: str = "Relationship"
    subject: str | APObject
    object: str | APObject
    relationship: str


class APTombstone(APObject):
    """
    Represents a Tombstone (deleted object).

    https://www.w3.org/TR/activitystreams-vocabulary/#tombstone
    """

    type: str = "Tombstone"
    former_type: str
    deleted_at: APDateTime


class APDocument(APObject):
    """
    Represents a Document.

    https://www.w3.org/TR/activitystreams-vocabulary/#document
    """

    type: str = DocumentType.DOCUMENT.value
    hash: str | None = Field(
        default=None,
        description="SHA256-Hash of the document that is stored as media. If a file is stored and the document is describing it, the file hash is used here. OPTIONAL",
    )


class APArticle(APDocument):
    """
    Represents an Article.

    https://www.w3.org/TR/activitystreams-vocabulary/#article
    """

    type: str = DocumentType.ARTICLE.value


class APAudio(APDocument):
    """
    Represents an Audio object.

    https://www.w3.org/TR/activitystreams-vocabulary/#audio
    """

    type: str = DocumentType.AUDIO.value
    duration: str | None = None


class APImage(APDocument):
    """
    Represents an Image.

    https://www.w3.org/TR/activitystreams-vocabulary/#image
    """

    type: str = DocumentType.IMAGE.value
    width: int | None = Field(None, gt=0, description="The width of the image")
    height: int | None = Field(None, gt=0, description="The height of the image")


class APNote(APDocument):
    """
    Represents a Note.

    https://www.w3.org/TR/activitystreams-vocabulary/#note
    """

    type: str = DocumentType.NOTE.value


class APPage(APDocument):
    """
    Represents a Page.

    https://www.w3.org/TR/activitystreams-vocabulary/#page
    """

    type: str = DocumentType.PAGE.value


class APVideo(APDocument):
    """
    Represents a Video object.

    https://www.w3.org/TR/activitystreams-vocabulary/#video
    """

    type: str = DocumentType.VIDEO.value
    duration: str | None = None


class APUserData(APDocument):
    """
    Sending this object (by a normal user of an instance) will assemble their data into a file on the instance.
    The media url of the file can be accessed through the object of the maintenance activity.
    """

    type: str = DocumentType.USERDATA.value