"""
Commands make it possible for layers to communicate with the "outer world",
e.g. to perform IO or to ask the master.
A command is issued by a proxy layer and is then passed upwards to the proxy server, and from there
possibly to the master and addons.

The counterpart to commands are events.
"""

import logging
import warnings
from typing import TYPE_CHECKING
from typing import Union

import mitmproxy.hooks
from mitmproxy.connection import Connection
from mitmproxy.connection import Server

if TYPE_CHECKING:
    import mitmproxy.proxy.layer


class Command:
    """
    Base class for all commands
    """

    blocking: Union[bool, "mitmproxy.proxy.layer.Layer"] = False
    """
    Determines if the command blocks until it has been completed.
    For practical purposes, this attribute should be thought of as a boolean value,
    layers may swap out `True` with a reference to themselves to signal to outer layers
    that they do not need to block as well.

    Example:

        reply = yield Hook("requestheaders", flow)  # blocking command
        yield Log("hello world", "info")            # non-blocking
    """

    def __repr__(self):
        x = self.__dict__.copy()
        x.pop("blocking", None)
        return f"{type(self).__name__}({x!r})"


class RequestWakeup(Command):
    """
    Request a `Wakeup` event after the specified amount of seconds.
    """

    delay: float

    def __init__(self, delay: float):
        self.delay = delay


class ConnectionCommand(Command):
    """
    Commands involving a specific connection
    """

    connection: Connection

    def __init__(self, connection: Connection):
        self.connection = connection


class SendData(ConnectionCommand):
    """
    Send data to a remote peer
    """

    data: bytes

    def __init__(self, connection: Connection, data: bytes):
        super().__init__(connection)
        self.data = data

    def __repr__(self):
        target = str(self.connection).split("(", 1)[0].lower()
        return f"SendData({target}, {self.data!r})"


class OpenConnection(ConnectionCommand):
    """
    Open a new connection
    """

    connection: Server
    blocking = True


class CloseConnection(ConnectionCommand):
    """
    Close a connection. If the client connection is closed,
    all other connections will ultimately be closed during cleanup.
    """


class CloseTcpConnection(CloseConnection):
    half_close: bool
    """
    If True, only close our half of the connection by sending a FIN packet.
    This is required from some protocols which close their end to signal completion and then continue reading,
    for example HTTP/1.0 without Content-Length header.
    """

    def __init__(self, connection: Connection, half_close: bool = False):
        super().__init__(connection)
        self.half_close = half_close


class StartHook(Command, mitmproxy.hooks.Hook):
    """
    Start an event hook in the mitmproxy core.
    This triggers a particular function (derived from the class name) in all addons.
    """

    name = ""
    blocking = True

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


class Log(Command):
    """
    Log a message.

    Layers could technically call `logging.log` directly, but the use of a command allows us to
    write more expressive playbook tests. Put differently, by using commands we can assert that
    a specific log message is a direct consequence of a particular I/O event.
    This could also be implemented with some more playbook magic in the future,
    but for now we keep the current approach as the fully sans-io one.
    """

    message: str
    level: int

    def __init__(
        self,
        message: str,
        level: int = logging.INFO,
    ):
        if isinstance(level, str):  # pragma: no cover
            warnings.warn(
                "commands.Log() now expects an integer log level, not a string.",
                DeprecationWarning,
                stacklevel=2,
            )
            level = getattr(logging, level.upper())
        self.message = message
        self.level = level

    def __repr__(self):
        return f"Log({self.message!r}, {logging.getLevelName(self.level).lower()})"
