#!/usr/bin/python2
# -!- coding: utf-8 -!-

# Copyright 2010-2014 Hector Martin "marcan" <marcan@marcan.st>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import random, os, os.path
from flask import Flask, request, session, g, render_template, flash, redirect, url_for
from wtforms import Form, BooleanField, TextField, PasswordField, validators
import sqlalchemy.exc

import users
import level
from ranking import get_ranking

# ====== Init ======

app = Flask(__name__)
app.config.from_object("config")
users.db.init_app(app)

import logging

class RequestFormatter(logging.Formatter):
    def format(self, record):
        s = logging.Formatter.format(self, record)
        try:
            return '[%s] [%s] [%s %s] '%(self.formatTime(record), request.remote_addr, request.method, request.path) + s
        except:
            return '[%s] [SYS] '%self.formatTime(record) + s

if not app.debug:
    import logging
    from logging.handlers import SMTPHandler
    mail_handler = SMTPHandler('127.0.0.1',
                                app.config['ADMIN_EMAIL'],
                                app.config['ADMIN_EMAIL'], 'Hack It ERROR')
    mail_handler.setLevel(logging.ERROR)
    app.logger.addHandler(mail_handler)

    handler = logging.FileHandler(os.path.join(app.root_path, app.config['LOG_FILE']))
    handler.setLevel(logging.INFO)
    handler.setFormatter(RequestFormatter())
    app.logger.addHandler(handler)

    app.logger.setLevel(logging.INFO)
    app.logger.warning('Starting...')

# ====== Simple pages ======

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

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

@app.route('/about')
def about():
    authors = set()
    for r in level.routes:
        for l in r.levels:
            if isinstance(l.author, list) or isinstance(l.author, tuple):
                for i in l.author:
                    authors.add(i)
            else:
                authors.add(l.author)
    authors = list(authors)
    random.shuffle(authors)
    return render_template('about.html', authors=authors)

@app.route('/ranking')
def ranking():
    grank, rrank = get_ranking(app.config)
    rrank = zip(level.routes, rrank)
    return render_template('ranking.html', grank=grank, rrank=rrank)

@app.errorhandler(404)
def page_not_found(e):
    return render_template('notfound.html'), 404

# ====== User management ======

class BaseProfileForm(Form):
    password2 = PasswordField(u'Repite la contraseña')
    pubname = TextField(u'Nombre público', [validators.Length(min=1, max=30,
        message=u'El nombre debe tener entre 1 y 30 caracteres')])
    email = TextField(u'Email', [
        validators.Email(message=u'Email no válido'),
        validators.Length(max=60, message=u'La longitud máxima del e-mail es de 60 caracteres')
    ])
    if app.config['USE_SEAT']:
        seat = TextField(u'Puesto en la party', [validators.Length(min=3, max=30,
            message=u'El puesto debe tener entre 3 y 30 caracteres')])

class ProfileForm(BaseProfileForm):
    username = TextField(u'Nombre de usuario', [validators.Optional()])
    password = PasswordField(u'Contraseña', [
        validators.Optional(),
        validators.EqualTo('password2', message=u'Las contraseñas no coinciden'),
        validators.Length(min=6, message=u'La contraseña debe tener como mínimo 6 caracteres')
    ])

class RegistrationForm(BaseProfileForm):
    username = TextField(u'Nombre de usuario', [validators.Length(min=2, max=30,
        message=u'El nombre de usuario debe tener entre 2 y 30 caracteres')])
    password = PasswordField(u'Contraseña', [
        validators.Required(message=u'Debes introducir una contraseña'),
        validators.EqualTo('password2', message=u'Las contraseñas no coinciden'),
        validators.Length(min=6, message=u'La contraseña debe tener como mínimo 6 caracteres')
    ])
    accept_rules = BooleanField(u'He leído y acepto las bases', [
        validators.Required(message=u'Debes aceptar las bases')])

@app.route('/register', methods=['GET', 'POST'])
def register():
    if g.user is not None:
        return redirect(url_for('index'))

    form = RegistrationForm(request.form)

    if request.method == 'POST' and form.validate():
        if form.username.data in app.config['BANNED_USERNAMES']:
            form.username.errors.append(u'Buen intento ;)')
            return render_template('register.html', form=form)
        if app.config['USE_SEAT']:
            seat = form.seat.data
        else:
            seat = None
        newuser = users.User(form.username.data, form.password.data,
            form.pubname.data, form.email.data, seat)
        try:
            users.db.session.add(newuser)
            users.db.session.commit()
        except sqlalchemy.exc.IntegrityError:
            form.username.errors.append(u'Ya existe un usuario con ese nombre')
        else:
            flash(u'Te has registrado con éxito. ¡Comienza el desafío!')
            session['user_id'] = newuser.id
            g.user = newuser
            app.logger.info('[%s] Registered %r,%r,%r', g.user, g.user.pubname, g.user.email, g.user.seat)
            return level.autosolve(app) or redirect(url_for('index'))

    return render_template('register.html', form=form)

@app.route('/profile', methods=['GET', 'POST'])
def profile():
    if g.user is None:
        return redirect(url_for('index'))

    form = ProfileForm(request.form)
    form.username.data = g.user.username

    if request.method == 'POST' and form.validate():
        g.user.pubname = form.pubname.data
        g.user.email = form.email.data
        if app.config['USE_SEAT']:
            g.user.seat = form.seat.data

        if form.password.data:
            g.user.changepassword(form.password.data)

        users.db.session.commit()
        app.logger.info('[%s] profile updated %r,%r,%r', g.user, g.user.pubname, g.user.email, g.user.seat)
        flash(u'Perfil actualizado correctamente')
    else:
        form.pubname.data = g.user.pubname
        form.email.data = g.user.email
        if app.config['USE_SEAT']:
            form.seat.data = g.user.seat

    return render_template('profile.html', form=form)

@app.route('/delete_account', methods=['POST'])
def delete_account():
    app.logger.warning('[%s] account deleted', g.user)
    users.db.session.delete(g.user)
    users.db.session.commit()
    del session['user_id']
    session['csrf_token'] = os.urandom(8).encode('hex')
    flash(u'Cuenta de usuario borrada')
    return redirect(url_for('index'))

class LoginForm(Form):
    username = TextField(u'Nombre de usuario')
    password = PasswordField(u'Contraseña')

@app.route('/login', methods=['POST'])
def login():
    if g.user is not None:
        return redirect(url_for('index'))

    form = LoginForm(request.form)

    if request.method == 'POST' and form.validate():
        user = users.User.query.filter_by(username=form.username.data).first()
        if user is None:
            flash(u'El usuario no existe')
        elif not user.checkpassword(form.password.data):
            flash(u'Contraseña incorrecta')
            app.logger.info('[%s] login failed', user)
        else:
            flash(u'Bienvenido de nuevo, %s'%user.username)
            session['user_id'] = user.id
            g.user = user
            app.logger.info('[%s] login succeeded', user)
            return level.autosolve(app) or redirect(url_for('index'))

    return redirect(url_for('index'))

@app.route('/logout')
def logout():
    session['csrf_token'] = os.urandom(8).encode('hex')
    if 'user_id' in session:
        del session['user_id']
        flash(u'Has cerrado tu sesión')
    return redirect(url_for('index'))

# ====== Levels ======

for r in level.routes:
    for l in r.levels:
        app.register_blueprint(l, url_prefix='/%s/%d'%(r.name, l.number))
        l.config = app.config
        l.logger = app.logger

# ====== Global request processing ======

@app.before_request
def setup_tasks():
    if request.method == 'POST':
        if ('csrf_token' not in session or (
            ('csrf_token' not in request.form or
                request.form['csrf_token'] != session['csrf_token'])
            and ('X-CSRF' not in request.headers or
                 request.headers['X-CSRF'] != session['csrf_token']))):
            flash(u'Error de token CSRF')
            return redirect(url_for('index'))

    if 'csrf_token' not in session:
        session['csrf_token'] = os.urandom(8).encode('hex')

    g.user = None

    if 'user_id' in session and not request.path.startswith('/static/'):
        g.user = users.User.query.filter_by(id=session['user_id']).first()
        if g.user is None:
            del session['user_id']

    g.sb_routes = []
    for r in level.routes:
        lv = []
        for l in r.levels:
            lv.append(l)
        g.sb_routes.append((r, lv))

# ====== Built-in server ======

if __name__ == '__main__':
    import sys
    if len(sys.argv) > 1:
        if sys.argv[1] == 'createdb':
            app.test_request_context().push()
            users.db.create_all()
            sys.exit(0)
    app.run()