#!/usr/local/bin/python3.12
# License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>

import sys
from functools import partial

from calibre import prints
from calibre.constants import preferred_encoding, iswindows
from calibre.utils.config import OptionParser
from polyglot.builtins import iteritems


def create_subcommand_parser(name, usage):
    usage = f'%prog --manage-users -- {name} ' + usage
    parser = OptionParser(usage)
    return parser


def add(user_manager, args):
    p = create_subcommand_parser('add', _('username [password]') + '\n\n' + '''\
Create a new user account with the specified name and password. If the password
is not specified on the command line, it will be read from STDIN.
''')
    p.add_option('--readonly', action='store_true', default=False, help=_('Give this user only read access'))
    opts, args = p.parse_args(['calibre-server'] + list(args))
    if len(args) < 2:
        p.print_help()
        raise SystemExit(_('username is required'))
    username = args[1]
    if len(args) > 2:
        pw = args[2]
    else:
        pw = sys.stdin.read()
    user_manager.add_user(username, pw, readonly=opts.readonly)


def remove(user_manager, args):
    p = create_subcommand_parser('remove', _('username') + '\n\n' + '''\
Remove the user account with the specified username.
''')
    opts, args = p.parse_args(['calibre-server'] + list(args))
    if len(args) < 2:
        p.print_help()
        raise SystemExit(_('username is required'))
    username = args[1]
    user_manager.remove_user(username)


def list_users(user_manager, args):
    p = create_subcommand_parser('list', '\n\n' + '''\
List all usernames.
''')
    opts, args = p.parse_args(['calibre-server'] + list(args))
    for name in user_manager.all_user_names:
        print(name)


def change_readonly(user_manager, args):
    p = create_subcommand_parser('readonly', _('username set|reset|toggle|show') + '\n\n' + '''\
Restrict the specified user account to prevent it from making changes. \
The value of set makes the account readonly, reset allows it to make \
changes, toggle flips the value and show prints out the current value. \
''')
    opts, args = p.parse_args(['calibre-server'] + list(args))
    if len(args) < 3:
        p.print_help()
        raise SystemExit(_('username and operation are required'))
    username, op = args[1], args[2]
    if op == 'toggle':
        val = not user_manager.is_readonly(username)
    elif op == 'set':
        val = True
    elif op == 'reset':
        val = False
    elif op == 'show':
        print('set' if user_manager.is_readonly(username) else 'reset', end='')
        return
    else:
        raise SystemExit(f'{op} is an unknown operation')
    user_manager.set_readonly(username, val)


def change_libraries(user_manager, args):
    p = create_subcommand_parser(
        'libraries', _('[options] username [library_name ...]') + '\n\n' + '''\
Manage the libraries the specified user account is restricted to.
''')
    p.add_option('--action', type='choice', choices='allow-all allow block per-library show'.split(), default='show', help=_(
        'Specify the action to perform.'
        '\nA value of "show" shows the current library restrictions for the specified user.'
        '\nA value of "allow-all" removes all library restrictions.'
        '\nA value of "allow" allows access to only the specified libraries.'
        '\nA value of "block" allows access to all, except the specified libraries.'
        '\nA value of "per-library" sets per library restrictions. In this case the libraries list'
        ' is interpreted as a list of library name followed by restriction to apply, followed'
        ' by next library name and so on. Using a restriction of "=" removes any previous restriction'
        ' on that library.'
    ))
    opts, args = p.parse_args(['calibre-server'] + list(args))
    if len(args) < 2:
        p.print_help()
        raise SystemExit(_('username is required'))
    username, libraries = args[1], args[2:]
    r = user_manager.restrictions(username)
    if r is None:
        raise SystemExit(f'The user {username} does not exist')

    if opts.action == 'show':
        if r['allowed_library_names']:

            print('Allowed:')
            for name in r['allowed_library_names']:
                print('\t' + name)
        if r['blocked_library_names']:
            print('Blocked:')
            for name in r['blocked_library_names']:
                print('\t' + name)
        if r['library_restrictions']:
            print('Per Library:')
            for name, res in r['library_restrictions'].items():
                print('\t' + name)
                print('\t\t' + res)
        if not r['allowed_library_names'] and not r['blocked_library_names'] and not r['library_restrictions']:
            print(f'{username} has no library restrictions')
    elif opts.action == 'allow-all':
        user_manager.update_user_restrictions(username, {})
    elif opts.action == 'per-library':
        if not libraries:
            p.print_help()
            raise SystemExit('Must specify at least one library and restriction')
        if len(libraries) % 2 != 0:
            p.print_help()
            raise SystemExit('Must specify a restriction for every library')
        lres = r['library_restrictions']
        for i in range(0, len(libraries), 2):
            name, res = libraries[i:i+2]
            if res == '=':
                lres.pop(name, None)
            else:
                lres[name] = res
        user_manager.update_user_restrictions(username, r)
    else:
        if not libraries:
            p.print_help()
            raise SystemExit('Must specify at least one library name')
        k = 'blocked_library_names' if opts.action == 'block' else 'allowed_library_names'
        r.pop('allowed_library_names', None)
        r.pop('blocked_library_names', None)
        r[k] = libraries
        user_manager.update_user_restrictions(username, r)


def chpass(user_manager, args):
    p = create_subcommand_parser('chpass', _('username [password]') + '\n\n' + '''\
Change the password of the new user account with the specified username. If the password
is not specified on the command line, it will be read from STDIN.
''')
    opts, args = p.parse_args(['calibre-server'] + list(args))
    if len(args) < 2:
        p.print_help()
        raise SystemExit(_('username is required'))
    username = args[1]
    if len(args) > 2:
        pw = args[2]
    else:
        pw = sys.stdin.read()
    user_manager.change_password(username, pw)


def main(user_manager, args):
    q, rest = args[0], args[1:]
    if q == 'add':
        return add(user_manager, rest)
    if q == 'remove':
        return remove(user_manager, rest)
    if q == 'chpass':
        return chpass(user_manager, rest)
    if q == 'list':
        return list_users(user_manager, rest)
    if q == 'readonly':
        return change_readonly(user_manager, rest)
    if q == 'libraries':
        return change_libraries(user_manager, rest)
    if q != 'help':
        print(_('Unknown command: {}').format(q), file=sys.stderr)
        print()
    print(_('Manage the user accounts for calibre-server. Available commands are:'))
    print('add, remove, chpass, list')
    print(_('Use {} for help on individual commands').format('calibre-server --manage-users -- command -h'))
    raise SystemExit(1)


def manage_users_cli(path=None, args=()):
    from calibre.srv.users import UserManager
    m = UserManager(path)
    if args:
        main(m, args)
        return
    enc = getattr(sys.stdin, 'encoding', preferred_encoding) or preferred_encoding

    def get_input(prompt):
        prints(prompt, end=' ')
        ans = input()
        if isinstance(ans, bytes):
            ans = ans.decode(enc)
        if iswindows:
            # https://bugs.python.org/issue11272
            ans = ans.rstrip('\r')
        return ans

    def choice(
        question=_('What do you want to do?'), choices=(), default=None, banner=''):
        prints(banner)
        for i, choice in enumerate(choices):
            prints('%d)' % (i + 1), choice)
        print()
        while True:
            prompt = question + ' [1-%d]:' % len(choices)
            if default is not None:
                prompt = question + ' [1-%d %s: %d]' % (
                    len(choices), _('default'), default + 1)
            reply = get_input(prompt)
            if not reply and default is not None:
                reply = str(default + 1)
            if not reply:
                prints(_('No choice selected, exiting...'))
                raise SystemExit(0)
            reply = reply.strip()
            try:
                num = int(reply) - 1
                if not (0 <= num < len(choices)):
                    raise Exception('bad num')
                return num
            except Exception:
                prints(_('%s is not a valid choice, try again') % reply)

    def get_valid(prompt, invalidq=lambda x: None):
        while True:
            ans = get_input(prompt + ':').strip()
            fail_message = invalidq(ans)
            if fail_message is None:
                return ans
            prints(fail_message)

    def get_valid_user():
        prints(_('Existing user names:'))
        users = sorted(m.all_user_names)
        if not users:
            raise SystemExit(_('There are no users, you must first add an user'))
        prints(', '.join(users))

        def validate(username):
            if not m.has_user(username):
                return _('The username %s does not exist') % username

        return get_valid(_('Enter the username'), validate)

    def get_pass(username):
        from getpass import getpass

        while True:
            one = getpass(
                _('Enter the new password for %s: ') % username)
            if not one:
                prints(_('Empty passwords are not allowed'))
                continue
            two = getpass(
                _('Re-enter the new password for %s, to verify: ') % username
            )
            if one != two:
                prints(_('Passwords do not match'))
                continue
            msg = m.validate_password(one)
            if msg is None:
                return one
            prints(msg)

    def add_user():
        username = get_valid(_('Enter the username'), m.validate_username)
        pw = get_pass(username)
        m.add_user(username, pw)
        prints(_('User %s added successfully!') % username)

    def remove_user():
        un = get_valid_user()
        if get_input((_('Are you sure you want to remove the user %s?') % un) +
                     ' [y/n]:') != 'y':
            raise SystemExit(0)
        m.remove_user(un)
        prints(_('User %s successfully removed!') % un)

    def change_password(username):
        pw = get_pass(username)
        m.change_password(username, pw)
        prints(_('Password for %s successfully changed!') % username)

    def show_password(username):
        pw = m.get(username)
        prints(_('Current password for {0} is: {1}').format(username, pw))

    def change_readonly(username):
        readonly = m.is_readonly(username)
        if readonly:
            q = _('Allow {} to make changes (i.e. grant write access)')
        else:
            q = _('Prevent {} from making changes (i.e. remove write access)')
        if get_input(q.format(username) + '? [y/n]:').lower() == 'y':
            m.set_readonly(username, not readonly)

    def change_restriction(username):
        r = m.restrictions(username)
        if r is None:
            raise SystemExit(f'The user {username} does not exist')
        if r['allowed_library_names']:
            libs = r['allowed_library_names']
            prints(
                ngettext(
                    '{} is currently only allowed to access the library named: {}',
                    '{} is currently only allowed to access the libraries named: {}',
                    len(libs)).format(username, ', '.join(libs)))
        if r['blocked_library_names']:
            libs = r['blocked_library_names']
            prints(
                ngettext(
                    '{} is currently not allowed to access the library named: {}',
                    '{} is currently not allowed to access the libraries named: {}',
                    len(libs)).format(username, ', '.join(libs)))
        if r['library_restrictions']:
            prints(
                _('{} has the following additional per-library restrictions:')
                .format(username))
            for k, v in iteritems(r['library_restrictions']):
                prints(k + ':', v)
        else:
            prints(_('{} has no additional per-library restrictions').format(username))
        c = choice(
            choices=[
                _('Allow access to all libraries'),
                _('Allow access to only specified libraries'),
                _('Allow access to all, except specified libraries'),
                _('Change per-library restrictions'),
                _('Cancel')])
        if c == 0:
            m.update_user_restrictions(username, {})
        elif c == 3:
            while True:
                library = get_input(_('Enter the name of the library:'))
                if not library:
                    break
                prints(
                    _(
                        'Enter a search expression, access will be granted only to books matching this expression.'
                        ' An empty expression will grant access to all books.'))
                plr = get_input(_('Search expression:'))
                if plr:
                    r['library_restrictions'][library] = plr
                else:
                    r['library_restrictions'].pop(library, None)
                m.update_user_restrictions(username, r)
                if get_input(_('Another restriction?') + ' (y/n):') != 'y':
                    break
        elif c == 4:
            pass
        else:
            names = get_input(_('Enter a comma separated list of library names:'))
            names = list(filter(None, [x.strip() for x in names.split(',')]))
            w = 'allowed_library_names' if c == 1 else 'blocked_library_names'
            t = _('Allowing access only to libraries: {}') if c == 1 else _(
                'Allowing access to all libraries, except: {}')
            prints(t.format(', '.join(names)))
            m.update_user_restrictions(username, {w: names})

    def edit_user(username=None):
        username = username or get_valid_user()
        c = choice(
            choices=[
                _('Show password for {}').format(username),
                _('Change password for {}').format(username),
                _('Change read/write permission for {}').format(username),
                _('Change the libraries {} is allowed to access').format(username),
                _('Cancel'), ],
            banner='\n' + _('{0} has {1} access').format(
                username,
                _('readonly') if m.is_readonly(username) else _('read-write')))
        print()
        if c > 3:
            actions.append(toplevel)
            return
        {
            0: show_password,
            1: change_password,
            2: change_readonly,
            3: change_restriction}[c](username)
        actions.append(partial(edit_user, username=username))

    def toplevel():
        {
            0: add_user,
            1: edit_user,
            2: remove_user,
            3: lambda: None}[choice(
                choices=[
                    _('Add a new user'),
                    _('Edit an existing user'),
                    _('Remove a user'),
                    _('Cancel')])]()

    actions = [toplevel]
    while actions:
        actions[0]()
        del actions[0]
