#!/usr/bin/env python2.7

from __future__ import print_function

import argparse
import StringIO
import traceback
import contextlib
import imp
import json
import os
import sys

parser = argparse.ArgumentParser(
    prog='run_handler',
    description='Runs a Lambda entry point (handler) with an optional event',
)

parser.add_argument(
    '--event', dest='event',
    type=json.loads,
    help=("The event that will be deserialized and passed to the function. "
          "This has to be valid JSON, and will be deserialized into a "
          "Python dictionary before your handler is invoked")
)

parser.add_argument(
    '--handler-path', dest='handler_path',
    help=("File path to the handler, e.g. `lib/function.py`")
)

parser.add_argument(
    '--handler-function', dest='handler_function',
    default='lambda_handler',
    help=("File path to the handler")
)


class FakeLambdaContext(object):
    def __init__(self, name='Fake', version='LATEST'):
        self.name = name
        self.version = version

    @property
    def get_remaining_time_in_millis(self):
        return 10000

    @property
    def function_name(self):
        return self.name

    @property
    def function_version(self):
        return self.version

    @property
    def invoked_function_arn(self):
        return 'arn:aws:lambda:serverless:' + self.name

    @property
    def memory_limit_in_mb(self):
        return 1024

    @property
    def aws_request_id(self):
        return '1234567890'


@contextlib.contextmanager
def preserve_value(namespace, name):
    """A context manager to restore a binding to its prior value

    At the beginning of the block, `__enter__`, the value specified is
    saved, and is restored when `__exit__` is called on the contextmanager

    namespace (object): Some object with a binding
    name (string): The name of the binding to be preserved.
    """
    saved_value = getattr(namespace, name)
    yield
    setattr(namespace, name, saved_value)


@contextlib.contextmanager
def capture_fds(stdout=None, stderr=None):
    """Replace stdout and stderr with a different file handle.

    Call with no arguments to just ignore stdout or stderr.
    """
    orig_stdout, orig_stderr = sys.stdout, sys.stderr
    orig_stdout.flush()
    orig_stderr.flush()

    temp_stdout = stdout or StringIO.StringIO()
    temp_stderr = stderr or StringIO.StringIO()
    sys.stdout, sys.stderr = temp_stdout, temp_stderr

    yield

    sys.stdout = orig_stdout
    sys.stderr = orig_stderr

    temp_stdout.flush()
    temp_stdout.seek(0)
    temp_stderr.flush()
    temp_stderr.seek(0)


def make_module_from_file(module_name, module_filepath):
    """Make a new module object from the source code in specified file.

    :param module_name: Desired name (must be valid import name)
    :param module_filepath: The filesystem path with the Python source
    :return: A loaded module

    The Python import mechanism is not used. No cached bytecode
    file is created, and no entry is placed in `sys.modules`.
    """
    py_source_open_mode = 'U'
    py_source_description = (".py", py_source_open_mode, imp.PY_SOURCE)

    with open(module_filepath, py_source_open_mode) as module_file:
        with preserve_value(sys, 'dont_write_bytecode'):
            sys.dont_write_bytecode = True
            module = imp.load_module(
                module_name,
                module_file,
                module_filepath,
                py_source_description
            )
    return module


def bail_out(code=99):
    output = {
        'success': False,
        'exception': traceback.format_exception(*sys.exc_info()),
    }
    print(json.dumps(output))
    sys.exit(code)


def import_program_as_module(handler_file):
    """Import module from `handler_file` and return it to be used.

    Since we don't want to clutter up the filesystem, we're going to turn
    off bytecode generation (.pyc file creation)
    """
    module = make_module_from_file('lambda_handler', handler_file)
    sys.modules['lambda_handler'] = module

    return module


def run_with_context(handler, function_path, event=None):
    function = getattr(handler, function_path)
    return function(event or {}, FakeLambdaContext())


if __name__ == '__main__':
    args = parser.parse_args(sys.argv[1:])
    path = os.path.expanduser(args.handler_path)
    if not os.path.isfile(path):
        message = (u'There is no such file "{}". --handler-path must be a '
              u'Python file'.format(path))
        print(json.dumps({"success": False, "exception": message}))
        sys.exit(100)

    try:
        handler = import_program_as_module(path)
    except Exception as e:
        bail_out()

    stdout, stderr = StringIO.StringIO(), StringIO.StringIO()
    output = {}
    with capture_fds(stdout, stderr):
        try:
            result = run_with_context(handler, args.handler_function, args.event)
            output['result'] = result
        except Exception as e:
            message = u'Failure running handler function {f} from file {file}:\n{tb}'
            output['exception'] = message.format(
                f=args.handler_function,
                file=path,
                tb=traceback.format_exception(*sys.exc_info()),
            )
            output['success'] = False
        else:
            output['success'] = True
    output.update({
        'stdout': stdout.read(),
        'stderr': stderr.read(),
    })

    print(json.dumps(output))
