from datetime import datetime, timedelta
from json import loads, dumps
from multiprocessing.dummy import Pool as ThreadPool

import requests
import jwt
from flask import Flask, render_template, request, abort, jsonify, redirect, url_for
from flask_wtf import FlaskForm
from redis import StrictRedis
from requests.auth import HTTPBasicAuth
from werkzeug.contrib.fixers import ProxyFix
from wtforms import StringField, IntegerField


# app setup
app = Flask(__name__)
app.config.from_pyfile('config.py')
if app.config['PREFERRED_URL_SCHEME'] == 'https':
    app.wsgi_app = ProxyFix(app.wsgi_app)

app.url_map.strict_slashes = False

# some helpers
redis_store = StrictRedis.from_url(app.config['REDIS_URL'], db=app.config['REDIS_DB'], decode_responses=True)
pool = ThreadPool(4)


def make_request(request_dict):
    response = requests.get(request_dict['url'], headers=request_dict['headers'])
    return response.json()


def get_integration_info(exam_id):
    data = redis_store.get(exam_id)
    if data:
        return loads(data)
    url = app.config['SEI_URL_BASE'] + '/api/integrations/' + exam_id + '/credentials'
    resp = requests.get(url, auth=HTTPBasicAuth(username=app.config['SEI_ID'], password=app.config['SEI_SECRET']))
    if resp.status_code != 200:
        raise ValueError('No access to this exam_id')
    data = resp.json()
    redis_store.set(data['exam_id'], dumps(data))
    return data


def build_template_data(delivery, to_dict):
    try:
        score_percent = delivery['points_earned'] / delivery['points_available']
    except Exception:
        score_percent = 0.0
    score_report_url = app.config['SEI_URL_BASE'] + '/score/' + delivery['score_token']
    data = {
        'name': to_dict['name'],
        'email': to_dict['email'],
        'examinee_info': delivery['examinee']['info'],
        'exam_name': delivery['exam']['name'],
        'score': delivery['score'],
        'score_scale': delivery['score_scale'],
        'points_earned': delivery['points_earned'],
        'points_available': delivery['points_available'],
        'score_percent': score_percent,
        'passed': delivery['passed'],
        'cutscore': delivery['cutscore'],
        'breakdown_objects': delivery['breakdown_objects'],
        'status': delivery['status'],
        'type': delivery['type'],
        'duration': delivery['used_seconds'],
        'score_report_url': score_report_url
    }
    return data


def build_to(name_map, email_map, examinee_info):
    name = str(name_map)
    for key in examinee_info:
        key_code = '[{0}]'.format(key)
        name = name.replace(key_code, examinee_info[key])
    email = str(email_map)
    for key in examinee_info:
        key_code = '[{0}]'.format(key)
        email = email.replace(key_code, examinee_info[key])
    return {'name': name, 'email': email}


class ApiKeyForm(FlaskForm):
    # TODO: build api key validator
    api_key = StringField('API Key')


# views
@app.route('/')
def index():
    return render_template('index.html')


@app.route('/sei_redirect')
def sei_redirect():
    confirm_token = request.args.get('confirm_token')
    if not confirm_token:
        abort(400)
    url = app.config['SEI_URL_BASE'] + '/api/integrations/confirm/' + confirm_token
    resp = requests.get(url, auth=HTTPBasicAuth(username=app.config['SEI_ID'], password=app.config['SEI_SECRET']))
    if resp.status_code != 200:
        abort(400)
    data = resp.json()
    exam_id = data['exam_id']
    secret = data['secret']
    existing_data = redis_store.get(exam_id)
    if existing_data:
        existing_data = loads(existing_data)
        existing_data.update(data)
        data = existing_data
    redis_store.set(exam_id, dumps(data))
    now = datetime.utcnow()
    exp_seconds = 3600
    exp_time = (now + timedelta(seconds=exp_seconds))
    payload = {'iss': 'SEI', 'sub': exam_id, 'iat': now, 'exp': exp_time}
    token = jwt.encode(payload, secret, algorithm='HS256').decode()
    return redirect(url_for('switch', jwt=token, exam_id=exam_id))


@app.route('/events', methods=['POST'])
def events():
    # authorize the request
    body = request.get_json()
    exam_id = body['exam_id']
    integration_info = get_integration_info(exam_id)
    auth_header = request.headers.get('Authorization')
    token = auth_header.split()[1]
    try:
        decoded = jwt.decode(token, integration_info['secret'], algorithms=['HS256'])
    except jwt.exceptions.InvalidTokenError:
        return jsonify(), 403

    # get full delivery object from SEI
    # TODO: only do this for delivery type events
    delivery_id = body['delivery_id']
    delivery_url = '{0}/api/exams/{1}/deliveries/{2}?include=exam,breakdown_objects,score_token'.format(app.config['SEI_URL_BASE'], exam_id, delivery_id)
    delivery_headers = {'Authorization': 'Bearer {0}'.format(integration_info['token'])}
    delivery_response = requests.get(delivery_url, headers=delivery_headers)
    delivery_json = delivery_response.json()
    examinee_info = delivery_json['examinee']['info']
    api_key = integration_info.get('api_key')
    if api_key:
        event = body['event']
        configs = integration_info.get('configs', [])
        sg_url = 'https://api.sendgrid.com/v3/mail/send'
        sg_headers = {'Authorization': 'Bearer {0}'.format(api_key)}
        for config in configs:
            if config['event'] == event:
                to_dict = build_to(config['name'], config['email'], examinee_info)
                from_dict = {'name': config.get('sender_name'), 'email': config.get('sender_email')}
                sg_payload = {
                    'from': from_dict,
                    'template_id': config['template_id'],
                    'personalizations': [{
                        'to': [to_dict],
                        'dynamic_template_data': build_template_data(delivery_json, to_dict)
                    }]
                }
                requests.post(sg_url, json=sg_payload, headers=sg_headers)
    return jsonify()


@app.route('/delivery_widget')
def delivery_widget():
    exam_id = request.args.get('exam_id')
    delivery_id = request.args.get('delivery_id')
    token = request.args.get('jwt')
    integration_info = get_integration_info(exam_id)
    try:
        decoded = jwt.decode(token, integration_info['secret'], algorithms=['HS256'])
    except jwt.exceptions.InvalidTokenError:
        abort(403)
    configs = integration_info.get('configs', [])
    configs_by_event = {}
    for config in configs:
        try:
            configs_by_event[config['event']].append(config)
        except KeyError:
            configs_by_event[config['event']] = [config]
    return render_template('delivery_widget.html', exam_id=exam_id, delivery_id=delivery_id, token=token, configs_by_event=configs_by_event)


@app.route('/switch')
def switch():
    exam_id = request.args.get('exam_id')
    token = request.args.get('jwt')
    integration_info = get_integration_info(exam_id)
    try:
        decoded = jwt.decode(token, integration_info['secret'], algorithms=['HS256'])
    except jwt.exceptions.InvalidTokenError:
        abort(403)

    if integration_info.get('api_key'):
        return redirect(url_for('configure', **request.args))
    return redirect(url_for('api_key', **request.args))


@app.route('/api_key', methods=['GET', 'POST'])
def api_key():
    exam_id = request.args.get('exam_id')
    token = request.args.get('jwt')
    integration_info = get_integration_info(exam_id)
    try:
        decoded = jwt.decode(token, integration_info['secret'], algorithms=['HS256'])
    except jwt.exceptions.InvalidTokenError:
        abort(403)
    form = ApiKeyForm(api_key=integration_info.get('api_key'))
    if form.validate_on_submit():
        integration_info['api_key'] = form.api_key.data
        redis_store.set(exam_id, dumps(integration_info))
        return redirect(url_for('configure', exam_id=exam_id, jwt=token))
    bad_key = request.args.get('bad_key') == 'true'
    return render_template('api_key.html', form=form, bad_key=bad_key)


@app.route('/configure')
def configure():
    exam_id = request.args.get('exam_id')
    token = request.args.get('jwt')
    integration_info = get_integration_info(exam_id)
    try:
        decoded = jwt.decode(token, integration_info['secret'], algorithms=['HS256'])
    except jwt.exceptions.InvalidTokenError:
        abort(403)

    request_dicts = []

    url = '{0}/api/exams/{1}?only=examinee_schema'.format(app.config['SEI_URL_BASE'], exam_id)
    headers = {'Authorization': 'Bearer {0}'.format(integration_info['token'])}
    request_dicts.append({'url': url, 'headers': headers})

    sg_base = 'https://api.sendgrid.com/v3'
    sg_headers = {'Authorization': 'Bearer {0}'.format(integration_info['api_key'])}

    templates_url = sg_base + '/templates?generations=dynamic'
    request_dicts.append({'url': templates_url, 'headers': sg_headers})

    exam_json, templates_json = pool.map(make_request, request_dicts)
    schema = exam_json['examinee_schema']
    try:
        templates = templates_json['templates']
    except KeyError:
        return redirect(url_for('api_key', exam_id=exam_id, jwt=token, bad_key='true'))

    configs = integration_info.get('configs', [])

    return render_template('configure.html', exam_id=exam_id, token=token, schema=schema,
                           templates=templates, configs=configs)


@app.route('/configure', methods=['POST'])
def post_configuration():
    data = request.get_json()
    exam_id = request.args.get('exam_id')
    token = request.args.get('jwt')
    integration_info = get_integration_info(exam_id)
    try:
        decoded = jwt.decode(token, integration_info['secret'], algorithms=['HS256'])
    except jwt.exceptions.InvalidTokenError:
        return jsonify(), 403
    integration_info['configs'] = data['configs']
    redis_store.set(exam_id, dumps(integration_info))
    return jsonify()


@app.route('/complete')
def complete():
    return render_template('complete.html')
