from datetime import datetime
from math import floor
from typing import List
from flask import render_template, redirect, url_for, flash, jsonify, request
from flask_login.utils import login_required, login_user, logout_user, current_user
from sqlalchemy.sql.elements import Null, and_
from werkzeug.exceptions import HTTPException, abort
from overflow import app, db
from overflow.models import Faculte, Message, Post, Utilisateur, Reponse, Like, load_user
from overflow.forms import *

###################################
#             PAGES               #
###################################


@app.route("/")
@login_required
def homePage():
    """
    This page is the main page of the website
    """
    if current_user.is_authenticated:
        return render_template("home.html.j2", posts=Post.query.order_by(db.desc(Post.id)).limit(5).all())
    else:
        return redirect(url_for('login'))


@app.route("/register", methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        flash('Vous êtes déjà connecté!', 'info')
        return redirect(url_for('homePage'))

    form = RegisterForm()
    print(form.is_submitted())
    if form.errors:
        print(form.errors)

    if form.validate_on_submit():
        print("submit")
        new = Utilisateur(matricule=form.matricule.data,
                          prenom=form.prenom.data,
                          nom=form.nom.data,
                          anniversaire=form.anniversaire.data,
                          email=form.email.data)
        new.set_password(form.mot_de_passe.data)
        db.session.add(new)
        db.session.commit()
        flash("Registration was successfull, please login")
        return redirect(url_for('login'))

    return render_template('register.html.j2', form=form)


@app.route("/login", methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('homePage'))

    form = LoginForm()

    if form.validate_on_submit():
        # Verify if the user exist in the users table
        user = Utilisateur.query.filter_by(
            matricule=form.matricule.data).first()

        # Verify the login condition (username, password, blocked)
        if user is not None and user.check_password(form.mot_de_passe.data):
            login_user(user)
            return redirect(url_for('homePage'))
        flash("Mauvais identifiant ou mot de passe", "error")

    return render_template('login.html.j2', form=form)


@app.route("/logout")
def logout():
    if current_user.is_authenticated:
        logout_user()
    return redirect(url_for('homePage'))


@app.route("/courses/<string:id_matiere>")
@login_required
def courses(id_matiere):
    """
    This page is the page list with all post for a course
    """

    return render_template('courses.html.j2', posts=Post.query.filter_by(matiere=id_matiere), length=len(Post.query.filter_by(matiere=id_matiere).all()))


@app.route("/post/<int:id_post>")
@login_required
def showPost(id_post):
    """
    This page redirect to the selected post
    """

    return render_template('post.html.j2', post=Post.query.get(id_post))


@app.route("/profile")
@login_required
def showProfile():
    """
    Show the current_user profile
    """
    return render_template("profile.html.j2")


@app.route("/password", methods=["GET", "POST"])
@login_required
def changePassword():
    form = PasswordForm()
    if form.validate_on_submit():
        current_user.set_password(form.password.data)
        db.session.commit()
        flash("Votre mot de passe a bien été modifié", 'success')
        return redirect(url_for('showProfile'))
    else:

        return render_template('password.html.j2', form=form)


@app.route("/new_post", methods=['GET', 'POST'])
@login_required
def new_post():
    """
    create a new post for a courses

    returns
    ------
    a web page to the post of this courses if the form validation fail then redirect to the post creation pages
    """

    # call to the post creation flaskForm
    form = NewAndEditPostForm()

    # if inputs are correct then read data form the web page dans save it into the Post table
    if form.validate_on_submit():

        # read data and put into variables
        matricule = form.matricule.data
        titres = form.titre.data
        description = form.description.data

        # save the new post into the database
        post = Post(auteur=current_user.get_id(), matiere=matricule,
                    titre=titres, contenu=description, date=datetime.now())

        # check if the post title already exist
        if form.v_titre(form.titre, post.id):
            # commmit the change to the database
            db.session.add(post)
            db.session.commit()
            return redirect(url_for("courses", id_matiere=matricule))

    return render_template("createPost.html.j2", form=form)


@app.route("/edit_post/<idpost>", methods=["GET", "POST"])
@login_required
def edit_post(idpost):
    """
    edit an existing post

    parameters
    ----------
    idpost: it's the post id to modify (Int)

    returns
    -------
    a web page to the post of this courses if the form validation fail then redirect to the post editing pages
    if the user doesn't have the previleges to commit the change then handle an http 401 error

    """

    # get the good IDPost
    post = db.session.query(Post).get(idpost)

    if not post:
        abort(404)

    # call to the post creation flaskForm
    form = NewAndEditPostForm(matricule=post.matiere)

    # check if the user has the previleges to commit the changes
    if current_user.est_modo == True or post.auteur == current_user.get_id():

        # check if inputs are correct
        if form.validate_on_submit() and form.v_titre(form.titre, post.id):

            # make the change into the database column via reading data from the editing page
            post.titre = form.titre.data
            post.contenu = form.description.data
            post.date = datetime.now()
            post.matiere = form.matricule.data

            # commit the change
            db.session.commit()

            return redirect(url_for("courses", id_matiere=post.matiere))
        else:
            form.description.data = post.contenu

        return render_template("editPost.html.j2", form=form, post=post)

    return abort(401)


@app.route("/delete_post/<idpost>")
@login_required
def delete_post(idpost):
    """
    delete an existing post


    parameters
    ----------
    idpost: it's the post id to modify (Int)

    returns
    -------
    the courses web page 'courses.html.j2' with the coursesID in parameters after deleting the post or an error page if the user doesn't have the previleges
    """

    # get the good IDPost
    post = db.session.query(Post).get(idpost)
    matiere = post.matiere

    # check if the user has the previleges for deleting the post
    if current_user.est_modo == True or post.auteur == current_user.get_id():

        # delete the post and commit the change into the database
        db.session.delete(post)
        db.session.commit()

        return redirect(url_for("courses", id_matiere=matiere))

    return abort(401)


@app.route("/new_answer/<int:id_post>", methods=['GET', 'POST'])
@login_required
def new_answer(id_post):
    """
    create a new answer for a post

    returns
    ------
    a web page to the post if the form validation fail then redirect to the post creation pages
    """

    # call to the post creation flaskForm
    form = AnswerForm()

    # if inputs are correct then read data form the web page dans save it into the Post table
    if form.validate_on_submit():

        # read data and put into variables
        contenu = form.contenu.data

        # save the new answer into the database
        answer = Reponse(auteur=current_user.get_id(), post_id=id_post,
                         contenu=contenu, date=datetime.now(), date_modif=datetime.now())

        # commmit the change to the database
        db.session.add(answer)
        db.session.commit()
        return redirect(url_for("showPost", id_post=id_post))

    return render_template("createAnswer.html.j2", form=form, id_post=id_post)


@app.route("/edit_answer/<int:id_answer>", methods=["GET", "POST"])
@login_required
def edit_answer(id_answer):
    """
    edit an existing answer

    parameters
    ----------
    id_answer: it's the answer id to modify (Int)

    """

    # call to the post creation flaskForm
    form = AnswerForm()

    # get the good IDPost
    answer = db.session.query(Reponse).get(id_answer)

    if not answer:
        abort(404)

    # check if the user has the previleges to commit the changes
    if current_user.est_modo == True or answer.auteur == current_user.get_id():

        # check if inputs are correct
        if form.validate_on_submit():

            # make the change into the database column via reading data from the editing page
            answer.contenu = form.contenu.data
            answer.date_modif = datetime.now()

            # commit the change
            db.session.commit()

            return redirect(url_for("showPost", id_post=answer.post_id))

        return render_template("editAnswer.html.j2", form=form, answer=answer)

    return handleException(401)


@app.route("/delete_answer/<int:id_answer>")
@login_required
def delete_answer(id_answer):
    """
    delete an existing answer


    parameters
    ----------
    id_anser: it's the answer id to modify (Int)

    """

    answer = db.session.query(Reponse).get(id_answer)
    post_id = answer.post_id

    # check if the user has the previleges for deleting the post
    if current_user.est_modo == True or answer.auteur == current_user.get_id():

        # delete the post and commit the change into the database
        db.session.delete(answer)
        db.session.commit()

        return redirect(url_for("showPost", id_post=post_id))

    return handleException(401)

###################################
#           API ROUTES            #
###################################


api_url = "/api/"
api_messaging_url = api_url + "messaging/"


@app.route(api_url + "/<int:answer_id>/set-author-vote/<int:state>")
@login_required
def set_author_vote(answer_id, state):
    """
    Sets whether the author of a post superlikes an answer or not.

    Parameters
    ----------
    answer_id: The id of the answer
    state: 1 if the author superlikes the answer, 0 otherwise.
    """
    res: Reponse = Reponse.query.get(answer_id)

    # Checking if the answer exists
    if not res:
        raise Exception(
            'Impossible de trouver la réponse avec l\'id ' + str(answer_id))

    # Checking if the related post exists
    post = Post.query.get(res.post_id)
    if not post or post.auteur != current_user.matricule:
        raise Exception("Vous n'avez la permission d'exécuter cette action")

    # Casting to boolean
    user_like = True if state == 1 else False

    if user_like == True:
        # We get all the answers that have an author like for this post
        answers: List[Reponse] = db.session.query(Reponse).filter(
            and_(Reponse.like_auteur == True, Reponse.post_id == post.id)
        ).all()

        # We remove the author like on these answers
        for answer in answers:
            answer.like_auteur = False

    # We set the new value of the author like and commit to the database
    res.like_auteur = user_like
    db.session.commit()

    # We send back the answer
    return jsonify({"type": "success"})


@app.route(api_url + "/<int:answer_id>/like/<int:state>")
@login_required
def set_like_answer(answer_id, state):
    """
    Sets whether the user likes a answer or not.

    Parameters
    ----------
    answer_id: The id of the answer
    state: 1 if the user likes the answer, 0 otherwise
    """
    res: Reponse = Reponse.query.get(answer_id)

    # Checking if the answer exists
    if not res:
        raise Exception(
            'Impossible de trouver la réponse avec l\'id ' + str(answer_id))

    # Getting the like in the database (if it exists)
    like = db.session.query(Like).filter(
        and_(Like.auteur_id == current_user.matricule, Like.reponse_id == res.id)).first()

    if state == 1:
        if not like:
            # Creating the like if needed
            like = Like(auteur_id=current_user.matricule, reponse_id=res.id)
            db.session.add(like)
    else:
        if like:
            # Removing the like if it exists
            db.session.delete(like)
    db.session.commit()

    # We send back the answer
    return jsonify({"type": "success"})


@app.route(api_url + "/<int:id_post>/answers")
@login_required
def answers(id_post):
    list_reponse = Reponse.query.filter_by(post_id=id_post)

    return jsonify([{
        "id": reponse.id,
        "contenu": reponse.contenu,
        "auteur": reponse.auteur,
        "nbr_like": Like.query.filter_by(reponse_id=reponse.id).count(),
        "like_auteur": reponse.like_auteur,
        "likes": [l.auteur_id for l in reponse.likes]

    } for reponse in list_reponse])


@app.route(api_messaging_url + "<user>/after/<int:id>")
@login_required
def messages(user, id):
    """
    Get all the messages in a conversation between two users after a certain id.
    One of the users is the current user.

    Parameters
    ----------
    user: The second user's id (str)
    id: The id from which the messages are fetched (int)
    """
    conversation = current_user.get_conversation(user, id)
    return jsonify([{
        "id": message.id,
        "content": message.contenu,
        "s": message.destinateur,
        "r": message.destinataire,
        "t": floor(datetime.timestamp(message.date))
    } for message in conversation])


@app.route(api_messaging_url + "conversations")
@login_required
def myConversations():
    """
    Retrieves the list of conversations the user has with other users
    """
    conversations = current_user.get_conversations()
    return jsonify({
        "current_user": current_user.matricule,
        "conversations": [{"id": usr.matricule, "name": usr.prenom + " " + usr.nom} for usr in conversations]
    })


@app.route(api_messaging_url + "<user>", methods=["POST"])
@login_required
def sendMessage(user):
    """
    Sends a new message

    Parameters
    ----------
    user: The id of the user that we want to send the message to (str)

    Raises
    ------
    Exception: if the message provided is empty or not defined
    """
    # Checking that a message was provided by the user
    if not request.json or not request.json["message"]:
        raise Exception('Le message ne peut être vide')
    message: str = request.json["message"]
    if message.strip() == "":
        raise Exception("Le message ne peut être vide")

    # Creating the message
    message = Message(destinateur=current_user.matricule, destinataire=user,
                      contenu=message, date=datetime.now())

    # Adding the message in the database
    db.session.add(message)
    db.session.commit()

    # Sending the answer to the client
    return jsonify({"type": "success"})


@app.route(api_url + "courses")
@login_required
def get_courses():
    """
    Gets a list of all the courses for all the faculties.
    """
    return jsonify([{
        "faculty": f.nom,
        "courses": [
            {"name": c.titre, "id": c.matricule} for c in f.cours
        ]
    } for f in Faculte.query.all()])


@app.route(api_url + 'edit_profile', methods=['POST'])
@login_required
def edit_profile():
    prenom = request.json['prenom']
    nom = request.json['nom']
    anniversaire = datetime.strptime(request.json['anniversaire'], '%Y-%m-%d')

    if len(prenom) < 3:
        prenom = None

    if len(nom) < 2:
        nom = None

    if anniversaire.date() > datetime.today().date():
        anniversaire = None

    current_user.prenom = prenom
    current_user.nom = nom
    current_user.anniversaire = anniversaire
    db.session.commit()
    return jsonify({'prenom': prenom, 'nom': nom, 'anniversaire': datetime.strftime(anniversaire, '%d/%m/%Y')})

###################################
#          ERROR HANDLING         #
###################################


@app.errorhandler(Exception)
def handleException(e):
    """
    All the exceptions are caught here and an error page is generated
    """
    # In case it's an API error, we want to return json instead of html
    if request.path.startswith("/api/"):
        return jsonify({"type": "error", "message": str(e)}), e.code if isinstance(e, HTTPException) else 500

    # In all the other cases, we will return the html version
    return (
        render_template("error.html.j2", e=e),
        e.code if isinstance(e, HTTPException) else 500,
    )
