#! /usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (C) Stéphane Glondu
# Licence : GPLv2

u"""Ce script permet au trésorier de repérer plus facilement les papiers que
les câbleurs n'ont pas encore rendus.

Utilisations possibles :
  * %(prog)s OPTIONS {paiement|carte|list}
  * %(prog)s OPTIONS mail <liste>

Les options sont:
  -d, --debug <adresse> envoyer tous les mails à l'<adresse> indiquée, plutôt
                        qu'aux vrais destinataires
  -h, --help            affiche ce message
  -t, --tri <champ>     trier suivant ce champ. Les champs possibles sont:
                        id, nom, cableur ou date. Le défaut est ``nom''
  -i, --inverse         trier par ordre inverse

Les commandes sont :
  * paiement            contrôle interactif des nouvelles (ré-)adhésions
  * carte               contrôle interactif des nouvelles cartes d'étudiant
  * list                affiche le récapitulatif des papiers manquants
  * mail <liste>        envoyer les mails de rappel aux câbleurs dont le
                        login est dans la <liste>, 'bureau' désignant le mail
                        récapitulatif envoyé à bureau ; si la liste est vide
                        tous les mails nécessaires seront envoyés
  * help                affiche ce message

Le modèle du mail envoyé aux câbleurs est lu sur l'entrée standard (typiquement
via une redirection). Les trois premières lignes doivent être :
        Encoding: <encodage du fichier>
        Subject: <sujet du mail>
        <ligne vide>
Le reste est le corps du message. Il doit contenir une occurrence de '%%s' qui
sera remplacée par la liste des papiers manquants.

Le mail récapitulatif envoyé à bureau est immuable."""


import sys, os, re, getopt
sys.path.append('/usr/scripts/gestion')

# Fonctions d'affichage
from affich_tools import coul, tableau, prompt, cprint

# Importation de la base de données
from ldap_crans import crans_ldap, ann_scol
db = crans_ldap()

# Détermination de l'uid du câbleur
from user_tests import getuser
uid = getuser()
if not uid :
    cprint(u"Impossible de déterminer l'utilisateur !")
    sys.exit(1)
cableur = db.search('uid=%s' % uid)['adherent'][0]

# Vérification des droits
if u'Tresorier' not in cableur.droits():
    cprint(u"Il faut etre tresorier pour exécuter ce script !")
    sys.exit(1)

# Lors des tests, on m'envoie tous les mails !
from socket import gethostname
debug = False

# Le champ sur lequel on veut trier (id, nom, cableur, date)
trier_par = "id"
tri_inverse = True

if gethostname().split(".")[0] == 'egon':
    debug = 'glondu@crans.org'
    ann_scol = 2004

def _controle_interactif_adherents(liste, quoi):
    """
    Contrôle interactif des adhérents de la liste (quoi = p ou c).
    Retourne (nb_OK, nb_pas_OK).
    """
    if quoi not in 'pc': raise ValueError
    explicite = {'p': u'du paiement', 'c': u"de la carte d'étudiant"}[quoi]
    restant = len(liste)
    if restant == 0:
        return 0, 0

    cprint(u'\nContrôle %s des adhérents' % explicite, 'cyan')
    cprint(u"Pour chaque entrée, il faut taper 'o' ou 'n' (défaut=n).")
    cprint(u"Une autre réponse entraîne l'interruption du processus.")
    cprint(u"Le format est [nb_restant] Nom, Prénom (aid).")
    cprint(u"")

    nb = 0
    for a in liste:
        ok = prompt(u'[%3d] %s, %s (%s) ?'
                    % (restant, a.nom(), a.prenom(), a.id()), 'n', '').lower()
        restant -= 1
        if ok == 'o':
            modifiable = db.search('aid=%s' % a.id(), 'w')['adherent'][0]
            if modifiable._modifiable:
                modifiable.controle('+%s' % quoi)
                cprint(modifiable.save())
                nb += 1
            else:
                cprint(u'Adhérent %s locké, réessayer plus tard' % modifiable.Nom(), 'rouge')
        elif ok != 'n':
            cprint(u'Arrêt du contrôle %s des adhérents' % explicite, 'rouge')
            break

    return nb, len(liste)-nb


def _controle_interactif_clubs(liste):
    """
    Contrôle interactif des clubs de la liste (uniquement la charte).
    Retourne (nb_OK, nb_pas_OK).
    """
    restant = len(liste)
    if restant == 0:
        return 0, 0

    cprint(u'\nContrôle de la charte des clubs', 'cyan')
    cprint(u"Pour chaque entrée, il faut taper 'o' ou 'n'.")
    cprint(u"Une autre réponse entraîne l'interruption du processus.")
    cprint(u"Le format est [nb_restant] Nom (cid).")
    cprint(u"")

    nb = 0
    for c in liste:
        ok = prompt(u'[%2d] %s (%s) ?'
                    % (restant, c.Nom(), c.id()), 'n', '').lower()
        restant -= 1
        if ok == 'o':
            modifiable = db.search('cid=%s' % c.id(), 'w')['club'][0]
            if modifiable._modifiable:
                modifiable.controle('+p')
                cprint(modifiable.save())
                nb += 1
            else:
                cprint(u'Club %s locké, réessayer plus tard' % modifiable.Nom(), 'rouge')
        elif ok != 'n':
            cprint(u'Arrêt du contrôle de la charte des clubs', 'rouge')
            break

    return nb, len(liste)-nb

def qui(historique, quoi):
    """
    Recherche le câbleur qui a effectué la dernière modification quoi
    dans l'historique, ou qui a inscrit l'adhérent.
    Retourne le couple (date, cableur) ou ('_inconnu_', '_inconnu_').
    """
    regexp = re.compile(r'^([^,]*), ([^ :]*)')
    cableur = ('_inconnu_', '_inconnu_')

    champ = re.compile(quoi)

    for ligne in historique:
        if champ.search(ligne) or 'inscription' in ligne:
            matched = regexp.search(ligne)
            if matched: cableur = matched.group(1), matched.group(2)

    return cableur

def controle_interactif(quoi):
    """
    Procédure interactive de contrôle des paiements/chartes (quoi=p) et cartes (quoi=c).
    """
    if quoi not in 'pc': raise ValueError
    todo_list = db.search('%s=%d&controle!=*%s*'
                          % ({'p': 'paiement', 'c': 'carteEtudiant'}[quoi], ann_scol, quoi))

    # Tri de la liste des adhérents selon nom, prénom
    todo_list['adherent'].sort(key = lambda x: (x.nom(), x.prenom()))

    if trier_par == 'id':
        todo_list['adherent'].sort(key = lambda x: int(x.id()))
    elif trier_par == 'nom':
        pass
    else:
        champ = { 'date': 0, 'cableur': 1 }[trier_par]
        todo_list['adherent'].sort(key = lambda x: qui(x.historique(), r'paiement')[champ])

    # Traitement des adhérents
    oka, noka = _controle_interactif_adherents(todo_list['adherent'], quoi)

    if quoi == 'p':
        # Tri de la liste des clubs
        todo_list['club'].sort(lambda x, y: cmp(x.nom(), y.nom()))

        # Traitement des clubs (uniquement la charte)
        okc, nokc =_controle_interactif_clubs(todo_list['club'])

    cprint(u'\nRécapitulatif des nouveaux contrôles +%s :' % quoi, 'violet')
    liste = [[u'adhérents', str(oka), str(noka)]]
    if quoi == 'p':
        liste.append([u'clubs', str(okc), str(nokc)])
    cprint(tableau(liste,
                  titre = [u'Catégorie', u'OK', u'pas OK'],
                  largeur = [15, 10, 10]))


def formater_pour_cableur(liste):
    """
    Formate la liste d'adhérents ou de clubs avec les dates correspondantes.
    liste est une liste de couples (date, objet).
    """
    lignes = []
    total = 0
    liste.sort(lambda x, y: cmp(x[1].nom(), y[1].nom()))

    for date, a in liste:
        lignes.append([a.id(), a.Nom(), date])
        total += 1

    if trier_par == 'id':
        lignes.sort(key = lambda ligne: int(ligne[0]))
    else:
        champ = {'nom' : 1, 'cableur' : 1, 'date' : 2}[trier_par]
        lignes.sort(key = lambda ligne: ligne[champ])

    if tri_inverse: lignes.reverse()

    return tableau(lignes,
                   titre = [u'id', u'Nom', u'Date Heure'],
                   largeur = [6, 40, 18],
                   alignement = ['d', 'c', 'c']) + u'\nTotal : %d' % total


def formater_pour_bureau(dico):
    """
    Formate la liste d'adhérents ou de clubs avec les câbleurs correspondantes
    pour le mail récapitulatif envoyé à bureau.
    """
    lignes = []
    total = 0

    liste = dico.keys()
    liste.sort()
    for cableur in liste:
        for date, a in dico[cableur]:
            lignes.append([a.id(), a.Nom(), cableur, date])
            total += 1

    if trier_par == 'id':
        lignes.sort(key = lambda ligne: int(ligne[0]))
    else:
        champ = {'nom' : 1, 'cableur' : 2, 'date' : 3}[trier_par]
        lignes.sort(key = lambda ligne: ligne[champ])

    if tri_inverse: lignes.reverse()

    return tableau(lignes,
                   titre = [u'id', u'Nom', u'Câbleur', u'Date'],
                   largeur = [6, 40, 18, 18],
                   alignement = ['d', 'c', 'c', 'c']) + u'\nTotal : %d' % total


def chercher_cableurs(liste, quoi):
    """
    Renvoie un dictionnaire cableur->adherents à partir de liste, quoi
    désigne le champ qui sera recherché dans l'historique.
    """
    resultat = {}

    for a in liste:
        date, cableur = qui(a.historique(), quoi)
        if not resultat.has_key(cableur): resultat[cableur] = []
        resultat[cableur].append((date, a))

    return resultat


from email_tools import send_email, parse_mail_template

# Mise en application du mémorable cours de Fred sur les objets :-)

class ControleMailer:

    def __init__(self):
        # Recherche des câbleurs possédant des cotisations/chartes
        todo_list = db.search('paiement=%d&controle!=*p*' % ann_scol)
        self._paiements = chercher_cableurs(todo_list['adherent'], r'(paiement|controle.*\+k)')
        self._chartes = chercher_cableurs(todo_list['club'], 'paiement')

        # Recherche des câbleurs possédant des cartes d'étudiant
        todo_list = db.search('carteEtudiant=%d&controle!=*c*' % ann_scol)
        self._cartes = chercher_cableurs(todo_list['adherent'], 'carteEtudiant')

        # Récupère tous les câbleurs qui doivent quelque chose
        cableurs = {}
        for a in self._paiements.keys():
            if a != '_inconnu_': cableurs[a] = None
        for a in self._chartes.keys():
            if a != '_inconnu_': cableurs[a] = None
        for a in self._cartes.keys():
            if a != '_inconnu_': cableurs[a] = None
        self._cableurs = cableurs.keys()

        # Vérification de l'alias pour l'expéditeur
        if u'tresorier' in cableur.alias():
            self._sender = u'%s <tresorier@crans.org>' % cableur.Nom()
        else:
            self._sender = u'%s <%s@crans.org>' % (cableur.Nom(),
                                                   cableur.canonical_alias() or cableur.mail())

        # Se connecte au serveur SMTP par défaut
        from smtplib import SMTP
        self._server = SMTP()
        self._server.connect()

    def __del__(self):
        # Termine la connexion au serveur SMTP
        self._server.quit()

    def recapitulatif(self):
        """
        Renvoie le récapitulatif pour le bureau.
        """
        msg = u''

        if self._paiements:
            msg += u"Fiches d'adhésion et cotisations et/ou caution des adhérents :\n"
            msg += formater_pour_bureau(self._paiements) + '\n\n'

        if self._chartes:
            msg += u"Chartes signées des clubs :\n"
            msg += formater_pour_bureau(self._chartes) + '\n\n'

        if self._cartes:
            msg += u"Carte d'étudiant des adhérents :\n"
            msg += formater_pour_bureau(self._cartes) + '\n\n'

        return msg.strip()

    def mail_bureau(self):
        """
        Envoie le mail récapitulatif à bureau (s'il y a qqch à envoyer).
        """
        msg = self.recapitulatif()
        if msg:
            msg += u"\n\n-- \nScript exécuté par %s\n" % cableur.Nom()
            send_email(self._sender,
                       u"Bureau <bureau@crans.org>",
                       u"Récapitulatif des papiers manquants",
                       msg,
                       server = self._server,
                       debug = debug)
            return coul(u'Mail envoyé à bureau avec succès !', 'vert')
        else:
            return coul(u'Tout est à jour, aucun mail envoyé.', 'vert')

    def mail_cableurs(self, subject, body, liste=None):
        """
        Envoie un mail aux câbleurs concernés qui figurent dans la liste.
        Si liste vaut None, envoie un mail à tous les câbleurs concernés.
        Les arguments subject, body permettent de personnaliser (un peu)
        le mail qui sera envoyé.
        """
        nb = 0
        if liste == None:
            liste = self._cableurs

        for c in liste:
            msg = u''

            if self._paiements.has_key(c):
                msg += u"Fiches d'adhésion, cotisations et/ou caution des adhérents :\n"
                msg += formater_pour_cableur(self._paiements[c]) + '\n\n'

            if self._chartes.has_key(c):
                msg += u"Chartes signées des clubs :\n"
                msg += formater_pour_cableur(self._chartes[c]) + '\n\n'

            if self._cartes.has_key(c):
                msg += u"Carte d'étudiant des adhérents :\n"
                msg += formater_pour_cableur(self._cartes[c]) + '\n\n'

            if msg:
                msg = msg.strip()
                send_email(self._sender,
                           "%s@crans.org" % c,
                           subject,
                           body % msg,
                           server = self._server,
                           debug = debug)
                nb += 1

        return coul(u'%d mail(s) envoyé(s) aux câbleurs avec succès !' % nb, 'vert')


def __usage(message=None):
    """ Comment ça marche ? """
    cprint(__doc__ % { 'prog': sys.argv[0] })
    if message:
        cprint(message)
    sys.exit(1)


if __name__ == '__main__' :

    try:
        options, arg = getopt.getopt(sys.argv[1:], 'ht:d:i', [ 'help', 'debug=', 'tri=', 'inverse' ])
    except getopt.error, msg:
        __usage(unicode(msg))

    for opt, val in options:
        if opt in [ '-h', '--help' ]:
            __usage()

        elif opt in [ '-d', '--debug' ]:
            debug = val
            cprint(u'Mode debug, tous les mails seront envoyés à %s.' % debug)

        elif opt in [ '-t', '--tri' ]:
            if val in [ 'id', 'nom', 'cableur', 'date' ]:
                trier_par = val
            else:
                __usage(u'Champ de tri invalie %s' % val)

        elif opt in [ '-i', '--inverse' ]:
            tri_inverse = True

        else:
            __usage("option inconnue « %s »'" % opt)

    if len(arg) == 0 or arg[0] == 'help':
        __usage()

    elif arg[0] == 'paiement':
        if len(arg) != 1:
            __usage(u'Mauvaise utilisation de paiement')
        controle_interactif('p')

    elif arg[0] == 'carte':
        if len(arg) != 1:
            __usage(u'Mauvaise utilisation de carte')
        controle_interactif('c')

    elif arg[0] == 'list':
        if len(arg) != 1:
            __usage(u'Mauvaise utilisation de list')
        cprint(ControleMailer().recapitulatif(), newline=False)

    elif arg[0] == 'mail':
        mailer = ControleMailer()
        cableurs = arg[1:]
        if cableurs:
            bureau = False
            if 'bureau' in cableurs:
                cableurs.remove('bureau')
                bureau = True
        else:
            bureau = True
            cableurs = mailer._cableurs
        if cableurs:
            cprint(u'Des mails vont être envoyés aux câbleurs, lecture du modèle...')
            subject, body = parse_mail_template(sys.stdin)
            try:
                body % u''
            except TypeError:
                cprint(u"Le format du modèle n'est pas correct, arrêt.")
                sys.exit(1)
            cprint(u'Modèle OK, on envoie les mails...')
            cprint(mailer.mail_cableurs(subject, body, cableurs))
        if bureau:
            cprint(mailer.mail_bureau())

    else:
        __usage(u'Commande inconnue : %s' % arg[0])

    sys.exit(0)


# pydoc n'aime pas l'unicode :-(
