import os
import random

import cairosvg
import flask
from flask_sqlalchemy import SQLAlchemy
import git
import mimetypes
import magic
from flask_bcrypt import Bcrypt
from markupsafe import escape, Markup
from flask_migrate import Migrate
from datetime import datetime
from enum import Enum
import shutil
from PIL import Image
from cairosvg import svg2png

import config

app = flask.Flask(__name__)

from flask_httpauth import HTTPBasicAuth
auth = HTTPBasicAuth()

app.config["SQLALCHEMY_DATABASE_URI"] = f"postgresql://root:{config.DB_PASSWORD}@localhost/gitme"
app.config["SECRET_KEY"] = config.DB_PASSWORD
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
bcrypt = Bcrypt(app)
migrate = Migrate(app, db)


def onlyChars(string, chars):
    for i in string:
        if i not in chars:
            return False
    return True


with app.app_context():
    class User(db.Model):
        username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True)
        displayName = db.Column(db.Unicode(128), unique=False, nullable=True)
        bio = db.Column(db.Unicode(512), unique=False, nullable=True)
        passwordHashed = db.Column(db.String(60), nullable=False)
        email = db.Column(db.String(254), nullable=True)
        company = db.Column(db.Unicode(64), nullable=True)
        companyURL = db.Column(db.String(256), nullable=True)
        URL = db.Column(db.String(256), nullable=True)
        showMail = db.Column(db.Boolean, default=False, nullable=False)
        location = db.Column(db.Unicode(64), nullable=True)
        creationDate = db.Column(db.DateTime, default=datetime.utcnow)

        repositories = db.relationship("Repo", backref="owner")

        def __init__(self, username, password, email=None, displayName=None):
            self.username = username
            self.passwordHashed = bcrypt.generate_password_hash(password, config.HASHING_ROUNDS).decode("utf-8")
            self.email = email
            self.displayName = displayName

            # Create the user's directory
            if not os.path.exists(os.path.join(config.REPOS_PATH, username)):
                os.makedirs(os.path.join(config.REPOS_PATH, username))
            if not os.path.exists(os.path.join(config.USERDATA_PATH, username)):
                os.makedirs(os.path.join(config.USERDATA_PATH, username))

            avatarName = random.choice(os.listdir(config.DEFAULT_AVATARS_PATH))
            if os.path.join(config.DEFAULT_AVATARS_PATH, avatarName).endswith(".svg"):
                cairosvg.svg2png(url=os.path.join(config.DEFAULT_AVATARS_PATH, avatarName), write_to="/tmp/gitme-avatar.png")
                avatar = Image.open("/tmp/gitme-avatar.png")
            else:
                avatar = Image.open(os.path.join(config.DEFAULT_AVATARS_PATH, avatarName))
            avatar.thumbnail(config.AVATAR_SIZE)
            avatar.save(os.path.join(config.USERDATA_PATH, username, "avatar.png"))


    class Repo(db.Model):
        route = db.Column(db.String(97), unique=True, nullable=False, primary_key=True)
        ownerName = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
        name = db.Column(db.String(64), nullable=False)
        # owner = db.relationship("User", back_populates="repositories")
        visibility = db.Column(db.SmallInteger(), nullable=False)
        info = db.Column(db.Unicode(512), nullable=True)
        URL = db.Column(db.String(256), nullable=True)
        creationDate = db.Column(db.DateTime, default=datetime.utcnow)

        defaultBranch = db.Column(db.String(64), nullable=False, default="master")

        def __init__(self, owner, name, visibility):
            self.route = f"/{owner.username}/{name}"
            self.name = name
            self.ownerName = owner.username
            self.owner = owner
            self.visibility = visibility

import gitHTTP


def humanSize(value, decimals=2, scale=1024, units=("B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB")):
    for unit in units:
        if value < scale:
            break
        value /= scale
    if int(value) == value:
        # do not return decimals, if the value is already round
        return int(value), unit
    return round(value * 10**decimals) / 10**decimals, unit


def guessMIME(path):
    if os.path.isdir(path):
        mimetype = "inode/directory"
    elif magic.from_file(path, mime=True):
        mimetype = magic.from_file(path, mime=True)
    else:
        mimetype = "application/octet-stream"
    return mimetype


def convertToHTML(path):
    with open(path, "r") as f:
        contents = f.read()
    return contents


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

    return {"loggedInUser": username}


@app.route("/")
def main():
    return flask.render_template("home.html", title="gitme")


@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("/accounts/", methods=["GET", "POST"])
def login():
    if flask.request.method == "GET":
        return flask.render_template("login.html", title="gitme")
    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.passwordHashed, password):
                flask.session["username"] = user.username
                flask.flash(Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged in as {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", title="gitme")
            else:
                flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>Invalid password"), category="error")
                return flask.render_template("login.html", title="gitme")
        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 onlyChars(username, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"):
                flask.flash(Markup("<iconify-icon icon='mdi:account-error'></iconify-icon>Usernames may only contain Latin alphabet, numbers, '-' and '_'"), category="error")
                return flask.render_template("login.html", title="gitme")

            if username in config.RESERVED_NAMES:
                flask.flash(Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>Sorry, {username} is a system path"), category="error")
                return flask.render_template("login.html", title="gitme")

            userCheck = User.query.filter_by(username=username).first()
            if userCheck:
                flask.flash(Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>The username {username} is taken"), category="error")
                return flask.render_template("login.html", title="gitme")

            if password2 != password:
                flask.flash(Markup("<iconify-icon icon='mdi:key-alert'></iconify-icon>Make sure the passwords match"), category="error")
                return flask.render_template("login.html", title="gitme")

            user = User(username, password, email, name)
            db.session.add(user)
            db.session.commit()
            flask.session["username"] = user.username
            flask.flash(Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully created and logged in as {username}"), category="success")
            return flask.redirect("/", code=303)


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


@app.route("/<username>/")
def userProfile(username):
    user = User.query.filter_by(username=username).first()
    repos = Repo.query.filter_by(ownerName=username, visibility=2)
    return flask.render_template("user-profile.html", user=user, repos=repos)


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


@app.route("/<username>/<repository>/raw/<branch>/<path:subpath>")
def repositoryRaw(username, repository, branch, subpath):
    serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository))

    app.logger.info(f"Loading {serverRepoLocation}")

    if not os.path.exists(serverRepoLocation):
        app.logger.error(f"Cannot load {serverRepoLocation}")
        return flask.render_template("not-found.html"), 404

    repo = git.Repo(serverRepoLocation)
    try:
        repo.git.checkout(branch)
    except git.exc.GitCommandError:
        return flask.render_template("not-found.html"), 404

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


@app.route("/info/<username>/avatar")
def userAvatar(username):
    serverUserdataLocation = os.path.join(config.USERDATA_PATH, username)

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

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


@app.route("/<username>/<repository>/tree/", defaults={"branch": None, "subpath": ""})
@app.route("/<username>/<repository>/tree/<branch>/", defaults={"subpath": ""})
@app.route("/<username>/<repository>/tree/<branch>/<path:subpath>")
def repositoryTree(username, repository, branch, subpath):
    serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository))

    app.logger.info(f"Loading {serverRepoLocation}")

    if not os.path.exists(serverRepoLocation):
        app.logger.error(f"Cannot load {serverRepoLocation}")
        return flask.render_template("not-found.html"), 404

    repo = git.Repo(serverRepoLocation)
    repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first()
    if not branch:
        branch = repoData.defaultBranch
        return flask.redirect(f"./{branch}", code=302)
    try:
        repo.git.checkout(branch)
    except git.exc.GitCommandError:
        return flask.render_template("not-found.html"), 404

    branches = repo.heads
    if os.path.isdir(os.path.join(serverRepoLocation, subpath)):
        files = []
        blobs = []

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

        infos = []

        for file in files:
            path = os.path.join(serverRepoLocation, file)
            mimetype = guessMIME(path)

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

            specialIcon = config.matchIcon(os.path.basename(file))
            if specialIcon:
                info["icon"] = specialIcon
            elif os.path.isdir(path):
                info["icon"] = config.folderIcon
            elif mimetypes.guess_type(path)[0] in config.fileIcons:
                info["icon"] = config.fileIcons[mimetypes.guess_type(path)[0]]
            else:
                info["icon"] = config.unknownIcon

            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=branches,
                current=branch
        )
    else:
        path = os.path.join(serverRepoLocation, subpath)

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

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

        specialIcon = config.matchIcon(os.path.basename(path))
        if specialIcon:
            icon = specialIcon
        elif os.path.isdir(path):
            icon = config.folderIcon
        elif guessMIME(path)[0] in config.fileIcons:
            icon = config.fileIcons[guessMIME(path)[0]]
        else:
            icon = config.unknownIcon

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

        return flask.render_template(
                "repo-file.html",
                username=username,
                repository=repository,
                file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath),
                branches=branches,
                current=branch,
                mode=mode,
                mimetype=mimetype,
                detailedtype=magic.from_file(path),
                size=size,
                icon=icon,
                subpath=os.path.join("/", subpath),
                basename=os.path.basename(path),
                contents=contents
        )


@app.route("/<username>/<repository>/forum/")
def repositoryForum(username, repository):
    return flask.render_template("repo-forum.html", username=username, repository=repository)


@app.route("/<username>/<repository>/docs/")
def repositoryDocs(username, repository):
    return flask.render_template("repo-docs.html", username=username, repository=repository)


@app.route("/<username>/<repository>/releases/")
def repositoryReleases(username, repository):
    return flask.render_template("repo-releases.html", username=username, repository=repository)


@app.route("/<username>/<repository>/branches/")
def repositoryBranches(username, repository):
    return flask.render_template("repo-branches.html", username=username, repository=repository)


@app.route("/<username>/<repository>/people/")
def repositoryPeople(username, repository):
    return flask.render_template("repo-people.html", username=username, repository=repository)


@app.route("/<username>/<repository>/activity/")
def repositoryActivity(username, repository):
    return flask.render_template("repo-activity.html", username=username, repository=repository)


@app.route("/<username>/<repository>/ci/")
def repositoryCI(username, repository):
    return flask.render_template("repo-ci.html", username=username, repository=repository)


@app.route("/<username>/<repository>/settings/")
def repositorySettings(username, repository):
    flask.abort(401)
    return flask.render_template("repo-settings.html", username=username, repository=repository)


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


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


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


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

