from flask import request, make_response, session, g, redirect, url_for, render_template, jsonify, flash, abort
from flask_cors import cross_origin
from honeybadger import app, db, logger
from honeybadger.processors import process_known_coords, process_wlan_survey, process_ip, process_json
from honeybadger.validators import is_valid_email, is_valid_password
from honeybadger.decorators import login_required, roles_required
from honeybadger.constants import ROLES
from honeybadger.utils import generate_token, generate_nonce
from honeybadger.models import User, Target, Beacon, Log
import os
from base64 import b64decode as b64d

# request preprocessors

@app.before_request
def load_user():
    g.user = None
    if session.get('user_id'):
        g.user = User.query.filter_by(id=session["user_id"]).first()

# control panel ui views

@app.route('/')
@app.route('/index')
@login_required
def index():
    return redirect(url_for('map'))

@app.route('/map')
@login_required
def map():
    return render_template('map.html', key=app.config['GOOGLE_API_KEY'])

@app.route('/beacons')
@login_required
def beacons():
    beacons = [b.serialized for t in Target.query.all() for b in t.beacons.all()]
    columns = ['id', 'target', 'agent', 'lat', 'lng', 'acc', 'ip', 'created']
    return render_template('beacons.html', columns=columns, beacons=beacons)

@app.route('/beacon/delete/<int:id>')
@login_required
@roles_required('admin')
def beacon_delete(id):
    beacon = Beacon.query.get(id)
    if beacon:
        db.session.delete(beacon)
        db.session.commit()
        flash('Beacon deleted.')
    else:
        flash('Invalid beacon ID.')
    return redirect(url_for('beacons'))

@app.route('/targets')
@login_required
def targets():
    targets = Target.query.all()
    columns = ['id', 'name', 'guid', 'beacon_count']
    return render_template('targets.html', columns=columns, targets=targets)

@app.route('/target/add', methods=['POST'])
@login_required
@roles_required('admin')
def target_add():
    name = request.form['target']
    if name:
        target = Target(
            name=name,
        )
        db.session.add(target)
        db.session.commit()
        flash('Target added.')
    return redirect(url_for('targets'))

@app.route('/target/delete/<string:guid>')
@login_required
@roles_required('admin')
def target_delete(guid):
    target = Target.query.filter_by(guid=guid).first()
    if target:
        db.session.delete(target)
        db.session.commit()
        flash('Target deleted.')
    else:
        flash('Invalid target GUID.')
    return redirect(url_for('targets'))

@app.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
    if request.method == 'POST':
        if g.user.check_password(request.form['current_password']):
            new_password = request.form['new_password']
            if new_password == request.form['confirm_password']:
                if is_valid_password(new_password):
                    g.user.password = new_password
                    db.session.add(g.user)
                    db.session.commit()
                    flash('Profile updated.')
                else:
                    flash('Password does not meet complexity requirements.')
            else:
                flash('Passwords do not match.')
        else:
            flash('Incorrect current password.')
    return render_template('profile.html', user=g.user)

# use an alternate route for reset as long as the logic is similar to init
@app.route('/password/reset/<string:token>', methods=['GET', 'POST'], endpoint='password_reset')
@app.route('/profile/activate/<string:token>', methods=['GET', 'POST'])
def profile_activate(token):
    user = User.query.filter_by(token=token).first()
    if user and user.status in (0, 3):
        if request.method == 'POST':
            new_password = request.form['new_password']
            if new_password == request.form['confirm_password']:
                if is_valid_password(new_password):
                    user.password = new_password
                    user.status = 1
                    user.token = None
                    db.session.add(user)
                    db.session.commit()
                    flash('Profile activated.')
                    return redirect(url_for('login'))
                else:
                    flash('Password does not meet complexity requirements.')
            else:
                flash('Passwords do not match.')
        return render_template('profile_activate.html', user=user)
    # abort to 404 for obscurity
    abort(404)

@app.route('/admin')
@login_required
@roles_required('admin')
def admin():
    users = User.query.all()
    columns = ['email', 'role_as_string', 'status_as_string']
    return render_template('admin.html', columns=columns, users=users, roles=ROLES)

@app.route('/admin/user/init', methods=['POST'])
@login_required
@roles_required('admin')
def admin_user_init():
    email = request.form['email']
    if is_valid_email(email):
        if not User.query.filter_by(email=email).first():
            user = User(
                email=email,
                token=generate_token(),
            )
            db.session.add(user)
            db.session.commit()
            flash('User initialized.')
        else:
            flash('Username already exists.')
    else:
        flash('Invalid email address.')
    # send notification to user
    return redirect(url_for('admin'))

@app.route('/admin/user/<string:action>/<int:id>')
@login_required
@roles_required('admin')
def admin_user(action, id):
    user = User.query.get(id)
    if user:
        if user != g.user:
            if action == 'activate' and user.status == 2:
                user.status = 1
                db.session.add(user)
                db.session.commit()
                flash('User activated.')
            elif action == 'deactivate' and user.status == 1:
                user.status = 2
                db.session.add(user)
                db.session.commit()
                flash('User deactivated.')
            elif action == 'reset' and user.status == 1:
                user.status = 3
                user.token = generate_token()
                db.session.add(user)
                db.session.commit()
                flash('User reset.')
            elif action == 'delete':
                db.session.delete(user)
                db.session.commit()
                flash('User deleted.')
            else:
                flash('Invalid user action.')
        else:
            flash('Self-modification denied.')
    else:
        flash('Invalid user ID.')
    return redirect(url_for('admin'))

@app.route('/login', methods=['GET', 'POST'])
def login():
    # redirect to home if already logged in
    if session.get('user_id'):
        return redirect(url_for('index'))
    if request.method == 'POST':
        user = User.get_by_email(request.form['email'])
        if user and user.status == 1 and user.check_password(request.form['password']):
            session['user_id'] = user.id
            flash('You have successfully logged in.')
            return redirect(url_for('index'))
        flash('Invalid username or password.')
    return render_template('login.html')

@app.route('/logout')
@login_required
def logout():
    session.pop('user_id', None)
    flash('You have been logged out')
    return redirect(url_for('index'))

@app.route('/demo/<string:guid>', methods=['GET', 'POST'])
def demo(guid):
    text = None
    if request.method == 'POST':
        text = request.values['text']
        key = request.values['key']
        if g.user.check_password(key):
            if text and 'alert(' in text:
                text = 'Congrats! You entered: {}'.format(text)
            else:
                text = 'Nope. Try again.'
        else:
            text = 'Incorrect password.'
    nonce = generate_nonce(24)
    response = make_response(render_template('demo.html', target=guid, text=text, nonce=nonce))
    response.headers['X-XSS-Protection'] = '0'#'1; report=https://hb.lanmaster53.com/api/beacon/{}/X-XSS-Protection'.format(guid)
    uri = url_for('api_beacon', target=guid, agent='Content-Security-Policy')
    response.headers['Content-Security-Policy-Report-Only'] = 'script-src \'nonce-{}\'; report-uri {}'.format(nonce, uri)
    return response

@app.route('/log')
@login_required
def log():
    # hidden capability to clear logs
    if request.values.get('clear'):
        Log.query.delete()
        db.session.commit()
        return redirect(url_for('log'))
    content = ''
    logs = Log.query.order_by(Log.created).all()
    for log in logs:
        content += '[{}] [{}] {}{}'.format(log.created_as_string, log.level_as_string, log.message, os.linesep)
    return render_template('log.html', content=content)

# control panel api views

@app.route('/api/beacons')
@login_required
def api_beacons():
    beacons = [b.serialized for t in Target.query.all() for b in t.beacons.all()]
    return jsonify(beacons=beacons)

# agent api views

@app.route('/api/beacon/<target>/<agent>', methods=['GET', 'POST'])
@cross_origin()
def api_beacon(target, agent):
    logger.info('{}'.format('='*50))
    data = {'target': target, 'agent': agent}
    logger.info('Target: {}'.format(target))
    logger.info('Agent: {}'.format(agent))
    # check if target is valid
    if target not in [x.guid for x in Target.query.all()]:
        logger.error('Invalid target GUID.')
        abort(404)
    # extract universal parameters
    comment = b64d(request.values.get('comment', '')) or None
    ip = request.environ['REMOTE_ADDR']
    port = request.environ['REMOTE_PORT']
    useragent = request.environ['HTTP_USER_AGENT']
    data.update({'comment': comment, 'ip': ip, 'port': port, 'useragent': useragent})
    logger.info('Connection from {} @ {}:{} via {}'.format(target, ip, port, agent))
    logger.info('Parameters: {}'.format(request.values.to_dict()))
    logger.info('User-Agent: {}'.format(useragent))
    logger.info('Comment: {}'.format(comment))
    data.update(request.values.to_dict())
    # process json payloads
    if request.json:
        if process_json(data, request.json):
            abort(404)
    # process known coordinates
    if all(k in data for k in ('lat', 'lng', 'acc')):
        if process_known_coords(data):
            abort(404)
    # process wireless survey
    elif all(k in data for k in ('os', 'data')):
        if process_wlan_survey(data):
            abort(404)
    # process ip geolocation (includes fallback)
    process_ip(data)
    abort(404)