#!/usr/bin/env python3
import datetime
import hashlib
import json
import os
import requests
import subprocess
import sys


from libcitizenwatt import cache
from libcitizenwatt import database
from libcitizenwatt import tools
from bottle import abort, Bottle, SimpleTemplate, static_file
from bottle import redirect, request, run
from bottle.ext import sqlalchemy
from bottlesession import PickleSession, authenticator
from libcitizenwatt.config import Config
from sqlalchemy import create_engine, desc
from sqlalchemy.exc import OperationalError, ProgrammingError


# =========
# Functions
# =========
def get_rate_type(db):
    """Returns "day" or "night" according to current time"""
    session = session_manager.get_session()
    user = db.query(database.User).filter_by(login=session.get("login")).first()
    now = datetime.datetime.now()
    now = 3600 * now.hour + 60 * now.minute
    if user is None:
        return None
    elif user.end_night_rate > user.start_night_rate:
        if now > user.start_night_rate and now < user.end_night_rate:
            return "night"
        else:
            return "day"
    else:
        if now > user.start_night_rate or now < user.end_night_rate:
            return "night"
        else:
            return "day"


def update_providers(fetch, db):
    """Updates the available providers. Simply returns them without updating if
    fetch is False.
    """
    try:
        assert(fetch)
        providers = requests.get(config.get("url_energy_providers")).json()
    except (requests.ConnectionError, AssertionError):
        providers = db.query(database.Provider).all()
        if not providers:
            providers = []
        return tools.to_dict(providers)

    old_current = db.query(database.Provider).filter_by(current=1).first()
    db.query(database.Provider).delete()

    for provider in providers:
        type_id = (db.query(database.MeasureType)
                   .filter_by(name=provider["type_name"])
                   .first())
        if not type_id:
            type_db = database.MeasureType(name=provider["type_name"])
            db.add(type_db)
            db.flush()
            type_id = database.MeasureType(name=provider["type_name"]).first()

        provider_db = database.Provider(name=provider["name"],
                                        day_constant_watt_euros=provider["day_constant_watt_euros"],
                                        day_slope_watt_euros=provider["day_slope_watt_euros"],
                                        night_constant_watt_euros=provider["night_constant_watt_euros"],
                                        night_slope_watt_euros=provider["night_slope_watt_euros"],
                                        type_id=type_id.id,
                                        current=(1 if old_current and old_current.name == provider["name"] else 0),
                                        threshold=int(provider["threshold"]))
        db.add(provider_db)
    return providers


def api_auth(post, db):
    """
    Handles login authentication for API.

    Returns True if login is ok, False otherwise.
    """
    login = post.get("login")
    user = db.query(database.User).filter_by(login=login).first()

    password = (config.get("salt") +
                hashlib.sha256(post.get("password", "").encode('utf-8'))
                .hexdigest())
    if user and user.password == password:
        return True
    else:
        return False


# ===============
# Initializations
# ===============
config = Config()
database_url = (config.get("database_type") + "://" + config.get("username") +
                ":" + config.get("password") + "@" + config.get("host") + "/" +
                config.get("database"))
engine = create_engine(database_url, echo=config.get("debug"))

app = Bottle()
plugin = sqlalchemy.Plugin(
    engine,
    database.Base.metadata,
    keyword='db',
    create=True,
    commit=True,
    use_kwargs=False
)
app.install(plugin)

session_manager = PickleSession()
valid_user = authenticator(session_manager, login_url='/login')


# ===
# API
# ===
@app.route("/api/sensors",
           apply=valid_user())
def api_sensors(db):
    """Returns a list of all the available sensors.

    If no sensors are found, returns null"""
    sensors = db.query(database.Sensor).all()
    if sensors:
        sensors = [{"id": sensor.id,
                    "name": sensor.name,
                    "type": sensor.type.name,
                    "type_id": sensor.type_id} for sensor in sensors]
    else:
        sensors = None

    return {"data": sensors}


@app.route("/api/sensors",
           method="post")
def api_sensors_post(db):
    if api_auth(request.POST, db):
        return api_sensors(db)
    else:
        abort(403, "Access forbidden")


@app.route("/api/sensors/<id:int>",
           apply=valid_user())
def api_sensor(id, db):
    """Returns the sensor with id <id>.

    If no matching sensor is found, returns null"""
    sensor = db.query(database.Sensor).filter_by(id=id).first()
    if sensor:
        sensor = {"id": sensor.id,
                  "name": sensor.name,
                  "type": sensor.type.name,
                  "type_id": sensor.type_id}
    else:
        sensor = None

    return {"data": sensor}


@app.route("/api/sensors/<id:int>",
           method="post")
def api_sensor_post(id, db):
    if api_auth(request.POST, db):
        return api_sensor(id, db)
    else:
        abort(403, "Access forbidden")


@app.route("/api/types",
           apply=valid_user())
def api_types(db):
    """Returns a list of all the available measure types.

    If no types are found, returns null"""
    types = db.query(database.MeasureType).all()
    if types:
        types = [{"id": mtype.id,
                  "name": mtype.name} for mtype in types]
    else:
        types = None

    return {"data": types}


@app.route("/api/types",
           method="post")
def api_types_post(db):
    if api_auth(request.POST, db):
        return api_types(db)
    else:
        abort(403, "Access forbidden")


@app.route("/api/time",
           apply=valid_user())
def api_time(db):
    """
    Returns current timestamp on the server side."""
    now = datetime.datetime.now()

    return {"data": now.timestamp()}


@app.route("/api/time",
           method="post")
def api_time_post(db):
    if api_auth(request.POST, db):
        return api_time(db)
    else:
        abort(403, "Access forbidden")


@app.route("/api/<sensor:int>/get/watts/by_id/<id1:int>",
           apply=valid_user())
def api_get_id(sensor, id1, db):
    """
    Returns measure with id <id1> associated to sensor <sensor>, in watts.

    If <id1> < 0, counts from the last measure, as in Python lists.

    If no matching data is found, returns null.
    """
    if id1 >= 0:
        data = (db.query(database.Measures)
                .filter_by(sensor_id=sensor, id=id1)
                .first())
    else:
        data = (db.query(database.Measures)
                .filter_by(sensor_id=sensor)
                .order_by(desc(database.Measures.timestamp))
                .slice(-id1, -id1)
                .first())

    if not data:
        data = None
    else:
        data = tools.to_dict(data)

    return {"data": data, "rate": get_rate_type(db)}


@app.route("/api/<sensor:int>/get/watts/by_id/<id1:int>",
           method="post")
def api_get_id_post(sensor, id1, db):
    if api_auth(request.POST, db):
        return api_get_id(sensor, id1, db)
    else:
        abort(403, "Access forbidden")


@app.route("/api/<sensor:int>/get/<watt_euros:re:watts|kwatthours|euros>/by_id/<id1:int>/<id2:int>",
           apply=valid_user())
def api_get_ids(sensor, watt_euros, id1, id2, db):
    """
    Returns measures between ids <id1> and <id2> from sensor <sensor> in
    watts or euros.

    If id1 and id2 are negative, counts from the end of the measures.

    * If `watts_euros` is watts, returns the list of measures.
    * If `watt_euros` is kwatthours, returns the total energy for all the
    measures (dict).
    * If `watt_euros` is euros, returns the cost of all the measures (dict).

    Returns measure in ASC order of timestamp.

    Returns null if no measures were found.
    """
    if (id2 - id1) > config.get("max_returned_values"):
        abort(403,
              "Too many values to return. " +
              "(Maximum is set to %d)" % config.get("max_returned_values"))
    elif id2 < id1 or id2 * id1 < 0:
        abort(400, "Invalid parameters")
    else:
        data = cache.do_cache_ids(sensor, watt_euros, id1, id2, db)

    return {"data": data, "rate": get_rate_type(db)}


@app.route("/api/<sensor:int>/get/<watt_euros:re:watts|kwatthours|euros>/by_id/<id1:int>/<id2:int>",
           method="post")
def api_get_ids_post(sensor, watt_euros, id1, id2, db):
    if api_auth(request.POST, db):
        return api_get_ids(sensor, watt_euros, id1, id2, db)
    else:
        abort(403, "Access forbidden")


@app.route("/api/<sensor:int>/get/<watt_euros:re:watts|kwatthours|euros>/by_id/<id1:int>/<id2:int>/<step:int>",
           apply=valid_user())
def api_get_ids_step(sensor, watt_euros, id1, id2, step, db,
                     timestep=config.get("default_timestep")):
    """
    Returns all the measures of sensor `sensor` between ids `id1`
    and `id2`, grouped by step, as a list of the number of steps element.
    Each item is null if no matching measures are found.

    * If `watts_euros` is watts, returns the mean power for each group.
    * If `watt_euros` is kwatthours, returns the total energy for each group.
    * If `watt_euros` is euros, returns the cost of each group.

    Returns measure in ASC order of timestamp.

    Returns null if no measures were found.
    """
    if id1 * id2 < 0 or id2 <= id1 or step <= 0:
        abort(400, "Invalid parameters")
    elif (id2 - id1) > config.get("max_returned_values"):
        abort(403,
              "Too many values to return. " +
              "(Maximum is set to %d)" % config.get("max_returned_values"))

    try:
        data = cache.do_cache_group_id(sensor,
                                       watt_euros,
                                       id1,
                                       id2,
                                       step,
                                       db,
                                       timestep)
    except ValueError:
        abort(400, "Wrong parameters id1 and id2.")

    return {"data": data, "rate": get_rate_type(db)}


@app.route("/api/<sensor:int>/get/<watt_euros:re:watts|kwatthours|euros>/by_id/<id1:int>/<id2:int>/<step:int>",
           method="post")
def api_get_ids_step_post(sensor, watt_euros, id1, id2, step, db,
                          timestep=config.get("default_timestep")):
    if api_auth(request.POST, db):
        return api_get_ids_step(sensor, watt_euros, id1, id2, step, db, timestep)
    else:
        abort(403, "Access forbidden")


@app.route("/api/<sensor:int>/get/watts/by_time/<time1:float>",
           apply=valid_user())
def api_get_time(sensor, time1, db):
    """
    Returns measure at timestamp <time1> for sensor <sensor>, in watts.

    Returns null if no measure is found.
    """
    if time1 < 0:
        abort(400, "Invalid timestamp.")

    data = (db.query(database.Measures)
            .filter_by(sensor_id=sensor,
                       timestamp=time1)
            .first())
    if not data:
        data = None
    else:
        data = tools.to_dict(data)

    return {"data": data, "rate": get_rate_type(db)}


@app.route("/api/<sensor:int>/get/watts/by_time/<time1:float>",
           method="post")
def api_get_time_post(sensor, time1, db):
    if api_auth(request.POST, db):
        return api_get_time(sensor, time1, db)
    else:
        abort(403, "Access forbidden")


@app.route("/api/<sensor:int>/get/<watt_euros:re:watts|kwatthours|euros>/by_time/<time1:float>/<time2:float>",
           apply=valid_user())
def api_get_times(sensor, watt_euros, time1, time2, db):
    """
    Returns measures between timestamps <time1> and <time2>
    from sensor <sensor> in watts or euros.

    * If `watts_euros` is watts, returns the list of measures.
    * If `watt_euros` is kwatthours, returns the total energy for all the
    measures (dict).
    * If `watt_euros` is euros, returns the cost of all the measures (dict).

    Returns measure in ASC order of timestamp.

    Returns null if no matching measures are found.
    """
    if time1 < 0 or time2 < time1:
        abort(400, "Invalid timestamps.")

    data = cache.do_cache_times(sensor, watt_euros, time1, time2, db)

    return {"data": data, "rate": get_rate_type(db)}


@app.route("/api/<sensor:int>/get/<watt_euros:re:watts|kwatthours|euros>/by_time/<time1:float>/<time2:float>",
           method="post")
def api_get_times_post(sensor, watt_euros, time1, time2, db):
    if api_auth(request.POST, db):
        return api_get_times(sensor, watt_euros, time1, time2, db)
    else:
        abort(403, "Access forbidden")


@app.route("/api/<sensor:int>/get/<watt_euros:re:watts|kwatthours|euros>/by_time/<time1:float>/<time2:float>/<step:float>",
           apply=valid_user())
def api_get_times_step(sensor, watt_euros, time1, time2, step, db):
    """
    Returns all the measures of sensor `sensor` between timestamps `time1`
    and `time2`, grouped by step, as a list of the number of steps element.
    Each item is null if no matching measures are found.

    * If `watts_euros` is watts, returns the mean power for each group.
    * If `watt_euros` is kwatthours, returns the total energy for each group.
    * If `watt_euros` is euros, returns the cost of each group.

    Returns measure in ASC order of timestamp.
    """
    if time1 < 0 or time2 < 0 or step <= 0:
        abort(400, "Invalid parameters")

    data = cache.do_cache_group_timestamp(sensor,
                                          watt_euros,
                                          time1,
                                          time2,
                                          step,
                                          db)

    return {"data": data, "rate": get_rate_type(db)}


@app.route("/api/<sensor:int>/get/<watt_euros:re:watts|kwatthours|euros>/by_time/<time1:float>/<time2:float>/<step:float>",
           method="post")
def api_get_times_step_post(sensor, watt_euros, time1, time2, step, db):
    if api_auth(request.POST, db):
        return api_get_times_step(sensor, watt_euros, time1, time2, step, db)
    else:
        abort(403, "Access forbidden")


@app.route("/api/energy_providers",
           apply=valid_user())
def api_energy_providers(db):
    """Returns all the available energy providers or null if none found."""
    providers = db.query(database.Provider).all()
    if not providers:
        providers = None
    else:
        providers = tools.to_dict(providers)
        for provider in providers:
            if provider["day_slope_watt_euros"] != provider["night_slope_watt_euros"]:
                session = session_manager.get_session()
                user = db.query(database.User).filter_by(login=session["login"]).first()
                start_night_rate = ("%02d" % (user.start_night_rate // 3600) + ":" +
                                    "%02d" % ((user.start_night_rate % 3600) // 60))
                end_night_rate = ("%02d" % (user.end_night_rate // 3600) + ":" +
                                  "%02d" % ((user.end_night_rate % 3600) // 60))
                provider["start_night_rate"] = start_night_rate
                provider["end_night_rate"] = end_night_rate

    return {"data": providers}


@app.route("/api/energy_providers",
           method="post")
def api_energy_providers_post(db):
    if api_auth(request.POST, db):
        return api_energy_providers(db)
    else:
        abort(403, "Access forbidden")


@app.route("/api/energy_providers/<id:re:current|\d*>",
           apply=valid_user())
def api_specific_energy_providers(id, db):
    """
    Returns the current energy provider,
    or the specified energy provider.
    """
    if id == "current":
        provider = (db.query(database.Provider)
                    .filter_by(current=1)
                    .first())
    else:
        try:
            id = int(id)
        except ValueError:
            abort(400, "Invalid parameter.")

        provider = (db.query(database.Provider)
                    .filter_by(id=id)
                    .first())

    if not provider:
        provider = None
    else:
        provider = tools.to_dict(provider)
        if provider["day_slope_watt_euros"] != provider["night_slope_watt_euros"]:
            session = session_manager.get_session()
            user = db.query(database.User).filter_by(login=session["login"]).first()
            start_night_rate = ("%02d" % (user.start_night_rate // 3600) + ":" +
                                "%02d" % ((user.start_night_rate % 3600) // 60))
            end_night_rate = ("%02d" % (user.end_night_rate // 3600) + ":" +
                              "%02d" % ((user.end_night_rate % 3600) // 60))
            provider["start_night_rate"] = start_night_rate
            provider["end_night_rate"] = end_night_rate

    return {"data": provider}


@app.route("/api/energy_providers/<id:re:current|\d*>",
           method="post")
def api_specific_energy_providers_post(id, db):
    if api_auth(request.POST, db):
        return api_specific_energy_providers(id, db)
    else:
        abort(403, "Access forbidden")


@app.route("/api/<energy_provider:re:current|\d>/watt_to_euros/<tariff:re:night|day>/<consumption:float>",
           apply=valid_user())
def api_watt_euros(energy_provider, tariff, consumption, db):
    """
    Returns the cost in € associated with a certain consumption, in kWh.

    One should specify the tariff (night or day) and the id of the
    energy_provider.

    Returns null if no valid result to return.
    """
    # Consumption should be in kWh !!!

    if energy_provider == "current":
        energy_provider = 0

    try:
        int(energy_provider)
    except ValueError:
        abort(400, "Wrong parameter energy_provider.")

    data = tools.watt_euros(energy_provider, tariff, consumption, db)
    return {"data": data}


@app.route("/api/<energy_provider:re:current|\d>/watt_to_euros/<tariff:re:night|day>/<consumption:float>",
           method="post")
def api_watt_euros_post(energy_provider, tariff, consumption, db):
    if api_auth(request.POST, db):
        return api_watt_euros(energy_provider, tariff, consumption, db)
    else:
        abort(403, "Access forbidden")


# ======
# Routes
# ======
@app.route("/static/<filename:path>",
           name="static")
def static(filename):
    """Routes static files"""
    return static_file(filename, root="static")


@app.route('/',
           name="index",
           template="index",
           apply=valid_user())
def index():
    """Index view"""
    return {}


@app.route("/conso",
           name="conso",
           template="conso",
           apply=valid_user())
def conso(db):
    """Conso view"""
    provider = db.query(database.Provider).filter_by(current=1).first()
    return {"provider": provider.name}


@app.route("/reset_timer/<sensor:int>", apply=valid_user())
def reset_timer(sensor, db):
    db.query(database.Sensor).filter_by(id=sensor).update({"last_timer": 0})
    redirect("/settings")


@app.route("/settings",
           name="settings",
           template="settings",
           apply=valid_user())
def settings(db):
    """Settings view"""
    sensors = db.query(database.Sensor).all()
    if sensors:
        sensors = [{"id": sensor.id,
                    "name": sensor.name,
                    "type": sensor.type.name,
                    "type_id": sensor.type_id,
                    "aes_key": sensor.aes_key,
                    "base_address": sensor.base_address}
                   for sensor in sensors]
    else:
        sensors = []

    sensor_cw = [sensor for sensor in sensors if sensor["name"] ==
                 "CitizenWatt"][0]

    providers = update_providers(True, db)

    session = session_manager.get_session()
    user = db.query(database.User).filter_by(login=session["login"]).first()
    start_night_rate = ("%02d" % (user.start_night_rate // 3600) + ":" +
                        "%02d" % ((user.start_night_rate % 3600) // 60))
    end_night_rate = ("%02d" % (user.end_night_rate // 3600) + ":" +
                      "%02d" % ((user.end_night_rate % 3600) // 60))

    return {"sensors": sensors,
            "providers": providers,
            "start_night_rate": start_night_rate,
            "end_night_rate": end_night_rate,
            "base_address": sensor_cw["base_address"],
            "aes_key": '-'.join([str(i) for i in
                                 json.loads(sensor_cw["aes_key"])])}


@app.route("/settings",
           name="settings",
           apply=valid_user(),
           method="post")
def settings_post(db):
    """Settings view with POST data"""
    error = None

    password = request.forms.get("password").strip()
    password_confirm = request.forms.get("password_confirm")

    if password:
        if password == password_confirm:
            password = (config.get("salt") +
                        hashlib.sha256(password.encode('utf-8')).hexdigest())
            session = session_manager.get_session()
            (db.query(database.User)
             .filter_by(login=session["login"])
             .update({"password": password},
                     synchronize_session=False))
        else:
            error = {"title": "Les mots de passe ne sont pas identiques.",
                     "content": ("Les deux mots de passe doient " +
                                 "être identiques.")}
            settings_json = settings(db)
            settings_json.update({"err": error})
            return settings_json

    provider = request.forms.get("provider")
    provider = (db.query(database.Provider)
                .filter_by(name=provider)
                .update({"current": 1}))

    raw_start_night_rate = request.forms.get("start_night_rate")
    raw_end_night_rate = request.forms.get("end_night_rate")

    raw_base_address = request.forms.get("base_address")
    raw_aes_key = request.forms.get("aes_key")

    try:
        base_address_int = int(raw_base_address.strip("L"), 16)
        base_address = str(hex(base_address_int)).upper() + "LL"
    except ValueError:
        error = {"title": "Format invalide",
                 "content": ("L'adresse de la base entrée est invalide.")}
        settings_json = settings(db)
        settings_json.update({"err": error})
        return settings_json

    sensor = db.query(database.Sensor).filter_by(name="CitizenWatt").first()
    if base_address != sensor.base_address:
        tools.update_base_address(base_address_int)

    try:
        aes_key = [int(i.strip()) for i in raw_aes_key.split("-")]
        if len(aes_key) != 16:
            raise ValueError
    except ValueError:
        error = {"title": "Format invalide",
                 "content": ("La clé AES doit être constituée de 16 " +
                             "chiffres entre 0 et 255, séparés " +
                             "par des tirets.")}
        settings_json = settings(db)
        settings_json.update({"err": error})
        return settings_json
    (db.query(database.Sensor)
     .filter_by(name="CitizenWatt")
     .update({"base_address": base_address, "aes_key": json.dumps(aes_key)}))
    db.commit()

    try:
        start_night_rate = raw_start_night_rate.split(":")
        assert(len(start_night_rate) == 2)
        start_night_rate = [int(i) for i in start_night_rate]
        assert(start_night_rate[0] >= 0 and start_night_rate[0] <= 23)
        assert(start_night_rate[1] >= 0 and start_night_rate[1] <= 59)
        start_night_rate = 3600 * start_night_rate[0] + 60*start_night_rate[1]
    except (AssertionError, ValueError):
        error = {"title": "Format invalide",
                 "content": ("La date de début d'heures " +
                             "creuses doit être au format hh:mm.")}
        settings_json = settings(db)
        settings_json.update({"err": error})
        return settings_json
    try:
        end_night_rate = raw_end_night_rate.split(":")
        assert(len(end_night_rate) == 2)
        end_night_rate = [int(i) for i in end_night_rate]
        assert(end_night_rate[0] >= 0 and end_night_rate[0] <= 23)
        assert(end_night_rate[1] >= 0 and end_night_rate[1] <= 59)
        end_night_rate = 3600 * end_night_rate[0] + 60*end_night_rate[1]
    except (AssertionError, ValueError):
        error = {"title": "Format invalide",
                 "content": ("La date de fin d'heures " +
                             "creuses doit être au format hh:mm.")}
        settings_json = settings(db)
        settings_json.update({"err": error})
        return settings_json

    session = session_manager.get_session()
    (db.query(database.User)
     .filter_by(login=session["login"])
     .update({"start_night_rate": start_night_rate,
              "end_night_rate": end_night_rate}))

    redirect("/settings")


@app.route("/update", name="update")
def update():
    """Handles updating"""
    subprocess.Popen([os.path.dirname(os.path.realpath(__file__)) +
                      "/updater.sh"])  # TODO
    redirect("/settings")


@app.route("/community",
           name="community",
           template="community")
def store():
    """Community view"""
    return {}


@app.route("/help",
           name="help",
           template="help")
def help():
    """Help view"""
    return {}


@app.route("/login",
           name="login",
           template="login")
def login(db):
    """Login view"""
    try:
        if not db.query(database.User).all():
            redirect("/install")
    except ProgrammingError:
        redirect("/install")
    session = session_manager.get_session()
    if session['valid'] is True:
        redirect('/')
    else:
        return {"login": ''}


@app.route("/login",
           name="login",
           template="login",
           method="post")
def login_post(db):
    """Login view with POST data"""
    login = request.forms.get("login")
    user = db.query(database.User).filter_by(login=login).first()
    session = session_manager.get_session()
    session['valid'] = False
    session_manager.save(session)

    password = (config.get("salt") +
                hashlib.sha256(request.forms.get("password")
                               .encode('utf-8')).hexdigest())
    if user and user.password == password:
        session['valid'] = True
        session['login'] = login
        session['is_admin'] = user.is_admin
        session_manager.save(session)
        redirect('/')
    else:
        return {
            "login": login,
            "err": {
                "title": "Identifiants incorrects.",
                "content": ("Aucun utilisateur n'est enregistré à ce nom."
                            if user else "Mot de passe erroné.")
            }
        }


@app.route("/logout",
           name="logout")
def logout():
    """Logout"""
    session = session_manager.get_session()
    session['valid'] = False
    del(session['login'])
    del(session['is_admin'])
    session_manager.save(session)
    redirect('/')


@app.route("/install",
           name="install",
           template="install")
def install(db):
    """Install view (first run)"""
    if db.query(database.User).all():
        redirect('/')

    db.query(database.MeasureType).delete()
    db.query(database.Provider).delete()
    db.query(database.Sensor).delete()

    electricity_type = database.MeasureType(name="Électricité")
    db.add(electricity_type)
    db.flush()

    providers = update_providers(True, db)

    sensor = database.Sensor(name="CitizenWatt",
                             type_id=electricity_type.id,
                             last_timer=0)
    db.add(sensor)

    return {"login": '',
            "providers": providers,
            "start_night_rate": '',
            "end_night_rate": '',
            "base_address": '',
            "aes_key": ''}


@app.route("/install",
           name="install",
           template="install",
           method="post")
def install_post(db):
    """Install view with POST data"""
    error = None
    try:
        if db.query(database.User).all():
            redirect('/')
    except OperationalError:
        error = {"title": "Connexion à la base de données impossible",
                 "content": ("Impossible d'établir une connexion avec la " +
                             "base de données.")}
        install_json = install(db)
        install_json.update({"err": error})
        return install_json

    login = request.forms.get("login").strip()
    password = request.forms.get("password")
    password_confirm = request.forms.get("password_confirm")
    provider = request.forms.get("provider")
    raw_start_night_rate = request.forms.get("start_night_rate")
    raw_end_night_rate = request.forms.get("end_night_rate")
    raw_base_address = request.forms.get("base_address")
    raw_aes_key = request.forms.get("aes_key")

    ret = {"login": login,
           "providers": update_providers(False, db),
           "start_night_rate": raw_start_night_rate,
           "end_night_rate": raw_end_night_rate,
           "base_address": raw_base_address,
           "aes_key": raw_aes_key}

    try:
        base_address_int = int(raw_base_address.strip("L"), 16)
        base_address = str(hex(base_address_int)).upper() + "LL"
    except ValueError:
        error = {"title": "Format invalide",
                 "content": ("L'adresse de la base entrée est invalide.")}
        ret.update({"err": error})
        return ret
    tools.update_base_address(base_address_int)
    try:
        aes_key = [int(i.strip()) for i in raw_aes_key.split("-")]
        if len(aes_key) != 16:
            raise ValueError
    except ValueError:
        error = {"title": "Format invalide",
                 "content": ("La clé AES doit être constituée de 16 " +
                             "chiffres entre 0 et 255, séparés " +
                             "par des tirets.")}
        ret.update({"err": error})
        return ret
    (db.query(database.Sensor)
     .filter_by(name="CitizenWatt")
     .update({"base_address": base_address, "aes_key": json.dumps(aes_key)}))
    db.commit()

    try:
        start_night_rate = raw_start_night_rate.split(":")
        assert(len(start_night_rate) == 2)
        start_night_rate = [int(i) for i in start_night_rate]
        assert(start_night_rate[0] >= 0 and start_night_rate[0] <= 23)
        assert(start_night_rate[1] >= 0 and start_night_rate[1] <= 59)
        start_night_rate = 3600 * start_night_rate[0] + 60*start_night_rate[1]
    except (AssertionError, ValueError):
        error = {"title": "Format invalide",
                 "content": ("La date de début d'heures creuses " +
                             "doit être au format hh:mm.")}
        ret.update({"err": error})
        return ret

    try:
        end_night_rate = raw_end_night_rate.split(":")
        assert(len(end_night_rate) == 2)
        end_night_rate = [int(i) for i in end_night_rate]
        assert(end_night_rate[0] >= 0 and end_night_rate[0] <= 23)
        assert(end_night_rate[1] >= 0 and end_night_rate[1] <= 59)
        end_night_rate = 3600 * end_night_rate[0] + 60*end_night_rate[1]
    except (AssertionError, ValueError):
        error = {"title": "Format invalide",
                 "content": ("La date de fin d'heures creuses " +
                             "doit être au format hh:mm.")}
        ret.update({"err": error})
        return ret

    if login and password and password == password_confirm:
        password = (config.get("salt") +
                    hashlib.sha256(password.encode('utf-8')).hexdigest())
        admin = database.User(login=login,
                              password=password,
                              is_admin=1,
                              start_night_rate=start_night_rate,
                              end_night_rate=end_night_rate)
        db.add(admin)

        provider = (db.query(database.Provider)
                    .filter_by(name=provider)
                    .update({"current": 1}))

        session = session_manager.get_session()
        session['valid'] = True
        session['login'] = login
        session['is_admin'] = 1
        session_manager.save(session)

        redirect('/')
    else:
        ret.update({"err": {"title": "Champs obligatoires manquants",
                            "content": ("Vous devez renseigner tous " +
                                        "les champs obligatoires.")}})
        return ret


if __name__ == '__main__':
    # ===
    # App
    # ===
    SimpleTemplate.defaults["get_url"] = app.get_url
    SimpleTemplate.defaults["API_URL"] = app.get_url("index")
    SimpleTemplate.defaults["valid_session"] = lambda: session_manager.get_session()['valid']
    run(app, host="0.0.0.0", port=config.get("port"), debug=config.get("debug"),
        reloader=config.get("autoreload"), server="cherrypy")
