#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# 2018-08-06 Friedrich Weber <friedrich.weber@netknights.it>
#
# 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 eduMFA
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 shutil
import string
import subprocess
from functools import wraps

import sqlalchemy
import sys
import warnings

from flask_script import Manager, Server
from tempfile import NamedTemporaryFile

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

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

EDUMFA_CFG_TEMPLATE = """import os, logging

INSTANCE_DIRECTORY = os.path.abspath(os.path.dirname(__file__))
EDUMFA_ENCFILE = os.path.join(INSTANCE_DIRECTORY, 'encKey')
EDUMFA_AUDIT_KEY_PRIVATE = os.path.join(INSTANCE_DIRECTORY, 'private.pem')
EDUMFA_AUDIT_KEY_PUBLIC = os.path.join(INSTANCE_DIRECTORY, 'public.pem')
EDUMFA_AUDIT_SQL_TRUNCATE = True
EDUMFA_LOGFILE = os.path.join(INSTANCE_DIRECTORY, 'edumfa.log')
EDUMFA_LOGLEVEL = logging.INFO
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(INSTANCE_DIRECTORY, 'edumfa.sqlite')

SECRET_KEY = b'{secret_key}'
EDUMFA_PEPPER = '{edumfa_pepper}'

"""

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


def invoke_edumfa_config(commandline, edumfa_cfg):
    """
    Invoke ``edumfa-manage`` with arguments, setting EDUMFA_CONFIGFILE TO ``edumfa_cfg``.
    :param commandline: arguments to pass as a list
    :param edumfa_cfg: location of the eduMFA config file
    """
    environment = os.environ.copy()
    environment['EDUMFA_CONFIGFILE'] = edumfa_cfg
    subprocess.check_call(['edumfa-manage'] + commandline, env=environment)


def _app_factory(instance):
    """
    Create a Flask app object with the given eduMFA standalone instance.
    """
    config_file = os.path.abspath(os.path.join(instance, 'edumfa.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, 'edumfa.cfg')
        if not os.path.exists(config_file):
            raise RuntimeError("{!r} does not exist! Create a new instance using"
                               " ``edumfa-standalone create``.".format(config_file))
        return f(*args, **kwargs)
    return deco


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

    @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('~/.edumfa'),
                   help='Location of the eduMFA instance (defaults to ~/.edumfa)')


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 EDUMFA_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 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 eduMFA instance """
    instance_dir = os.path.abspath(manager.app.instance_directory)
    if os.path.exists(manager.app.instance_directory):
        print("Instance at {!s} exists already! Aborting.".format(manager.app.instance_directory))
    else:
        try:
            os.makedirs(instance_dir)

            # create SECRET_KEY and EDUMFA_PEPPER
            secret_key = DefaultSecurityModule.random(24)
            edumfa_pepper = create_pepper()

            secret_key_hex = ''.join('\\x{:02x}'.format(b) for b in secret_key)
            # create a edumfa.cfg
            edumfa_cfg = os.path.join(instance_dir, 'edumfa.cfg')
            with open(edumfa_cfg, 'w') as f:
                f.write(EDUMFA_CFG_TEMPLATE.format(
                    secret_key=secret_key_hex,
                    edumfa_pepper=edumfa_pepper
                ))

            # create an enckey
            invoke_edumfa_config(['create_enckey'], edumfa_cfg)
            invoke_edumfa_config(['create_audit_keys'], edumfa_cfg)
            invoke_edumfa_config(['create_tables'], edumfa_cfg)

            print()
            print('Please enter a password for the new admin `super`.')
            invoke_edumfa_config(['admin', 'add', 'super'], edumfa_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 eduMFA SQLite database to store the users.
        You can add users via the eduMFA 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_edumfa_config(['resolver', 'create_internal', 'defresolver'], edumfa_cfg)
                else:
                    with NamedTemporaryFile(delete=False) as f:
                        f.write('{"fileName": "/etc/passwd"}')
                    invoke_edumfa_config(['resolver', 'create', 'defresolver', 'passwdresolver',
                                      f.name], edumfa_cfg)
                    os.unlink(f.name)
                invoke_edumfa_config(['realm', 'create', 'defrealm', 'defresolver'], edumfa_cfg)

            print()
            print('Configuration is complete. You can now configure eduMFA in '
                  'the web browser by running')
            print("  edumfa-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 eduMFA to standard output')
@require_instance
def check(show_response=False):
    """
    Check the given username and password against eduMFA.
    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()
