# -*- mode: python -*-
# -*- coding: utf-8 -*-

##############################################################################
#
# ScoDoc
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet.  All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#   Emmanuel Viennet      emmanuel.viennet@viennet.net
#
##############################################################################

"""
Module users: interface gestion utilisateurs
ré-écriture pour Flask ScoDoc7 / ZScoUsers.py

Vues s'appuyant sur auth et sco_users

Emmanuel Viennet, 2021
"""
import re
from xml.etree import ElementTree

from flask import g
from flask_login import current_user

from app import db

from app.auth.models import Permission
from app.auth.models import User
from app.auth.models import Role
from app.auth.models import UserRole
from app.decorators import (
    scodoc7func,
    ScoDoc7Context,
    permission_required,
)

from app.scodoc import html_sco_header
from app.scodoc import sco_users
from app.scodoc import sco_utils as scu
from app.scodoc import sco_xml
from app.scodoc.notes_log import log
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.sco_permissions_check import can_handle_passwd
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.views import users_bp as bp
from scodoc_manager import sco_mgr
from six.moves import range


context = ScoDoc7Context("users")  # sco8


@bp.route("/")
@bp.route("/index_html")
@permission_required(Permission.ScoUsersView)
@scodoc7func(context)
def index_html(context, REQUEST, all_depts=False, with_inactives=False, format="html"):
    return sco_users.index_html(
        context,
        REQUEST=REQUEST,
        all_depts=all_depts,
        with_inactives=with_inactives,
        format=format,
    )


@bp.route("/user_info")
@permission_required(Permission.ScoUsersView)
@scodoc7func(context)
def user_info(user_name, format="json", REQUEST=None):
    info = sco_users.user_info(user_name=user_name)
    return scu.sendResult(REQUEST, info, name="user", format=format)


@bp.route("/create_user_form", methods=["GET", "POST"])
@permission_required(Permission.ScoUsersAdmin)
@scodoc7func(context)
def create_user_form(context, REQUEST, user_name=None, edit=0):
    "form. creation ou edit utilisateur"
    auth_dept = current_user.dept
    initvalues = {}
    edit = int(edit)
    H = [html_sco_header.sco_header(context, REQUEST, bodyOnLoad="init_tf_form('')")]
    F = html_sco_header.sco_footer(context, REQUEST)
    if edit:
        if not user_name:
            raise ValueError("missing argument: user_name")
        u = User.query.filter_by(user_name=user_name).first()
        if not u:
            raise ScoValueError("utilisateur inexistant")
        initvalues = u.to_dict()
        H.append("<h2>Modification de l'utilisateur %s</h2>" % user_name)
    else:
        H.append("<h2>Création d'un utilisateur</h2>")

    is_super_admin = False
    if current_user.has_permission(Permission.ScoSuperAdmin, g.scodoc_dept):
        H.append("""<p class="warning">Vous êtes super administrateur !</p>""")
        is_super_admin = True

    # Les rôles standards créés à l'initialisation de ScoDoc:
    standard_roles = [Role.get_named_role(r) for r in (u"Ens", u"Secr", u"Admin")]
    # Rôles pouvant etre attribués aux utilisateurs via ce dialogue:
    #    si  SuperAdmin, tous les rôles standards dans tous les départements
    #    sinon, les départements dans lesquels l'utilisateur a le droit
    if is_super_admin:
        log("create_user_form called by %s (super admin)" % (current_user.user_name,))
        dept_ids = sco_mgr.get_dept_ids()
    else:
        # Si on n'est pas SuperAdmin, liste les départements dans lesquels on a la
        # permission ScoUsersAdmin
        dept_ids = sorted(
            set(
                [
                    x.dept
                    for x in UserRole.query.filter_by(user=current_user)
                    if x.role.has_permission(Permission.ScoUsersAdmin) and x.dept
                ]
            )
        )

    editable_roles_set = {(r, dept) for r in standard_roles for dept in dept_ids}
    #
    if not edit:
        submitlabel = "Créer utilisateur"
        orig_roles = set()
    else:
        submitlabel = "Modifier utilisateur"
        if "roles_string" in initvalues:
            initvalues["roles"] = initvalues["roles_string"].split(",")
        else:
            initvalues["roles"] = []
        orig_roles = {  # set des roles existants avant édition
            UserRole.role_dept_from_string(role_dept)
            for role_dept in initvalues["roles"]
        }
        if not initvalues["active"]:
            editable_roles_set = set()  # can't change roles of a disabled user
    editable_roles_strings = {r.name + "_" + dept for (r, dept) in editable_roles_set}
    orig_roles_strings = {r.name + "_" + dept for (r, dept) in orig_roles}
    # add existing user roles
    displayed_roles = list(editable_roles_set.union(orig_roles))
    displayed_roles.sort(key=lambda x: (x[1], x[0].name))
    displayed_roles_strings = [r.name + "_" + dept for (r, dept) in displayed_roles]
    displayed_roles_labels = [
        "{dept}: {r.name}".format(dept=dept, r=r) for (r, dept) in displayed_roles
    ]
    disabled_roles = {}  # pour desactiver les roles que l'on ne peut pas editer
    for i in range(len(displayed_roles_strings)):
        if displayed_roles_strings[i] not in editable_roles_strings:
            disabled_roles[i] = True

    descr = [
        ("edit", {"input_type": "hidden", "default": edit}),
        ("nom", {"title": "Nom", "size": 20, "allow_null": False}),
        ("prenom", {"title": "Prénom", "size": 20, "allow_null": False}),
    ]
    if current_user.user_name != user_name:
        # no one can change its own status
        descr.append(
            (
                "status",
                {
                    "title": "Statut",
                    "input_type": "radio",
                    "labels": ("actif", "ancien"),
                    "allowed_values": ("", "old"),
                },
            )
        )
    if not edit:
        descr += [
            (
                "user_name",
                {
                    "title": "Pseudo (login)",
                    "size": 20,
                    "allow_null": False,
                    "explanation": "nom utilisé pour la connexion. Doit être unique parmi tous les utilisateurs.",
                },
            ),
            (
                "passwd",
                {
                    "title": "Mot de passe",
                    "input_type": "password",
                    "size": 14,
                    "allow_null": False,
                },
            ),
            (
                "passwd2",
                {
                    "title": "Confirmer mot de passe",
                    "input_type": "password",
                    "size": 14,
                    "allow_null": False,
                },
            ),
        ]
    else:
        descr += [
            (
                "user_name",
                {"input_type": "hidden", "default": initvalues["user_name"]},
            ),
            ("user_name", {"input_type": "hidden", "default": initvalues["user_name"]}),
        ]
    descr += [
        (
            "email",
            {
                "title": "e-mail",
                "input_type": "text",
                "explanation": "vivement recommandé: utilisé pour contacter l'utilisateur",
                "size": 20,
                "allow_null": True,
            },
        )
    ]

    if not auth_dept:
        # si auth n'a pas de departement (admin global)
        # propose de choisir le dept du nouvel utilisateur
        # sinon, il sera créé dans le même département que auth
        descr.append(
            (
                "dept",
                {
                    "title": "Département",
                    "input_type": "text",
                    "size": 12,
                    "allow_null": True,
                    "explanation": """département d\'appartenance de l\'utilisateur (s'il s'agit d'un administrateur, laisser vide si vous voulez qu'il puisse créer des utilisateurs dans d'autres départements)""",
                },
            )
        )
        can_choose_dept = True
    else:
        can_choose_dept = False
        if edit:
            descr.append(
                (
                    "d",
                    {
                        "input_type": "separator",
                        "title": "L'utilisateur appartient au département %s"
                        % auth_dept,
                    },
                )
            )
        else:
            descr.append(
                (
                    "d",
                    {
                        "input_type": "separator",
                        "title": "L'utilisateur  sera crée dans le département %s"
                        % auth_dept,
                    },
                )
            )

    descr += [
        (
            "date_expiration",
            {
                "title": "Date d'expiration",  # j/m/a
                "input_type": "date",
                "explanation": "j/m/a, laisser vide si pas de limite",
                "size": 9,
                "allow_null": True,
            },
        ),
        (
            "roles",
            {
                "title": "Rôles",
                "input_type": "checkbox",
                "vertical": True,
                "labels": displayed_roles_labels,
                "allowed_values": displayed_roles_strings,
                "disabled_items": disabled_roles,
            },
        ),
        (
            "force",
            {
                "title": "Ignorer les avertissements",
                "input_type": "checkbox",
                "explanation": "passer outre les avertissements (homonymes, etc)",
                "labels": ("",),
                "allowed_values": ("1",),
            },
        ),
    ]
    if "tf-submitted" in REQUEST.form and not "roles" in REQUEST.form:
        REQUEST.form["roles"] = []
    if "tf-submitted" in REQUEST.form:
        # Ajoute roles existants mais non modifiables (disabled dans le form)
        REQUEST.form["roles"] = list(
            set(REQUEST.form["roles"]).union(
                orig_roles_strings - editable_roles_strings
            )
        )

    tf = TrivialFormulator(
        REQUEST.URL0,
        REQUEST.form,
        descr,
        initvalues=initvalues,
        submitlabel=submitlabel,
        cancelbutton="Annuler",
    )
    if tf[0] == 0:
        return "\n".join(H) + "\n" + tf[1] + F
    elif tf[0] == -1:
        return REQUEST.RESPONSE.redirect(scu.UsersURL())
    else:
        vals = tf[2]
        roles = set(vals["roles"]).intersection(editable_roles_strings)
        if "edit" in REQUEST.form:
            edit = int(REQUEST.form["edit"])
        else:
            edit = 0
        try:
            force = int(vals["force"][0])
        except (ValueError, TypeError):
            force = 0

        if edit:
            user_name = initvalues["user_name"]
        else:
            user_name = vals["user_name"]
        # ce login existe ?
        err = None
        users = sco_users._user_list(user_name)
        if edit and not users:  # safety net, le user_name ne devrait pas changer
            err = "identifiant %s inexistant" % user_name
        if not edit and users:
            err = "identifiant %s déjà utilisé" % user_name
        if err:
            H.append(tf_error_message("""Erreur: %s""" % err))
            return "\n".join(H) + "\n" + tf[1] + F

        if not force:
            ok, msg = sco_users.check_modif_user(
                edit,
                user_name=user_name,
                nom=vals["nom"],
                prenom=vals["prenom"],
                email=vals["email"],
                roles=vals["roles"],
            )
            if not ok:
                H.append(
                    tf_error_message(
                        """Attention: %s (vous pouvez forcer l'opération en cochant "<em>Ignorer les avertissements</em>" en bas de page)"""
                        % msg
                    )
                )

                return "\n".join(H) + "\n" + tf[1] + F

        if edit:  # modif utilisateur (mais pas passwd ni user_name !)
            if (not can_choose_dept) and "dept" in vals:
                del vals["dept"]
            if "passwd" in vals:
                del vals["passwd"]
            if "date_modif_passwd" in vals:
                del vals["date_modif_passwd"]
            if "user_name" in vals:
                del vals["user_name"]
            if (current_user.user_name == user_name) and "status" in vals:
                del vals["status"]  # no one can't change its own status

            # traitement des roles: ne doit pas affecter les roles
            # que l'on en controle pas:
            for role in orig_roles_strings:  # { "Ens_RT", "Secr_CJ", ... }
                if role and not role in editable_roles_strings:
                    roles.add(role)

            vals["roles_string"] = ",".join(roles)

            # ok, edit
            log("sco_users: editing %s by %s" % (user_name, current_user.user_name))
            log("sco_users: previous_values=%s" % initvalues)
            log("sco_users: new_values=%s" % vals)
            sco_users.user_edit(user_name, vals)
            return REQUEST.RESPONSE.redirect(
                "user_info_page?user_name=%s&head_message=Utilisateur %s modifié"
                % (user_name, user_name)
            )
        else:  # creation utilisateur
            vals["roles"] = ",".join(vals["roles"])
            # check identifiant
            if not re.match(r"^[a-zA-Z0-9@\\\-_\\\.]+$", vals["user_name"]):
                msg = tf_error_message(
                    "identifiant invalide (pas d'accents ni de caractères spéciaux)"
                )
                return "\n".join(H) + msg + "\n" + tf[1] + F
            # check passwords
            if vals["passwd"] != vals["passwd2"]:
                msg = tf_error_message(
                    """Les deux mots de passes ne correspondent pas !"""
                )
                return "\n".join(H) + msg + "\n" + tf[1] + F
            if not sco_users.is_valid_password(vals["passwd"]):
                msg = tf_error_message("""Mot de passe trop simple, recommencez !""")
                return "\n".join(H) + msg + "\n" + tf[1] + F
            if not can_choose_dept:
                vals["dept"] = auth_dept
            # ok, go
            log(
                "sco_users: new_user %s by %s"
                % (vals["user_name"], current_user.user_name)
            )
            u = User()
            u.from_dict(vals, new_user=True)
            db.session.add(u)
            db.session.commit()
            return REQUEST.RESPONSE.redirect(
                "user_info_page?user_name=%s&head_message=Nouvel utilisateur créé"
                % (user_name)
            )


@bp.route("/import_users_form")
def import_users_form():
    raise NotImplementedError()


@bp.route("/user_info_page")
@permission_required(Permission.ScoUsersView)
@scodoc7func(context)
def user_info_page(user_name, REQUEST=None):
    return sco_users.user_info_page(context, user_name=user_name, REQUEST=REQUEST)


@bp.route("/get_user_list_xml")
@permission_required(Permission.ScoView)
@scodoc7func(context)
def get_user_list_xml(dept=None, start="", limit=25, REQUEST=None):
    """Returns XML list of users with name (nomplogin) starting with start.
    Used for forms auto-completion."""
    userlist = sco_users.get_user_list(dept=dept)
    start = scu.suppression_diacritics(unicode(start, "utf-8"))  # utf8 #sco8
    start = scu.strlower(str(start))
    # TODO : à refaire avec une requete SQL #py3
    # (et en json)
    userlist = [
        user
        for user in userlist
        if scu.suppress_accents(scu.strlower(user.nom or "")).startswith(start)
    ]
    if REQUEST:
        REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
    doc = ElementTree.Element("results")
    for user in userlist[:limit]:
        x_rs = ElementTree.Element("rs", id=user.id, info="")
        x_rs.text = user.get_nomplogin()
        doc.append(x_rs)
    return sco_xml.XML_HEADER + ElementTree.tostring(doc)


@bp.route("/form_change_password")
@permission_required(Permission.ScoView)
@scodoc7func(context)
def form_change_password(REQUEST, user_name=None):
    """Formulaire de changement mot de passe de l'utilisateur user_name.
    Un utilisateur peut toujours changer son propre mot de passe.
    """
    if not user_name:
        u = current_user
    else:
        u = User.query.filter_by(user_name=user_name).first()
    H = [html_sco_header.sco_header(context, REQUEST, user_check=False)]
    F = html_sco_header.sco_footer(context, REQUEST)
    # check access
    if not can_handle_passwd(u):
        return (
            "\n".join(H)
            + "<p>Vous n'avez pas la permission de changer ce mot de passe</p>"
            + F
        )
    #
    H.append(
        """<h2>Changement du mot de passe de <font color="red">%(nomplogin)s</font></h2>
    <p>
    <form action="change_password" method="post"><table>
    <tr><td>Nouveau mot de passe:</td><td><input type="password" size="14" name="password"/></td></tr>
    <tr><td>Confirmation: </td><td><input type="password" size="14" name="password2" /></td></tr>
    </table>
    <input type="hidden" value="%(user_name)s" name="user_name">
    <input type="submit" value="Changer">
    </p>
    <p>Vous pouvez aussi: <a class="stdlink" href="reset_password_form?user_name=%(user_name)s">renvoyer un mot de passe aléatoire temporaire par mail à l'utilisateur</a>
"""
        % {"nomplogin": u.get_nomplogin(), "user_name": user_name}
    )
    return "\n".join(H) + F


@bp.route("/change_password", methods=["POST"])
@permission_required(Permission.ScoView)
@scodoc7func(context)
def change_password(user_name, password, password2, REQUEST):
    "Change the password for user given by user_name"
    u = User.query.filter_by(user_name=user_name).first()
    # Check access permission
    if not can_handle_passwd(u):
        # access denied
        log(
            "change_password: access denied (authuser=%s, user_name=%s, ip=%s)"
            % (REQUEST.AUTHENTICATED_USER, user_name, REQUEST.REMOTE_ADDR)
        )
        raise AccessDenied("vous n'avez pas la permission de changer ce mot de passe")
    H = []
    F = html_sco_header.sco_footer(context, REQUEST)
    # check password
    if password != password2:
        H.append(
            """<p>Les deux mots de passes saisis sont différents !</p>
        <p><a href="form_change_password?user_name=%s" class="stdlink">Recommencer</a></p>"""
            % user_name
        )
    else:
        if not sco_users.is_valid_password(password):
            H.append(
                """<p><b>ce mot de passe n\'est pas assez compliqué !</b><br/>(oui, il faut un mot de passe vraiment compliqué !)</p>
            <p><a href="form_change_password?user_name=%s" class="stdlink">Recommencer</a></p>
            """
                % user_name
            )
        else:
            # ok, strong password
            db.session.add(u)
            u.set_password(password)
            db.session.commit()
            #
            # ici page simplifiee car on peut ne plus avoir
            # le droit d'acceder aux feuilles de style
            H.append(
                "<h2>Changement effectué !</h2><p>Ne notez pas ce mot de passe, mais mémorisez le !</p><p>Rappel: il est <b>interdit</b> de communiquer son mot de passe à un tiers, même si c'est un collègue de confiance !</p><p><b>Si vous n'êtes pas administrateur, le système va vous redemander votre login et nouveau mot de passe au prochain accès.</b></p>"
            )
            return (
                """<?xml version="1.0" encoding="%s"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Mot de passe changé</title>
<meta http-equiv="Content-Type" content="text/html; charset=%s" />
<body><h1>Mot de passe changé !</h1>
"""
                % (scu.SCO_ENCODING, scu.SCO_ENCODING)
                + "\n".join(H)
                + '<a href="%s"  class="stdlink">Continuer</a></body></html>'
                % scu.ScoURL()
            )
    return html_sco_header.sco_header(context, REQUEST) + "\n".join(H) + F


@bp.route("/delete_user_form", methods=["GET", "POST"])
@permission_required(Permission.ScoUsersAdmin)
@scodoc7func(context)
def delete_user_form(REQUEST, user_name, dialog_confirmed=False):
    "delete user"
    u = User.query.filter_by(user_name=user_name).first()
    # Check access permission
    if not can_handle_passwd(u):
        # access denied (or non existent user)
        return (
            html_sco_header.sco_header(context, REQUEST, user_check=False)
            + "<p>Vous n'avez pas la permission de supprimer cet utilisateur</p>"
            + html_sco_header.sco_footer(context, REQUEST)
        )
    if not dialog_confirmed:
        return scu.confirm_dialog(
            context,
            """<h2>Confirmer la suppression de l\'utilisateur %s ?</h2>
            <p class="warning">En général, il est <b>déconseillé de supprimer un utilisateur</b>, son
            identité étant référencé dans les modules de formation. N'utilisez
            cette fonction qu'en cas d'erreur (création de doublons, etc).
            </p>
            """
            % user_name,
            dest_url="",
            REQUEST=REQUEST,
            cancel_url=scu.UsersURL(),
            parameters={"user_name": user_name},
        )

    db.session.delete(u)
    db.session.commit()

    return REQUEST.RESPONSE.redirect(
        scu.UsersURL() + r"?head_message=Utilisateur%20supprimé"
    )