#!/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.
#
from __future__ import print_function

__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 six
import json
import os
import shutil
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_AUDIT_SQL_TRUNCATE = True
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 = '{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 string of the specified length
    :rtype: str
    """
    pepper = ''
    while len(pepper) < length:
        random_bytes = DefaultSecurityModule.random(chunk_size)
        printables = ''.join(chr(b) for b in six.iterbytes(random_bytes) if chr(b) in charset)
        pepper += printables
    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):
        print(u"Instance at {!s} exists already! Aborting.".format(manager.app.instance_directory))
    else:
        try:
            os.makedirs(instance_dir)

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

            secret_key_hex = ''.join('\\x{:02x}'.format(b) for b in six.iterbytes(secret_key))
            # 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_hex,
                    pi_pepper=pi_pepper
                ))

            # create an enckey
            invoke_pi_manage(['create_enckey'], pi_cfg)
            invoke_pi_manage(['create_audit_keys'], pi_cfg)
            invoke_pi_manage(['create_tables'], 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))
        except Exception as e:
            print('Could not finish creation process! Removing instance.')
            shutil.rmtree(instance_dir)
            raise e


@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()
