# SPDX-License-Identifier: MIT

from __future__ import annotations

from typing import TYPE_CHECKING, List, Optional

from .enums import (
    AutoModerationActionType,
    AutoModerationEventType,
    AutoModerationTriggerType,
    KeywordPresetType,
    try_enum,
)
from .errors import InvalidArgument
from .mixins import Hashable
from .object import Object
from .utils import MISSING, get_as_snowflake

if TYPE_CHECKING:
    from .abc import GuildChannel, Snowflake
    from .guild import Guild
    from .member import Member
    from .message import Message
    from .role import Role
    from .state import ConnectionState
    from .types.auto_moderation import (
        AutoModerationAction as AutoModerationActionPayload,
        AutoModerationActionExecution as ActionExecutionPayload,
        AutoModerationActionMetadata as ActionMetadataPayload,
        AutoModerationRule as AutoModerationRulePayload,
        AutoModerationRuleModify,
        AutoModerationTriggerMetadata as TriggerMetadataPayload,
    )

__all__ = (
    "AutoModerationTriggerMetadata",
    "AutoModerationActionMetadata",
    "AutoModerationAction",
    "AutoModerationRule",
    "AutoModerationActionExecution",
)


class AutoModerationTriggerMetadata:
    """Represents data about an auto moderation trigger rule.

    .. versionadded:: 2.1

    Attributes
    ----------
    keyword_filter: List[:class:`str`]
        A list of substrings which will be searched for in content.

        .. note::

            This is ``None`` and cannot be provided if the trigger type of the rule is not
            :attr:`AutoModerationTriggerType.keyword`.
    presets: List[:class:`KeywordPresetType`]
        A list of Discord pre-defined wordsets which will be searched for in content.

        .. note::

            This is ``None`` and cannot be provided if the trigger type of the rule is not
            :attr:`AutoModerationTriggerType.keyword_preset`.
    allow_list: List[:class:`str`]
        A list of exempt strings that will not trigger the preset type.

        .. note::

            This is ``None`` and cannot be provided if the trigger type of the rule is not
            :attr:`AutoModerationTriggerType.keyword_preset`.

        .. warning::

            Wildcard syntax (`*`) is not supported here.
    mention_total_limit: Optional[:class:`int`]
        The maximum amount of mentions allowed in a messsage.

        .. versionadded:: 2.3

        .. note::

            This is ``None`` and cannot be provided if the trigger type of this rule is not :attr:`AutoModerationTriggerType.mention_spam`.
    """

    __slots__ = ("keyword_filter", "presets", "allow_list", "mention_total_limit")

    def __init__(
        self,
        *,
        keyword_filter: Optional[List[str]] = None,
        presets: Optional[List[KeywordPresetType]] = None,
        allow_list: Optional[List[str]] = None,
        mention_total_limit: Optional[int] = None,
    ) -> None:
        self.keyword_filter: Optional[List[str]] = keyword_filter
        self.presets: Optional[List[KeywordPresetType]] = presets
        self.allow_list: Optional[List[str]] = allow_list
        self.mention_total_limit: Optional[int] = mention_total_limit

    @classmethod
    def from_data(cls, data: TriggerMetadataPayload):
        keyword_filter = data.get("keyword_filter")
        presets = (
            [try_enum(KeywordPresetType, preset) for preset in data["presets"]]
            if "presets" in data
            else None
        )
        allow_list = data.get("allow_list")
        mention_total_limit = data.get("mention_total_limit")

        return cls(
            keyword_filter=keyword_filter,
            presets=presets,
            allow_list=allow_list,
            mention_total_limit=mention_total_limit,
        )

    @property
    def payload(self) -> TriggerMetadataPayload:
        payload: TriggerMetadataPayload = {}

        if self.keyword_filter is not None:
            payload["keyword_filter"] = self.keyword_filter

        if self.presets is not None:
            payload["presets"] = [enum.value for enum in self.presets]

        if self.allow_list is not None:
            payload["allow_list"] = self.allow_list

        if self.mention_total_limit is not None:
            payload["mention_total_limit"] = self.mention_total_limit

        return payload


class AutoModerationActionMetadata:
    """Represents additional data that is used when an action is executed.

    .. versionadded:: 2.1

    Attributes
    ----------
    channel: Optional[:class:`abc.Snowflake`]
        The channel to which message content should be logged.

        .. note::

            This is ``None`` and cannot be provided if the action type of the rule is not
            :attr:`AutoModerationActionType.send_alert_message`.
    duration_seconds: Optional[:class:`int`]
        The duration of the timeout in seconds.

        .. note::

            This is ``None`` and cannot be provided if the action type of the rule is not
            :attr:`AutoModerationActionType.timeout`.

        .. note::

            The maximum value that can be used is ``2419200`` seconds (4 weeks)
    """

    __slots__ = ("channel_id", "duration_seconds")

    def __init__(
        self, *, channel: Optional[Snowflake] = None, duration_seconds: Optional[int] = None
    ):
        self.channel_id: Optional[int] = channel.id if channel is not None else None
        self.duration_seconds: Optional[int] = duration_seconds

    @classmethod
    def from_data(cls, data: ActionMetadataPayload):
        channel_id = get_as_snowflake(data, "channel_id")
        channel = Object(id=channel_id) if channel_id is not None else None
        duration_seconds = data.get("duration_seconds")

        return cls(channel=channel, duration_seconds=duration_seconds)

    @property
    def payload(self) -> ActionMetadataPayload:
        payload: ActionMetadataPayload = {}

        if self.channel_id is not None:
            payload["channel_id"] = self.channel_id

        if self.duration_seconds is not None:
            payload["duration_seconds"] = self.duration_seconds

        return payload


class AutoModerationAction:
    """Represents an auto moderation action that will execute whenever a rule is triggered.

    .. versionadded:: 2.1

    Parameters
    ----------
    type: :class:`AutoModerationActionType`
        The type to use for this action.
    metadata: :class:`AutoModerationActionMetadata`
        The additional data to use during execution of this action.

    Attributes
    ----------
    type: :class:`AutoModerationActionType`
        The type of this action.
    metadata: :class:`AutoModerationActionMetadata`
        The additional metadata needed during execution for this specific action type.
    """

    __slots__ = ("type", "metadata")

    def __init__(
        self,
        *,
        type: AutoModerationActionType,
        metadata: Optional[AutoModerationActionMetadata] = None,
    ) -> None:
        self.type: AutoModerationActionType = type
        self.metadata: Optional[AutoModerationActionMetadata] = metadata

    @classmethod
    def from_data(cls, data: AutoModerationActionPayload) -> AutoModerationAction:
        type = try_enum(AutoModerationActionType, data["type"])
        metadata = AutoModerationActionMetadata.from_data(data.get("metadata", {}))
        return cls(type=type, metadata=metadata)

    @property
    def payload(self) -> AutoModerationActionPayload:
        data: AutoModerationActionPayload = {
            "type": self.type.value,
        }

        if self.metadata is not None:
            data["metadata"] = self.metadata.payload

        return data


class AutoModerationRule(Hashable):
    """Represents a Discord auto moderation rule.

    .. versionadded:: 2.1

    .. container:: operations

        .. describe:: x == y

            Checks if two rules are equal.

        .. describe:: x != y

            Checks if two rules are not equal.

        .. describe:: hash(x)

            Returns the rules's hash.

        .. describe:: str(x)

            Returns the rules's name.

    Attributes
    ----------
    id: :class:`int`
        The rule's unique ID.
    guild_id: :class:`int`
        The guild's unique ID which this rule belongs to.
    guild: Optional[:class:`Guild`]
        The guild which this rule belongs to, if found in cache.
    name: :class:`str`
        The rule's name.
    creator_id: :class:`int`
        The user's unique ID which first created this rule.
    creator: Optional[:class:`Member`]
        The member which first created this rule, if found in cache.
    event_type: :class:`AutoModerationEventType`
        The event context in which this rule is checked.
    trigger_type: :class:`AutoModerationTriggerType`
        The type of content that can trigger this rule.
    trigger_metadata: :class:`AutoModerationTriggerMetadata`
        Additional data that is used when checking if this rule is triggered.
    actions: List[:class:`AutoModerationAction`]
        The actions which will execute when the rule is triggered.
    enabled: :class:`bool`
        Whether the rule is enabled.
    exempt_role_ids: List[:class:`int`]
        The role ids that should not be affected by the rule.
    exempt_channel_ids: List[:class:`int`]
        The channel ids that should not be affected by the rule.
    exempt_roles: List[:class:`Role`]
        The roles that should not be affected by the rule, if found in cache.
    exempt_channels: List[:class:`abc.GuildChannel`]
        The channels that should not be affected by the rule, if found in cache.
    """

    __slots__ = (
        "_state",
        "id",
        "guild_id",
        "guild",
        "name",
        "creator_id",
        "creator",
        "event_type",
        "trigger_type",
        "trigger_metadata",
        "actions",
        "enabled",
        "exempt_role_ids",
        "exempt_channel_ids",
        "exempt_roles",
        "exempt_channels",
    )

    def __init__(self, *, data: AutoModerationRulePayload, state: ConnectionState):
        self._state = state
        self.id: int = int(data["id"])
        self.guild_id: int = int(data["guild_id"])
        self.guild: Optional[Guild] = state._get_guild(self.guild_id)
        self.name: str = data["name"]
        self.creator_id: int = int(data["creator_id"])
        self.creator: Optional[Member] = (
            self.guild.get_member(self.creator_id) if self.guild is not None else None
        )
        self.event_type: AutoModerationEventType = try_enum(
            AutoModerationEventType, data["event_type"]
        )
        self.trigger_type: AutoModerationTriggerType = try_enum(
            AutoModerationTriggerType, data["trigger_type"]
        )
        self.trigger_metadata: AutoModerationTriggerMetadata = (
            AutoModerationTriggerMetadata.from_data(data["trigger_metadata"])
        )
        self.actions: List[AutoModerationAction] = [
            AutoModerationAction.from_data(data=action) for action in data["actions"]
        ]
        self.enabled: bool = data["enabled"]
        self.exempt_role_ids: List[int] = [int(role_id) for role_id in data["exempt_roles"]]
        self.exempt_channel_ids: List[int] = [
            int(channel_id) for channel_id in data["exempt_channels"]
        ]
        self.exempt_roles: List[Optional[Role]] = (
            [self.guild.get_role(role_id) for role_id in self.exempt_role_ids]
            if self.guild is not None
            else []
        )
        self.exempt_channels: List[Optional[GuildChannel]] = (
            [self.guild.get_channel(channel_id) for channel_id in self.exempt_channel_ids]
            if self.guild is not None
            else []
        )

    def __str__(self) -> str:
        return self.name

    async def delete(self, *, reason: Optional[str] = None) -> None:
        """|coro|

        Delete the current rule from its guild.

        Requires the :attr:`~Permissions.manage_guild` permission.

        Parameters
        ----------
        reason: Optional[:class:`str`]
            The reason for deleting this rule. Shows up on the audit log.

        Raises
        ------
        Forbidden
            You do not have permission to delete this rule.
        HTTPException
            Deleting the rule failed.
        """

        await self._state.http.delete_auto_moderation_rule(self.guild_id, self.id, reason=reason)

    async def edit(
        self,
        *,
        name: str = MISSING,
        event_type: AutoModerationEventType = MISSING,
        trigger_metadata: AutoModerationTriggerMetadata = MISSING,
        actions: List[AutoModerationAction] = MISSING,
        enabled: bool = MISSING,
        exempt_roles: List[Snowflake] = MISSING,
        exempt_channels: List[Snowflake] = MISSING,
        reason: Optional[str] = None,
    ) -> AutoModerationRule:
        """Modify the current auto moderation rule.

        Requires the :attr:`~Permissions.manage_guild` permission.

        Parameters
        ----------
        name: :class:`str`
            The new name for this rule.
        event_type: :class:`AutoModerationEventType`
            The new event context in which this rule is checked.
        trigger_metadata: :class:`AutoModerationTriggerMetadata`
            The new additional data that is used when checking if this rule is triggered.
        actions: List[:class:`AutoModerationAction`]
            The new actions which will execute when the rule is triggered.
        enabled: :class:`bool`
            Whether the rule is enabled.
        exempt_roles: List[:class:`abc.Snowflake`]
            The new roles that should not be affected by the rule.
        exempt_channels: List[:class:`abc.Snowflake`]
            The new channels that should not be affected by the rule.
        reason: Optional[:class:`str`]
            The reason for editing this rule. Shows in the audit log.

        Raises
        ------
        Forbidden
            You do not have permission to edit this rule.
        HTTPException
            Editing the rule failed.
        InvalidArgument
            An incorrect type was passed.

        Returns
        -------
        :class:`AutoModerationRule`
            The newly edited auto moderation rule.
        """

        payload: AutoModerationRuleModify = {}

        if name is not MISSING:
            payload["name"] = name

        if event_type is not MISSING:
            if not isinstance(event_type, AutoModerationEventType):
                raise InvalidArgument("event_type must be of type AutoModerationEventType")

            payload["event_type"] = event_type.value

        if trigger_metadata is not MISSING:
            if not isinstance(trigger_metadata, AutoModerationTriggerMetadata):
                raise InvalidArgument(
                    "trigger_metadata must be of type AutoModerationTriggerMetadata"
                )

            payload["trigger_metadata"] = trigger_metadata.payload

        if actions is not MISSING:
            payload["actions"] = [action.payload for action in actions]

        if enabled is not MISSING:
            payload["enabled"] = enabled

        if exempt_roles is not MISSING:
            payload["exempt_roles"] = [str(role.id) for role in exempt_roles]

        if exempt_channels is not MISSING:
            payload["exempt_channels"] = [str(channel.id) for channel in exempt_channels]

        data = await self._state.http.modify_auto_moderation_rule(
            self.guild_id, self.id, data=payload, reason=reason
        )
        return AutoModerationRule(data=data, state=self._state)


class AutoModerationActionExecution:
    """Represents the execution of an auto moderation action

    .. versionadded:: 2.1

    Attributes
    ----------
    guild_id: :class:`int`
        The guild ID where this action was executed.
    guild: Optional[:class:`Guild`]
        The guild where this action was executed, if it was found in cache.
    channel_id: Optional[:class:`int`]
        The channel ID where this action was executed, if applicable.
    channel: Optional[:class:`abc.GuildChannel`]
        The channel where this action was executed, if applicable and found in cache.
    message_id: Optional[:class:`int`]
        The message ID that executed this action, if it was not blocked.
    message: Optional[:class:`Message`]
        The message that executed this action, if it was not blocked and found in cache.
    alert_system_message_id: Optional[:class:`int`]
        The ID of the system alert message, if sent.
    alert_system_message: Optional[:class:`Message`]
        The system alert message, if sent and found in cache.
    action: :class:`AutoModerationAction`
        The action that was executed.
    rule_id: :class:`int`
        The id of the rule that was executed.
    rule_trigger_type: :class:`AutoModerationTriggerType`
        The type of rule that was executed.
    member_id: :class:`int`
        The ID of the user that triggered this action.
    member: Optional[:class:`Member`]
        The member that triggered this action, if found in cache.
    content: :class:`str`
        The content the user sent in the message

        .. note::

            This requires :attr:`Intents.message_content` to not be empty.
    matched_keyword: Optional[:class:`str`]
        The keyword configured in the rule that matched this message, if applicable.
    matched_content: Optional[:class:`str`]
        The content in the message that matched the keyword, if applicable.

        .. note::

            This requires :attr:`Intents.message_conrent` to not be empty.
    """

    def __init__(self, *, data: ActionExecutionPayload, state: ConnectionState) -> None:
        self.guild_id: int = int(data["guild_id"])
        self.guild: Optional[Guild] = state._get_guild(self.guild_id)
        self.channel_id: Optional[int] = get_as_snowflake(data, "channel_id")
        if self.guild is not None and self.channel_id is not None:
            self.channel: Optional[GuildChannel] = self.guild.get_channel(self.channel_id)
        else:
            self.channel: Optional[GuildChannel] = None
        self.message_id: Optional[int] = get_as_snowflake(data, "message_id")
        self.message: Optional[Message] = state._get_message(self.message_id)
        self.alert_system_message_id: Optional[int] = get_as_snowflake(
            data, "alert_system_message_id"
        )
        self.alert_system_message: Optional[Message] = state._get_message(
            self.alert_system_message_id
        )
        self.action: AutoModerationAction = AutoModerationAction.from_data(data=data["action"])
        self.rule_id: int = int(data["rule_id"])
        self.rule_trigger_type: AutoModerationTriggerType = try_enum(
            AutoModerationTriggerType, data["rule_trigger_type"]
        )
        self.member_id: int = int(data["user_id"])
        self.member: Optional[Member] = (
            self.guild.get_member(self.member_id) if self.guild is not None else None
        )
        self.content: str = data.get("content", "")
        self.matched_keyword: Optional[str] = data["matched_keyword"]
        self.matched_content: Optional[str] = data.get("matched_content", "")
