#!/usr/bin/env python3

# TODO(longer term): turn this into a more general project that goes beyond SIPB, then beyond MIT

# This could also be done by separating the frontend and backend and having JavaScript,
# but this makes JavaScript optional.

# Generating the HTML from within Hugo would be ideal. Maybe parsing the HTML is kind of a hack.

# NOTE: The cgi module is deprecated but we are using scripts, so this does not matter
# There does not seem to be a clear consensus for how to read POST form data without it:
# https://discuss.python.org/t/alternative-function-for-deprecated-cgi/21960

WEB_ROOT = "/mit/sipb-www/web_scripts"
REPO_ROOT = "/mit/sipb-www/checkout"

import os
import cgi
from subprocess import check_output, STDOUT, CalledProcessError
from urllib.parse import parse_qs
from bs4 import BeautifulSoup

# this error message global will determine whether we show an error message instead
# of the expected behavior (seeing an edit form or redirect back to the page we edited)
error_message = None

def init():
    """
    Does some initialization we need no matter whether
    the method is GET or POST.
    """
    # oops, too many globals?
    global error_message, args, request_method, email, kerb, name, redirect_url, path, full_path, current_content

    # make sure we are on web_scripts
    os.chdir(WEB_ROOT)

    # force port 444 for cert authentication
    if int(os.environ['SERVER_PORT']) != 444:
        print(f"Location: https://{os.environ['SERVER_NAME']}:444{os.environ['REQUEST_URI']}\n")
        exit()

    # get GET parameters
    args = parse_qs(os.environ['QUERY_STRING'])

    # force GET or POST method
    request_method = os.environ['REQUEST_METHOD']
    if request_method not in ('GET', 'POST'):
        print('Status: 405 Method Not Allowed')
        print('Content-Type: text/plain\n')
        print(f'Request method {request_method} not allowed!')
        exit()

    # extract info from cert
    email = os.environ.get('SSL_CLIENT_S_DN_Email')
    if email:
        kerb = email.split('@')[0]
        email = email.lower()
        name = os.environ.get('SSL_CLIENT_S_DN_CN')
    
    # show error if not authenticated
    if not email:
        error_message = 'Error: no cert given'
        return
    if not is_sipb_member(kerb):
        error_message = 'Error: you are not a SIPB member'
        return
    
    # show error if a GET argument is missing...
    if 'path' not in args:
        error_message = "Error: Missing parameter path"
        return
    # ...but don't be as harsh about the absence of a redirect URL
    if 'returnto' not in args:
        redirect_url = f"https://{os.environ['SERVER_NAME']}"
    else:
        redirect_url = args['returnto'][0]
    
    path = args['path'][0]
    full_path = os.path.join(REPO_ROOT, 'content', path)

    # show path related errors
    if '..' in path:
        error_message = 'Error: Found ".." in path'
        return
    if not os.path.exists(full_path):
        error_message = f"Error: {path} does not exist"
        return
    if not os.path.isfile(full_path):
        error_message = f"Error: {path} is not a file"
        return
    
    with open(full_path, 'r') as f:
        current_content = f.read()


# TODO: don't hardcode sipb - this is useful for anyone wanting to have hugo pages on scripts
def is_sipb_member(kerb):
    sipb_members = {kerb.strip() for kerb in check_output([
            "pts","membership",
            "-cell", "athena.mit.edu",
            "-nameorid", "system:gsipb",
        ]).decode().split('\n')[1:]}
    return kerb in sipb_members or f'{kerb}.root' in sipb_members

def make_form(soup: BeautifulSoup, contents: str, name: str) -> BeautifulSoup:
    """
    Makes an HTML form, to edit the given file, as a BeautifulSoup
    """

    # add markdown library
    soup.head.append(soup.new_tag("link", rel="stylesheet", href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css"))
    soup.head.append(soup.new_tag("script", src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"))

    # add editor / form
    form = BeautifulSoup("""
        <form method="POST">
            <textarea id="editor" name="editor"/><br>
            <label for="gitname">Name:</label>
            <input type="text" id="gitname" name="gitname" required><br>
            <label for="commitmsg">What did you change (commit message)?</label>
            <input type="text" id="commitmsg" name="commitmsg" required>
            <input type="submit" value="Save changes">
            <script>var simplemde = new SimpleMDE({spellChecker: false});</script>
        </form>""", 'html.parser')
    form.textarea.string = contents
    form.find("input")['value'] = name

    return form    


def commit_edit():
    """
    Commit the page edit that the user requested
    """
    assert request_method == 'POST'
    
    # TODO: switch to the non-deprecated way of doing things
    # (see pythoncgi.net; there is a polyfill once scripts **eventually** upgrades)
    post = cgi.FieldStorage()
    # TODO: check that the post fields actually do exist
    name = post['gitname'].value # use preferred git name
    commitmsg = post['commitmsg'].value
    new_content = post['editor'].value

    # ensure no windows new lines are added (they are for some reason)
    new_content.replace('\r\n', '\n')

    # (TODO) oops idk what to do with it rn - can delete if exception handling is "enough"
    # if current_content == new_content:
    #     title.string = "Error: You did not change anything."
    #     return soup
    
    # update the file
    with open(full_path, 'w') as f:
        f.write(new_content)

    # go to git repository
    os.chdir(REPO_ROOT)

    # add file to commit
    check_output(['git', 'add', full_path], stderr=STDOUT)
    
    # actual commit
    check_output([
        'git',
        '-c', f"user.name='{name}'",
        '-c', f'user.email={email}',
        'commit',
        '-m', commitmsg
    ], stderr=STDOUT)
    
    # push the changes  
    check_output(['git', 'push'], stderr=STDOUT)


def generate_html() -> BeautifulSoup:
    """
    Get the root HTML to render, as a BeautifulSoup
    """
    # open home page and clear content
    with open('hugo/index.html', 'rb') as f:
        soup = BeautifulSoup(f, 'html.parser')
    content = soup.main
    content.string = ''

    # unselect home
    for nav in soup.find_all("nav"):
        del nav.find("li").a["aria-current"]

    # delete edit and view history links
    soup.footer.find("p").decompose()

	# add title
    title = soup.new_tag("h1")
    content.append(title)

    if error_message:
        title.string = error_message
        return soup

    title.string = f"Editing {path}"

    form = make_form(soup, current_content, name)
    content.append(form)

    return soup


try:
    init()
    if request_method == 'GET':        
        soup = generate_html()
        print('Content-Type: text/html; charset=utf-8\n')
        print(soup)
    elif request_method == 'POST':
        commit_edit()
        print('Status: 303 See Other')
        print(f'Location: {redirect_url}\n')
except CalledProcessError as e:
    print('Content-Type: text/plain\n')
    print(e)
    print(e.output.decode())
except Exception as e:
    print(e)

# vim: tabstop=4 softtabstop=4 shiftwidth=4 expandtab
