__version__ = "0.4.0"

import os
import shutil
import random
import subprocess
import platform

import PIL
import git
import mimetypes
import magic
import flask
import cairosvg
import celery
import shlex
from functools import wraps
from datetime import datetime
from enum import Enum
from cairosvg import svg2png
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from markupsafe import escape, Markup
from flask_migrate import Migrate
from PIL import Image
from flask_httpauth import HTTPBasicAuth
import config
import markdown
from common import git_command
from flask_babel import Babel, gettext, ngettext, force_locale
from jinja2_fragments.flask import render_block

import logging


class No304(logging.Filter):
    def filter(self, record):
        return not record.getMessage().strip().endswith("304 -")


logging.getLogger("werkzeug").addFilter(No304())

_ = gettext
n_ = ngettext

app = flask.Flask(__name__)
app.config.from_mapping(
        CELERY=dict(
                broker_url=config.REDIS_URI,
                result_backend=config.REDIS_URI,
                task_ignore_result=True,
        ),
)

auth = HTTPBasicAuth()

app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI
app.config["SECRET_KEY"] = config.DB_PASSWORD
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["BABEL_TRANSLATION_DIRECTORIES"] = "i18n"
app.config["MAX_CONTENT_LENGTH"] = config.MAX_PAYLOAD_SIZE
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
app.config["SESSION_COOKIE_SECURE"] = config.suggest_https       # only send cookies over HTTPS if the server is configured for it
app.config["SESSION_COOKIE_HTTPONLY"] = True                     # don't allow JS to access the cookie
if config.restrict_cookie_domain:
    app.config["SESSION_COOKIE_DOMAIN"] = config.BASE_DOMAIN         # don't share across subdomains, since user content is hosted there

db = SQLAlchemy(app)
bcrypt = Bcrypt(app)
migrate = Migrate(app, db)

from misc_utils import *

import git_http
import api
import jinja_utils
import celery_tasks
from celery import Celery, Task
import celery_integration
import pathlib

from models import *

babel = Babel(app)


def get_locale():
    if flask.request.cookies.get("language"):
        return flask.request.cookies.get("language")
    return flask.request.accept_languages.best_match(config.available_locales)


babel.init_app(app, locale_selector=get_locale)

with app.app_context():
    locale_names = {}
    for language in config.available_locales:
        with force_locale(language):
            # NOTE: Translate this to the language's name in that language, for example in French you would use français
            locale_names[language] = gettext("English")

worker = celery_integration.init_celery_app(app)

repositories = flask.Blueprint("repository", __name__, template_folder="templates/repository/")

app.jinja_env.add_extension("jinja2.ext.do")
app.jinja_env.add_extension("jinja2.ext.loopcontrols")
app.jinja_env.add_extension("jinja2.ext.debug")


@app.context_processor
def default():
    username = flask.session.get("username")

    user_object = User.query.filter_by(username=username).first()

    return {
        "logged_in_user": username,
        "user_object": user_object,
        "Notification": Notification,
        "unread": UserNotification.query.filter_by(user_username=username).filter(
                UserNotification.attention_level > 0).count(),
        "config": config,
        "Markup": Markup,
        "locale_names": locale_names,
        "set": set,          # since using {} is impossible in Jinja
        "request": flask.request,
    }


@app.route("/")
def main():
    if flask.session.get("username"):
        return flask.render_template("home.html")
    else:
        return flask.render_template("no-home.html")


@app.route("/userstyle")
def userstyle():
    if flask.session.get("username") and os.path.exists(
            os.path.join(config.REPOS_PATH, flask.session.get("username"), ".config",
                         "theme.css")):
        return flask.send_from_directory(
            os.path.join(config.REPOS_PATH, flask.session.get("username"), ".config"),
            "theme.css")
    else:
        return flask.Response("", mimetype="text/css")


@app.route("/about/")
def about():
    return flask.render_template("about.html", platform=platform, version=__version__)


@app.route("/search")
def search():
    query = flask.request.args.get("q")
    page_number = flask.request.args.get("page", 1, type=int)
    if flask.session.get("username"):
        default_page_length = db.session.get(User, flask.session.get("username")).default_page_length
    else:
        default_page_length = 16
    page_length = flask.request.args.get("per_page", default_page_length, type=int)
    if not query:
        flask.abort(400)

    results = Repo.query.filter(Repo.name.ilike(f"%{query}%")).filter_by(visibility=2).paginate(
            page=page_number, per_page=page_length)

    if results.has_next:
        next_page = results.next_num
    else:
        next_page = None

    if results.has_prev:
        prev_page = results.prev_num
    else:
        prev_page = None

    return flask.render_template("search.html", results=results, query=query,
                                 page_number=page_number,
                                 page_length=page_length,
                                 next_page=next_page,
                                 prev_page=prev_page,
                                 num_pages=results.pages)


@app.route("/language", methods=["POST"])
def set_locale():
    response = flask.redirect(flask.request.referrer if flask.request.referrer else "/",
                              code=303)
    if not flask.request.form.get("language"):
        response.delete_cookie("language")
    else:
        response.set_cookie("language", flask.request.form.get("language"))

    return response


@app.route("/cookie-dismiss")
def dismiss_banner():
    response = flask.redirect(flask.request.referrer if flask.request.referrer else "/",
                              code=303)
    response.set_cookie("cookie-banner", "1")
    return response


@app.route("/help/")
def help_redirect():
    return flask.redirect(config.help_url, code=302)


@app.route("/settings/")
def settings():
    if not flask.session.get("username"):
        flask.abort(401)
    user = User.query.filter_by(username=flask.session.get("username")).first()

    return flask.render_template("user-settings.html", user=user)


@app.route("/settings/confirm-email/<code>")
def confirm_email(code):
    request = EmailChangeRequest.query.filter_by(code=code).first()
    if not request:
        flask.abort(404)

    user = db.session.get(User, request.user_username)
    user.email = request.new_email
    db.session.delete(request)
    db.session.commit()

    return flask.redirect("/settings", code=303)


@app.route("/settings/profile", methods=["POST"])
def settings_profile():
    user = User.query.filter_by(username=flask.session.get("username")).first()

    user.display_name = flask.request.form["displayname"]
    user.URL = flask.request.form["url"]
    user.company = flask.request.form["company"]
    user.company_URL = flask.request.form["companyurl"]
    if not flask.request.form.get("email"):
        # Deleting the email can be instant; no need to confirm
        user.email = ""
    elif flask.request.form.get("email") != user.email:
        # Changing the email requires confirmation from the address holder
        celery_tasks.request_email_change.delay(user.username, flask.request.form["email"])
    user.location = flask.request.form["location"]
    user.show_mail = True if flask.request.form.get("showmail") else False
    user.bio = flask.request.form.get("bio")

    db.session.commit()

    flask.flash(
            Markup("<iconify-icon icon='mdi:check'></iconify-icon>" + _("Settings saved")),
            category="success")
    return flask.redirect(f"/{flask.session.get('username')}", code=303)


@app.route("/settings/preferences", methods=["POST"])
def settings_prefs():
    user = User.query.filter_by(username=flask.session.get("username")).first()

    user.default_page_length = int(flask.request.form["page_length"])
    user.max_post_nesting = int(flask.request.form["max_post_nesting"])

    db.session.commit()

    flask.flash(
            Markup("<iconify-icon icon='mdi:check'></iconify-icon>" + _("Settings saved")),
            category="success")
    return flask.redirect(f"/{flask.session.get('username')}", code=303)


@app.route("/favourites/", methods=["GET", "POST"])
def favourites():
    if not flask.session.get("username"):
        flask.abort(401)
    if flask.request.method == "GET":
        relationships = RepoFavourite.query.filter_by(
                user_username=flask.session.get("username"))

        return flask.render_template("favourites.html", favourites=relationships)


@app.route("/favourites/<int:id>", methods=["POST"])
def favourite_edit(id):
    if not flask.session.get("username"):
        flask.abort(401)
    favourite = db.session.get(RepoFavourite, id)
    if favourite.user_username != flask.session.get("username"):
        flask.abort(403)
    data = flask.request.form
    favourite.notify_commit = js_to_bool(data.get("commit"))
    favourite.notify_forum = js_to_bool(data.get("forum"))
    favourite.notify_pr = js_to_bool(data.get("pull_request"))
    favourite.notify_admin = js_to_bool(data.get("administrative"))
    db.session.commit()
    return flask.render_template_string(
            """
        <tr hx-post="/favourites/{{ favourite.id }}" hx-trigger="change" hx-include="#commit-{{ favourite.id }}, #forum-{{ favourite.id }}, #pull_request-{{ favourite.id }}, #administrative-{{ favourite.id }}" hx-headers='{"Content-Type": "application/json"}' hx-swap="outerHTML">
            <td><a href="{{ favourite.repo.route }}">{{ favourite.repo.owner.username }}/{{ favourite.repo.name }}</a></td>
            <td style="text-align: center;"><input type="checkbox" name="commit" id="commit-{{ favourite.id }}" value="true" {% if favourite.notify_commit %}checked{% endif %}></td>
            <td style="text-align: center;"><input type="checkbox" name="forum" id="forum-{{ favourite.id }}" value="true" {% if favourite.notify_forum %}checked{% endif %}></td>
            <td style="text-align: center;"><input type="checkbox" name="pull_request" id="pull_request-{{ favourite.id }}" value="true" {% if favourite.notify_pr %}checked{% endif %}></td>
            <td style="text-align: center;"><input type="checkbox" name="administrative" id="administrative-{{ favourite.id }}" value="true" {% if favourite.notify_admin %}checked{% endif %}></td>
        </tr>
        """,
            favourite=favourite
    )


@app.route("/notifications/", methods=["GET", "POST"])
def notifications():
    if not flask.session.get("username"):
        flask.abort(401)
    if flask.request.method == "GET":
        page_number = flask.request.args.get("page", 1, type=int)
        if flask.session.get("username"):
            default_page_length = db.session.get(User, flask.session.get(
                "username")).default_page_length
        else:
            default_page_length = 16
        page_length = flask.request.args.get("per_page", default_page_length, type=int)

        results = UserNotification.query.filter_by(
                user_username=flask.session.get("username")).order_by(UserNotification.id.desc()).paginate(
                page=page_number, per_page=page_length)

        if results.has_next:
            next_page = results.next_num
        else:
            next_page = None

        if results.has_prev:
            prev_page = results.prev_num
        else:
            prev_page = None

        return flask.render_template("notifications.html",
                                     notifications=results,
                                     db=db, Commit=Commit, Post=Post, PullRequest=PullRequest,
                                     page_number=page_number,
                                     page_length=page_length,
                                     next_page=next_page,
                                     prev_page=prev_page,
                                     num_pages=results.pages
        )


@app.route("/notifications/<int:notification_id>/read", methods=["POST"])
def mark_read(notification_id):
    if not flask.session.get("username"):
        flask.abort(401)
    notification = UserNotification.query.filter_by(id=notification_id).first()
    if notification.user_username != flask.session.get("username"):
        flask.abort(403)
    notification.mark_read()
    db.session.commit()
    return flask.render_template_string(
        "<button hx-post='/notifications/{{ notification.id }}/unread' hx-swap='outerHTML'>Mark as unread</button>",
        notification=notification), 200


@app.route("/notifications/<int:notification_id>/unread", methods=["POST"])
def mark_unread(notification_id):
    if not flask.session.get("username"):
        flask.abort(401)
    notification = UserNotification.query.filter_by(id=notification_id).first()
    if notification.user_username != flask.session.get("username"):
        flask.abort(403)
    notification.mark_unread()
    db.session.commit()
    return flask.render_template_string(
        "<button hx-post='/notifications/{{ notification.id }}/read' hx-swap='outerHTML'>Mark as read</button>",
        notification=notification), 200


@app.route("/notifications/mark-all-read", methods=["POST"])
def mark_all_read():
    if not flask.session.get("username"):
        flask.abort(401)

    notifications = UserNotification.query.filter_by(
            user_username=flask.session.get("username"))
    for notification in notifications:
        notification.mark_read()
    db.session.commit()
    return flask.redirect("/notifications/", code=303)


@app.route("/accounts/", methods=["GET", "POST"])
def login():
    if flask.request.method == "GET":
        return flask.render_template("login.html")
    else:
        if "login" in flask.request.form:
            username = flask.request.form["username"]
            password = flask.request.form["password"]

            user = User.query.filter_by(username=username).first()

            if user and bcrypt.check_password_hash(user.password_hashed, password):
                flask.session["username"] = user.username
                flask.flash(
                        Markup("<iconify-icon icon='mdi:account'></iconify-icon>" + _(
                                "Successfully logged in as {username}").format(
                            username=username)),
                        category="success")
                return flask.redirect("/", code=303)
            elif not user:
                flask.flash(Markup(
                        "<iconify-icon icon='mdi:account-question'></iconify-icon>" + _(
                                "User not found")),
                        category="alert")
                return flask.render_template("login.html")
            else:
                flask.flash(Markup(
                        "<iconify-icon icon='mdi:account-question'></iconify-icon>" + _(
                                "Invalid password")),
                        category="error")
                return flask.render_template("login.html")
        if "signup" in flask.request.form:
            username = flask.request.form["username"]
            password = flask.request.form["password"]
            password2 = flask.request.form["password2"]
            email = flask.request.form.get("email")
            email2 = flask.request.form.get("email2")  # repeat email is a honeypot
            name = flask.request.form.get("name")

            if not only_chars(username,
                              "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"):
                flask.flash(Markup(
                        _("Usernames may only contain Latin alphabet, numbers and '-'")),
                        category="error")
                return flask.render_template("login.html")
            if "--" in username:
                flask.flash(Markup(
                        _("Usernames may not contain consecutive hyphens")),
                        category="error")
                return flask.render_template("login.html")
            if username.startswith("-") or username.endswith("-"):
                flask.flash(Markup(
                        _("Usernames may not start or end with a hyphen")),
                        category="error")
                return flask.render_template("login.html")
            if username in config.RESERVED_NAMES:
                flask.flash(
                        Markup(
                                _("Sorry, {username} is a system path").format(
                                        username=username)),
                        category="error")
                return flask.render_template("login.html")

            if not username.islower():
                if not name:           # infer display name from the wanted username if not customised
                    display_name = username
                username = username.lower()
                flask.flash(Markup(
                        _("Usernames must be lowercase, so it's been converted automatically")),
                        category="info")

            user_check = User.query.filter_by(username=username).first()
            if user_check or email2:   # make the honeypot look like a normal error
                flask.flash(
                        Markup(
                                _(
                                        "The username {username} is taken").format(
                                        username=username)),
                        category="error")
                return flask.render_template("login.html")

            if password2 != password:
                flask.flash(Markup(_(
                        "Make sure the passwords match")),
                            category="error")
                return flask.render_template("login.html")

            user = User(username, password, email, name)
            db.session.add(user)
            db.session.commit()
            flask.session["username"] = user.username
            flask.flash(Markup(
                            _(
                                "Successfully created and logged in as {username}").format(
                                username=username)),
                        category="success")

            notification = Notification({"type": "welcome"})
            db.session.add(notification)
            db.session.commit()

            return flask.redirect("/", code=303)


@app.route("/newrepo/", methods=["GET", "POST"])
def new_repo():
    if not flask.session.get("username"):
        flask.abort(401)
    if flask.request.method == "GET":
        return flask.render_template("new-repo.html")
    else:
        name = flask.request.form["name"]
        visibility = int(flask.request.form["visibility"])

        if not only_chars(name,
                          "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"):
            flask.flash(Markup(
                    "<iconify-icon icon='mdi:error'></iconify-icon>" + _(
                            "Repository names may only contain Latin alphabet, numbers, '-' and '_'")),
                    category="error")
            return flask.render_template("new-repo.html")

        user = User.query.filter_by(username=flask.session.get("username")).first()

        repo = Repo(user, name, visibility)
        db.session.add(repo)
        db.session.commit()

        flask.flash(Markup(_("Successfully created repository {name}").format(name=name)),
                    category="success")
        return flask.redirect(repo.route, code=303)


@app.route("/logout")
def logout():
    flask.session.clear()
    flask.flash(Markup(
            "<iconify-icon icon='mdi:account'></iconify-icon>" + _("Successfully logged out")),
            category="info")
    return flask.redirect("/", code=303)


@app.route("/<username>/", methods=["GET", "POST"])
def user_profile(username):
    if db.session.get(User, username) is None:
        flask.abort(404)
    old_relationship = UserFollow.query.filter_by(
            follower_username=flask.session.get("username"),
            followed_username=username).first()
    if flask.request.method == "GET":
        user = User.query.filter_by(username=username).first()
        match flask.request.args.get("action"):
            case "repositories":
                repos = Repo.query.filter_by(owner_name=username, visibility=2)
                return flask.render_template("user-profile-repositories.html", user=user,
                                             repos=repos,
                                             relationship=old_relationship)
            case "followers":
                return flask.render_template("user-profile-followers.html", user=user,
                                             relationship=old_relationship)
            case "follows":
                return flask.render_template("user-profile-follows.html", user=user,
                                             relationship=old_relationship)
            case _:
                return flask.render_template("user-profile-overview.html", user=user,
                                             relationship=old_relationship)

    elif flask.request.method == "POST":
        match flask.request.args.get("action"):
            case "follow":
                if username == flask.session.get("username"):
                    flask.abort(403)
                if old_relationship:
                    db.session.delete(old_relationship)
                else:
                    relationship = UserFollow(
                            flask.session.get("username"),
                            username
                    )
                    db.session.add(relationship)
                    db.session.commit()

                    user = db.session.get(User, username)
                    author = db.session.get(User, flask.session.get("username"))
                    notification = Notification({"type": "update", "version": "0.0.0"})
                    db.session.add(notification)
                    db.session.commit()

                db.session.commit()
                return flask.redirect("?", code=303)


@app.route("/<username>/<repository>/")
def repository_index(username, repository):
    return flask.redirect("./tree", code=302)


@app.route("/info/<username>/avatar")
def user_avatar(username):
    server_userdata_location = os.path.join(config.USERDATA_PATH, username)
    if not os.path.exists(server_userdata_location):
        return flask.render_template("errors/not-found.html"), 404

    return flask.send_from_directory(server_userdata_location, "avatar.png")


@app.route("/info/<username>/avatar", methods=["POST"])
def user_avatar_upload(username):
    server_userdata_location = os.path.join(config.USERDATA_PATH, username)

    if not os.path.exists(server_userdata_location):
        flask.abort(404)
    if not flask.session.get("username") == username:
        flask.abort(403)

    # Convert image to PNG
    try:
        image = Image.open(flask.request.files["avatar"])
    except PIL.UnidentifiedImageError:
        flask.abort(400)
    image.save(os.path.join(server_userdata_location, "avatar.png"))

    return flask.redirect(f"/{username}", code=303)


@app.route("/<username>/<repository>/raw/<branch>/<path:subpath>")
def repository_raw(username, repository, branch, subpath):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    if not os.path.exists(server_repo_location):
        return flask.render_template("errors/not-found.html"), 404

    repo = git.Repo(server_repo_location)
    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    if not repo_data.default_branch:
        if repo.heads:
            repo_data.default_branch = repo.heads[0].name
        else:
            return flask.render_template("empty.html",
                                         remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200
    if not branch:
        branch = repo_data.default_branch
        return flask.redirect(f"./{branch}", code=302)

    if branch.startswith("tag:"):
        ref = f"tags/{branch[4:]}"
    elif branch.startswith("~"):
        ref = branch[1:]
    else:
        ref = f"heads/{branch}"

    ref = ref.replace("~", "/")  # encode slashes for URL support

    try:
        repo.git.checkout("-f", ref)
    except git.exc.GitCommandError:
        return flask.render_template("errors/not-found.html"), 404

    return flask.send_from_directory(config.REPOS_PATH,
                                     os.path.join(username, repository, subpath))


@repositories.route("/<username>/<repository>/tree/", defaults={"branch": None, "subpath": ""})
@repositories.route("/<username>/<repository>/tree/<branch>/", defaults={"subpath": ""})
@repositories.route("/<username>/<repository>/tree/<branch>/<path:subpath>")
def repository_tree(username, repository, branch, subpath):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    repo = git.Repo(server_repo_location)
    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    if not repo_data.default_branch:
        if repo.heads:
            repo_data.default_branch = repo.heads[0].name
        else:
            return flask.render_template("empty.html",
                                         remote=f"{config.www_protocol}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200
    if not branch:
        branch = repo_data.default_branch
        return flask.redirect(f"./{branch}", code=302)

    if branch.startswith("tag:"):
        ref = f"tags/{branch[4:]}"
    elif branch.startswith("~"):
        ref = branch[1:]
    else:
        ref = f"heads/{branch}"

    ref = ref.replace("~", "/")  # encode slashes for URL support

    try:
        repo.git.checkout("-f", ref)
    except git.exc.GitCommandError:
        return flask.render_template("errors/not-found.html"), 404

    branches = repo.heads

    all_refs = []
    for ref in repo.heads:
        all_refs.append((ref, "head"))
    for ref in repo.tags:
        all_refs.append((ref, "tag"))

    if os.path.isdir(os.path.join(server_repo_location, subpath)):
        files = []
        blobs = []

        for entry in os.listdir(os.path.join(server_repo_location, subpath)):
            if not os.path.basename(entry) == ".git":
                files.append(os.path.join(subpath, entry))

        infos = []

        for file in files:
            path = os.path.join(server_repo_location, file)
            mimetype = guess_mime(path)

            text = git_command(server_repo_location, None, "log", "--format='%H\n'",
                               shlex.quote(file)).decode()

            sha = text.split("\n")[0]
            identifier = f"/{username}/{repository}/{sha}"

            last_commit = db.session.get(Commit, identifier)

            info = {
                "name": os.path.basename(file),
                "serverPath": path,
                "relativePath": file,
                "link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file),
                "size": human_size(os.path.getsize(path)),
                "mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}",
                "commit": last_commit,
                "shaSize": 7,
            }

            special_icon = config.match_icon(os.path.basename(file))
            if special_icon:
                info["icon"] = special_icon
            elif os.path.isdir(path):
                info["icon"] = config.folder_icon
            elif mimetypes.guess_type(path)[0] in config.file_icons:
                info["icon"] = config.file_icons[mimetypes.guess_type(path)[0]]
            else:
                info["icon"] = config.unknown_icon

            if os.path.isdir(path):
                infos.insert(0, info)
            else:
                infos.append(info)

        return flask.render_template(
                "repo-tree.html",
                username=username,
                repository=repository,
                files=infos,
                subpath=os.path.join("/", subpath),
                branches=all_refs,
                current=branch,
                remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
                is_favourite=get_favourite(flask.session.get("username"), username, repository),
                repo_data=repo_data,
        )
    else:
        path = os.path.join(server_repo_location, subpath)

        if not os.path.exists(path):
            return flask.render_template("errors/not-found.html"), 404

        mimetype = guess_mime(path)
        mode = mimetype.split("/", 1)[0]
        size = human_size(os.path.getsize(path))

        special_icon = config.match_icon(os.path.basename(path))
        if special_icon:
            icon = special_icon
        elif os.path.isdir(path):
            icon = config.folder_icon
        elif mimetypes.guess_type(path)[0] in config.file_icons:
            icon = config.file_icons[mimetypes.guess_type(path)[0]]
        else:
            icon = config.unknown_icon

        contents = None
        if mode == "text":
            contents = convert_to_html(path)

        return flask.render_template(
                "repo-file.html",
                username=username,
                repository=repository,
                file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath),
                branches=all_refs,
                current=branch,
                mode=mode,
                mimetype=mimetype,
                detailedtype=magic.from_file(path),
                size=size,
                icon=icon,
                subpath=os.path.join("/", subpath),
                extension=pathlib.Path(path).suffix,
                basename=os.path.basename(path),
                contents=contents,
                remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
                is_favourite=get_favourite(flask.session.get("username"), username, repository),
                repo_data=repo_data,
        )


@repositories.route("/<username>/<repository>/commit/<sha>")
def repository_commit(username, repository, sha):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    if not os.path.exists(server_repo_location):
        return flask.render_template("errors/not-found.html"), 404

    repo = git.Repo(server_repo_location)
    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()

    files = git_command(os.path.join(server_repo_location, ".git"), None, "diff-tree", "-r",
                        "--name-only", "--no-commit-id", sha).decode().split("\n")[:-1]

    return flask.render_template(
            "repo-commit.html",
            username=username,
            repository=repository,
            remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
            is_favourite=get_favourite(flask.session.get("username"), username, repository),
            diff={file: git_command(os.path.join(server_repo_location, ".git"), None, "diff",
                                    str(sha) + "^!", "--", file).decode().split("\n") for
                  file in files},
            data=db.session.get(Commit, f"/{username}/{repository}/{sha}"),
            repo_data=repo_data,
            comment_query=Comment.query,
            permission_level=get_permission_level(flask.session.get("username"), username, repository),
    )


@repositories.route("/<username>/<repository>/commit/<sha>/add_comment", methods=["POST"])
def repository_commit_add_comment(username, repository, sha):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    comment = Comment(
        db.session.get(User, flask.session.get("username")),
        db.session.get(Repo, f"/{username}/{repository}"),
        db.session.get(Commit, f"/{username}/{repository}/{sha}"),
        flask.request.form["comment"],
        flask.request.form["file"],
        flask.request.form["line"],
    )

    db.session.add(comment)
    db.session.commit()

    return flask.redirect(
        flask.url_for(".repository_commit", username=username, repository=repository, sha=sha),
        code=303
    )


@repositories.route("/<username>/<repository>/commit/<sha>/delete_comment/<int:id>", methods=["POST"])
def repository_commit_delete_comment(username, repository, sha, id):
    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    comment = Comment.query.filter_by(identifier=f"/{username}/{repository}/{id}").first()
    commit = Commit.query.filter_by(identifier=f"/{username}/{repository}/{sha}").first()
    if (
        comment.owner.username == flask.session.get("username")
        or get_permission_level(flask.session.get("username"), username, repository) >= 2
        or comment.commit.owner.username == flask.session.get("username")
    ):
        db.session.delete(comment)
        db.session.commit()

    return flask.redirect(
        flask.url_for(".repository_commit", username=username, repository=repository, sha=sha),
        code=303
    )


@repositories.route("/<username>/<repository>/commit/<sha>/resolve_comment/<int:id>", methods=["POST"])
def repository_commit_resolve_comment(username, repository, sha, id):
    comment = Comment.query.filter_by(identifier=f"/{username}/{repository}/{id}").first()
    if (
        comment.commit.owner.username == flask.session.get("username")
        or get_permission_level(flask.session.get("username"), username, repository) >= 2
        or comment.owner.username == flask.session.get("username")
    ):
        comment.state = int(not comment.state)
        db.session.commit()

    return flask.redirect(
        flask.url_for(".repository_commit", username=username, repository=repository, sha=sha),
        code=303
    )


@repositories.route("/<username>/<repository>/forum/")
def repository_forum(username, repository):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    if not os.path.exists(server_repo_location):
        return flask.render_template("errors/not-found.html"), 404

    repo = git.Repo(server_repo_location)
    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    user = User.query.filter_by(username=flask.session.get("username")).first()
    relationships = RepoAccess.query.filter_by(repo=repo_data)
    user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()

    return flask.render_template(
            "repo-forum.html",
            username=username,
            repository=repository,
            repo_data=repo_data,
            relationships=relationships,
            repo=repo,
            user_relationship=user_relationship,
            Post=Post,
            remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
            is_favourite=get_favourite(flask.session.get("username"), username, repository),
            default_branch=repo_data.default_branch
    )


@repositories.route("/<username>/<repository>/forum/topic/<int:id>")
def repository_forum_topic(username, repository, id):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    if not os.path.exists(server_repo_location):
        return flask.render_template("errors/not-found.html"), 404

    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    user = User.query.filter_by(username=flask.session.get("username")).first()
    relationships = RepoAccess.query.filter_by(repo=repo_data)
    user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()

    post = Post.query.filter_by(id=id).first()

    return flask.render_template(
            "repo-topic.html",
            username=username,
            repository=repository,
            repo_data=repo_data,
            relationships=relationships,
            user_relationship=user_relationship,
            post=post,
            remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
            is_favourite=get_favourite(flask.session.get("username"), username, repository),
            default_branch=repo_data.default_branch
    )


@repositories.route("/<username>/<repository>/forum/new", methods=["POST", "GET"])
def repository_forum_new(username, repository):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    if not os.path.exists(server_repo_location):
        return flask.render_template("errors/not-found.html"), 404

    repo = git.Repo(server_repo_location)
    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    user = User.query.filter_by(username=flask.session.get("username")).first()
    relationships = RepoAccess.query.filter_by(repo=repo_data)
    user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()

    post = Post(user, repo_data, None, flask.request.form["subject"],
                flask.request.form["message"])

    db.session.add(post)
    db.session.commit()

    return flask.redirect(
            flask.url_for(".repository_forum_thread", username=username, repository=repository,
                          post_id=post.number),
            code=303)


@repositories.route("/<username>/<repository>/forum/<int:post_id>")
def repository_forum_thread(username, repository, post_id):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    if not os.path.exists(server_repo_location):
        return flask.render_template("errors/not-found.html"), 404

    repo = git.Repo(server_repo_location)
    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    user = User.query.filter_by(username=flask.session.get("username")).first()
    relationships = RepoAccess.query.filter_by(repo=repo_data)
    user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()

    if user:
        max_post_nesting = user.max_post_nesting
    else:
        max_post_nesting = 2

    return flask.render_template(
            "repo-forum-thread.html",
            username=username,
            repository=repository,
            repo_data=repo_data,
            relationships=relationships,
            repo=repo,
            Post=Post,
            user_relationship=user_relationship,
            post_id=post_id,
            max_post_nesting=max_post_nesting,
            remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
            is_favourite=get_favourite(flask.session.get("username"), username, repository),
            parent=Post.query.filter_by(repo=repo_data, number=post_id).first(),
            has_permission=not ((not get_permission_level(flask.session.get("username"), username,
                                                     repository)) and db.session.get(Post,
                                                                                     f"/{username}/{repository}/{post_id}").owner.username != flask.session.get("username")),
    )


@repositories.route("/<username>/<repository>/forum/<int:post_id>/change-state",
                    methods=["POST"])
def repository_forum_change_state(username, repository, post_id):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if (not get_permission_level(flask.session.get("username"), username, repository)) and db.session.get(Post, f"/{username}/{repository}/{post_id}").owner.username != flask.session.get("username"):
        flask.abort(403)

    repo = git.Repo(server_repo_location)
    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    user = User.query.filter_by(username=flask.session.get("username")).first()
    relationships = RepoAccess.query.filter_by(repo=repo_data)
    user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()

    post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()

    if not post:
        flask.abort(404)

    post.state = int(flask.request.form["new-state"])

    db.session.commit()

    return flask.redirect(
            flask.url_for(".repository_forum_thread", username=username, repository=repository,
                          post_id=post_id),
            code=303)


@repositories.route("/<username>/<repository>/forum/<int:post_id>/reply", methods=["POST"])
def repository_forum_reply(username, repository, post_id):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    if not os.path.exists(server_repo_location):
        return flask.render_template("errors/not-found.html"), 404

    repo = git.Repo(server_repo_location)
    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    user = User.query.filter_by(username=flask.session.get("username")).first()
    relationships = RepoAccess.query.filter_by(repo=repo_data)
    user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
    if not user:
        flask.abort(401)

    parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()
    post = Post(user, repo_data, parent, flask.request.form["subject"],
                flask.request.form["message"])

    db.session.add(post)
    post.update_date()
    db.session.commit()

    return flask.redirect(
            flask.url_for(".repository_forum_thread", username=username, repository=repository,
                          post_id=post_id),
            code=303)


@repositories.route("/<username>/<repository>/forum/<int:post_id>/edit", methods=["POST"])
def repository_forum_edit(username, repository, post_id):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
        flask.session.get("username"), username,
        repository) is not None):
        flask.abort(403)

    if not os.path.exists(server_repo_location):
        return flask.render_template("errors/not-found.html"), 404

    repo = git.Repo(server_repo_location)
    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    user = User.query.filter_by(username=flask.session.get("username")).first()
    relationships = RepoAccess.query.filter_by(repo=repo_data)
    user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
    if not user:
        flask.abort(401)
    post = db.session.get(Post, f"/{username}/{repository}/{post_id}")
    if user != post.owner:
        flask.abort(403)

    post.subject = flask.request.form["subject"]
    post.message = flask.request.form["message"]
    post.html = markdown.markdown2html(post.message).prettify()
    post.update_date()
    db.session.commit()

    return flask.redirect(
        flask.url_for(".repository_forum_thread", username=username, repository=repository,
                        post_id=post_id),
        code=303)


@repositories.route("/<username>/<repository>/forum/<int:post_id>/edit", methods=["GET"])
def repository_forum_edit_form(username, repository, post_id):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
        flask.session.get("username"), username,
        repository) is not None):
        flask.abort(403)

    if not os.path.exists(server_repo_location):
        return flask.render_template("errors/not-found.html"), 404

    repo = git.Repo(server_repo_location)
    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    user = User.query.filter_by(username=flask.session.get("username")).first()
    relationships = RepoAccess.query.filter_by(repo=repo_data)
    user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
    if not user:
        flask.abort(401)
    post = db.session.get(Post, f"/{username}/{repository}/{post_id}")
    if user != post.owner:
        flask.abort(403)

    return flask.render_template(
            "repo-forum-edit.html",
            username=username,
            repository=repository,
            repo_data=repo_data,
            relationships=relationships,
            repo=repo,
            user_relationship=user_relationship,
            post=post,
            remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
            is_favourite=get_favourite(flask.session.get("username"), username, repository),
            default_branch=repo_data.default_branch
    )

@repositories.route("/<username>/<repository>/forum/<int:post_id>/voteup",
                    defaults={"score": 1})
@repositories.route("/<username>/<repository>/forum/<int:post_id>/votedown",
                    defaults={"score": -1})
@repositories.route("/<username>/<repository>/forum/<int:post_id>/votes", defaults={"score": 0})
def repository_forum_vote(username, repository, post_id, score):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    if not os.path.exists(server_repo_location):
        return flask.render_template("errors/not-found.html"), 404

    repo = git.Repo(server_repo_location)
    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    user = User.query.filter_by(username=flask.session.get("username")).first()
    relationships = RepoAccess.query.filter_by(repo=repo_data)
    user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
    if not user:
        flask.abort(401)

    post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first()

    if score:
        old_relationship = PostVote.query.filter_by(user_username=user.username,
                                                    post_identifier=post.identifier).first()
        if old_relationship:
            if score == old_relationship.vote_score:
                db.session.delete(old_relationship)
                post.vote_sum -= old_relationship.vote_score
            else:
                post.vote_sum -= old_relationship.vote_score
                post.vote_sum += score
                old_relationship.vote_score = score
        else:
            relationship = PostVote(user, post, score)
            post.vote_sum += score
            db.session.add(relationship)

        db.session.commit()

    user_vote = PostVote.query.filter_by(user_username=user.username,
                                         post_identifier=post.identifier).first()
    response = flask.make_response(
            str(post.vote_sum) + " " + str(user_vote.vote_score if user_vote else 0))
    response.content_type = "text/plain"

    return response


@repositories.route("/<username>/<repository>/favourite")
def repository_favourite(username, repository):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    if not os.path.exists(server_repo_location):
        return flask.render_template("errors/not-found.html"), 404

    repo = git.Repo(server_repo_location)
    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    user = User.query.filter_by(username=flask.session.get("username")).first()
    relationships = RepoAccess.query.filter_by(repo=repo_data)
    user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()
    if not user:
        flask.abort(401)

    old_relationship = RepoFavourite.query.filter_by(user_username=user.username,
                                                     repo_route=repo_data.route).first()
    if old_relationship:
        db.session.delete(old_relationship)
    else:
        relationship = RepoFavourite(user, repo_data)
        db.session.add(relationship)

    db.session.commit()

    return flask.redirect(flask.url_for("favourites"), code=303)


@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"])
def repository_users(username, repository):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    if not os.path.exists(server_repo_location):
        return flask.render_template("errors/not-found.html"), 404

    repo = git.Repo(server_repo_location)
    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    user = User.query.filter_by(username=flask.session.get("username")).first()
    relationships = RepoAccess.query.filter_by(repo=repo_data)
    user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first()

    if flask.request.method == "GET":
        return flask.render_template(
                "repo-users.html",
                username=username,
                repository=repository,
                repo_data=repo_data,
                relationships=relationships,
                repo=repo,
                user_relationship=user_relationship,
                remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
                is_favourite=get_favourite(flask.session.get("username"), username, repository)
        )
    else:
        if get_permission_level(flask.session.get("username"), username, repository) != 2:
            flask.abort(401)

        if flask.request.form.get("new-username"):
            # Create new relationship
            new_user = User.query.filter_by(
                    username=flask.request.form.get("new-username")).first()
            relationship = RepoAccess(new_user, repo_data, flask.request.form.get("new-level"))
            db.session.add(relationship)
            db.session.commit()
        if flask.request.form.get("update-username"):
            # Create new relationship
            updated_user = User.query.filter_by(
                    username=flask.request.form.get("update-username")).first()
            relationship = RepoAccess.query.filter_by(repo=repo_data, user=updated_user).first()
            if flask.request.form.get("update-level") == -1:
                relationship.delete()
            else:
                relationship.access_level = flask.request.form.get("update-level")
            db.session.commit()

        return flask.redirect(
                app.url_for(".repository_users", username=username, repository=repository))


@repositories.route("/<username>/<repository>/branches/")
def repository_branches(username, repository):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    if not os.path.exists(server_repo_location):
        return flask.render_template("errors/not-found.html"), 404

    repo = git.Repo(server_repo_location)
    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()

    return flask.render_template(
            "repo-branches.html",
            username=username,
            repository=repository,
            repo_data=repo_data,
            repo=repo,
            remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
            is_favourite=get_favourite(flask.session.get("username"), username, repository)
    )


@repositories.route("/<username>/<repository>/log/", defaults={"branch": None})
@repositories.route("/<username>/<repository>/log/<branch>/")
def repository_log(username, repository, branch):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    if not os.path.exists(server_repo_location):
        return flask.render_template("errors/not-found.html"), 404

    repo = git.Repo(server_repo_location)
    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    if not repo_data.default_branch:
        if repo.heads:
            repo_data.default_branch = repo.heads[0].name
        else:
            return flask.render_template("empty.html",
                                         remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200
    if not branch:
        branch = repo_data.default_branch
        return flask.redirect(f"./{branch}", code=302)

    if branch.startswith("tag:"):
        ref = f"tags/{branch[4:]}"
    elif branch.startswith("~"):
        ref = branch[1:]
    else:
        ref = f"heads/{branch}"

    ref = ref.replace("~", "/")  # encode slashes for URL support

    try:
        repo.git.checkout("-f", ref)
    except git.exc.GitCommandError:
        return flask.render_template("errors/not-found.html"), 404

    branches = repo.heads

    all_refs = []
    for ref in repo.heads:
        all_refs.append((ref, "head"))
    for ref in repo.tags:
        all_refs.append((ref, "tag"))

    commit_list = [f"/{username}/{repository}/{sha}" for sha in
                   git_command(server_repo_location, None, "log",
                               "--format='%H'").decode().split("\n")]

    commits = Commit.query.filter(Commit.identifier.in_(commit_list)).order_by(Commit.author_date.desc())
    page_number = flask.request.args.get("page", 1, type=int)
    if flask.session.get("username"):
        default_page_length = db.session.get(User, flask.session.get("username")).default_page_length
    else:
        default_page_length = 16
    page_length = flask.request.args.get("per_page", default_page_length, type=int)
    page_listing = db.paginate(commits, page=page_number, per_page=page_length)

    if page_listing.has_next:
        next_page = page_listing.next_num
    else:
        next_page = None

    if page_listing.has_prev:
        prev_page = page_listing.prev_num
    else:
        prev_page = None

    return flask.render_template(
            "repo-log.html",
            username=username,
            repository=repository,
            branches=all_refs,
            current=branch,
            repo_data=repo_data,
            repo=repo,
            commits=page_listing,
            remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
            is_favourite=get_favourite(flask.session.get("username"), username, repository),
            page_number=page_number,
            page_length=page_length,
            next_page=next_page,
            prev_page=prev_page,
            num_pages=page_listing.pages
    )


@repositories.route("/<username>/<repository>/prs/", methods=["GET", "POST"])
def repository_prs(username, repository):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    if not os.path.exists(server_repo_location):
        return flask.render_template("errors/not-found.html"), 404

    if flask.request.method == "GET":
        repo = git.Repo(server_repo_location)
        repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
        user = User.query.filter_by(username=flask.session.get("username")).first()

        return flask.render_template(
                "repo-prs.html",
                username=username,
                repository=repository,
                repo_data=repo_data,
                repo=repo,
                PullRequest=PullRequest,
                remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
                is_favourite=get_favourite(flask.session.get("username"), username, repository),
                default_branch=repo_data.default_branch,
                branches=repo.branches
        )

    else:
        repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
        head = flask.request.form.get("head")
        head_route = flask.request.form.get("headroute")
        base = flask.request.form.get("base")

        if not head and base and head_route:
            return flask.redirect(".", 400)

        head_repo = git.Repo(os.path.join(config.REPOS_PATH, head_route.lstrip("/")))
        base_repo = git.Repo(server_repo_location)

        if head not in head_repo.branches or base not in base_repo.branches:
            flask.flash(Markup(
                    "<iconify-icon icon='mdi:error'></iconify-icon>" + _("Bad branch name")),
                    category="error")
            return flask.redirect(".", 303)

        head_data = db.session.get(Repo, head_route)
        if not head_data.visibility:
            flask.flash(Markup(
                    "<iconify-icon icon='mdi:error'></iconify-icon>" + _(
                            "Head can't be restricted")),
                    category="error")
            return flask.redirect(".", 303)

        pull_request = PullRequest(head_data, head, repo_data, base,
                                   db.session.get(User, flask.session["username"]))

        db.session.add(pull_request)
        db.session.commit()

        # Create the notification
        notification = Notification({"type": "pr", "head": pull_request.head.route, "base": pull_request.base.route, "pr": pull_request.id})
        db.session.add(notification)
        db.session.commit()

        # Send a notification to all users who have enabled PR notifications for this repo
        for relationship in RepoFavourite.query.filter_by(repo_route=pull_request.base.route, notify_pr=True).all():
            user = relationship.user
            user_notification = UserNotification(user, notification, 1)
            db.session.add(user_notification)
            db.session.commit()
            celery_tasks.send_notification.apply_async(args=[user_notification.id])

        return flask.redirect(".", 303)


@repositories.route("/<username>/<repository>/prs/merge", methods=["POST"])
def repository_prs_merge(username, repository):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    if not get_permission_level(flask.session.get("username"), username, repository):
        flask.abort(401)

    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    repo = git.Repo(server_repo_location)
    id = flask.request.form.get("id")

    pull_request = db.session.get(PullRequest, id)

    if pull_request:
        result = celery_tasks.merge_heads.delay(
                pull_request.head_route,
                pull_request.head_branch,
                pull_request.base_route,
                pull_request.base_branch,
                simulate=True
        )
        task_result = worker.AsyncResult(result.id)

        return flask.redirect(f"/task/{result.id}?pr-id={id}", 303)       # should be 202 Accepted but we must use a redirect
        # db.session.delete(pull_request)
        # db.session.commit()
    else:
        flask.abort(400)


@repositories.route("/<username>/<repository>/prs/<int:id>/merge")
def repository_prs_merge_stage_two(username, repository, id):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    if not get_permission_level(flask.session.get("username"), username, repository):
        flask.abort(401)

    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    repo = git.Repo(server_repo_location)

    pull_request = db.session.get(PullRequest, id)

    if pull_request:
        result = celery_tasks.merge_heads.delay(
                pull_request.head_route,
                pull_request.head_branch,
                pull_request.base_route,
                pull_request.base_branch,
                simulate=False
        )
        task_result = worker.AsyncResult(result.id)

        pull_request.state = 1
        db.session.commit()

        return flask.redirect(f"/task/{result.id}?pr-id={id}", 303)
        # db.session.delete(pull_request)
    else:
        flask.abort(400)


@app.route("/task/<task_id>")
def task_monitor(task_id):
    task_result = worker.AsyncResult(task_id)

    if flask.request.args.get("partial"):
        # htmx partial update
        return render_block("task-monitor.html", "content", result=task_result, query_string=flask.request.query_string.decode(), delay=1000)

    # Since most tasks finish rather quickly, the initial delay is faster, so it doesn't wait for too long
    return flask.render_template("task-monitor.html", result=task_result, query_string=flask.request.query_string.decode(), delay=125)


@repositories.route("/<username>/<repository>/prs/delete", methods=["POST"])
def repository_prs_delete(username, repository):
    server_repo_location = os.path.join(config.REPOS_PATH, username, repository)
    if not os.path.exists(server_repo_location):
        flask.abort(404)
    if not (get_visibility(username, repository) or get_permission_level(
            flask.session.get("username"), username,
            repository) is not None):
        flask.abort(403)

    if not get_permission_level(flask.session.get("username"), username, repository):
        flask.abort(401)

    repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    repo = git.Repo(server_repo_location)
    id = flask.request.form.get("id")

    pull_request = db.session.get(PullRequest, id)

    if pull_request:
        pull_request.state = 2
        db.session.commit()

    return flask.redirect(".", 303)


@repositories.route("/<username>/<repository>/settings/")
def repository_settings(username, repository):
    if get_permission_level(flask.session.get("username"), username, repository) != 2:
        flask.abort(401)

    repo = git.Repo(os.path.join(config.REPOS_PATH, username, repository))

    site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/{repository}</code>")
    primary_site_link = Markup(f"<code>http{'s' if config.suggest_https else ''}://{username}.{config.BASE_DOMAIN}/</code>")

    return flask.render_template("repo-settings.html", username=username, repository=repository,
                                 repo_data=db.session.get(Repo, f"/{username}/{repository}"),
                                 branches=[branch.name for branch in repo.branches],
                                 site_link=site_link, primary_site_link=primary_site_link,
                                 remote=f"http{'s' if config.suggest_https else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}",
                                 is_favourite=get_favourite(flask.session.get("username"), username, repository),
    )


@repositories.route("/<username>/<repository>/settings/", methods=["POST"])
def repository_settings_post(username, repository):
    if get_permission_level(flask.session.get("username"), username, repository) != 2:
        flask.abort(401)

    repo = db.session.get(Repo, f"/{username}/{repository}")

    repo.visibility = flask.request.form.get("visibility", type=int)
    repo.info = flask.request.form.get("description")
    repo.default_branch = flask.request.form.get("default_branch")
    repo.url = flask.request.form.get("url")

    # Update site settings
    had_site = repo.has_site
    old_branch = repo.site_branch
    if flask.request.form.get("site_branch"):
        repo.site_branch = flask.request.form.get("site_branch")
        if flask.request.form.get("primary_site"):
            if had_site != 2:
                # Remove primary site from other repos
                for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=2):
                    other_repo.has_site = 1                           # switch it to a regular site
                    flask.flash(Markup(
                            _("Your repository {repository} has been demoted from a primary site to a regular site because there can only be one primary site per user.").format(
                                    repository=other_repo.route
                            )), category="warning")
            repo.has_site = 2
        else:
            repo.has_site = 1
    else:
        repo.site_branch = None
        repo.has_site = 0

    db.session.commit()

    if not (had_site, old_branch) == (repo.has_site, repo.site_branch):
        # Deploy the newly activated site
        result = celery_tasks.copy_site.delay(repo.route)

    if had_site and not repo.has_site:
        # Remove the site
        result = celery_tasks.delete_site.delay(repo.route)

    if repo.has_site == 2 or (had_site == 2 and had_site != repo.has_site):
        # Deploy all other sites which were destroyed by the primary site
        for other_repo in Repo.query.filter_by(owner=repo.owner, has_site=1):
            result = celery_tasks.copy_site.delay(other_repo.route)

    return flask.redirect(f"/{username}/{repository}/settings", 303)


@app.errorhandler(404)
def e404(error):
    return flask.render_template("errors/not-found.html"), 404


@app.errorhandler(401)
def e401(error):
    return flask.render_template("errors/unauthorised.html"), 401


@app.errorhandler(403)
def e403(error):
    return flask.render_template("errors/forbidden.html"), 403


@app.errorhandler(418)
def e418(error):
    return flask.render_template("errors/teapot.html"), 418


@app.errorhandler(405)
def e405(error):
    return flask.render_template("errors/method-not-allowed.html"), 405


@app.errorhandler(500)
def e500(error):
    return flask.render_template("errors/server-error.html"), 500


@app.errorhandler(400)
def e400(error):
    return flask.render_template("errors/bad-request.html"), 400


@app.errorhandler(410)
def e410(error):
    return flask.render_template("errors/gone.html"), 410


@app.errorhandler(415)
def e415(error):
    return flask.render_template("errors/media-type.html"), 415


if __name__ == "__main__":
    app.run(debug=True, port=8080, host="0.0.0.0")

app.register_blueprint(repositories)
