from __future__ import annotations

import os
import re
from urllib.parse import unquote

from ..constants import BusType
from ..errors import InvalidAddressError

invalid_address_chars_re = re.compile(r"[^-0-9A-Za-z_/.%]")

str_ = str


def parse_address(address_str: str_) -> list[tuple[str, dict[str, str]]]:
    """Parse a dbus address string into a list of addresses."""
    addresses: list[tuple[str, dict[str, str]]] = []

    for address in address_str.split(";"):
        if not address:
            continue
        if address.find(":") == -1:
            raise InvalidAddressError("address did not contain a transport")

        transport, opt_string = address.split(":", 1)
        options: dict[str, str] = {}

        for kv in opt_string.split(","):
            if not kv:
                continue
            if kv.find("=") == -1:
                raise InvalidAddressError("address option did not contain a value")
            k, v = kv.split("=", 1)
            if invalid_address_chars_re.search(v):
                raise InvalidAddressError("address contains invalid characters")
            # XXX the actual unquote rules are simpler than this
            options[k] = unquote(v)

        addresses.append((transport, options))

    if not addresses:
        raise InvalidAddressError(
            f'address string contained no addresses: "{address_str}"'
        )

    return addresses


def get_system_bus_address() -> str:
    """Get the system bus address from the environment or return the default."""
    return (
        os.environ.get("DBUS_SYSTEM_BUS_ADDRESS")
        or "unix:path=/var/run/dbus/system_bus_socket"
    )


display_re = re.compile(r".*:([0-9]+)\.?.*")
remove_quotes_re = re.compile(r"""^['"]?(.*?)['"]?$""")


def get_session_bus_address() -> str:
    """Get the session bus address from the environment or return the default."""
    dbus_session_bus_address = os.environ.get("DBUS_SESSION_BUS_ADDRESS")
    if dbus_session_bus_address:
        return dbus_session_bus_address

    home = os.environ["HOME"]
    if "DISPLAY" not in os.environ:
        raise InvalidAddressError(
            "DBUS_SESSION_BUS_ADDRESS not set and could not get DISPLAY environment variable to get bus address"
        )

    display = os.environ["DISPLAY"]
    try:
        display = display_re.search(display).group(1)
    except Exception as ex:
        raise InvalidAddressError(
            f"DBUS_SESSION_BUS_ADDRESS not set and could not parse DISPLAY environment variable to get bus address: {display}"
        ) from ex

    # XXX: this will block but they're very small files and fs operations
    # should be fairly reliable. fix this by passing in an async func to read
    # the file for each io backend.
    machine_id = None
    with open("/var/lib/dbus/machine-id") as f:
        machine_id = f.read().rstrip()

    dbus_info_file_name = f"{home}/.dbus/session-bus/{machine_id}-{display}"
    dbus_info: str | None = None
    try:
        with open(dbus_info_file_name) as f:
            dbus_info = f.read().rstrip()
    except Exception as ex:
        raise InvalidAddressError(
            f"could not open dbus info file: {dbus_info_file_name}"
        ) from ex

    for line in dbus_info.split("\n"):
        if line.strip().startswith("DBUS_SESSION_BUS_ADDRESS="):
            _, addr = line.split("=", 1)
            if not addr:
                raise InvalidAddressError(
                    f"DBUS_SESSION_BUS_ADDRESS variable not set correctly in dbus info file: {dbus_info_file_name}"
                )
            addr = remove_quotes_re.search(addr).group(1)
            return addr

    raise InvalidAddressError("could not find dbus session bus address")


def get_bus_address(bus_type: BusType) -> str:
    """Get the address of the bus specified by the bus type."""
    if bus_type == BusType.SESSION:
        return get_session_bus_address()
    if bus_type == BusType.SYSTEM:
        return get_system_bus_address()
    raise Exception(f"got unknown bus type: {bus_type}")
