"""
When IO actions occur at the proxy server, they are passed down to layers as events.
Events represent the only way for layers to receive new data from sockets.
The counterpart to events are commands.
"""

import typing
import warnings
from dataclasses import dataclass
from dataclasses import is_dataclass
from typing import Any
from typing import Generic
from typing import TypeVar

from mitmproxy import flow
from mitmproxy.connection import Connection
from mitmproxy.proxy import commands


class Event:
    """
    Base class for all events.
    """

    def __repr__(self):
        return f"{type(self).__name__}({self.__dict__!r})"


class Start(Event):
    """
    Every layer initially receives a start event.
    This is useful to emit events on startup.
    """


@dataclass
class ConnectionEvent(Event):
    """
    All events involving connection IO.
    """

    connection: Connection


@dataclass
class DataReceived(ConnectionEvent):
    """
    Remote has sent some data.
    """

    data: bytes

    def __repr__(self):
        target = type(self.connection).__name__.lower()
        return f"DataReceived({target}, {self.data!r})"


class ConnectionClosed(ConnectionEvent):
    """
    Remote has closed a connection.
    """


class CommandCompleted(Event):
    """
    Emitted when a command has been finished, e.g.
    when the master has replied or when we have established a server connection.
    """

    command: commands.Command
    reply: Any

    def __new__(cls, *args, **kwargs):
        if cls is CommandCompleted:
            raise TypeError("CommandCompleted may not be instantiated directly.")
        assert is_dataclass(cls)
        return super().__new__(cls)

    def __init_subclass__(cls, **kwargs):
        command_cls = typing.get_type_hints(cls).get("command", None)
        valid_command_subclass = (
            isinstance(command_cls, type)
            and issubclass(command_cls, commands.Command)
            and command_cls is not commands.Command
        )
        if not valid_command_subclass:
            warnings.warn(
                f"{cls} needs a properly annotated command attribute.",
                RuntimeWarning,
            )
        if command_cls in command_reply_subclasses:
            other = command_reply_subclasses[command_cls]
            warnings.warn(
                f"Two conflicting subclasses for {command_cls}: {cls} and {other}",
                RuntimeWarning,
            )
        command_reply_subclasses[command_cls] = cls  # type: ignore

    def __repr__(self):
        return f"Reply({self.command!r}, {self.reply!r})"


command_reply_subclasses: dict[commands.Command, type[CommandCompleted]] = {}


@dataclass(repr=False)
class OpenConnectionCompleted(CommandCompleted):
    command: commands.OpenConnection
    reply: str | None
    """error message"""


@dataclass(repr=False)
class HookCompleted(CommandCompleted):
    command: commands.StartHook
    reply: None = None


T = TypeVar("T")


@dataclass
class MessageInjected(Event, Generic[T]):
    """
    The user has injected a custom WebSocket/TCP/... message.
    """

    flow: flow.Flow
    message: T


@dataclass
class Wakeup(CommandCompleted):
    """
    Event sent to layers that requested a wakeup using RequestWakeup.
    """

    command: commands.RequestWakeup
