#! /usr/bin/python3

import argparse
import os
import sys
import gi

gi.require_version('Accounts', '1.0')
gi.require_version('Signon', '2.0')

from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Accounts
from gi.repository import Signon

class GStrv(list):
    __gtype__ = GObject.type_from_name('GStrv')

class AccountConsole:
    def __init__(self):
        self.manager = Accounts.Manager()

    def list_accounts(self, args):
        accounts = self.manager.list()
        if not accounts:
            print('No accounts')
            main_loop.quit()
            return

        for account_id in accounts:
            account = self.manager.get_account(account_id)
            enabledness = 'enabled' if account.get_enabled() else 'disabled'
            print('account: id %s, %s, provider: %s' % (account_id, enabledness, account.get_provider_name()))
        main_loop.quit()


    def show_account(self, args):
        self.args = args
        account = self.manager.get_account(args.account)
        if not account:
            print('Account "%s" not found' % args.account, file=sys.stderr)
            sys.exit(1)

        enabledness = 'enabled' if account.get_enabled() else 'disabled'
        print('account: id %s, %s, provider: %s' % (account.id, enabledness, account.get_provider_name()))
        print('  Global settings:')
        i = account.get_settings_iter(None)
        self.enumerate_settings(i)

        # enumerate services and their settings
        services = account.list_services()
        for s in services:
            service = Accounts.AccountService.new(account, s)
            print('  Settings for %s' % (s.get_name(),))
            i = service.get_settings_iter(None)
            self.enumerate_settings(i)
        main_loop.quit()


    def create_account(self, args):
        self.args = args
        account = self.manager.create_account(args.provider)
        account.set_enabled(not args.disabled)
        for setting in args.s:
            (name, value) = self.parse_setting(setting)
            account.set_value(name, value)
        account.store_async(None, self.on_account_stored, None)

    def edit_account(self, args):
        self.args = args
        account = self.manager.get_account(args.account)
        if not account:
            print('Account "%s" not found' % args.account, file=sys.stderr)
            sys.exit(1)
        self.account = account

        if args.service:
            service = self.manager.get_service(args.service)
            if not service:
                print('Service "%s" not found' % args.service, file=sys.stderr)
                sys.exit(1)
            account.select_service(service)

        if args.enable:
            account.set_enabled(True)
        elif args.disable:
            account.set_enabled(False)

        for setting in args.s:
            (name, value) = self.parse_setting(setting)
            account.set_value(name, value)

        for setting in args.u:
            account.set_value(setting, None)

        if args.username is not None or args.password is not None or args.caption:
            value = GObject.Value()
            value.init(GObject.TYPE_UINT)
            signon_id = account.get_value(args.signon_id_field, value)
            if signon_id != Accounts.SettingSource.NONE and value.get_uint() != 0:
                self.identity = Signon.Identity.new_from_db(value.get_uint())
                self.identity.query_info(None, self.on_info_ready, None)
            else:
                self.identity = Signon.Identity.new()
                info = Signon.IdentityInfo.new()
                self.write_signon_info(info)
        else:
            account.store(self.on_account_stored, None)


    def on_info_ready(self, identity, res, userdata):
        try:
            info = identity.query_info_finish(res)
        except GLib.Error as error:
            print('Couldn\'t get identity info', file=sys.stderr)
            sys.exit(1)

        self.write_signon_info(info)


    def write_signon_info(self, info):
        if self.args.username is not None:
            info.set_username(self.args.username)
        if self.args.caption or self.args.username:
            info.set_caption(self.args.caption or self.args.username)
        if self.args.password is not None or info.get_id() == 0:
            info.set_secret(self.args.password or '', True)

        self.identity.store_info(info,
                None, self.on_credentials_stored, self.account)


    def on_credentials_stored(self, identity, res, account):
        try:
            identity.store_info_finish(res)
        except GLib.Error as err:
            print(
                f"Failed to store credentials for {account.id}: {err.message}",
                file=sys.stderr)
            sys.exit(1)

        id = identity.get_id()
        if id == 0:
            # XXX: must wait asynchronously for Identity's id change?
            print(f"Failed to get credential ID for {account.id}",
                file=sys.stderr)
            sys.exit(1)

        account.set_variant(self.args.signon_id_field, GLib.Variant('u', id))
        account.store(self.on_account_stored, None)


    def delete_account(self, args):
        self.args = args
        account = self.manager.get_account(args.account)
        if not account:
            print('Account "%s" not found' % args.account, file=sys.stderr)
            sys.exit(1)

        account.delete()
        account.store(self.on_account_stored_deleted, None)


    def login_identity(self, args):
        session_data = {}
        for parameter in args.p:
            (key, value) = self.parse_setting(parameter)
            session_data[key] = value

        self.login(args.identity, args.method, args.mechanism,session_data)


    def login_account(self, args):
        self.args = args
        account = self.manager.get_account(args.account)
        if not account:
            print('Account "%s" not found' % args.account, file=sys.stderr)
            sys.exit(1)
        self.account = account

        service = None
        if args.service:
            service = self.manager.get_service(args.service)
            if not service:
                print('Service "%s" not found' % args.service, file=sys.stderr)
                sys.exit(1)

        account_service = Accounts.AccountService.new(account, service)
        auth_data = account_service.get_auth_data()
        identity = auth_data.get_credentials_id()
        method = auth_data.get_method()
        mechanism = auth_data.get_mechanism()
        session_data = auth_data.get_login_parameters()

        # last, add session data from the command line
        for parameter in args.p:
            (key, value) = self.parse_setting(parameter)
            session_data[key] = value

        self.login(identity, method, mechanism, session_data)


    def login(self, identity, method, mechanism, session_data):
        if identity:
            self.session = Signon.AuthSession.new(identity, method)
        else:
            self.session = Signon.AuthSession.new(0, method)

        print(session_data)

        # Workaround https://gitlab.com/accounts-sso/libsignon-glib/-/issues/13
        # Bypass faulty Pygobject override and get the raw GI class.
        # FIXME: remove when the above bug is fixed.
        AuthSession = Signon.AuthSession
        if AuthSession.__module__ == 'gi.overrides.Signon':
            AuthSession = AuthSession.__bases__[0]
        AuthSession.process(self.session, session_data, mechanism,
                None, self.login_process_cb, None)


    def load_auth_parameters(self, iterator, parameters):
        allsettings = {}
        (ok, key, value) = iterator.next()
        while ok:
            allsettings[key] = value
            (ok, key, value) = iterator.next()

        for (key, value) in allsettings.iteritems():
            # if the param key itself contains a '/', assume it's a list
            if '/' in key:
                (key, item_id) = key.split('/', 1)
                if not item_id.startswith('item'):
                    continue
                if not parameters.has_key(key):
                    parameters[key] = []
                parameters[key].append(value)
            else:
                parameters[key] = value


    def login_process_cb(self, session, res, userdata):
        try:
            reply = session.process_finish(res)
        except GLib.Error as error:
            print('Got authentication error: %s' % error.message, file=sys.stderr)
            sys.exit(1)

        print('Got reply: ', reply)
        main_loop.quit()


    def on_account_stored_deleted(self, account, error, userdata):
        if not error:
            print('OK')
        else:
            print('Error occurred: %s' % error.message, file=sys.stderr)
        main_loop.quit()

    def on_account_stored(self, account, res, userdata):
        try:
            account.store_finish(res)

            if 'print_id' in self.args and self.args.print_id:
                print('%s' % (account.id))
            else:
                print('OK %s' % (account.id))
        except GLib.Error as error:
            print('Error occurred: %s' % error.message, file=sys.stderr)
        main_loop.quit()

    def enumerate_settings(self, iterator):
        settings = []
        (ok, key, value) = iterator.next()
        while ok:
            settings.append((key, value))
            (ok, key, value) = iterator.next()

        settings.sort()
        for (key, value) in settings:
            print('    %s: %s (%s)' % (key, value, type(value)))

    def parse_setting(self, setting):
        (name, value_str) = setting.split('=')
        if ':' in name:
            (type, name) = name.split(':')
        else:
            type = 's'

        if type == 'i':
            value = int(value_str)
        elif type == 'u':
            value = GObject.Value()
            value.init(GObject.TYPE_UINT)
            value.set_uint(int(value_str))
        elif type == 'b':
            value = bool(value_str)
        elif type == 's':
            value = value_str
        elif type == 'as':
            value = GStrv(eval(value_str))
        return (name, value)

app = AccountConsole()

parser = argparse.ArgumentParser(description='Command-line tool for account handling')
subparsers = parser.add_subparsers(title='Valid actions')

subparser = subparsers.add_parser('list',
        help='List existing accounts')
subparser.set_defaults(func=app.list_accounts)

subparser = subparsers.add_parser('show',
        help='Show an account\'s settings')
subparser.add_argument('account', type=int,
        help='Id of the account')
subparser.set_defaults(func=app.show_account)

subparser = subparsers.add_parser('create',
        help='Create a new account')
subparser.add_argument('provider',
        help='Provider name (see /usr/share/accounts/providers/lomiri/)')
subparser.add_argument('--disabled', action='store_true',
        help='Create the account in disabled state')
subparser.add_argument('--print-id', action='store_true',
        help='Print the account ID on stdout')
subparser.add_argument('-s', action='append', default=[],
        help='Add a service setting, in the form [<type>:]<name>=<value>')
subparser.set_defaults(func=app.create_account)

subparser = subparsers.add_parser('edit',
        help='Edit an existing account')
subparser.add_argument('account', type=int,
        help='Id of the account')
subparser.add_argument('--disable', action='store_true',
        help='Disable the account')
subparser.add_argument('--enable', action='store_true',
        help='Enable the account')
subparser.add_argument('--service', type=str,
        help='Operates on the given service')
subparser.add_argument('-s', action='append', default=[], metavar='SETTING',
        help='Add or changes a service setting, in the form [<type>:]<name>=<value>')
subparser.add_argument('-u', action='append', default=[],
        metavar='SETTING_NAME', help='Unset a setting')
subparser.add_argument('--signon-id-field', type=str, default='CredentialsId',
        help='Name of the key holding the SignOn ID')
subparser.add_argument('--caption', type=str,
        help='SignOn caption (username description)')
subparser.add_argument('--username', type=str,
        help='SignOn username')
subparser.add_argument('--password', type=str,
        help='SignOn password')
subparser.set_defaults(func=app.edit_account)

subparser = subparsers.add_parser('delete',
        help='Deletes an existing account')
subparser.add_argument('account', type=int,
        help='Id of the account')
subparser.set_defaults(func=app.delete_account)

subparser = subparsers.add_parser('signon_login',
        help='Authenticate the given identity')
subparser.add_argument('identity', type=int,
        help='Id of the SignOn identity')
subparser.add_argument('method', type=str,
        help='Authentication method')
subparser.add_argument('mechanism', type=str,
        help='Authentication mechanism')
subparser.add_argument('-p', action='append', default=[], metavar='PARAMETER',
        help='Session parameter, in the form [<type>:]<name>=<value>')
subparser.set_defaults(func=app.login_identity)

subparser = subparsers.add_parser('login',
        help='Authenticate the given identity')
subparser.add_argument('account', type=int,
        help='Id of the SignOn identity')
subparser.add_argument('--service', type=str,
        help='Account service')
subparser.add_argument('--signon-id-field', type=str, default='CredentialsId',
        help='Name of the key holding the SignOn ID')
mm = subparser.add_argument_group()
mm.add_argument('--method', type=str,
        help='Authentication method')
mm.add_argument('--mechanism', type=str,
        help='Authentication mechanism')
subparser.add_argument('-p', action='append', default=[], metavar='PARAMETER',
        help='Session parameter, in the form [<type>:]<name>=<value>')
subparser.set_defaults(func=app.login_account)

args = parser.parse_args()
if 'func' in args:
    # We're working with "lomiri" online accounts.
    os.environ['XDG_CURRENT_DESKTOP'] = 'lomiri'

    main_loop = GLib.MainLoop()
    GLib.idle_add(args.func, args)
    main_loop.run()
else:
    parser.print_help()

