from typing import List, Optional

from twitchio import PartialUser, Client, Channel, CustomReward, parse_timestamp


__all__ = (
    "PoolError",
    "PoolFull",
    "PubSubMessage",
    "PubSubBitsMessage",
    "PubSubBitsBadgeMessage",
    "PubSubChatMessage",
    "PubSubBadgeEntitlement",
    "PubSubChannelPointsMessage",
    "PubSubModerationAction",
    "PubSubModerationActionModeratorAdd",
    "PubSubModerationActionBanRequest",
    "PubSubModerationActionChannelTerms",
    "PubSubChannelSubscribe",
)


class PubSubError(Exception):
    pass


class ConnectionFailure(PubSubError):
    pass


class PoolError(PubSubError):
    pass


class PoolFull(PoolError):
    pass


class PubSubChatMessage:
    """
    A message received from twitch.

    Attributes
    -----------
    content: :class:`str`
        The content received
    id: :class:`str`
        The id of the payload
    type: :class:`str`
        The payload type
    """

    __slots__ = "content", "id", "type"

    def __init__(self, content: str, id: str, type: str):
        self.content = content
        self.id = id
        self.type = type


class PubSubBadgeEntitlement:
    """
    A badge entitlement

    Attributes
    -----------
    new: :class:`int`
        The new badge
    old: :class:`int`
        The old badge
    """

    __slots__ = "new", "old"

    def __init__(self, new: int, old: int):
        self.new = new
        self.old = old


class PubSubMessage:
    """
    A message from the pubsub websocket

    Attributes
    -----------
    topic: :class:`str`
        The topic subscribed to
    """

    __slots__ = "topic", "_data"

    def __init__(self, client: Client, topic: Optional[str], data: dict):
        self.topic = topic
        self._data = data


class PubSubBitsMessage(PubSubMessage):
    """
    A Bits message

    Attributes
    -----------
    message: :class:`PubSubChatMessage`
        The message sent along with the bits.
    badge_entitlement: Optional[:class:`PubSubBadgeEntitlement`]
        The badges received, if any.
    bits_used: :class:`int`
        The amount of bits used.
    channel_id: :class:`int`
        The channel the bits were given to.
    user: Optional[:class:`twitchio.PartialUser`]
        The user giving the bits. Can be None if anonymous.
    version: :class:`str`
        The event version.
    """

    __slots__ = "badge_entitlement", "bits_used", "channel_id", "context", "anonymous", "message", "user", "version"

    def __init__(self, client: Client, topic: str, data: dict):
        super().__init__(client, topic, data)

        data = data["message"]
        self.message = PubSubChatMessage(data["data"]["chat_message"], data["message_id"], data["message_type"])
        self.badge_entitlement = (
            PubSubBadgeEntitlement(
                data["data"]["badge_entitlement"]["new_version"], data["data"]["badge_entitlement"]["old_version"]
            )
            if data["data"]["badge_entitlement"]
            else None
        )
        self.bits_used: int = data["data"]["bits_used"]
        self.channel_id: int = int(data["data"]["channel_id"])
        self.user = (
            PartialUser(client._http, data["data"]["user_id"], data["data"]["user_name"])
            if data["data"]["user_id"]
            else None
        )
        self.version: str = data["version"]


class PubSubBitsBadgeMessage(PubSubMessage):
    """
    A Badge message

    Attributes
    -----------
    user: :class:`twitchio.PartialUser`
        The user receiving the badge.
    channel: :class:`twitchio.Channel`
        The channel the user received the badge on.
    badge_tier: :class:`int`
        The tier of the badge
    message: :class:`str`
        The message sent in chat.
    timestamp: :class:`datetime.datetime`
        The time the event happened
    """

    __slots__ = "user", "channel", "badge_tier", "message", "timestamp"

    def __init__(self, client: Client, topic: str, data: dict):
        super().__init__(client, topic, data)
        data = data["message"]
        self.user = PartialUser(client._http, data["user_id"], data["user_name"])
        self.channel: Channel = client.get_channel(data["channel_name"]) or Channel(
            name=data["channel_name"], websocket=client._connection
        )
        self.badge_tier: int = data["badge_tier"]
        self.message: str = data["chat_message"]
        self.timestamp = parse_timestamp(data["time"])


class PubSubChannelPointsMessage(PubSubMessage):
    """
    A Channel points redemption

    Attributes
    -----------
    timestamp: :class:`datetime.datetime`
        The timestamp the event happened.
    channel_id: :class:`int`
        The channel the reward was redeemed on.
    id: :class:`str`
        The id of the reward redemption.
    user: :class:`twitchio.PartialUser`
        The user redeeming the reward.
    reward: :class:`twitchio.CustomReward`
        The reward being redeemed.
    input: Optional[:class:`str`]
        The input the user gave, if any.
    status: :class:`str`
        The status of the reward.
    """

    __slots__ = "timestamp", "channel_id", "user", "id", "reward", "input", "status"

    def __init__(self, client: Client, topic: str, data: dict):
        super().__init__(client, topic, data)

        redemption = data["message"]["data"]["redemption"]

        self.timestamp = parse_timestamp(redemption["redeemed_at"])
        self.channel_id: int = int(redemption["channel_id"])
        self.id: str = redemption["id"]
        self.user = PartialUser(client._http, redemption["user"]["id"], redemption["user"]["display_name"])
        self.reward = CustomReward(client._http, redemption["reward"], PartialUser(client._http, self.channel_id, None))
        self.input: Optional[str] = redemption.get("user_input")
        self.status: str = redemption["status"]


class PubSubModerationAction(PubSubMessage):
    """
    A basic moderation action.

    Attributes
    -----------
    action: :class:`str`
        The action taken.
    args: List[:class:`str`]
        The arguments given to the command.
    created_by: :class:`twitchio.PartialUser`
        The user that created the action.
    message_id: Optional[:class:`str`]
        The id of the message that created this action.
    target: :class:`twitchio.PartialUser`
        The target of this action.
    from_automod: :class:`bool`
        Whether this action was done automatically or not.
    """

    __slots__ = "action", "args", "created_by", "message_id", "target", "from_automod"

    def __init__(self, client: Client, topic: str, data: dict):
        super().__init__(client, topic, data)
        self.action: str = data["message"]["data"]["moderation_action"]
        self.args: List[str] = data["message"]["data"]["args"]
        self.created_by = PartialUser(
            client._http, data["message"]["data"]["created_by_user_id"], data["message"]["data"]["created_by"]
        )
        self.message_id: Optional[str] = data["message"]["data"].get("msg_id")
        self.target = (
            PartialUser(
                client._http, data["message"]["data"]["target_user_id"], data["message"]["data"]["target_user_login"]
            )
            if data["message"]["data"]["target_user_id"]
            else None
        )
        self.from_automod: bool = data["message"]["data"].get("from_automod", False)


class PubSubModerationActionBanRequest(PubSubMessage):
    """
    A Ban/Unban event

    Attributes
    -----------
    action: :class:`str`
        The action taken.
    args: List[:class:`str`]
        The arguments given to the command.
    created_by: :class:`twitchio.PartialUser`
        The user that created the action.
    target: :class:`twitchio.PartialUser`
        The target of this action.
    """

    __slots__ = "action", "args", "created_by", "message_id", "target"

    def __init__(self, client: Client, topic: str, data: dict):
        super().__init__(client, topic, data)
        self.action: str = data["message"]["data"]["moderation_action"]
        self.args: List[str] = data["message"]["data"]["moderator_message"]
        self.created_by = PartialUser(
            client._http, data["message"]["data"]["created_by_id"], data["message"]["data"]["created_by_login"]
        )
        self.target = (
            PartialUser(
                client._http, data["message"]["data"]["target_user_id"], data["message"]["data"]["target_user_login"]
            )
            if data["message"]["data"]["target_user_id"]
            else None
        )


class PubSubModerationActionChannelTerms(PubSubMessage):
    """
    A channel Terms update.

    Attributes
    -----------
    type: :class:`str`
        The type of action taken.
    channel_id: :class:`int`
        The channel id the action occurred on.
    id: :class:`str`
        The id of the Term.
    text: :class:`str`
        The text of the modified Term.
    requester: :class:`twitchio.PartialUser`
        The requester of this Term.
    """

    __slots__ = "type", "channel_id", "id", "text", "requester", "expires_at", "updated_at"

    def __init__(self, client: Client, topic: str, data: dict):
        super().__init__(client, topic, data)
        self.type: str = data["message"]["data"]["type"]
        self.channel_id = int(data["message"]["data"]["channel_id"])
        self.id: str = data["message"]["data"]["id"]
        self.text: str = data["message"]["data"]["text"]
        self.requester = PartialUser(
            client._http, data["message"]["data"]["requester_id"], data["message"]["data"]["requester_login"]
        )

        self.expires_at = (
            parse_timestamp(data["message"]["data"]["expires_at"]) if data["message"]["data"]["expires_at"] else None
        )
        self.updated_at = (
            parse_timestamp(data["message"]["data"]["updated_at"]) if data["message"]["data"]["updated_at"] else None
        )


class PubSubChannelSubscribe(PubSubMessage):
    """
    Channel subscription

    Attributes
    -----------
    channel: :class:`twitchio.Channel`
        Channel that has been subscribed or subgifted.
    context: :class:`str`
        Event type associated with the subscription product.
    user: Optional[:class:`twitchio.PartialUser`]
        The person who subscribed or sent a gift subscription. Can be None if anonymous.
    message: :class:`str`
        Message sent with the sub/resub.
    emotes: Optional[List[:class:`dict`]]
        Emotes sent with the sub/resub.
    is_gift: :class:`bool`
        If this sub message was caused by a gift subscription.
    recipient: Optional[:class:`twitchio.PartialUser`]
        The person the who received the gift subscription.
    sub_plan: :class:`str`
        Subscription Plan ID.
    sub_plan_name: :class:`str`
        Channel Specific Subscription Plan Name.
    time: :class:`datetime.datetime`
        Time when the subscription or gift was completed. RFC 3339 format.
    cumulative_months: :class:`int`
        Cumulative number of tenure months of the subscription.
    streak_months: Optional[:class:`int`]
        Denotes the user's most recent (and contiguous) subscription tenure streak in the channel.
    multi_month_duration: Optional[:class:`int`]
        Number of months gifted as part of a single, multi-month gift OR number of months purchased as part of a multi-month subscription.
    """

    __slots__ = (
        "channel",
        "context",
        "user",
        "message",
        "emotes",
        "is_gift",
        "recipient",
        "sub_plan",
        "sub_plan_name",
        "time",
        "cumulative_months",
        "streak_months",
        "multi_month_duration",
    )

    def __init__(self, client: Client, topic: str, data: dict):
        super().__init__(client, topic, data)

        subscription = data["message"]

        self.channel: Channel = client.get_channel(subscription["channel_name"]) or Channel(
            name=subscription["channel_name"], websocket=client._connection
        )
        self.context: str = subscription["context"]
        try:
            self.user = PartialUser(client._http, int(subscription["user_id"]), subscription["user_name"])
        except KeyError:
            self.user = None

        self.message: str = subscription["sub_message"]["message"]
        try:
            self.emotes = subscription["sub_message"]["emotes"]
        except KeyError:
            self.emotes = None

        self.is_gift: bool = subscription["is_gift"]
        try:
            self.recipient = PartialUser(
                client._http, int(subscription["recipient_id"]), subscription["recipient_user_name"]
            )
        except KeyError:
            self.recipient = None

        self.sub_plan: str = subscription["sub_plan"]
        self.sub_plan_name: str = subscription["sub_plan_name"]
        self.time = parse_timestamp(subscription["time"])
        try:
            self.cumulative_months = int(subscription["cumulative_months"])
        except KeyError:
            self.cumulative_months = None
        try:
            self.streak_months = int(subscription["streak_months"])
        except KeyError:
            self.streak_months = None
        try:
            self.multi_month_duration = int(subscription["multi_month_duration"])
        except KeyError:
            self.multi_month_duration = None


class PubSubModerationActionModeratorAdd(PubSubMessage):
    """
    A moderator add event.

    Attributes
    -----------
    channel_id: :class:`int`
        The channel id the moderator was added to.
    moderation_action: :class:`str`
        Redundant.
    target: :class:`twitchio.PartialUser`
        The person who was added as a mod.
    created_by: :class:`twitchio.PartialUser`
        The person who added the mod.
    """

    __slots__ = "channel_id", "target", "moderation_action", "created_by"

    def __init__(self, client: Client, topic: str, data: dict):
        super().__init__(client, topic, data)
        self.channel_id = int(data["message"]["data"]["channel_id"])
        self.moderation_action: str = data["message"]["data"]["moderation_action"]
        self.target = PartialUser(
            client._http, data["message"]["data"]["target_user_id"], data["message"]["data"]["target_user_login"]
        )
        self.created_by = PartialUser(
            client._http, data["message"]["data"]["created_by_user_id"], data["message"]["data"]["created_by"]
        )


_mod_actions = {
    "approve_unban_request": PubSubModerationActionBanRequest,
    "deny_unban_request": PubSubModerationActionBanRequest,
    "channel_terms_action": PubSubModerationActionChannelTerms,
    "moderator_added": PubSubModerationActionModeratorAdd,
    "moderation_action": PubSubModerationAction,
}


def _find_mod_action(client: Client, topic: str, data: dict):
    typ = data["message"]["type"]
    if typ in _mod_actions:
        return _mod_actions[typ](client, topic, data)

    else:
        raise ValueError(f"unknown pubsub moderation action '{typ}'")


_mapping = {
    "channel-bits-events-v2": ("pubsub_bits", PubSubBitsMessage),
    "channel-bits-badge-unlocks": ("pubsub_bits_badge", PubSubBitsBadgeMessage),
    "channel-subscribe-events-v1": ("pubsub_subscription", PubSubChannelSubscribe),
    "chat_moderator_actions": ("pubsub_moderation", _find_mod_action),
    "channel-points-channel-v1": ("pubsub_channel_points", PubSubChannelPointsMessage),
    "whispers": ("pubsub_whisper", None),
}


def create_message(client, msg: dict):
    topic = msg["data"]["topic"].split(".")[0]
    r = _mapping[topic]
    return r[0], r[1](client, topic, msg["data"])
