#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# 2018-08-06 Friedrich Weber <friedrich.weber@netknights.it>
#            Add standalone tool
#
# Copyright (c) 2018, Friedrich Weber
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#

__doc__ = """
This script can be used to create a self-contained local privacyIDEA
instance that does not require a web server to run. Instead,
authentication requests are validated via the command line.

The ``create`` command launches a wizard that creates a new instance.
The ``configure`` command starts a local development server that can
be used to setup tokens. This server must not be exposed to the network!
The ``check`` command can then be used to authenticate users."""

import json
import os
import string
import subprocess
from functools import wraps
from six.moves import input

import sqlalchemy
import sys
import warnings

from flask_script import Manager, Server
from tempfile import NamedTemporaryFile

from privacyidea.app import create_app
from privacyidea.lib.security.default import DefaultSecurityModule

warnings.simplefilter("ignore", category=sqlalchemy.exc.SAWarning)

PI_CFG_TEMPLATE = """import os, logging

INSTANCE_DIRECTORY = os.path.abspath(os.path.dirname(__file__))
PI_ENCFILE = os.path.join(INSTANCE_DIRECTORY, 'encKey')
PI_AUDIT_KEY_PRIVATE = os.path.join(INSTANCE_DIRECTORY, 'private.pem')
PI_AUDIT_KEY_PUBLIC = os.path.join(INSTANCE_DIRECTORY, 'public.pem')
PI_LOGFILE = os.path.join(INSTANCE_DIRECTORY, 'privacyidea.log')
PI_LOGLEVEL = logging.INFO
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(INSTANCE_DIRECTORY, 'privacyidea.sqlite')

SECRET_KEY = b'{secret_key}'
PI_PEPPER = b'{pi_pepper}'

"""

RSA_KEYSIZE = 2048
PEPPER_CHARSET = string.ascii_letters + string.digits + '_'


def invoke_pi_manage(commandline, pi_cfg):
    """
    Invoke ``pi-manage`` with arguments, setting PRIVACYIDEA_CONFIGFILE TO ``pi_cfg``.
    :param commandline: arguments to pass as a list
    :param pi_cfg: location of the privacyIDEA config file
    """
    environment = os.environ.copy()
    environment['PRIVACYIDEA_CONFIGFILE'] = pi_cfg
    subprocess.check_call(['pi-manage'] + commandline, env=environment)


def _app_factory(instance):
    """
    Create a Flask app object with the given privacyIDEA standalone instance.
    """
    config_file = os.path.abspath(os.path.join(instance, 'pi.cfg'))
    app = create_app(config_name='production', config_file=config_file, silent=True)
    app.instance_directory = instance
    return app


def require_instance(f):
    """
    Decorator that marks commands that require an already set-up instance directory
    """
    @wraps(f)
    def deco(*args, **kwargs):
        config_file = os.path.join(manager.app.instance_directory, 'pi.cfg')
        if not os.path.exists(config_file):
            raise RuntimeError("{!r} does not exist! Create a new instance using"
                               " ``privacyidea-standalone create``.".format(config_file))
        return f(*args, **kwargs)
    return deco


class Configure(Server):
    help = description = "Run a local webserver to configure privacyIDEA"

    @require_instance
    def __call__(self, *args, **kwargs):
        Server.__call__(self, *args, **kwargs)

    def run(self):
        pass


manager = Manager(_app_factory, with_default_commands=False, description=__doc__)
manager.add_command("configure", Configure())
manager.add_option('-i', '--instance', dest='instance', required=False,
                   default=os.path.expanduser('~/.privacyidea'),
                   help='Location of the privacyIDEA instance (defaults to ~/.privacyidea)')


def read_credentials(fobj):
    """
    read username and password from a file. This could be sys.stdin.

    The first line specifies the username, the second line specifies the password.

    :param fobj: a Python file object
    :return: a tuple (user, password)
    """
    username = fobj.readline().strip()
    password = fobj.readline().strip()
    return username, password


def create_pepper(length=24, chunk_size=8, charset=PEPPER_CHARSET):
    """
    create a valid PI_PEPPER value of a given length from urandom,
    choosing characters from a given charset
    :param length: pepper length to generate
    :param chunk_size: number of bytes to read from urandom per iteration
    :param charset: list of valid characters
    :return: a bytestring of the specified length
    """
    pepper = b''
    while len(pepper) < length:
        random_bytes = DefaultSecurityModule.random(chunk_size)
        printable_bytes = ''.join(b for b in random_bytes if b in charset)
        pepper += printable_bytes
    return pepper[:length]


def choice(question, choices, case_insensitive=True):
    """
    Ask a question interactively until one of the given choices is selected.
    Return the choice then.
    :param question: Question to ask the user as a string
    :param choices: Dictionary mapping user answers to return values
    :param case_insensitive: Set to true if the answer should be handled case-insensitively.
                             Then, ``choices`` should contain only lowercase keys.
    :return: a value of ``choices``
    """
    while True:
        answer = input(question)
        if case_insensitive:
            answer = answer.lower()
        if answer in choices:
            return choices[answer]
        else:
            print('{!r} is not a valid answer.'.format(answer))


def yesno(question, default):
    """
    Ask a y/n question with a default value.
    :param question: Question to ask the user as a string
    :param default: Default return value (boolean)
    :return: boolean
    """
    return choice(question, {'y': True,
                             'n': False,
                             '': default})


@manager.command
def create():
    """ Create a new privacyIDEA instance """
    instance_dir = os.path.abspath(manager.app.instance_directory)
    if os.path.exists(manager.app.instance_directory):
        raise RuntimeError("Instance at {!r} exists already!".format(manager.app.instance_directory))
    os.makedirs(instance_dir)

    # create SECRET_KEY and PI_PEPPER
    secret_key = DefaultSecurityModule.random(24)
    pi_pepper = create_pepper()

    # create a pi.cfg
    pi_cfg = os.path.join(instance_dir, 'pi.cfg')
    with open(pi_cfg, 'w') as f:
        f.write(PI_CFG_TEMPLATE.format(
            secret_key=secret_key.encode('string-escape'),
            pi_pepper=pi_pepper.encode('string-escape')
        ))

    # create an enckey
    invoke_pi_manage(['create_enckey'], pi_cfg)
    invoke_pi_manage(['create_audit_keys'], pi_cfg)
    invoke_pi_manage(['createdb'], pi_cfg)

    print()
    print('Please enter a password for the new admin `super`.')
    invoke_pi_manage(['admin', 'add', 'super'], pi_cfg)

    # create users
    if yesno('Would you like to create a default resolver and realm (Y/n)? ', True):
        print("""
There are two possibilities to create a resolver:
 1) We can create a table in the privacyIDEA SQLite database to store the users.
    You can add users via the privacyIDEA Web UI.
 2) We can create a resolver that contains the users from /etc/passwd
""")
        print()
        create_sql_resolver = choice('Please choose (default=1): ', {
            '1': True,
            '2': False,
            '': True
        })
        if create_sql_resolver:
            invoke_pi_manage(['resolver', 'create_internal', 'defresolver'], pi_cfg)
        else:
            with NamedTemporaryFile(delete=False) as f:
                f.write('{"fileName": "/etc/passwd"}')
            invoke_pi_manage(['resolver', 'create', 'defresolver', 'passwdresolver', f.name],
                             pi_cfg)
            os.unlink(f.name)
        invoke_pi_manage(['realm', 'create', 'defrealm', 'defresolver'], pi_cfg)

    print()
    print('Configuration is complete. You can now configure privacyIDEA in '
          'the web browser by running')
    print("  privacyidea-standalone -i '{}' "
          "configure".format(instance_dir.encode('unicode-escape')))


@manager.option('-r', '--response', dest='show_response', action='store_true',
                help='Print the JSON response of privacyIDEA to standard output')
@require_instance
def check(show_response=False):
    """
    Check the given username and password against privacyIDEA.
    This command reads two lines from standard input: The first line is
    the username, the second line is the password (which consists of a
    static part and the OTP).

    This commands exits with return code 0 if the user could be authenticated
    successfully.
    """
    user, password = read_credentials(sys.stdin)
    exitcode = 255
    try:
        with manager.app.test_request_context('/validate/check', method='POST',
                                              data={'user': user, 'pass': password}):
            response = manager.app.full_dispatch_request()
            data = json.loads(response.data)
            result = data['result']
            if result['value'] is True:
                exitcode = 0
            else:
                exitcode = 1
            if show_response:
                print(response.data)
    except Exception as e:
        print(repr(e))
    sys.exit(exitcode)


if __name__ == '__main__':
    manager.run()
