#!/usr/bin/env python3

"""Online accounts, through GNOME Online Accounts.

GOA is a daemon plus a D-Bus API, and it is already running in this session --
gvfs activates it, and the four accounts on this machine work without
gnome-shell involved anywhere. What GNOME owns is only the *panel*; the accounts
themselves are ordinary D-Bus objects that anything may read and modify.

So everything except the initial sign-in is available to us: listing accounts,
turning individual services on and off, and removing an account. That is the
whole Online Accounts panel apart from one OAuth handshake.

Adding an account is the part that splits. The daemon's AddAccount takes
credentials as an ARGUMENT -- it stores them, it does not obtain them. For
password-based providers (Nextcloud, IMAP, WebDAV) that is a username and a
password, which a settings app can reasonably collect. For Google it is an OAuth
token, and the code that runs that exchange lives in libgoa-backend, which
Fedora ships without a GIR binding -- reachable from C only. Reimplementing it
would mean our own Google client credentials. So Google sign-in is handed to
GNOME's panel, and only the sign-in.

Usage:
  panama-accounts list
  panama-accounts set <object-path> <service> <true|false>
  panama-accounts remove <object-path>
"""

import json
import sys

# Every service GOA models. The account object carries one interface per service
# it supports, so presence of the interface is what "this account can do mail"
# means -- there is no capability list to read.
SERVICES = [
    ("mail", "Mail", "get_mail", "mail_disabled"),
    ("calendar", "Calendar", "get_calendar", "calendar_disabled"),
    ("contacts", "Contacts", "get_contacts", "contacts_disabled"),
    ("files", "Files", "get_files", "files_disabled"),
    ("photos", "Photos", "get_photos", "photos_disabled"),
    ("music", "Music", "get_music", "music_disabled"),
    ("chat", "Chat", "get_chat", "chat_disabled"),
]


def load_client():
    import gi

    gi.require_version("Goa", "1.0")
    from gi.repository import Goa

    return Goa.Client.new_sync(None)


def describe(obj):
    account = obj.get_account()
    services = []
    for key, label, getter, disabled_prop in SERVICES:
        if getattr(obj, getter)() is None:
            continue
        services.append({
            "key": key,
            "label": label,
            "enabled": not getattr(account.props, disabled_prop),
        })

    return {
        "path": obj.get_object_path(),
        "provider": account.props.provider_type,
        "providerName": account.props.provider_name,
        # PresentationIdentity is the human one (an email address); Identity is
        # the internal handle and is not always readable.
        "identity": account.props.presentation_identity or account.props.identity,
        # GOA raises this when stored credentials stop working -- an expired
        # token, a changed password. It is the one piece of state a user must
        # act on, and nothing else surfaces it.
        "needsAttention": bool(account.props.attention_needed),
        "services": services,
    }


def find(client, path):
    for obj in client.get_accounts():
        if obj.get_object_path() == path:
            return obj
    return None


def main():
    action = sys.argv[1] if len(sys.argv) > 1 else "list"

    try:
        client = load_client()
    except Exception as error:  # noqa: BLE001 - any failure here means "no GOA"
        # A machine without GOA is a legitimate state, not a crash.
        print(json.dumps({
            "accounts": [],
            "error": f"GNOME Online Accounts is not available: {error}",
        }))
        return 0

    if action == "list":
        print(json.dumps({
            "accounts": [describe(obj) for obj in client.get_accounts()],
            "error": "",
        }))
        return 0

    if action == "set":
        if len(sys.argv) != 5:
            print("usage: panama-accounts set <path> <service> <true|false>", file=sys.stderr)
            return 2
        path, service, value = sys.argv[2], sys.argv[3], sys.argv[4]
        obj = find(client, path)
        if obj is None:
            print(f"panama-accounts: no account at {path}", file=sys.stderr)
            return 1
        match = next((s for s in SERVICES if s[0] == service), None)
        if match is None:
            print(f"panama-accounts: unknown service {service!r}", file=sys.stderr)
            return 2
        if getattr(obj, match[2])() is None:
            print(f"panama-accounts: this account does not support {service}", file=sys.stderr)
            return 1
        # The property is "disabled", so enabling a service clears it.
        setattr(obj.get_account().props, match[3], value.lower() not in ("true", "1", "yes"))
        return 0

    if action == "remove":
        if len(sys.argv) != 3:
            print("usage: panama-accounts remove <path>", file=sys.stderr)
            return 2
        obj = find(client, sys.argv[2])
        if obj is None:
            print(f"panama-accounts: no account at {sys.argv[2]}", file=sys.stderr)
            return 1
        obj.get_account().call_remove_sync(None)
        return 0

    print("usage: panama-accounts [list|set|remove]", file=sys.stderr)
    return 2


if __name__ == "__main__":
    sys.exit(main())
