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

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

"""Import d'utilisateurs via fichier Excel
"""
import random
import time

from flask import url_for
from flask_login import current_user

from app import db
from app import email
from app.auth.models import User, UserRole
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc import sco_excel
from app.scodoc import sco_users


TITLES = (
    "user_name",
    "nom",
    "prenom",
    "email",
    "roles",
    "dept",
    "cas_id",
    "cas_allow_login",
    "cas_allow_scodoc_login",
    "email_institutionnel",
)
COMMENTS = (
    """user_name:
    L'identifiant (login).
    Composé de lettres (minuscules ou majuscules), de chiffres ou du caractère _
    """,
    """nom:
    Maximum 64 caractères.""",
    """prenom:
    Maximum 64 caractères.""",
    """email:
    L'adresse mail utilisée en priorité par ScoDoc pour contacter l'utilisateur.
    Maximum 120 caractères.""",
    """roles:
    un plusieurs rôles séparés par ','
    chaque rôle est fait de 2 composantes séparées par _:
    1. Le rôle (Ens, Secr ou Admin)
    2. Le département (en majuscule)
    Exemple: "Ens_RT,Admin_INFO".
    """,
    """dept:
    Le département d'appartenance de l'utilisateur. Laisser vide si l'utilisateur
    intervient dans plusieurs départements.
    """,
    """cas_id:
    identifiant de l'utilisateur sur CAS (optionnel).
    """,
    """cas_allow_login:
    autorise la connexion via CAS (optionnel, faux par défaut)
    """,
    """cas_allow_scodoc_login
    autorise connexion via ScoDoc même si CAS obligatoire (optionnel, faux par défaut)
    """,
    """email_institutionnel
    optionnel, le mail officiel de l'utilisateur.
    Maximum 120 caractères.""",
)


def generate_excel_sample():
    """generates an excel document suitable to import users"""
    style = sco_excel.excel_make_style(bold=True)
    titles = TITLES
    titles_styles = [style] * len(titles)
    return sco_excel.excel_simple_table(
        titles=titles,
        titles_styles=titles_styles,
        sheet_name="Utilisateurs ScoDoc",
        comments=COMMENTS,
    )


def read_users_excel_file(datafile, titles=TITLES) -> list[dict]:
    """Lit le fichier excel et renvoie liste de dict
    titles : titres (ids) des colonnes attendues
    """
    # Read the data from the stream
    exceldata = datafile.read()
    if not exceldata:
        raise ScoValueError("Ficher excel vide ou invalide")
    _, data = sco_excel.excel_bytes_to_list(exceldata)
    if not data:
        raise ScoValueError("Le fichier xlsx attendu semble vide !")
    # 1-  --- check title line
    xls_titles = [scu.stripquotes(s).lower() for s in data[0]]
    # log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
    # check cols
    cols = {}.fromkeys(titles)
    unknown = []
    for tit in xls_titles:
        if tit not in cols:
            unknown.append(tit)
        else:
            del cols[tit]
    if cols or unknown:
        raise ScoValueError(
            f"""colonnes incorrectes (on attend {len(titles)}, et non {len(xls_titles)})
            <br>
            (colonnes manquantes: {list(cols.keys())}, colonnes invalides: {unknown})
            """
        )
    # ok, same titles... : build the list of dictionaries
    users = []
    for line in data[1:]:
        d = {}
        for i, field in enumerate(xls_titles):
            d[field] = line[i]
        users.append(d)
    return users


def import_excel_file(datafile, force="") -> tuple[bool, list[str], int]:
    """
    Import scodoc users from Excel file.
    This method:
        * checks that the current_user has the ability to do so (at the moment only a SuperAdmin).
            He may thereoff import users with any well formed role into any department (or all)
        * Once the check is done ans successfull, build the list of users (does not check the data)
        * call :func:`import_users` to actually do the job

    :param datafile:  the stream to be imported
    :return: same as import users
    """
    if not current_user.is_administrator():
        raise AccessDenied(f"invalid user ({current_user}) must be SuperAdmin")

    log("sco_import_users.import_excel_file by {current_user}")

    users = read_users_excel_file(datafile)

    return import_users(users=users, force=force)


def import_users(users, force="") -> tuple[bool, list[str], int]:
    """
    Import users from a list of users_descriptors.

    descriptors are dictionaries hosting users's data.
    The operation is atomic (all the users are imported or none)

    :param users: list of descriptors to be imported

    :return: a tuple that describe the result of the import:
        * ok: import ok or aborted
        * messages: the list of messages
        * the # of users created

    Implémentation:
    Pour chaque utilisateur à créer:
        * vérifier données (y compris que le même nom d'utilisateur n'est pas utilisé
            plusieurs fois)
        * générer mot de passe aléatoire
        * créer utilisateur et mettre le mot de passe
        * envoyer mot de passe par mail
    Les utilisateurs à créer sont stockés dans un dictionnaire.
    L'ajout effectif ne se fait qu'en fin de fonction si aucune erreur n'a été détectée.
    """

    created = {}  # uid créés
    if len(users) == 0:
        import_ok = False
        msg_list = ["Feuille vide ou illisible"]
    else:
        msg_list = []
        line = 1  # start from excel line #2
        import_ok = True

        def append_msg(msg):
            msg_list.append(f"Ligne {line} : {msg}")

        try:
            for u in users:
                line = line + 1
                user_ok, msg = sco_users.check_modif_user(
                    0,
                    enforce_optionals=not force,
                    user_name=u["user_name"],
                    nom=u["nom"],
                    prenom=u["prenom"],
                    email=u["email"],
                    roles=[r for r in u["roles"].split(",") if r],
                    dept=u["dept"],
                    cas_id=u["cas_id"],
                )
                if not user_ok:
                    append_msg(f"""identifiant '{u["user_name"]}' {msg}""")

                u["passwd"] = generate_password()
                #
                # check identifiant
                if u["user_name"] in created.keys():
                    user_ok = False
                    append_msg(
                        f"""l'utilisateur '{u["user_name"]}' a déjà été décrit ligne {
                            created[u["user_name"]]["line"]}"""
                    )
                # check roles / ignore whitespaces around roles / build roles_string
                # roles_string (expected by User) appears as column 'roles' in excel file
                roles_list = []
                for role in u["roles"].split(","):
                    try:
                        role = role.strip()
                        if role:
                            _, _ = UserRole.role_dept_from_string(role)
                            roles_list.append(role)
                    except ScoValueError as value_error:
                        user_ok = False
                        append_msg(f"role {role} : {value_error}")
                u["roles_string"] = ",".join(roles_list)
                if user_ok:
                    u["line"] = line
                    created[u["user_name"]] = u
                else:
                    import_ok = False
        except ScoValueError as value_error:
            log(f"import_users: exception: abort create {str(created.keys())}")
            raise ScoValueError(msg) from value_error
        if import_ok:
            for u in created.values():
                # Création de l'utilisateur (via SQLAlchemy)
                user = User()
                user.from_dict(u, new_user=True)
                db.session.add(user)
                db.session.commit()
                mail_password(u)
        else:
            created = {}  # reset # of created users to 0
    return import_ok, msg_list, len(created)


#  --------- Génération du mot de passe initial -----------
# Adapté de http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440564
# Alphabet tres simple pour des mots de passe simples...


ALPHABET = r"""ABCDEFGHIJKLMNPQRSTUVWXYZ123456789123456789AEIOU"""
PASSLEN = 8
RNG = random.Random(time.time())


def generate_password():
    """This function creates a pseudo random number generator object, seeded with
    the cryptographic hash of the passString. The contents of the character set
    is then shuffled and a selection of passLength words is made from this list.
    This selection is returned as the generated password."""
    l = list(ALPHABET)  # make this mutable so that we can shuffle the characters
    RNG.shuffle(l)  # shuffle the character set
    # pick up only a subset from the available characters:
    return "".join(RNG.sample(l, PASSLEN))


def mail_password(user: dict, reset=False) -> None:
    "Send password by email"
    if not user["email"]:
        return

    user["url"] = url_for("scodoc.index", _external=True)
    txt = (
        """
Bonjour %(prenom)s %(nom)s,

"""
        % user
    )
    if reset:
        txt += (
            """
votre mot de passe ScoDoc a été ré-initialisé.

Le nouveau mot de passe est:  %(passwd)s
Votre nom d'utilisateur est %(user_name)s

Vous devrez changer ce mot de passe lors de votre première connexion
sur %(url)s
"""
            % user
        )
    else:
        txt += (
            """
vous avez été déclaré comme utilisateur du logiciel de gestion de scolarité ScoDoc.

Votre nom d'utilisateur est %(user_name)s
Votre mot de passe est: %(passwd)s

Le logiciel est accessible sur: %(url)s

Vous êtes invité à changer ce mot de passe au plus vite (cliquez sur votre nom en haut à gauche de la page d'accueil).
"""
            % user
        )

    txt += (
        """
_______
ScoDoc est un logiciel libre développé par Emmanuel Viennet et l'association ScoDoc.
Pour plus d'informations sur ce logiciel, voir %s

"""
        % scu.SCO_WEBSITE
    )

    if reset:
        subject = "Mot de passe ScoDoc"
    else:
        subject = "Votre accès ScoDoc"
    email.send_email(subject, email.get_from_addr(), [user["email"]], txt)