# mautrix-instagram - A Matrix-Instagram puppeting bridge.
# Copyright (C) 2020 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 __future__ import annotations

from typing import Any
from enum import Enum
from uuid import uuid4
import json


class SkywalkerSubscription:
    @staticmethod
    def direct_sub(user_id: str | int) -> str:
        return f"ig/u/v1/{user_id}"

    @staticmethod
    def live_sub(user_id: str | int) -> str:
        return f"ig/live_notification_subscribe/{user_id}"


class GraphQLQueryID(Enum):
    APP_PRESENCE = "17846944882223835"
    ASYNC_AD_SUB = "17911191835112000"
    CLIENT_CONFIG_UPDATE = "17849856529644700"
    DIRECT_STATUS = "17854499065530643"
    DIRECT_TYPING = "17867973967082385"
    LIVE_WAVE = "17882305414154951"
    INTERACTIVITY_ACTIVATE_QUESTION = "18005526940184517"
    INTERACTIVITY_REALTIME_QUESTION_SUBMISSION_STATUS = "18027779584026952"
    INTERACTIVITY_SUB = "17907616480241689"
    LIVE_REALTIME_COMMENTS = "17855344750227125"
    LIVE_TYPING_INDICATOR = "17926314067024917"
    MEDIA_FEEDBACK = "17877917527113814"
    REACT_NATIVE_OTA = "17861494672288167"
    VIDEO_CALL_CO_WATCH_CONTROL = "17878679623388956"
    VIDEO_CALL_IN_ALERT = "17878679623388956"
    VIDEO_CALL_PROTOTYPE_PUBLISH = "18031704190010162"
    VIDEO_CALL_PARTICIPANT_DELIVERY = "17977239895057311"
    ZERO_PROVISION = "17913953740109069"
    INAPP_NOTIFICATION = "17899377895239777"
    BUSINESS_DELIVERY = "17940467278199720"


everclear_subscriptions = {
    "inapp_notification_subscribe_comment": GraphQLQueryID.INAPP_NOTIFICATION.value,
    "inapp_notification_subscribe_comment_mention_and_reply": GraphQLQueryID.INAPP_NOTIFICATION.value,
    "video_call_participant_state_delivery": GraphQLQueryID.VIDEO_CALL_PARTICIPANT_DELIVERY.value,
    "inapp_notification_subscribe_story_emoji_reaction": GraphQLQueryID.INAPP_NOTIFICATION.value,
    "inapp_notification_subscribe_prompt_sticker_reply": GraphQLQueryID.INAPP_NOTIFICATION.value,
    "inapp_notification_subscribe_fundraiser_cohost_invited": GraphQLQueryID.INAPP_NOTIFICATION.value,
    "inapp_notification_subscribe_watch_receipt": GraphQLQueryID.INAPP_NOTIFICATION.value,
}


class GraphQLSubscription:
    @staticmethod
    def _fmt(
        query_id: GraphQLQueryID, input_params: Any, client_logged: bool | None = None
    ) -> str:
        params = {
            "input_data": input_params,
            **(
                {"%options": {"client_logged": client_logged}} if client_logged is not None else {}
            ),
        }
        return f"1/graphqlsubscriptions/{query_id.value}/{json.dumps(params)}"

    @classmethod
    def app_presence(
        cls, subscription_id: str | None = None, client_logged: bool | None = None
    ) -> str:
        return cls._fmt(
            GraphQLQueryID.APP_PRESENCE,
            input_params={"client_subscription_id": subscription_id or str(uuid4())},
            client_logged=client_logged,
        )

    @classmethod
    def async_ad(
        cls,
        user_id: str,
        subscription_id: str | None = None,
        client_logged: bool | None = None,
    ) -> str:
        return cls._fmt(
            GraphQLQueryID.ASYNC_AD_SUB,
            input_params={
                "client_subscription_id": subscription_id or str(uuid4()),
                "user_id": user_id,
            },
            client_logged=client_logged,
        )

    @classmethod
    def client_config_update(
        cls, subscription_id: str | None = None, client_logged: bool | None = None
    ) -> str:
        return cls._fmt(
            GraphQLQueryID.CLIENT_CONFIG_UPDATE,
            input_params={"client_subscription_id": subscription_id or str(uuid4())},
            client_logged=client_logged,
        )

    @classmethod
    def direct_status(
        cls, subscription_id: str | None = None, client_logged: bool | None = None
    ) -> str:
        return cls._fmt(
            GraphQLQueryID.DIRECT_STATUS,
            input_params={"client_subscription_id": subscription_id or str(uuid4())},
            client_logged=client_logged,
        )

    @classmethod
    def direct_typing(cls, user_id: str, client_logged: bool | None = None) -> str:
        return cls._fmt(
            GraphQLQueryID.DIRECT_TYPING,
            input_params={"user_id": user_id},
            client_logged=client_logged,
        )

    @classmethod
    def ig_live_wave(
        cls,
        broadcast_id: str,
        receiver_id: str,
        subscription_id: str | None = None,
        client_logged: bool | None = None,
    ) -> str:
        return cls._fmt(
            GraphQLQueryID.LIVE_WAVE,
            input_params={
                "client_subscription_id": subscription_id or str(uuid4()),
                "broadcast_id": broadcast_id,
                "receiver_id": receiver_id,
            },
            client_logged=client_logged,
        )

    @classmethod
    def interactivity_activate_question(
        cls,
        broadcast_id: str,
        subscription_id: str | None = None,
        client_logged: bool | None = None,
    ) -> str:
        return cls._fmt(
            GraphQLQueryID.INTERACTIVITY_ACTIVATE_QUESTION,
            input_params={
                "client_subscription_id": subscription_id or str(uuid4()),
                "broadcast_id": broadcast_id,
            },
            client_logged=client_logged,
        )

    @classmethod
    def interactivity_realtime_question_submissions_status(
        cls,
        broadcast_id: str,
        subscription_id: str | None = None,
        client_logged: bool | None = None,
    ) -> str:
        return cls._fmt(
            GraphQLQueryID.INTERACTIVITY_REALTIME_QUESTION_SUBMISSION_STATUS,
            input_params={
                "client_subscription_id": subscription_id or str(uuid4()),
                "broadcast_id": broadcast_id,
            },
            client_logged=client_logged,
        )

    @classmethod
    def interactivity(
        cls,
        broadcast_id: str,
        subscription_id: str | None = None,
        client_logged: bool | None = None,
    ) -> str:
        return cls._fmt(
            GraphQLQueryID.INTERACTIVITY_SUB,
            input_params={
                "client_subscription_id": subscription_id or str(uuid4()),
                "broadcast_id": broadcast_id,
            },
            client_logged=client_logged,
        )

    @classmethod
    def live_realtime_comments(
        cls,
        broadcast_id: str,
        subscription_id: str | None = None,
        client_logged: bool | None = None,
    ) -> str:
        return cls._fmt(
            GraphQLQueryID.LIVE_REALTIME_COMMENTS,
            input_params={
                "client_subscription_id": subscription_id or str(uuid4()),
                "broadcast_id": broadcast_id,
            },
            client_logged=client_logged,
        )

    @classmethod
    def live_realtime_typing_indicator(
        cls,
        broadcast_id: str,
        subscription_id: str | None = None,
        client_logged: bool | None = None,
    ) -> str:
        return cls._fmt(
            GraphQLQueryID.LIVE_TYPING_INDICATOR,
            input_params={
                "client_subscription_id": subscription_id or str(uuid4()),
                "broadcast_id": broadcast_id,
            },
            client_logged=client_logged,
        )

    @classmethod
    def media_feedback(
        cls,
        feedback_id: str,
        subscription_id: str | None = None,
        client_logged: bool | None = None,
    ) -> str:
        return cls._fmt(
            GraphQLQueryID.MEDIA_FEEDBACK,
            input_params={
                "client_subscription_id": subscription_id or str(uuid4()),
                "feedback_id": feedback_id,
            },
            client_logged=client_logged,
        )

    @classmethod
    def react_native_ota_update(
        cls,
        build_number: str,
        subscription_id: str | None = None,
        client_logged: bool | None = None,
    ) -> str:
        return cls._fmt(
            GraphQLQueryID.REACT_NATIVE_OTA,
            input_params={
                "client_subscription_id": subscription_id or str(uuid4()),
                "build_number": build_number,
            },
            client_logged=client_logged,
        )

    @classmethod
    def video_call_co_watch_control(
        cls,
        video_call_id: str,
        subscription_id: str | None = None,
        client_logged: bool | None = None,
    ) -> str:
        return cls._fmt(
            GraphQLQueryID.VIDEO_CALL_CO_WATCH_CONTROL,
            input_params={
                "client_subscription_id": subscription_id or str(uuid4()),
                "video_call_id": video_call_id,
            },
            client_logged=client_logged,
        )

    @classmethod
    def video_call_in_call_alert(
        cls,
        video_call_id: str,
        subscription_id: str | None = None,
        client_logged: bool | None = None,
    ) -> str:
        return cls._fmt(
            GraphQLQueryID.VIDEO_CALL_IN_ALERT,
            input_params={
                "client_subscription_id": subscription_id or str(uuid4()),
                "video_call_id": video_call_id,
            },
            client_logged=client_logged,
        )

    @classmethod
    def video_call_prototype_publish(
        cls,
        video_call_id: str,
        subscription_id: str | None = None,
        client_logged: bool | None = None,
    ) -> str:
        return cls._fmt(
            GraphQLQueryID.VIDEO_CALL_PROTOTYPE_PUBLISH,
            input_params={
                "client_subscription_id": subscription_id or str(uuid4()),
                "video_call_id": video_call_id,
            },
            client_logged=client_logged,
        )

    @classmethod
    def zero_provision(
        cls,
        device_id: str,
        subscription_id: str | None = None,
        client_logged: bool | None = None,
    ) -> str:
        return cls._fmt(
            GraphQLQueryID.ZERO_PROVISION,
            input_params={
                "client_subscription_id": subscription_id or str(uuid4()),
                "device_id": device_id,
            },
            client_logged=client_logged,
        )


_topic_map: dict[str, str] = {
    "/pp": "34",  # unknown
    "/ig_sub_iris": "134",
    "/ig_sub_iris_response": "135",
    "/ig_message_sync": "146",
    "/ig_send_message": "132",
    "/ig_send_message_response": "133",
    "/ig_realtime_sub": "149",
    "/pubsub": "88",
    "/t_fs": "102",  # Foreground state
    "/graphql": "9",
    "/t_region_hint": "150",
    "/mqtt_health_stats": "/mqtt_health_stats",
    "/ls_resp": "179",
    "/rs_req": "244",
    "/rs_resp": "245",
    "/t_rtc_log": "274",
}

_reverse_topic_map: dict[str, str] = {value: key for key, value in _topic_map.items()}


class RealtimeTopic(Enum):
    SUB_IRIS = "/ig_sub_iris"
    SUB_IRIS_RESPONSE = "/ig_sub_iris_response"
    MESSAGE_SYNC = "/ig_message_sync"
    SEND_MESSAGE = "/ig_send_message"
    SEND_MESSAGE_RESPONSE = "/ig_send_message_response"
    REALTIME_SUB = "/ig_realtime_sub"
    PUBSUB = "/pubsub"
    FOREGROUND_STATE = "/t_fs"
    GRAPHQL = "/graphql"
    REGION_HINT = "/t_region_hint"
    MQTT_HEALTH_STATS = "/mqtt_health_stats"
    UNKNOWN_PP = "/pp"
    LIGHTSPEED_RESPONSE = "/ls_resp"
    RS_REQ = "/rs_req"
    RS_RESP = "/rs_resp"
    T_RTC_LOG = "/t_rtc_log"

    @property
    def encoded(self) -> str:
        return _topic_map[self.value]

    @staticmethod
    def decode(val: str) -> RealtimeTopic:
        return RealtimeTopic(_reverse_topic_map[val])
