#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# stats_cableurs.py
# -----------------
#
# Copyright (C) 2008, 2009 François Bobot <bobot@crans.org>,
#                          Jeremie Dimino <jeremie@dimino.org>,
#                          Michel Blockelet <blockelet@crans.org>,
#                          Antoine Durand-Gasselin <adg@crans.org>
#
# This file 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 file 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 Street #330, Boston, MA 02111-1307, USA.

import sys, re, datetime
# hack temporaire pour pallier avoir la version 2.5 de optparse
#from optparse import OptionParser
sys.path.append('/usr/scripts/lib')
from optparse import OptionParser, OptionGroup

sys.path.append("/usr/scripts/gestion/")
from ldap_crans import CransLdap, Adherent
from config import ann_scol



db = CransLdap()

__DAYS_IN_PREVIOUS_MONTH = [datetime.timedelta(days) for days in
           [ 31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30 ] ]

def parse_ligne_histo(ligne):
    u"""Parse une ligne d'historique et renvoie (['dd', 'mm', 'yyyy'],
    ['date', 'heure', 'who', 'what'])"""
    champ = ligne.replace(',', '').replace(': ', '').split(' ')
    sdate = champ[0].split('/')
    date = datetime.date(int(sdate[2]), int(sdate[1]), int(sdate[0]))
    return date, champ

def parse_historique(truc):
    u"""Parse l'historique et renvoie la liste des actions sous la forme
    (['dd', 'mm', 'yyyy'], ['date', 'heure', 'who', 'what'])"""
    return [ parse_ligne_histo(ligne) for ligne in truc.historique() ]

def dec_date(date):
    u"""retranche un mois à la date."""
    return (date - __DAYS_IN_PREVIOUS_MONTH[date.month -1])



class StatsCableursBase:
    u"""Classe prototype pour afficher des stats de câbleurs."""

    colonnes = []
    nbmois = 4
    stats = {}
    score = {}
    kwargs = {}

    def __init__(self, **kwargs):

        self.kwargs.update(kwargs)
        self.affiche (self.kwargs, 'd')

        # On regarde s'il y a un mois à partir duquel on veuille partir.
        mois = kwargs.get("mois", -4)

        # On compte ensuite combien de mois ça fait
        if mois > 0:
            # si on demande depuis 09 (septembre) , on remonte
            # jusqu'à septembre
            self.nbmois = (datetime.date.today().month + 12 - mois) % 12
        else:
            # si on demande les stats sur 4 mois, on remonte sur 4
            # mois
            self.nbmois =  - mois

        # On regarde si on avait pas déjà précisé sur combien de mois on
        # voulait partir.
        self.nbmois = kwargs.get("nbmois", self.nbmois)

        # bout de code à mon avis explicite
        self.def_columns()
        self.affiche("Génération des stats")
        self.get_stats()
        self.calc_score()
        cableurs = self.sort_cableurs()
        self.print_stats(cableurs)


    def def_columns(self):
        u"""Fonction qui sera appelée pour éventuellement mettre à jour
        self.colonnes"""
        pass

    def get_stats(self):
        u"""Fonction censée récupérer les stats des différents câbleurs,
        et les fouttre dans self.stats"""
        raise NotImplementedError

    def calc_score(self):
        u"""Fonction censée calculer le score en fonction des stats, et
        le mettre dans self.score"""
        raise NotImplementedError

    def sort_cableurs(self):
        u"""Renvoie la liste des câbleurs triés, en fonction de leur score"""
        cableurs = self.stats.keys()
        cableurs.sort(lambda x, y : cmp(self.score[x], self.score[y]), reverse=True)

        if self.kwargs.get('top', 10) != 0:
            cableurs = cableurs[:self.kwargs.get('top', 10)]
        return cableurs

    def print_stats(self, cableurs):
        u"""Affiche pour chaque cableur les valeurs de ses différentes colonnes
        définies dans self.colonnes."""

        # On génère la liste des noms sous lesquels on va afficher nos câbleurs
        noms = {}
        for cableur in cableurs:
            try:
                ldap_cableur = db.search('login=%s' % cableur)['adherent'][0]
                ids = { 'nom' : ldap_cableur.nom(),
                        'prenom' : ldap_cableur.prenom(),
                        'droits' : reduce(unicode.__add__,
                                          [d[0] for d in ldap_cableur.droits()], u''),
                        'tel' : ldap_cableur.tel(),
                        'login' : cableur }
                noms[cableur] = self.kwargs.get("fqn", "%(prenom)s %(nom)s") % ids
            except IndexError:
                self.affiche ("Unknown cableur %s!" % cableur, 'd')
                noms[cableur] = cableur+'*'

        # On récupère la liste des câbleurs, triés en fonction de leur score
        long_max = reduce(lambda m, c : max(m, len(c)), noms.values(), len('cableur'))

        # Titres des colonnes
        ligne = u"%-*s" % (long_max, u'Câbleur')
        ligne += u" | Score"
        for col in self.colonnes:
            ligne += u" | %s" % col
        print(ligne)

        # Ligne pour délimiter
        ligne = u'-' * long_max
        ligne += u"-+-%s" % (u'-' * len ("score"))
        for col in self.colonnes:
            ligne += u"-+-%s" % (u'-' * len(col))
        print(ligne)

        # Statiqtiques par câbleur
        for cableur in cableurs:
            acts = self.stats[cableur]
            ligne = u"%-*s" % (long_max, noms[cableur])
            ligne += u" | %*d" % (len ("score"), self.score[cableur])
            for i in range (len (self.colonnes)):
                ligne += u" | %*d" % (len(self.colonnes[i]), acts[i])
            print(ligne)

    def sort_events(self, events):
        u"""Split une liste d'évènements selon les mois"""
        sorted_events = []
        since = datetime.date.replace(datetime.date.today(), day = 1)
        backwards = self.nbmois

        while backwards > 0:
            sorted_events.append([ ev for ev in events if ev[0] >= since ])
            events = [ ev for ev in events if ev[0] < since ]
            backwards -= 1
            since = dec_date(since)

        sorted_events.append(events)
        return sorted_events

    def affiche(self, msg, reqs= 'v'):
        u"""Fonction de print qui n'affiche que si on est dans un mode
        kikoolol, debug ou verbose suffisamment avancé."""
        try:
            pool = list (self.kwargs.get('flags', ''))
            for i in reqs:
                pool.remove(i)
            print msg
        except ValueError:
            return


class StatsCableursMachines(StatsCableursBase):
    u"""Cette classe s'occupe de recenser l'activité des différents
    câbleurs sur les machines, ce qui représente le travail typique du
    câbleur en milieu d'année"""

    kwargs = { 'fqn' : '%(login)s' , 'top' : 0 }

    def ajoute_actions(self, machine, hist):
        u"""Ajoute dans hist la liste des actions effectués par
        chacuns des câbleurs sur la machine pendant les mois."""

        for date, champ in parse_historique(machine):

            # on ne va pas non plus compter les actions qu'on fait sur
            # ses propres machines.
            proprio = machine.proprietaire()
            if isinstance(proprio, Adherent) and proprio.compte() != champ[2]:
                # Il se peut qu'une personne sans les droits câbleur ait
                # effectué une action.
                try:
                    hist[champ[2]].append( (date, champ[3]) )
                except KeyError:
                    hist[champ[2]] = [ (date, champ[3]) ]

    def def_columns(self):
        u"""Simplement une par mois, plus une pour toutes les actions
        d'avant."""

        month = datetime.date.replace(datetime.date.today(), day=1)
        for i in range(self.nbmois):
            self.colonnes.append("%d/%02d" % (month.month, month.year - 2000))
            month = dec_date(month)

        self.colonnes.append("< " + self.colonnes[-1])


    def get_stats(self):
        u"""Récupère les statistiques des différents câbleurs sur les
        différentes machines"""

        # La liste des câbleurs en tant qu'entité ldap, histoire de bien
        # faire apparaître ceux qui n'ont rien fait
        ldap_cableurs = db.search("droits=cableur")['adherent']

        # On récupère la liste des machines
        all_machines = db.search('mid=*')
        machines = all_machines['machineFixe'] + all_machines['machineWifi']

        # hists permet de répartir toutes les actions sur les différents
        # câbleurs.
        hists = {}
        for i in ldap_cableurs:
            hists[i.compte()] = []

        # on récupère l'historique des machines que l'on met dans hists
        for becane in machines:
            self.ajoute_actions(becane, hists)

        split_hist = {}
        for cableur in hists.keys():
            split_hist[cableur] = self.sort_events(hists[cableur])
            self.stats[cableur] = [ len(i) for i in split_hist[cableur] ]


    def calc_score(self):
        u"""Le calcul se fait simplement comme la somme des actions
        (indépendamment de leur type pour l'instant) sur les mois
        concernés."""

        for cableur in self.stats.keys():
            self.score[cableur] = sum (self.stats[cableur][:-1])



class StatsCableursAdherents(StatsCableursBase):
    u"""Cette classe s'occupe de recenser les actions que les câbleurs
    font sur les adhérents (en fait les {,re}adhésions)."""

    colonnes = [ u'Inscriptions', u'Réadhésions' ]

    debut_ann_scol = datetime.date(ann_scol, 8, 1)
    paiement_ann_scol = "paiement+%d" % ann_scol

    def donne_cableur(self, adherent):
        u""" Cherche le cableur qui a inscrit ou réinscrit un adhérent. """

        # Est-ce qu'on recherche une inscription ou une reinscription?
        date_inscr = datetime.date.fromtimestamp(adherent.dateInscription())
        if date_inscr < self.debut_ann_scol:
            action = 1 # 'reinscription'
            action_filtre = self.paiement_ann_scol
        else:
            action = 0 # 'inscription'
            action_filtre = 'inscription'

        for date, champ in parse_historique(adherent):
            if date >= self.debut_ann_scol:
                # Maintenant on regarde si l'action recherchee est ici
                if action_filtre in champ[3:]:
                    return action, champ[2]

        return action, None


    def get_stats(self):
        u"""Calcule le nombre d'inscriptions et réinscription pour tous
        les cableurs ayant inscrit ou réinscrit quelqu'un pour l'année
        en cours."""

        liste = db.search("paiement=%s" % ann_scol)['adherent']

        for adherent in liste:
            action, cableur = self.donne_cableur(adherent)
            if cableur:
                if not self.stats.has_key(cableur):
                    self.stats[cableur] = [0, 0]
                self.stats[cableur][action] += 1


    def calc_score(self):
        u"""Calcule le score d'un câbleur, en fonction du nombre
        d'adhésions qu'il a effectué"""

        # On calcule le score total pour chaque cableur
        for cableur in self.stats.keys():
            self.score[cableur] = 2 * self.stats[cableur][0] + self.stats[cableur][1]




def update_fqn(option, opt_str, value, this, fmt):
    u"""Utilisée dans le parser, cette fonction permet de mettre à jour
    correctement la chaîne de format"""
    try:
        if opt_str not in ['-d', '--droits']:
            if re.match ('^ \(%\(droits\)s\)', this.values.fqn):
                this.values.fqn = fmt + this.values.fqn
            else:
                this.values.fqn = this.values.fqn + (' (%s)' % fmt)
        else:
            this.values.fqn = this.values.fqn + fmt
    except TypeError:
        this.values.fqn = fmt

def notimplerr(option, opt_str, value, parseur):
    u"""Un callback pour le parser pour renvoyer un NotImplementedError"""
    raise NotImplementedError

if __name__ == "__main__":
    usage = "usage: %prog [options]\n       %prog [options] [--efficiency -e]"
    parser = OptionParser(usage=usage)

    # options pour décider le nombre de câbleurs à afficher
    group = OptionGroup(parser, "Options de filtrage",
                       u"Option pour filtrer les câbleurs à afficher")
    group.add_option('-a', '--all', help= u"Affiche tous les câbleurs",
                      action='store_const', const=0, dest='top')
    group.add_option('-t', '--top', metavar= "NB", type= 'int', dest='top',
                     help= u"N'affiche que les NB meilleurs câbleurs")
    parser.add_option_group(group)

    # options pour le format d'affichage des câbleurs
    group = OptionGroup(parser, u"Format d'affichage",
                       u"Pour définir le format d'affichage des câbleurs")
    group.add_option('-d', '--droits', help= u"Affiche les droits du câbleur",
                      action='callback', callback= update_fqn,
                      callback_kwargs= { 'fmt': ' (%(droits)s)'})
    group.add_option('-F', '--full-name',
                     help=u"Affiche Prenom Nom des câbleurs",
                     action='callback', callback= update_fqn,
                     callback_kwargs= { 'fmt': '%(prenom)s %(nom)s'})
    group.add_option('-l', '--login', help= u"Affiche le login des câbleurs",
                      action='callback', callback= update_fqn,
                      callback_kwargs= { 'fmt': '%(login)s'})
    parser.add_option('-T', '--tel', help= u"Affiche le n° de téléphone",
                      action='callback', callback= update_fqn,
                      callback_kwargs= { 'fmt': '%(tel)s'})
    group.add_option('--fqn', dest='fqn',
                      help= u"Définit le format d'affichage du nom du câbleur, les champs possibles sont %(droits)s, %(nom)s, %(prenom)s, %(login)s, %(tel)s")
    parser.add_option_group(group)

    # options de verbosité
    group = OptionGroup(parser, u"Commander la verbosité",
                       u"Pour définir ce que le script doit afficher en plus")
    group.add_option('-D', '--debug',
                     help= u"Affiche des informations de debuggage",
                     action='store_const', const='d', dest= 'debug')
    group.add_option('-k', '--kikoolol', help="Affiche des trucs kikoolol",
                      action='callback', callback= notimplerr)
    group.add_option('-v', '--verbose', help= u"Augmente la verbosité",
                      action='store_const', const= 'v', dest= 'verbose')
    parser.add_option_group(group)

    # options sur la durée étudiée
    group = OptionGroup(parser, u"Durée étudiée",
                       u"Pour définir la durée sur laquelle on travaille")
    group.add_option('-f', '--for', metavar= 'N',
                     help= u"Affiche les statistiqes depuis N mois",
                     type= 'int', dest='nbmois')
    group.add_option('-s', '--since', metavar= 'MM',
                     help= u'Affiche les stats depuis MOIS',
                     type= 'int', dest='mois')
    parser.add_option_group(group)

    # Devrait plutôt être un argument qu'une option
    parser.add_option('-e', '--efficiency',
                      help= u"Compte les actions effectuées sur les machines",
                      action= 'store_const',
                      const= StatsCableursMachines,
                      dest= 'default_stats',
                      default= StatsCableursAdherents)

    # on parse, enfin, on laisse optparse le faire pour nous
    (options, args) = parser.parse_args()

    # Parce que les clefs ont par défaut la valeur None
    kwargs = {}
    for lbl, field in options.__dict__.items():
        if field is not None and lbl != 'default_stats':
            kwargs[lbl] = field

    # XXX: l'action append_const dans optparse est ajoutée en Python 2.5
    flaglist = []
    if options.verbose:
        flaglist.append("v")
    if options.debug:
        flaglist.append("D")
    flags = "".join(flaglist)

    kwargs["flags"] = flags

    # On apelle les stats que l'on veut calculer
    options.default_stats(**kwargs)
