#!/usr/bin/python3
import os
import sqlite3
import time
import secrets
import configparser
import asyncio
from hypercorn.config import Config
from hypercorn.asyncio import serve
from werkzeug.security import generate_password_hash, check_password_hash
from quart import Quart, render_template, request, url_for, flash, redirect, session, make_response, send_from_directory, stream_with_context, Response, request

# Parse configuration file, and check if anything is wrong with it
config = configparser.ConfigParser()
config.read("config.ini")

HOST = config["config"]["HOST"]
PORT = config["config"]["PORT"]
SECRET_KEY = config["config"]["SECRET_KEY"]
MAX_STORAGE = config["config"]["MAX_STORAGE"]

if SECRET_KEY == "placeholder":
    print("[WARNING] Secret key not set")

# Define Quart
app = Quart(__name__)
app.config["SECRET_KEY"] = SECRET_KEY

# Database functions
def get_db_connection():
    conn = sqlite3.connect("database.db")
    conn.row_factory = sqlite3.Row
    return conn

def get_user(id):
    conn = get_db_connection()
    post = conn.execute("SELECT * FROM users WHERE id = ?",
                        (id,)).fetchone()
    conn.close()
    if post is None:
        return None
    return post

def get_note(id):
    conn = get_db_connection()
    post = conn.execute("SELECT * FROM notes WHERE id = ?",
                        (id,)).fetchone()
    conn.close()
    if post is None:
        return None
    return post

def get_space(id):
    conn = get_db_connection()
    notes = conn.execute("SELECT content, title FROM notes WHERE creator = ? ORDER BY id DESC;", (id,)).fetchall()
    conn.close()
    spacetaken = 0
    for x in notes:
        spacetaken = spacetaken + len(x["content"].encode("utf-8"))
        spacetaken = spacetaken + len(x["title"].encode("utf-8"))
    return spacetaken

def get_note_count(id):
    conn = get_db_connection()
    notes = conn.execute("SELECT content, title FROM notes WHERE creator = ? ORDER BY id DESC;", (id,)).fetchall()
    conn.close()
    notecount = 0
    for x in notes:
        notecount = notecount + 1
    return notecount

def get_session(id):
    conn = get_db_connection()
    post = conn.execute("SELECT * FROM sessions WHERE session = ?",
                        (id,)).fetchone()
    conn.close()
    if post is None:
        return None
    return post

def get_session_from_sessionid(id):
    conn = get_db_connection()
    post = conn.execute("SELECT * FROM sessions WHERE sessionid = ?",
                        (id,)).fetchone()
    conn.close()
    if post is None:
        return None
    return post

def check_username_taken(username):
    conn = get_db_connection()
    post = conn.execute("SELECT * FROM users WHERE lower(username) = ?",
                        (username.lower(),)).fetchone()
    conn.close()
    if post is None:
        return None
    return post["id"]

# Disable CORS
@app.after_request
async def add_cors_headers(response):
    response.headers.add("Access-Control-Allow-Origin", "*")
    response.headers.add("Access-Control-Allow-Headers", "*")
    response.headers.add("Access-Control-Allow-Methods", "*")
    return response

# Main page
@app.route("/index.html")
async def mainrdir():
    return redirect("/", code=302)
@app.route("/")
async def main():
    return await render_template("main.html")

# Web app
@app.route("/app/index.html")
async def apprdir():
    return redirect("/app", code=302)
@app.route("/app")
async def webapp():
    return await render_template("app.html")

# Login and signup
@app.route("/signup/index.html")
async def signuprdir():
    return redirect("/signup", code=302)
@app.route("/signup")
async def signup():
    return await render_template("signup.html")

@app.route("/login/index.html")
async def loginrdir():
    return redirect("/login", code=302)
@app.route("/login")
async def login():
    return await render_template("login.html")

# Privacy policy
@app.route("/privacy/index.html")
async def privacyrdir():
    return redirect("/privacy", code=302)
@app.route("/privacy")
async def privacy():
    return await render_template("privacy.html")

# API
@app.route("/api/version", methods=("GET", "POST"))
async def apiversion():
    return "Burgernotes Version 1.1"

@app.route("/api/signup", methods=("GET", "POST"))
async def apisignup():
    if request.method == "POST":
        data = await request.get_json()
        username = data["username"]
        password = data["password"]

        if username == "":
            return {}, 422

        if len(username) > 20:
            return {}, 422

        if not username.isalnum():
            return {}, 422

        if password == "":
            return {}, 422

        if len(password) < 14:
            return {}, 422

        if not check_username_taken(username) == None:
            return {}, 409

        hashedpassword = generate_password_hash(password)

        conn = get_db_connection()
        conn.execute("INSERT INTO users (username, password, created) VALUES (?, ?, ?)",
                     (username, hashedpassword, str(time.time())))
        conn.commit()
        conn.close()

        userID = check_username_taken(username)
        user = get_user(userID)

        randomCharacters = secrets.token_hex(512)

        conn = get_db_connection()
        conn.execute("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)",
                     (randomCharacters, userID, request.headers.get("user-agent")))
        conn.commit()
        conn.close()

        return {
            "key": randomCharacters
        }, 200

@app.route("/api/login", methods=("GET", "POST"))
async def apilogin():
    if request.method == "POST":
        data = await request.get_json()
        username = data["username"]
        password = data["password"]
        passwordchange = data["passwordchange"]
        newpass = data["newpass"]

        check_username_thing = check_username_taken(username)

        if check_username_thing == None:
            return {}, 401

        userID = check_username_taken(username)
        user = get_user(userID)

        if not check_password_hash(user["password"], (password)):
            return {}, 401

        randomCharacters = secrets.token_hex(512)

        conn = get_db_connection()
        conn.execute("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)",
                     (randomCharacters, userID, request.headers.get("user-agent")))
        conn.commit()
        conn.close()

        if passwordchange == "yes":
            hashedpassword = generate_password_hash(newpass)
            conn = get_db_connection()
            conn.execute("UPDATE users SET password = ? WHERE username = ?", (hashedpassword, username))
            conn.commit()
            conn.close()

        return {
            "key": randomCharacters,
        }, 200


@app.route("/api/userinfo", methods=("GET", "POST"))
async def apiuserinfo():
    if request.method == "POST":
        data = await request.get_json()
        secretKey = data["secretKey"]

        userCookie = get_session(secretKey)
        user = get_user(userCookie["id"])
        datatemplate = {
            "username": user["username"],
            "id": user["id"],
            "created": user["created"],
            "storageused": get_space(user["id"]),
            "storagemax": MAX_STORAGE,
            "notecount": get_note_count(user["id"])
        }
        return datatemplate

@app.route("/api/listnotes", methods=("GET", "POST"))
async def apilistnotes():
    if request.method == "POST":
        data = await request.get_json()
        secretKey = data["secretKey"]

        userCookie = get_session(secretKey)
        user = get_user(userCookie["id"])

        conn = get_db_connection()
        notes = conn.execute("SELECT * FROM notes WHERE creator = ? ORDER BY edited DESC;", (user["id"],)).fetchall()
        conn.close()

        datatemplate = []

        for note in notes:
            notetemplate = {
                "id": note["id"],
                "title": note["title"]
            }
            datatemplate.append(notetemplate)

        return datatemplate, 200

@app.route("/api/exportnotes", methods=("GET", "POST"))
async def apiexportnotes():
    if request.method == "POST":
        data = await request.get_json()
        secretKey = data["secretKey"]

        userCookie = get_session(secretKey)
        user = get_user(userCookie["id"])

        conn = get_db_connection()
        notes = conn.execute("SELECT * FROM notes WHERE creator = ? ORDER BY id DESC;", (user["id"],)).fetchall()
        conn.close()

        datatemplate = []

        for note in notes:
            notetemplate = {
                "id": note["id"],
                "created": note["created"],
                "edited": note["edited"],
                "title": note["title"],
                "content": note["content"]
            }
            datatemplate.append(notetemplate)

        return datatemplate, 200
    
@app.route("/api/newnote", methods=("GET", "POST"))
async def apinewnote():
    if request.method == "POST":
        data = await request.get_json()
        secretKey = data["secretKey"]
        noteName = data["noteName"]

        userCookie = get_session(secretKey)
        user = get_user(userCookie["id"])
        
        conn = get_db_connection()
        conn.execute("INSERT INTO notes (title, content, creator, created, edited) VALUES (?, ?, ?, ?, ?)",
                         (noteName, "", user["id"], str(time.time()), str(time.time())))
        conn.commit()
        conn.close()

        return {}, 200

@app.route("/api/readnote", methods=("GET", "POST"))
async def apireadnote():
    if request.method == "POST":
        data = await request.get_json()
        secretKey = data["secretKey"]
        noteId = data["noteId"]

        userCookie = get_session(secretKey)
        user = get_user(userCookie["id"])

        note = get_note(noteId)

        if (note != None):
            if (user["id"] == note["creator"]):
                contenttemplate = {
                    "content": note["content"]
                }

                return contenttemplate, 200
            else:
                return {}, 422
        else:
            return {}, 422

@app.route("/api/editnote", methods=("GET", "POST"))
async def apieditnote():
    if request.method == "POST":
        data = await request.get_json()
        secretKey = data["secretKey"]
        noteId = data["noteId"]
        content = data["content"]
        title = data["title"]

        userCookie = get_session(secretKey)
        user = get_user(userCookie["id"])

        note = get_note(noteId)
        
        if get_space(user["id"]) + len(content.encode("utf-8")) > int(MAX_STORAGE):
            return {}, 418

        if (note != None):
            if (user["id"] == note["creator"]):
                conn = get_db_connection()
                conn.execute("UPDATE notes SET content = ?, title = ?, edited = ? WHERE id = ?", (content, title, str(time.time()), noteId))
                conn.commit()
                conn.close()

                return {}, 200
            else:
                return {}, 403
        else:
            return {}, 422
        

@app.route("/api/editnotetitle", methods=("GET", "POST"))
async def apieditnotetitle():
    if request.method == "POST":
        data = await request.get_json()
        secretKey = data["secretKey"]
        noteId = data["noteId"]
        content = data["content"]

        userCookie = get_session(secretKey)
        user = get_user(userCookie["id"])

        note = get_note(noteId)
        
        if get_space(user["id"]) + len(content.encode("utf-8")) > int(MAX_STORAGE):
            return {}, 418

        if (note != None):
            if (user["id"] == note["creator"]):
                conn = get_db_connection()
                conn.execute("UPDATE notes SET title = ?, edited = ? WHERE id = ?", (content, str(time.time()), noteId))
                conn.commit()
                conn.close()

                return {}, 200
            else:
                return {}, 403
        else:
            return {}, 422


@app.route("/api/removenote", methods=("GET", "POST"))
async def apiremovenote():
    if request.method == "POST":
        data = await request.get_json()
        secretKey = data["secretKey"]
        noteId = data["noteId"]

        userCookie = get_session(secretKey)
        user = get_user(userCookie["id"])

        note = get_note(noteId)

        if (note != None):
            if (user["id"] == note["creator"]):
                conn = get_db_connection()
                conn.execute("DELETE FROM notes WHERE id = ?", (noteId,))
                conn.commit()
                conn.close()

                return {}, 200
            else:
                return {}, 403
        else:
            return {}, 422


@app.route("/api/deleteaccount", methods=("GET", "POST"))
async def apideleteaccount():
    if request.method == "POST":
        data = await request.get_json()
        secretKey = data["secretKey"]

        userCookie = get_session(secretKey)
        user = get_user(userCookie["id"])

        conn = get_db_connection()
        conn.execute("DELETE FROM notes WHERE creator = ?", (userCookie["id"],))
        conn.commit()
        conn.close()

        conn = get_db_connection()
        conn.execute("DELETE FROM users WHERE id = ?", (userCookie["id"],))
        conn.commit()
        conn.close()

        return {}, 200

@app.route("/api/sessions/list", methods=("GET", "POST"))
async def apisessionslist():
    if request.method == "POST":
        data = await request.get_json()
        secretKey = data["secretKey"]

        userCookie = get_session(secretKey)
        user = get_user(userCookie["id"])

        conn = get_db_connection()
        sessions = conn.execute("SELECT * FROM sessions WHERE id = ? ORDER BY id DESC;", (user["id"],)).fetchall()
        conn.close()

        datatemplate = []

        for x in sessions:
            device = x["device"]
            thisSession = False
            if (x["session"] == secretKey):
                thisSession = True
            sessiontemplate = {
                "id": x["sessionid"],
                "thisSession": thisSession,
                "device": device
            }
            datatemplate.append(sessiontemplate)

        return datatemplate, 200

@app.route("/api/sessions/remove", methods=("GET", "POST"))
async def apisessionsremove():
    if request.method == "POST":
        data = await request.get_json()
        secretKey = data["secretKey"]
        sessionId = data["sessionId"]

        userCookie = get_session(secretKey)
        user = get_user(userCookie["id"])

        session = get_session_from_sessionid(sessionId)

        if (session != None):
            if (user["id"] == session["id"]):
                conn = get_db_connection()
                conn.execute("DELETE FROM sessions WHERE sessionid = ?", (session["sessionid"],))
                conn.commit()
                conn.close()

                return {}, 200
            else:
                return {}, 403
        else:
            return {}, 422


@app.route("/listusers/<secretkey>", methods=("GET", "POST"))
def listusers(secretkey):
    if secretkey == SECRET_KEY:
        conn = get_db_connection()
        users = conn.execute("SELECT * FROM users").fetchall()
        conn.close()
        thing = ""
        for x in users:
            thing = str(x["id"]) + " - " + x["username"] + " - " + str(get_space(x["id"])) + "<br>" + thing

        return thing
    else:
        return redirect("/")

@app.route("/logout/index.html")
async def logoutrdir():
    return redirect("/logout", code=302)
@app.route("/logout")
async def apilogout():
    return await render_template("logout.html")

@app.errorhandler(500)
async def burger(e):
    return {}, 500

@app.errorhandler(404)
async def burger(e):
    return await render_template("error.html", errorCode=404, errorMessage="Page not found"), 404

# Start server
hypercornconfig = Config()
hypercornconfig.bind = (HOST + ":" + PORT)

if __name__ == "__main__":
    print("[INFO] Server started")
    asyncio.run(serve(app, hypercornconfig))
    print("[INFO] Server stopped")
