#!/usr/bin/env python

import os
import sys
import socket
import traceback
from functools import wraps

import eventlet
{% if debug %}
eventlet.monkey_patch(thread=True, time=True)
{% endif %}
from builtins import bytes
import click
import msgpack
import flask
from flask import (Flask, render_template, make_response, copy_current_request_context,
                   jsonify, request, Response)
from flask_socketio import SocketIO
from bowtie._component import COMPONENT_REGISTRY, Event


# python 2 compatibility
try:
    FileNotFoundError
except NameError:
    FileNotFoundError = OSError


class GetterNotDefined(AttributeError):
    pass


def check_auth(username, password):
    """This function is called to check if a username /
    password combination is valid.
    """
    return username == '{{ username }}' and password == '{{ password }}'

def authenticate():
    """Sends a 401 response that enables basic auth"""
    return Response(
        'Could not verify your access level for that URL.\n'
        'You have to login with proper credentials', 401,
        {'WWW-Authenticate': 'Basic realm="Login Required"'}
    )

def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth or not check_auth(auth.username, auth.password):
            return authenticate()
        return f(*args, **kwargs)
    return decorated


app = Flask(__name__)
app.debug = {{ debug|default(False) }}
socketio = SocketIO(app, binary=True, path='{{ socketio }}' + 'socket.io')
# not sure if this is secure or how much it matters
app.secret_key = os.urandom(256)


# import the user created module
{% if notebook %}
from bowtie._magic import load_notebook
sourcemod = load_notebook('{{ notebook }}')
{% else %}
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
import {{source_module}} as sourcemod
{% endif %}

def context(func):
    def foo():
        with app.app_context():
            func()
    return foo


class Scheduler(object):

    def __init__(self, seconds, func):
        self.seconds = seconds
        self.func = func
        self.thread = None

    def start(self):
        self.thread = eventlet.spawn(self.run)

    def run(self):
        ret = eventlet.spawn(context(self.func))
        eventlet.sleep(self.seconds)
        try:
            ret.wait()
        except:
            traceback.print_exc()
        self.thread = eventlet.spawn(self.run)

    def stop(self):
        if self.thread:
            self.thread.cancel()


{% for route in routes %}
@app.route('{{ route.path }}')
{% endfor %}
{% if basic_auth %}
@requires_auth
{% endif %}
def index():
    return render_template('index.html')


{% if login %}
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        success = sourcemod.{{ login }}()
        if success:
            return redirect(url_for('index'))
        else:
            return redirect(url_for('login'))
    return {{ loginpage }}
{% endif %}


@app.route('/static/bundle.js')
def getbundle():
    basedir = os.path.dirname(os.path.realpath(__file__))
    bundle_path = basedir + '/static/bundle.js'
    bundle_path_gz = bundle_path + '.gz'

    try:
        if os.path.getmtime(bundle_path) > os.path.getmtime(bundle_path_gz):
            return open(bundle_path, 'r').read()
        else:
            bundle = open(bundle_path_gz, 'rb').read()
            response = flask.make_response(bundle)
            response.headers['Content-Encoding'] = 'gzip'
            response.headers['Vary'] = 'Accept-Encoding'
            response.headers['Content-Length'] = len(response.data)
            return response
    except FileNotFoundError:
        if os.path.isfile(bundle_path_gz):
            bundle = open(bundle_path_gz, 'rb').read()
            response = flask.make_response(bundle)
            response.headers['Content-Encoding'] = 'gzip'
            response.headers['Vary'] = 'Accept-Encoding'
            response.headers['Content-Length'] = len(response.data)
            return response
        else:
            return open(bundle_path, 'r').read()


{% if initial %}
@socketio.on('INITIALIZE')
def _():
    foo = copy_current_request_context(sourcemod.{{ initial }})
    eventlet.spawn(foo)
{% endif %}


{% for uuid, function in uploads.items() %}
@app.route('/upload{{ uuid }}', methods=['POST'])
def _():
    upfile = request.files['file']
    retval = sourcemod.{{ function }}(upfile.filename, upfile.stream)
    if retval:
        return make_response(jsonify(), 400)
    return make_response(jsonify(), 200)
{% endfor %}


{% for page, function in pages.items() %}
@socketio.on('resp#{{ page._uuid }}')
def _():
    foo = copy_current_request_context(sourcemod.{{ function }})
    eventlet.spawn(foo)
{% endfor %}


{% for event, supports in subscriptions.items() %}
@socketio.on('{{ event.signal }}')
def _(*args):
    def wrapuser():
        event_data = {}
        uniq_events = set()
        {% for support in supports %}
        uniq_events.update({{ support[0] }})
        {% endfor %}
        uniq_events.remove({{ event }})
        for ev in uniq_events:
            comp = COMPONENT_REGISTRY[ev.uuid]
            if ev.getter is None:
                raise GetterNotDefined('{ctype} has no getter associated with event "on_{ename}"'
                                       .format(ctype=type(comp), ename=ev.name))
            getter = getattr(comp, ev.getter)
            event_data[ev.signal] = getter()

        # if there is no getter, then there is no data to unpack
        # if there is a getter, then we need to unpack the data sent
        {% set post = event.getter %}
        {% if post is not none %}
        event_data['{{ event.signal }}'] = COMPONENT_REGISTRY[{{ event.uuid }}].{{ '_' ~ post }}(
            msgpack.unpackb(args[0], encoding='utf8')
        )
        {% endif %}

        # gather the remaining data from the other events through their getter methods
        {% for support in supports %}
        user_args = []
            {% if post is not none %}
                {% for ev in support[0] %}
        user_args.append(event_data['{{ ev.signal }}'])
                {% endfor %}
            {% endif %}

        # finally call the user method
        sourcemod.{{ support[1] }}(*user_args)
        {% endfor %}

    foo = copy_current_request_context(wrapuser)
    eventlet.spawn(foo)

{% endfor %}

@click.command()
@click.option('--host', '-h', default={{host}}, help='Host IP')
@click.option('--port', '-p', default={{port}}, help='port number')
def main(host, port):
    scheduled = not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true'
    if scheduled:
        scheds = []
        {% for schedule in schedules %}
        sched = Scheduler({{ schedule.seconds }},
                          sourcemod.{{ schedule.function }})
        scheds.append(sched)
        {% endfor %}

        for sched in scheds:
            sched.start()
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    result = sock.connect_ex((host, port))
    if result == 0:
        raise Exception('Port {} is unavailable on host {}, aborting.'.format(port, host))
    socketio.run(app, host=host, port=port)
    if scheduled:
        for sched in scheds:
            sched.stop()

if __name__ == '__main__':
    main()
