# -*- coding: utf-8 -*-

import re
from typing import Any, Coroutine, Dict, Optional

from dbus_next.aio.message_bus import MessageBus
from dbus_next.errors import InvalidObjectPathError
from dbus_next.message import Message
from dbus_next.validators import (
    assert_interface_name_valid,
    assert_member_name_valid,
    assert_object_path_valid,
)

# TODO: this stuff should be improved and submitted upstream to dbus-next
# https://github.com/altdesktop/python-dbus-next/issues/53

_message_types = ["signal", "method_call", "method_return", "error"]


class InvalidMessageTypeError(TypeError):
    def __init__(self, type):
        super().__init__(f"invalid message type: {type}")


def is_message_type_valid(type: str) -> bool:
    """Whether this is a valid message type.

    .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules

    :param type: The message type to validate.
    :type name: str

    :returns: Whether the name is a valid message type.
    :rtype: bool
    """
    return type in _message_types


def assert_bus_name_valid(type: str):
    """Raise an error if this is not a valid message type.

    .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules

    :param type: The message type to validate.
    :type name: str

    :raises:
        - :class:`InvalidBusNameError` - If this is not a valid message type.
    """
    if not is_message_type_valid(type):
        raise InvalidMessageTypeError(type)


class MatchRules:
    """D-Bus signal match rules.

    .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules
    """

    def __init__(
        self,
        type: str = "signal",
        sender: Optional[str] = None,
        interface: Optional[str] = None,
        member: Optional[str] = None,
        path: Optional[str] = None,
        path_namespace: Optional[str] = None,
        destination: Optional[str] = None,
        arg0namespace: Optional[str] = None,
        **kwargs,
    ):
        assert_bus_name_valid(type)
        self.type: str = type

        if sender:
            assert_bus_name_valid(sender)
            self.sender: str = sender
        else:
            self.sender = None

        if interface:
            assert_interface_name_valid(interface)
            self.interface: str = interface
        else:
            self.interface = None

        if member:
            assert_member_name_valid(member)
            self.member: str = member
        else:
            self.member = None

        if path:
            assert_object_path_valid(path)
            self.path: str = path
        else:
            self.path = None

        if path_namespace:
            assert_object_path_valid(path_namespace)
            self.path_namespace: str = path_namespace
        else:
            self.path_namespace = None

        if path and path_namespace:
            raise TypeError(
                "message rules cannot have both 'path' and 'path_namespace' at the same time"
            )

        if destination:
            assert_bus_name_valid(destination)
            self.destination: str = destination
        else:
            self.destination = None

        if arg0namespace:
            assert_bus_name_valid(arg0namespace)
            self.arg0namespace: str = arg0namespace
        else:
            self.arg0namespace = None

        if kwargs:
            for k, v in kwargs.items():
                if re.match(r"^arg\d+$", k):
                    if not isinstance(v, str):
                        raise TypeError(f"kwarg '{k}' must have a str value")
                elif re.match(r"^arg\d+path$", k):
                    if not isinstance(v, str):
                        raise InvalidObjectPathError(v)
                    assert_object_path_valid(v[:-1] if v.endswith("/") else v)
                else:
                    raise ValueError("kwargs must be in the form 'arg0' or 'arg0path'")
            self.args: Dict[str, str] = kwargs
        else:
            self.args = None

    @staticmethod
    def parse(rules: str):
        return MatchRules(**dict(r.split("=") for r in rules.split(",")))

    def __str__(self) -> str:
        rules = [f"type={self.type}"]

        if self.sender:
            rules.append(f"sender={self.sender}")

        if self.interface:
            rules.append(f"interface={self.interface}")

        if self.member:
            rules.append(f"member={self.member}")

        if self.path:
            rules.append(f"path={self.path}")

        if self.path_namespace:
            rules.append(f"path_namespace={self.path_namespace}")

        if self.destination:
            rules.append(f"destination={self.destination}")

        if self.args:
            for k, v in self.args.items():
                rules.append(f"{k}={v}")

        if self.arg0namespace:
            rules.append(f"arg0namespace={self.arg0namespace}")

        return ",".join(rules)

    def __repr__(self) -> str:
        return f"MatchRules({self})"


def add_match(bus: MessageBus, rules: MatchRules) -> Coroutine[Any, Any, Message]:
    """Calls org.freedesktop.DBus.AddMatch using ``rules``."""
    return bus.call(
        Message(
            destination="org.freedesktop.DBus",
            interface="org.freedesktop.DBus",
            path="/org/freedesktop/DBus",
            member="AddMatch",
            signature="s",
            body=[str(rules)],
        )
    )


def remove_match(bus: MessageBus, rules: MatchRules) -> Coroutine[Any, Any, Message]:
    """Calls org.freedesktop.DBus.RemoveMatch using ``rules``."""
    return bus.call(
        Message(
            destination="org.freedesktop.DBus",
            interface="org.freedesktop.DBus",
            path="/org/freedesktop/DBus",
            member="RemoveMatch",
            signature="s",
            body=[str(rules)],
        )
    )
