import logging

logger = logging.getLogger(__name__)

try:
    import dbus  # pyright: ignore[reportMissingImports]
except ImportError:
    dbus = None


# HACK: Hack to workaround introspection bug caused by gnome-keyring, should be
# fixed by version 3.5 per:
# https://git.gnome.org/browse/gnome-keyring/commit/?id=5dccbe88eb94eea9934e2b7
EMPTY_STRING = dbus.String("", variant_level=1) if dbus else ""


FETCH_ERROR = (
    "Fetching passwords from your keyring failed. Any passwords "
    "stored in the keyring will not be available."
)


def fetch() -> list[tuple[str, str, bytes]]:  # noqa: PLR0911
    if not dbus:
        logger.debug("%s (dbus not installed)", FETCH_ERROR)
        return []

    try:
        bus = dbus.SessionBus()
    except dbus.exceptions.DBusException as e:
        logger.debug("%s (%s)", FETCH_ERROR, e)
        return []

    if not bus.name_has_owner("org.freedesktop.secrets"):
        logger.debug("%s (org.freedesktop.secrets service not running)", FETCH_ERROR)
        return []

    service = _service(bus)
    try:
        session = service.OpenSession("plain", EMPTY_STRING)[1]
    except dbus.exceptions.DBusException as e:
        logger.debug("%s (%s)", FETCH_ERROR, e)
        return []

    items, locked = service.SearchItems({"service": "mopidy"})

    if not locked and not items:
        return []

    if locked:
        # There is a chance we can unlock without prompting the users...
        items, prompt = service.Unlock(locked)
        if prompt != "/":
            _prompt(bus, prompt).Dismiss()
            logger.debug("%s (Keyring is locked)", FETCH_ERROR)
            return []

    result = []
    secrets = service.GetSecrets(items, session, byte_arrays=True)
    for item_path, values in secrets.items():
        session_path, parameters, value, content_type = values
        attrs = _item_attributes(bus, item_path)
        result.append((attrs["section"], attrs["key"], bytes(value)))
    return result


def set(  # noqa: A001, PLR0911
    section: str,
    key: str,
    value: str | bytes,
) -> bool:
    """Store a secret config value for a given section/key.

    Indicates if storage failed or succeeded.
    """
    if not dbus:
        logger.debug(
            "Saving %s/%s to keyring failed. (dbus not installed)",
            section,
            key,
        )
        return False

    try:
        bus = dbus.SessionBus()
    except dbus.exceptions.DBusException as e:
        logger.debug("Saving %s/%s to keyring failed. (%s)", section, key, e)
        return False

    if not bus.name_has_owner("org.freedesktop.secrets"):
        logger.debug(
            "Saving %s/%s to keyring failed. "
            "(org.freedesktop.secrets service not running)",
            section,
            key,
        )
        return False

    service = _service(bus)
    collection = _collection(bus)
    if not collection:
        return False

    if isinstance(value, str):
        value = value.encode()

    session = service.OpenSession("plain", EMPTY_STRING)[1]
    secret = dbus.Struct(
        (session, "", dbus.ByteArray(value), "plain/text; charset=utf8"),
    )
    label = f"mopidy: {section}/{key}"
    attributes = {"service": "mopidy", "section": section, "key": key}
    properties = {
        "org.freedesktop.Secret.Item.Label": label,
        "org.freedesktop.Secret.Item.Attributes": attributes,
    }

    try:
        item, prompt = collection.CreateItem(properties, secret, True)
    except dbus.exceptions.DBusException as e:
        # TODO: catch IsLocked errors etc.
        logger.debug("Saving %s/%s to keyring failed. (%s)", section, key, e)
        return False

    if prompt == "/":
        return True

    _prompt(bus, prompt).Dismiss()
    logger.debug("Saving secret %s/%s failed. (Keyring is locked)", section, key)
    return False


def _service(bus):
    return _interface(bus, "/org/freedesktop/secrets", "org.freedesktop.Secret.Service")


# NOTE: depending on versions and setup 'default' might not exists, so try and
# use it but fall back to the 'login' collection, and finally the 'session' one
# if all else fails. We should probably create a keyring/collection setting
# that allows users to set this so they have control over where their secrets
# get stored.
def _collection(bus):
    for name in "aliases/default", "collection/login", "collection/session":
        path = "/org/freedesktop/secrets/" + name
        if _collection_exists(bus, path):
            break
    else:
        return None
    return _interface(bus, path, "org.freedesktop.Secret.Collection")


# NOTE: Hack to probe if a given collection actually exists. Needed to work
# around an introspection bug in setting passwords for non-existant aliases.
def _collection_exists(bus, path):
    assert dbus
    try:
        item = _interface(bus, path, "org.freedesktop.DBus.Properties")
        item.Get("org.freedesktop.Secret.Collection", "Label")
    except dbus.exceptions.DBusException:
        return False
    else:
        return True


# NOTE: We could call prompt.Prompt('') to unlock the keyring when it is not
# '/', but we would then also have to arrange to setup signals to wait until
# this has been completed. So for now we just dismiss the prompt and expect
# keyrings to be unlocked.
def _prompt(bus, path):
    return _interface(bus, path, "Prompt")


def _item_attributes(bus, path):
    item = _interface(bus, path, "org.freedesktop.DBus.Properties")
    result = item.Get("org.freedesktop.Secret.Item", "Attributes")
    return {str(k): str(v) for k, v in result.items()}


def _interface(bus, path, interface):
    assert dbus
    obj = bus.get_object("org.freedesktop.secrets", path)
    return dbus.Interface(obj, interface)
