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

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 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
#
##############################################################################

"""Fonctions sur les utilisateurs
"""

# Anciennement ZScoUsers.py, fonctions de gestion des données réécrites avec flask/SQLAlchemy
import re

from flask import url_for, g, request
from flask_login import current_user


from app import db, Departement

from app.auth.models import Permission, Role, User, UserRole
from app.models import ScoDocSiteConfig, USERNAME_STR_LEN
from app.scodoc import html_sco_header
from app.scodoc import sco_preferences
from app.scodoc.gen_tables import GenTable
from app import cache

from app.scodoc.sco_exceptions import ScoValueError


def index_html(
    all_depts=False, having_role_name: str = "", with_inactives=False, fmt="html"
):
    "gestion utilisateurs..."
    all_depts = int(all_depts)
    with_inactives = int(with_inactives)

    H = [html_sco_header.html_sem_header("Gestion des utilisateurs")]

    if current_user.has_permission(Permission.UsersAdmin, g.scodoc_dept):
        H.append(
            f"""<p><a href="{url_for("users.create_user_form",
                scodoc_dept=g.scodoc_dept)
            }" class="stdlink">Ajouter un utilisateur</a>"""
        )
        if current_user.is_administrator():
            H.append(
                f"""&nbsp;&nbsp; <a href="{url_for("users.import_users_form",
                    scodoc_dept=g.scodoc_dept)
                }" class="stdlink">Importer des utilisateurs</a></p>"""
            )

        else:
            H.append(
                """&nbsp;&nbsp; Pour importer des utilisateurs en masse (via fichier xlsx)
                contactez votre administrateur scodoc."""
            )
    if all_depts:
        checked = "checked"
    else:
        checked = ""
    if with_inactives:
        olds_checked = "checked"
    else:
        olds_checked = ""

    menu_roles = "\n".join(
        f"""<option value="{r.name}" {
            'selected' if having_role_name == r.name else ''
        }>{r.name}</option>"""
        for r in Role.query.order_by(Role.name)
    )
    H.append(
        f"""
    <form name="f" action="{request.base_url}" method="get">
    <input type="checkbox" name="all_depts" value="1" onchange="document.f.submit();"
    {checked}>Tous les départements</input>
    <input type="checkbox" name="with_inactives" value="1" onchange="document.f.submit();"
    {olds_checked}>Avec anciens utilisateurs</input>

    <label for="having_role_name" style="margin-left:16px;">Filtrer par rôle:</label>
    <select id="having_role_name" name="having_role_name" onchange="document.f.submit();">
        <option value="">--Choisir--</option>
        {menu_roles}
    </select>

    </form>
    """
    )
    if having_role_name:
        having_role: Role = Role.query.filter_by(name=having_role_name).first()
        if not having_role:
            raise ScoValueError("nom de rôle invalide")
    else:
        having_role = None
    content = list_users(
        g.scodoc_dept,
        all_depts=all_depts,
        fmt=fmt,
        having_role=having_role,
        with_inactives=with_inactives,
        with_links=current_user.has_permission(Permission.UsersAdmin, g.scodoc_dept),
    )
    if fmt != "html":
        return content
    H.append(content)

    F = html_sco_header.sco_footer()
    return "\n".join(H) + F


def list_users(
    dept,
    all_depts=False,  # tous les departements
    fmt="html",
    having_role: Role = None,
    with_inactives=False,
    with_links=True,
):
    """List users, returns a table in the specified format.
    Si with_inactives, inclut les anciens utilisateurs (status "old").
    Si having_role, ne liste que les utilisateurs ayant ce rôle.
    """
    from app.scodoc.sco_permissions_check import can_handle_passwd

    if dept and not all_depts:
        users = get_user_list(
            dept=dept, having_role=having_role, with_inactives=with_inactives
        )
        comm = f"dept. {dept}"
    else:
        users = get_user_list(having_role=having_role, with_inactives=with_inactives)
        comm = "tous"
    if with_inactives:
        comm += ", avec anciens"
    comm = "(" + comm + ")"
    if having_role:
        comm += f" avec le rôle {having_role.name}"
    # -- Add some information and links:
    rows = []
    for u in users:
        # Can current user modify this user ?
        can_modify = can_handle_passwd(u, allow_admindepts=True)

        d = u.to_dict()
        rows.append(d)
        # Add links
        if with_links and can_modify:
            target = url_for(
                "users.user_info_page", scodoc_dept=dept, user_name=u.user_name
            )
            d["_user_name_target"] = target
            d["_nom_target"] = target
            d["_prenom_target"] = target

        # Hide passwd modification date (depending on visitor's permission)
        if can_modify:
            d["non_migre"] = (
                "NON MIGRÉ" if u.passwd_temp or u.password_scodoc7 else "ok"
            )
        else:
            d["date_modif_passwd"] = "(non visible)"
            d["non_migre"] = ""

    columns_ids = [
        "user_name",
        "nom_fmt",
        "prenom_fmt",
        "email",
        "dept",
        "roles_string",
        "date_expiration",
        "date_modif_passwd",
        "non_migre",
        "status_txt",
    ]
    # Seul l'admin peut voir les dates de dernière connexion
    # et les infos CAS
    if current_user.is_administrator():
        columns_ids.append("last_seen")
        if ScoDocSiteConfig.is_cas_enabled():
            columns_ids += [
                "cas_id",
                "cas_allow_login",
                "cas_allow_scodoc_login",
                "cas_last_login",
            ]
    columns_ids += ["email_institutionnel", "edt_id"]

    title = "Utilisateurs définis dans ScoDoc"
    tab = GenTable(
        rows=rows,
        columns_ids=columns_ids,
        titles={
            "user_name": "Login",
            "nom_fmt": "Nom",
            "prenom_fmt": "Prénom",
            "email": "Mail",
            "email_institutionnel": "Mail institutionnel (opt.)",
            "dept": "Dept.",
            "roles_string": "Rôles",
            "date_expiration": "Expiration",
            "date_modif_passwd": "Modif. mot de passe",
            "last_seen": "Dernière cnx.",
            "non_migre": "Non migré (!)",
            "status_txt": "Etat",
            "cas_id": "Id CAS",
            "cas_allow_login": "CAS autorisé",
            "cas_allow_scodoc_login": "Cnx sans CAS",
            "cas_last_login": "Dernier login CAS",
            "edt_id": "Identifiant emploi du temps",
        },
        caption=title,
        page_title="title",
        html_title=f"""<h2>{len(rows)} utilisateurs {comm}</h2>
        <p class="help">Cliquer sur un nom pour changer son mot de passe</p>""",
        html_class="table_leftalign list_users",
        html_with_td_classes=True,
        html_sortable=True,
        base_url="%s?all_depts=%s" % (request.base_url, 1 if all_depts else 0),
        pdf_link=False,  # table is too wide to fit in a paper page => disable pdf
        preferences=sco_preferences.SemPreferences(),
    )

    return tab.make_page(fmt=fmt, with_html_headers=False)


def get_users_count(dept=None) -> int:
    """Nombre de comptes utilisateurs, tout état confondu, dans ce dept
    (ou dans tous si None)"""
    q = User.query
    if dept is not None:
        q = q.filter_by(dept=dept)
    return q.count()


def get_user_list(
    dept=None, with_inactives=False, having_role: Role = None
) -> list[User]:
    """Returns list of users.
    If dept, select users from this dept,
    else return all users.
    """
    # was get_userlist
    q = User.query
    if dept is not None:
        q = q.filter_by(dept=dept)
    if not with_inactives:
        q = q.filter_by(active=True)
    if having_role:
        q = q.join(UserRole).filter_by(role_id=having_role.id)
    return q.order_by(User.nom, User.prenom, User.user_name).all()


@cache.memoize(timeout=50)  # seconds
def user_info(user_name_or_id=None, user: User = None):
    """Dict avec infos sur l'utilisateur (qui peut ne pas etre dans notre base).
    Si user_name est specifie (string ou id), interroge la BD. Sinon, user doit etre une instance
    de User.
    """
    if user_name_or_id is not None:
        if isinstance(user_name_or_id, int):
            u = User.query.filter_by(id=user_name_or_id).first()
        else:
            u = User.query.filter_by(user_name=user_name_or_id).first()
        if u:
            user_name = u.user_name
            info = u.to_dict()
        else:
            info = None
            user_name = "inconnu"
    else:
        info = user.to_dict()
        user_name = user.user_name

    if not info:
        # special case: user is not in our database
        return {
            "user_name": user_name,
            "nom": user_name,
            "prenom": "",
            "email": "",
            "dept": "",
            "nomprenom": user_name,
            "prenomnom": user_name,
            "prenom_fmt": "",
            "nom_fmt": user_name,
            "nomcomplet": user_name,
            "nomplogin": user_name,
            # "nomnoacc": scu.suppress_accents(user_name),
            "passwd_temp": 0,
            "status": "",
            "date_expiration": None,
        }
    else:
        # Ensure we never publish password hash
        if "password_hash" in info:
            del info["password_hash"]
        return info


MSG_OPT = """<br>Attention: (vous pouvez forcer l'opération en cochant "<em>Ignorer les avertissements</em>" en bas de page)"""


def check_modif_user(
    edit: bool,
    enforce_optionals: bool = False,
    user_name: str = "",
    nom: str = "",
    prenom: str = "",
    email: str = "",
    dept: str = "",
    roles: list = None,
    cas_id: str = None,
) -> tuple[bool, str]:
    """Vérifie que cet utilisateur peut être créé (edit=0) ou modifié (edit=1)
    Cherche homonymes.
    Ne vérifie PAS que l'on a la permission de faire la modif.

    edit: si vrai, mode "edition" (modif d'un objet existant)
    enforce_optionals: vérifie que les champs optionnels sont cohérents.

    Returns (ok, msg)
        - ok : si vrai, peut continuer avec ces parametres
            (si ok est faux, l'utilisateur peut quand même forcer la creation)
        - msg: message warning à presenter à l'utilisateur
    """
    roles = roles or []
    # ce login existe ?
    user = User.query.filter_by(user_name=user_name).first()
    if edit and not user:  # safety net, le user_name ne devrait pas changer
        return False, f"identifiant {user_name} inexistant"
    if not edit and user:
        return False, f"identifiant {user_name} déjà utilisé"
    if not user_name or not nom or not prenom:
        return False, "champ requis vide"
    if not re.match(r"^[a-zA-Z0-9@\\\-_\\\.]*$", user_name):
        return (
            False,
            f"identifiant '{user_name}' invalide (pas d'accents ni de caractères spéciaux)",
        )
    if len(user_name) > USERNAME_STR_LEN:
        return (
            False,
            f"identifiant '{user_name}' trop long ({USERNAME_STR_LEN} caractères)",
        )
    if len(nom) > USERNAME_STR_LEN:
        return False, f"nom '{nom}' trop long ({USERNAME_STR_LEN} caractères)" + MSG_OPT
    if len(prenom) > 64:
        return (
            False,
            f"prenom '{prenom}' trop long ({USERNAME_STR_LEN} caractères)" + MSG_OPT,
        )
    # check that same user_name has not already been described in this import
    if not email:
        return False, "vous devriez indiquer le mail de l'utilisateur créé !"
    if len(email) > 120:
        return False, f"email '{email}' trop long (120 caractères)"
    if not re.fullmatch(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", email):
        return False, "l'adresse mail semble incorrecte"
    # check département
    if (
        enforce_optionals
        and dept
        and Departement.query.filter_by(acronym=dept).first() is None
    ):
        return False, f"département '{dept}' inexistant" + MSG_OPT
    if enforce_optionals and not roles:
        return False, "aucun rôle sélectionné, êtes vous sûr ?" + MSG_OPT
    # Unicité du mail
    users_with_this_mail = User.query.filter_by(email=email).all()
    if edit:  # modification
        if email != user.email and len(users_with_this_mail) > 0:
            return False, "un autre utilisateur existe déjà avec cette adresse mail"
    else:  # création utilisateur
        if len(users_with_this_mail) > 0:
            return False, "un autre utilisateur existe déjà avec cette adresse mail"

    # Unicité du cas_id
    if cas_id:
        cas_users = User.query.filter_by(cas_id=str(cas_id)).all()
        if edit:
            if cas_users and (
                len(cas_users) > 1 or cas_users[0].user_name != user_name
            ):
                return (
                    False,
                    "un autre utilisateur existe déjà avec cet identifiant CAS",
                )
        elif cas_users:
            return False, "un autre utilisateur existe déjà avec cet identifiant CAS"
    # Des noms/prénoms semblables existent ?
    nom = nom.lower().strip()
    prenom = prenom.lower().strip()
    similar_users = User.query.filter(
        User.nom.ilike(nom), User.prenom.ilike(prenom)
    ).all()
    if edit:
        minmatch = 1
    else:
        minmatch = 0
    if enforce_optionals and len(similar_users) > minmatch:
        return (
            False,
            "des utilisateurs proches existent: "
            + ", ".join(
                [
                    "%s %s (pseudo=%s)" % (x.prenom, x.nom, x.user_name)
                    for x in similar_users
                ]
            )
            + MSG_OPT,
        )
    # Roles ?
    return True, ""