import glob
import json

import re
import sshpubkeys

from flask import Flask, redirect, url_for, render_template, request

# lyadmin
# scripts and web form for a tilde / PAUS instance
#
# gashapwn
# Nov 2020
#
# https://git.lain.church/gashapwn/lyadmin
# gashapwn@protonmail.com
# or
# gasahwpn on irc.lainchan.org

app=Flask(__name__)

# Paths for conf file,
#           user list,
#           directory containing
#           account request files...
ACCOUNT_PATH = "./req/";
CONF_FN = "lyadmin.conf.json"
CONF_PATH = "./" + str(CONF_FN)

# validation stuff
MAX_PUB_KEY_LEN = 5000
EMAIL_REGEX = "^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,10}$"
KEY_REGEX = "^[ -~]+$"

# Account requests are given ID numbers
# the first request will have the below
# id number
INIT_REQ_ID = "00000"

# The main home page
@app.route("/")
def home():
    app.route('/')

    # Load the list of tilde users
    # to generate links for
    u_list = [];
    with open("user_list.txt") as u_file:
        for line in u_file.readlines():
            u_list.append(line.strip());
    
    return render_template("index.html", u_list=u_list, page_name="home")

# Generates the page with rule. No logic needed.
def rules():
    return render_template("rules.html")

# Generate HTML for a form widget
def widg_fun(widg):
    if(widg.w_type == "input"):
        return "input id=id_%s name=%s type=text></input"%(
            widg.w_name, widg.w_name
        )
    elif(widg.w_type == "textarea"):
        return "textarea cols=40 id=id_%s name=%s rows=10 required=\"\""%(
            widg.w_name, widg.w_name
        )
    elif(widg.w_type == "check"):
        return "input id=id_%s name=%s type=checkbox required=\"\""%(
            widg.w_name, widg.w_name)
    return widg.w_type;

# Generate HTML for request form
# probably a strange way to do this...
def req():
    app.route('/req')
    class Widg:
        def __init__(self, w_name, w_type, w_opt):
            self.w_name = w_name
            self.w_type = w_type
            self.w_opt = w_opt # only for choice type widg

    # Configuration for our request form
    rt = {
        "username": Widg(
            "username",
            "input",
            None
        ),
        "email for account lockout / registration confirmation (optional)": Widg(
            "email",
            "input",
            None
        ),
        "SSH public key": Widg(
            "pub_key",
            "textarea",
            None
        ),
        "shell of choice": Widg(
            "shell",
            "choice",
            conf_obj["shell_tup_list"]
        ),
        "have you read the rules?": Widg(
            "rule_read", "check", None
        )
        };
    return render_template(
        "req.html",
        req_tab = rt,
        widg_fun = widg_fun,
        page_name="req"
    )

def handle_invalid_data(req):
    # print(str(e))
    return render_template("signup.html", is_email_user = False)

# Process input from user creation POST request
def signup():
    app.route('/req/signup')

    # Get all the params from the POST
    # request
    username = request.form["username"].strip()
    email = request.form["email"].strip()
    pub_key = request.form["pub_key"].strip()
    shell = request.form["shell"].strip()
    rule_read = request.form["rule_read"].strip()
    xff_header = request.headers["X-Forwarded-For"]

    is_email_user = False;

    # If a user didnt read the rules
    # send them back
    if(rule_read != "on"):
        return redirect(url_for('req'))

    # Set placeholder if user didnt send an email
    if(len(email) > 1):
        is_email_user = True
    else:
        email = "NO_EMAIL"

    # Validate shell
    if(not shell in conf_obj["shell"]):
        print("failed shell validation")
        return handle_invalid_data(req)

    # Validate email
    if( is_email_user and not re.search(EMAIL_REGEX, email)):
        print("failed email validation")
        return handle_invalid_data(req)
        
    # Validate the SSH pub key
    # Most software only handles up to 4096 bit keys
    if(len(pub_key) > MAX_PUB_KEY_LEN):
        print("key failed len check")
        return handle_invalid_data(req)

    # Only printable ascii characters in
    # a valid key
    # if(not re.search("^[ -~]+$", pub_key)):
    if(not re.search(KEY_REGEX, pub_key)):
        print("key failed regex")
        return handle_invalid_data(req)

    # Check the key against a library
    key = sshpubkeys.SSHKey(
        pub_key,
        strict_mode=False,
        skip_option_parsing=True
    )
    try:
        key.parse()
    except Exception as e:
        print("key failed lib validation")
        return handle_invalid_data(request)

    if(len(xff_header) < 1):
        xff_header = "NO_XFF"
    
    # All users requests have a sequential ID
    # The below picks the next ID based on
    # how many requests we already have saved
    # to disk
    if(len(glob.glob(ACCOUNT_PATH + str("[0-9]*ident*"))) == 0):
        new_id = int(INIT_REQ_ID)
        new_id_str = INIT_REQ_ID
    else:
        max_id = max(
            list(map(
                lambda path : path.split("/")[-1].split(".")[0],
                glob.glob(str(ACCOUNT_PATH) + "[0-9]*ident*")))
        )
        zpad = len(max_id)
        new_id = int(max_id)+1
        new_id_str = str(new_id).zfill(zpad)

    # write the request to disk
    # fn1 = str(FULL_PATH) + str(new_id_str) + ".ident"
    fn1 = str(ACCOUNT_PATH) + str(new_id_str) + ".ident"
    with open(fn1, "w") as ident_file:
        ident_file.write(str(username) + "\n")
        ident_file.write(str(email) + "\n")
        ident_file.write(str(shell) + "\n")
        ident_file.write(str(pub_key) + "\n")
        ident_file.write(str(xff_header) + "\n")
        
    return render_template("signup.html", is_email_user = is_email_user)

@app.context_processor
def get_site_name():
      return {"site_name": conf_obj["site_name"]}

@app.context_processor
def get_admin_email():
      return {"admin_email": conf_obj["admin_email"]}

def check_pwd_for_conf():
    pwd_file_list = list(map(
        lambda path : path.split("/")[-1],
        glob.glob("*")
    ))
    if(not CONF_FN in pwd_file_list):
        print("could not find " + str(CONF_PATH))
        print("please run in the installation directory")
        return False
    return True

# MAIN STARTS HERE
if(__name__=="__main__" and check_pwd_for_conf()):
    # Slurp the conf file
    with open(CONF_PATH) as c: conf_json_str = c.read()
    conf_obj = json.loads(conf_json_str)

    # A global list of all the shell enums
    conf_obj["shell_tup_list"] = list(map(
        lambda k : (
            k, conf_obj["shell"][k]
        ),
        list(conf_obj["shell"].keys())
    ))

    # Setup URL rules
    app.add_url_rule('/rules', 'rules', rules)
    app.add_url_rule('/req', 'req', req, methods = ['POST', 'GET'])
    app.add_url_rule('/req/signup', 'signup', signup, methods = ['POST'])

    # Run that app!
    app.run(host=conf_obj["listen_ip"],debug=True)