# mautrix-instagram - A Matrix-Instagram puppeting bridge.
# Copyright (C) 2022 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
from typing import List, Optional, Union
import logging

from attr import dataclass
from yarl import URL
import attr

from mautrix.types import (
    JSON,
    ExtensibleEnum,
    Obj,
    SerializableAttrs,
    SerializableEnum,
    SerializerError,
)
from mautrix.types.util.serializable_attrs import _dict_to_attrs

from .account import BaseResponseUser, UserIdentifier

log = logging.getLogger("mauigpapi.types")


class ThreadItemType(ExtensibleEnum):
    DELETION = "deletion"
    MEDIA = "media"
    TEXT = "text"
    LIKE = "like"
    HASHTAG = "hashtag"
    PROFILE = "profile"
    MEDIA_SHARE = "media_share"
    CONFIGURE_PHOTO = "configure_photo"
    CONFIGURE_VIDEO = "configure_video"
    SHARE_VOICE = "share_voice"
    LOCATION = "location"
    ACTION_LOG = "action_log"
    TITLE = "title"
    USER_REACTION = "user_reaction"
    HISTORY_EDIT = "history_edit"
    REACTION_LOG = "reaction_log"
    REEL_SHARE = "reel_share"
    DEPRECATED_CHANNEL = "deprecated_channel"
    LINK = "link"
    RAVEN_MEDIA = "raven_media"
    LIVE_VIDEO_SHARE = "live_video_share"
    TEST = "test"
    STORY_SHARE = "story_share"
    REEL_REACT = "reel_react"
    LIVE_INVITE_GUEST = "live_invite_guest"
    LIVE_VIEWER_INVITE = "live_viewer_invite"
    TYPE_MAX = "type_max"
    PLACEHOLDER = "placeholder"
    PRODUCT = "product"
    PRODUCT_SHARE = "product_share"
    VIDEO_CALL_EVENT = "video_call_event"
    POLL_VOTE = "poll_vote"
    FELIX_SHARE = "felix_share"
    ANIMATED_MEDIA = "animated_media"
    CTA_LINK = "cta_link"
    VOICE_MEDIA = "voice_media"
    STATIC_STICKER = "static_sticker"
    AR_EFFECT = "ar_effect"
    SELFIE_STICKER = "selfie_sticker"
    REACTION = "reaction"
    CLIP = "clip"
    GUIDE_SHARE = "guide_share"
    XMA_MEDIA_SHARE = "xma_media_share"
    XMA_REEL_SHARE = "xma_reel_share"
    XMA_STORY_SHARE = "xma_story_share"
    XMA_REEL_MENTION = "xma_reel_mention"
    XMA_CLIP = "xma_clip"
    EXPIRED_PLACEHOLDER = "expired_placeholder"
    AVATAR_STICKER = "avatar_sticker"
    PHOTO_ATTACHMENT = "photo_attachment"
    VIDEO_ATTACHMENT = "video_attachment"
    VOICE_ATTACHMENT = "voice_attachment"


@dataclass(kw_only=True)
class ThreadItemActionLog(SerializableAttrs):
    description: str
    # TODO bold, text_attributes


class ViewMode(SerializableEnum):
    ONCE = "once"
    REPLAYABLE = "replayable"
    PERMANENT = "permanent"


@dataclass(kw_only=True)
class CreativeConfig(SerializableAttrs):
    capture_type: str
    camera_facing: Optional[str] = None
    should_render_try_it_on: Optional[bool] = None


@dataclass(kw_only=True)
class CreateModeAttribution(SerializableAttrs):
    type: str
    name: str


@dataclass(kw_only=True)
class ImageVersion(SerializableAttrs):
    width: int
    height: int
    url: str
    estimated_scan_sizes: Optional[List[int]] = None


@dataclass(kw_only=True)
class ImageVersions(SerializableAttrs):
    candidates: List[ImageVersion]


@dataclass
class ImageVersionsContainer(SerializableAttrs):
    image_versions2: Optional[ImageVersions] = None
    original_width: Optional[int] = None
    original_height: Optional[int] = None

    @property
    def best_image(self) -> Optional[ImageVersion]:
        if not self.image_versions2:
            return None
        best: Optional[ImageVersion] = None
        for version in self.image_versions2.candidates:
            if version.width == self.original_width and version.height == self.original_height:
                return version
            elif not best or (version.width * version.height > best.width * best.height):
                best = version
        return best


@dataclass(kw_only=True)
class VideoVersion(SerializableAttrs):
    type: int
    width: int
    height: int
    url: str
    id: Optional[str] = None


class MediaType(SerializableEnum):
    IMAGE = 1
    VIDEO = 2
    AD_MAP = 6
    LIVE = 7
    CAROUSEL = 8
    LIVE_REPLAY = 9
    COLLECTION = 10
    AUDIO = 11
    SHOWREEL_NATIVE = 12

    @property
    def human_name(self) -> str:
        return self.name.lower().replace("_", " ")

    @property
    def articled_alt_human_name(self) -> str:
        if self == MediaType.IMAGE:
            return "a photo"
        elif self == MediaType.VIDEO:
            return "a video"
        elif self == MediaType.CAROUSEL:
            return "photos"
        elif self == MediaType.AUDIO:
            return "an audio message"
        else:
            return "a media message"


@dataclass(kw_only=True)
class ExpiredMediaItem(SerializableAttrs):
    media_type: Optional[MediaType] = None
    user: Optional[BaseResponseUser] = None


@dataclass(kw_only=True)
class RegularMediaItem(ImageVersionsContainer, SerializableAttrs):
    id: str
    video_versions: Optional[List[VideoVersion]] = None
    media_type: MediaType
    media_id: Optional[int] = None
    organic_tracking_token: Optional[str] = None
    creative_config: Optional[CreativeConfig] = None
    create_mode_attribution: Optional[CreateModeAttribution] = None
    is_commercial: Optional[bool] = None
    commerciality_status: Optional[str] = None  # TODO enum? commercial

    @property
    def best_video(self) -> Optional[VideoVersion]:
        if not self.video_versions:
            return None
        best: Optional[VideoVersion] = None
        for version in self.video_versions:
            if version.width == self.original_width and version.height == self.original_height:
                return version
            elif not best or (version.width * version.height > best.width * best.height):
                best = version
        return best


@dataclass(kw_only=True)
class Caption(SerializableAttrs):
    pk: int
    user_id: int
    text: str
    # TODO enum? 1
    type: int
    created_at: int
    created_at_utc: int
    # TODO enum? comment
    content_type: str
    # TODO enum? Active
    status: str
    # TODO enum-ish thing?
    bit_flags: int
    user: BaseResponseUser
    media_id: int

    # Might not be in all captions
    is_covered: Optional[bool] = None
    private_reply_status: Optional[int] = None
    # did_report_as_spam: bool
    # share_enabled: bool


@dataclass
class Location(SerializableAttrs):
    pk: int
    short_name: str
    facebook_places_id: int
    # TODO enum?
    external_source: str  # facebook_places
    name: str
    address: str
    city: str
    lng: Optional[float] = None
    lat: Optional[float] = None
    is_eligible_for_guides: bool = False


@dataclass(kw_only=True)
class CarouselMediaItem(RegularMediaItem, SerializableAttrs):
    carousel_parent_id: str
    pk: int


@dataclass
class UserTag(SerializableAttrs):
    user: BaseResponseUser
    position: List[float]
    # start_time_in_video_in_sec
    # duration_in_video_in_sec


@dataclass
class UserTags(SerializableAttrs):
    in_: List[UserTag] = attr.ib(metadata={"json": "in"}, factory=lambda: [])


@dataclass(kw_only=True)
class MediaShareItem(RegularMediaItem, SerializableAttrs):
    taken_at: int
    pk: int
    device_timestamp: int
    code: str
    client_cache_key: str
    filter_type: int
    user: BaseResponseUser
    # Not present in reel shares
    can_viewer_reshare: Optional[bool] = None
    caption_is_edited: bool = False
    comment_likes_enabled: bool = False
    comment_threading_enabled: bool = False
    has_more_comments: bool = False
    max_num_visible_preview_comments: int = 0
    # preview_comments: List[TODO]
    can_view_more_preview_comments: bool = False
    comment_count: int = 0
    like_count: int = 0
    has_liked: bool = False
    photo_of_you: bool = False
    usertags: Optional[UserTags] = None
    caption: Optional[Caption] = None
    can_viewer_save: bool = True
    location: Optional[Location] = None
    carousel_media_count: Optional[int] = None
    carousel_media: Optional[List[CarouselMediaItem]] = None


@dataclass
class SharingFrictionInfo(SerializableAttrs):
    should_have_sharing_friction: bool
    bloks_app_url: Optional[str]


# The fields in this class have been observed in reel share items, but may exist elsewhere too.
# If they're observed in other types, they should be moved to MediaShareItem.
@dataclass(kw_only=True)
class ReelMediaShareItem(MediaShareItem, SerializableAttrs):
    # These three are apparently sometimes not present
    # TODO enum?
    caption_position: Optional[int] = None
    is_reel_media: Optional[bool] = None
    timezone_offset: Optional[int] = None
    # likers: List[TODO]
    can_see_insights_as_brand: bool = False
    expiring_at: Optional[int] = None
    sharing_friction_info: Optional[SharingFrictionInfo] = None
    is_in_profile_grid: bool = False
    profile_grid_control_enabled: bool = False
    is_shop_the_look_eligible: bool = False
    # TODO enum?
    deleted_reason: Optional[int] = None
    integrity_review_decision: Optional[str] = None
    # Not present in story_share, only reel_share
    story_is_saved_to_archive: Optional[bool] = None


@dataclass(kw_only=True)
class ReplayableMediaItem(SerializableAttrs):
    view_mode: ViewMode
    seen_count: int
    seen_user_ids: List[int]
    replay_expiring_at_us: Optional[int] = None


@dataclass(kw_only=True)
class VisualMedia(ReplayableMediaItem, SerializableAttrs):
    url_expire_at_secs: Optional[int] = None
    playback_duration_secs: Optional[int] = None
    media: Union[RegularMediaItem, ExpiredMediaItem]

    @classmethod
    def deserialize(cls, data: JSON) -> "VisualMedia":
        data = {**data}
        if "id" not in data["media"]:
            data["media"] = ExpiredMediaItem.deserialize(data["media"])
        else:
            data["media"] = RegularMediaItem.deserialize(data["media"])
        return _dict_to_attrs(cls, data)


@dataclass(kw_only=True)
class AudioInfo(SerializableAttrs):
    audio_src: str
    duration: int
    waveform_data: Optional[List[int]] = None
    waveform_sampling_frequency_hz: Optional[int] = None


@dataclass(kw_only=True)
class VoiceMediaData(SerializableAttrs):
    id: str
    audio: AudioInfo
    organic_tracking_token: str
    user: UserIdentifier
    # TODO enum?
    product_type: str  # "direct_audio"
    media_type: MediaType  # MediaType.AUDIO


@dataclass(kw_only=True)
class VoiceMediaItem(ReplayableMediaItem, SerializableAttrs):
    media: VoiceMediaData


@dataclass(kw_only=True)
class AnimatedMediaImage(SerializableAttrs):
    height: str
    mp4: str
    mp4_size: str
    size: str
    url: str
    webp: str
    webp_size: str
    width: str


@dataclass(kw_only=True)
class AnimatedMediaImages(SerializableAttrs):
    fixed_height: Optional[AnimatedMediaImage] = None


@dataclass(kw_only=True)
class AnimatedMediaItem(SerializableAttrs):
    id: str
    images: AnimatedMediaImages
    # user: {is_verified: bool, username: str}
    # is_random: str | None
    # is_sticker: str | bool


class ReactionType(SerializableEnum):
    LIKES = "likes"
    EMOJIS = "emojis"


@dataclass
class Reaction(SerializableAttrs):
    sender_id: int
    timestamp: Optional[int]
    emoji: Optional[str] = "❤️"
    super_react_type: Optional[str] = None
    client_context: Optional[str] = None
    type: Optional[ReactionType] = None

    @property
    def timestamp_ms(self) -> int:
        return self.timestamp // 1000


@dataclass
class Reactions(SerializableAttrs):
    likes_count: int = 0
    likes: List[Reaction] = attr.ib(factory=lambda: [])
    emojis: List[Reaction] = attr.ib(factory=lambda: [])


@dataclass
class LinkContext(SerializableAttrs):
    link_url: str
    link_title: str
    link_summary: str
    link_image_url: str


@dataclass
class LinkItem(SerializableAttrs):
    text: str
    link_context: LinkContext
    client_context: str
    mutation_token: Optional[str] = None


class ReelShareType(ExtensibleEnum):
    REPLY = "reply"
    REACTION = "reaction"
    MENTION = "mention"
    REPLY_GIF = "reply_gif"


@dataclass
class ReelShareReactionInfo(SerializableAttrs):
    emoji: str
    # TODO find type
    # intensity: Any


@dataclass
class ReelShareItem(SerializableAttrs):
    text: str
    type: ReelShareType
    reel_owner_id: int
    is_reel_persisted: int
    reel_type: str
    media: Union[ReelMediaShareItem, ExpiredMediaItem]
    reaction_info: Optional[ReelShareReactionInfo] = None
    mentioned_user_id: Optional[int] = None

    @classmethod
    def deserialize(cls, data: JSON) -> "ReelShareItem":
        data = {**data}
        if "id" not in data["media"]:
            data["media"] = ExpiredMediaItem.deserialize(data["media"])
        else:
            data["media"] = ReelMediaShareItem.deserialize(data["media"])
        return _dict_to_attrs(cls, data)


@dataclass
class StoryShareItem(SerializableAttrs):
    text: str
    media: Union[ReelMediaShareItem, ExpiredMediaItem]

    # Only present when not expired
    is_reel_persisted: Optional[bool] = None
    # TODO enum?
    reel_type: Optional[str] = None  # user_reel
    reel_id: Optional[str] = None
    # TODO enum?
    story_share_type: Optional[str] = None  # default

    # Only present when expired
    message: Optional[str] = None
    # TODO enum
    reason: Optional[int] = None  # 3 = expired?

    @classmethod
    def deserialize(cls, data: JSON) -> "StoryShareItem":
        data = {**data}
        if "media" not in data:
            data["media"] = ExpiredMediaItem()
        else:
            data["media"] = ReelMediaShareItem.deserialize(data["media"])
        return _dict_to_attrs(cls, data)


@dataclass
class DirectMediaShareItem(SerializableAttrs):
    text: str
    # TODO enum?
    media_share_type: str  # tag
    tagged_user_id: int
    media: MediaShareItem


@dataclass
class PreviewURLInfo(SerializableAttrs):
    url: str
    width: int
    height: int


@dataclass
class XMAMediaShareItem(SerializableAttrs):
    xma_layout_type: int

    title_text: Optional[str] = None

    target_url: Optional[str] = None

    header_title_text: Optional[str] = None
    subtitle_text: Optional[str] = None
    caption_body_text: Optional[str] = None

    preview_url: Optional[str] = None
    preview_url_mime_type: Optional[str] = None
    preview_url_info: Optional[PreviewURLInfo] = None
    preview_width: Optional[int] = None
    preview_height: Optional[int] = None

    # For avatar_stickers
    is_sharable: Optional[bool] = None
    is_borderless: Optional[bool] = None
    should_respect_server_preview_size: Optional[bool] = None

    @property
    def reel_share_clip_id(self) -> Optional[int]:
        if "instagram.com/reel/" not in self.target_url:
            return None
        try:
            real_id, extra_id = URL(self.target_url).query["id"].split("_")
            return int(real_id)
        except (ValueError, KeyError):
            return None


@dataclass
class XMAMediaProfileShareItem(SerializableAttrs):
    xma_layout_type: int

    header_title_text: str
    header_subtitle_text: Optional[str]
    target_url: str


@dataclass
class ClipItem(SerializableAttrs):
    # TODO there are some additional fields in clips
    clip: MediaShareItem


@dataclass
class FelixShareItem(SerializableAttrs):
    video: MediaShareItem
    text: Optional[str] = None


@dataclass
class ProfileItem(BaseResponseUser, SerializableAttrs):
    pass


@dataclass
class VideoCallEvent(SerializableAttrs):
    action: str
    description: str


@dataclass
class PlaceholderItem(SerializableAttrs):
    title: Optional[str] = None
    message: Optional[str] = None
    reason: Optional[int] = None
    # is_linked: bool


@dataclass
class FetchedClipInfo(SerializableAttrs):
    media: MediaShareItem
    status: str


@dataclass
class TextEntities(SerializableAttrs):
    mentioned_user_ids: List[int] = attr.ib(factory=lambda: [])


@dataclass
class MentionedEntity(SerializableAttrs):
    fbid: str
    offset: int
    length: int
    interop_user_type: int


@dataclass(kw_only=True)
class ThreadImage(ImageVersionsContainer, SerializableAttrs):
    id: int
    media_type: int


@dataclass(kw_only=True)
class ThreadItem(SerializableAttrs):
    item_id: Optional[str] = None
    user_id: Optional[int] = None
    timestamp: int = 0
    item_type: Optional[ThreadItemType] = None
    is_shh_mode: bool = False
    new_reaction: Optional[Reaction] = None

    text: Optional[str] = None
    text_entities: Optional[TextEntities] = None
    mentioned_entities: List[MentionedEntity] = None
    client_context: Optional[str] = None
    show_forward_attribution: Optional[bool] = None
    action_log: Optional[ThreadItemActionLog] = None
    thread_image: Optional[ThreadImage] = None
    auxiliary_text: Optional[str] = None
    auxiliary_text_source_type: Optional[int] = None
    message_item_type: Optional[str] = None

    replied_to_message: Optional["ThreadItem"] = None

    media: Optional[RegularMediaItem] = None
    voice_media: Optional[VoiceMediaItem] = None
    animated_media: Optional[AnimatedMediaItem] = None
    visual_media: Optional[VisualMedia] = None
    media_share: Optional[MediaShareItem] = None
    direct_media_share: Optional[DirectMediaShareItem] = None
    generic_xma: Optional[List[XMAMediaShareItem]] = None
    xma_media_share: Optional[List[XMAMediaShareItem]] = None
    xma_story_share: Optional[List[XMAMediaShareItem]] = None
    xma_reel_share: Optional[List[XMAMediaShareItem]] = None
    xma_reel_mention: Optional[List[XMAMediaShareItem]] = None
    xma_clip: Optional[List[XMAMediaShareItem]] = None
    xma_profile: Optional[List[XMAMediaProfileShareItem]] = None
    avatar_sticker: Optional[List[XMAMediaShareItem]] = None
    reel_share: Optional[ReelShareItem] = None
    story_share: Optional[StoryShareItem] = None
    location: Optional[Location] = None
    reactions: Optional[Reactions] = None
    like: Optional[str] = None
    link: Optional[LinkItem] = None
    clip: Optional[ClipItem] = None
    felix_share: Optional[FelixShareItem] = None
    profile: Optional[ProfileItem] = None
    placeholder: Optional[PlaceholderItem] = None
    video_call_event: Optional[VideoCallEvent] = None

    @property
    def timestamp_ms(self) -> int:
        return self.timestamp // 1000

    @classmethod
    def deserialize(cls, data: JSON, catch_errors: bool = True) -> Union["ThreadItem", Obj]:
        if not catch_errors:
            return _dict_to_attrs(cls, data)
        try:
            return _dict_to_attrs(cls, data)
        except SerializerError:
            log.debug("Failed to deserialize ThreadItem %s", data, exc_info=True)
            return Obj(**data)

    @property
    def unhandleable_type(self) -> str:
        if self.action_log:
            return "action log"
        return "unknown"

    @property
    def is_handleable(self) -> bool:
        return bool(
            self.media
            or self.animated_media
            or self.voice_media
            or self.visual_media
            or self.location
            or self.profile
            or self.reel_share
            or self.media_share
            or self.direct_media_share
            or self.story_share
            or self.clip
            or self.felix_share
            or self.text
            or self.like
            or self.link
        )


# This resolves the 'ThreadItem' string into an actual type.
# Starting Python 3.10, all type annotations will be strings and have to be resolved like this.
# TODO do this automatically for all SerializableAttrs somewhere in mautrix-python
attr.resolve_types(ThreadItem)
