# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2018  Gabriel Détraz
# Copyright © 2018  Goulven Kermarec
# Copyright © 2018  Augustin Lemesle
# Copyright © 2018  Hugo Levy-Falk
#
# 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.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

# App de gestion des statistiques pour re2o
# Gabriel Détraz
# Gplv2
"""
Vues des logs et statistiques générales.

La vue index générale affiche une selection des dernières actions,
classées selon l'importance, avec date, et user formatés.

Stats_logs renvoie l'ensemble des logs.

Les autres vues sont thématiques, ensemble des statistiques et du
nombre d'objets par models, nombre d'actions par user, etc
"""

from __future__ import unicode_literals
from itertools import chain

from django.urls import reverse
from django.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import Http404
from django.db.models import Count
from django.apps import apps
from django.utils.translation import ugettext as _

from reversion.models import Revision
from reversion.models import Version, ContentType

from users.models import (
    User,
    ServiceUser,
    School,
    ListRight,
    ListShell,
    Ban,
    Whitelist,
    Adherent,
    Club
)
from cotisations.models import (
    Facture,
    Vente,
    Article,
    Banque,
    Paiement,
    Cotisation
)
from machines.models import (
    Machine,
    MachineType,
    IpType,
    Extension,
    Interface,
    Domain,
    IpList,
    OuverturePortList,
    Service,
    Vlan,
    Nas,
    SOA,
    Mx,
    Ns
)
from topologie.models import (
    Switch,
    Port,
    Room,
    Stack,
    ModelSwitch,
    ConstructorSwitch,
    AccessPoint
)
from preferences.models import GeneralOption
from re2o.views import form
from re2o.utils import (
    all_whitelisted,
    all_baned,
    all_has_access,
    all_adherent,
    re2o_paginator,
)
from re2o.acl import (
    can_view_all,
    can_view_app,
    can_edit_history,
)
from re2o.utils import all_active_assigned_interfaces_count
from re2o.utils import all_active_interfaces_count, SortTable


@login_required
@can_view_app('logs')
def index(request):
    """Affiche les logs affinés, date reformatées, selectionne
    les event importants (ajout de droits, ajout de ban/whitelist)"""
    pagination_number = GeneralOption.get_cached_value('pagination_number')
    # The types of content kept for display
    content_type_filter = ['ban', 'whitelist', 'vente', 'interface', 'user']
    # Select only wanted versions
    versions = Version.objects.filter(
        content_type__in=ContentType.objects.filter(
            model__in=content_type_filter
        )
    ).select_related('revision')
    versions = SortTable.sort(
        versions,
        request.GET.get('col'),
        request.GET.get('order'),
        SortTable.LOGS_INDEX
    )
    versions = re2o_paginator(request, versions, pagination_number)
    # Force to have a list instead of QuerySet
    versions.count(0)
    # Items to remove later because invalid
    to_remove = []
    # Parse every item (max = pagination_number)
    for i in range(len(versions.object_list)):
        if versions.object_list[i].object:
            version = versions.object_list[i]
            versions.object_list[i] = {
                'rev_id': version.revision.id,
                'comment': version.revision.comment,
                'datetime': version.revision.date_created.strftime(
                    '%d/%m/%y %H:%M:%S'
                ),
                'username':
                    version.revision.user.get_username()
                    if version.revision.user else '?',
                'user_id': version.revision.user_id,
                'version': version}
        else:
            to_remove.insert(0, i)
    # Remove all tagged invalid items
    for i in to_remove:
        versions.object_list.pop(i)
    return render(request, 'logs/index.html', {'versions_list': versions})


@login_required
@can_view_all(GeneralOption)
def stats_logs(request):
    """Affiche l'ensemble des logs et des modifications sur les objets,
    classés par date croissante, en vrac"""
    pagination_number = GeneralOption.get_cached_value('pagination_number')
    revisions = Revision.objects.all().select_related('user')\
        .prefetch_related('version_set__object')
    revisions = SortTable.sort(
        revisions,
        request.GET.get('col'),
        request.GET.get('order'),
        SortTable.LOGS_STATS_LOGS
    )
    revisions = re2o_paginator(request, revisions, pagination_number)
    return render(request, 'logs/stats_logs.html', {
        'revisions_list': revisions
    })


@login_required
@can_edit_history
def revert_action(request, revision_id):
    """ Annule l'action en question """
    try:
        revision = Revision.objects.get(id=revision_id)
    except Revision.DoesNotExist:
        messages.error(request, _("Nonexistent revision."))
    if request.method == "POST":
        revision.revert()
        messages.success(request, _("The action was deleted."))
        return redirect(reverse('logs:index'))
    return form({
        'objet': revision,
        'objet_name': revision.__class__.__name__
    }, 'logs/delete.html', request)


@login_required
@can_view_all(IpList, Interface, User)
def stats_general(request):
    """Statistiques générales affinées sur les ip, activées, utilisées par
    range, et les statistiques générales sur les users : users actifs,
    cotisants, activés, archivés, etc"""
    ip_dict = dict()
    for ip_range in IpType.objects.select_related('vlan').all():
        all_ip = IpList.objects.filter(ip_type=ip_range)
        used_ip = Interface.objects.filter(ipv4__in=all_ip).count()
        active_ip = all_active_assigned_interfaces_count().filter(
            ipv4__in=IpList.objects.filter(ip_type=ip_range)
        ).count()
        ip_dict[ip_range] = [ip_range, ip_range.vlan, all_ip.count(),
                             used_ip, active_ip, all_ip.count()-used_ip]
    _all_adherent = all_adherent()
    _all_has_access = all_has_access()
    _all_baned = all_baned()
    _all_whitelisted = all_whitelisted()
    _all_active_interfaces_count = all_active_interfaces_count()
    _all_active_assigned_interfaces_count = \
        all_active_assigned_interfaces_count()
    stats = [
        [   # First set of data (about users)
            [   # Headers
                _("Category"),
                _("Number of users (members and clubs)"),
                _("Number of members"),
                _("Number of clubs")
            ],
            {   # Data
                'active_users': [
                    _("Activated users"),
                    User.objects.filter(state=User.STATE_ACTIVE).count(),
                    (Adherent.objects
                     .filter(state=Adherent.STATE_ACTIVE)
                     .count()),
                    Club.objects.filter(state=Club.STATE_ACTIVE).count()
                ],
                'inactive_users': [
                    _("Disabled users"),
                    User.objects.filter(state=User.STATE_DISABLED).count(),
                    (Adherent.objects
                     .filter(state=Adherent.STATE_DISABLED)
                     .count()),
                    Club.objects.filter(state=Club.STATE_DISABLED).count()
                ],
                'archive_users': [
                    _("Archived users"),
                    User.objects.filter(state=User.STATE_ARCHIVE).count(),
                    (Adherent.objects
                     .filter(state=Adherent.STATE_ARCHIVE)
                     .count()),
                    Club.objects.filter(state=Club.STATE_ARCHIVE).count()
                ],
                'adherent_users': [
                    _("Contributing members"),
                    _all_adherent.count(),
                    _all_adherent.exclude(adherent__isnull=True).count(),
                    _all_adherent.exclude(club__isnull=True).count()
                ],
                'connexion_users': [
                    _("Users benefiting from a connection"),
                    _all_has_access.count(),
                    _all_has_access.exclude(adherent__isnull=True).count(),
                    _all_has_access.exclude(club__isnull=True).count()
                ],
                'ban_users': [
                    _("Banned users"),
                    _all_baned.count(),
                    _all_baned.exclude(adherent__isnull=True).count(),
                    _all_baned.exclude(club__isnull=True).count()
                ],
                'whitelisted_user': [
                    _("Users benefiting from a free connection"),
                    _all_whitelisted.count(),
                    _all_whitelisted.exclude(adherent__isnull=True).count(),
                    _all_whitelisted.exclude(club__isnull=True).count()
                ],
                'actives_interfaces': [
                    _("Active interfaces (with access to the network)"),
                    _all_active_interfaces_count.count(),
                    (_all_active_interfaces_count
                     .exclude(machine__user__adherent__isnull=True)
                     .count()),
                    (_all_active_interfaces_count
                     .exclude(machine__user__club__isnull=True)
                     .count())
                ],
                'actives_assigned_interfaces': [
                    _("Active interfaces assigned IPv4"),
                    _all_active_assigned_interfaces_count.count(),
                    (_all_active_assigned_interfaces_count
                     .exclude(machine__user__adherent__isnull=True)
                     .count()),
                    (_all_active_assigned_interfaces_count
                     .exclude(machine__user__club__isnull=True)
                     .count())
                ]
            }
        ],
        [   # Second set of data (about ip adresses)
            [   # Headers
                _("IP range"),
                _("VLAN"),
                _("Total number of IP addresses"),
                _("Number of assigned IP addresses"),
                _("Number of IP address assigned to an activated machine"),
                _("Number of nonassigned IP addresses")
            ],
            ip_dict  # Data already prepared
        ]
    ]
    return render(request, 'logs/stats_general.html', {'stats_list': stats})


@login_required
@can_view_app('users', 'cotisations', 'machines', 'topologie')
def stats_models(request):
    """Statistiques générales, affiche les comptages par models:
    nombre d'users, d'écoles, de droits, de bannissements,
    de factures, de ventes, de banque, de machines, etc"""
    stats = {
        _("Users"): {
            'users': [User._meta.verbose_name, User.objects.count()],
            'adherents': [Adherent._meta.verbose_name, Adherent.objects.count()],
            'clubs': [Club._meta.verbose_name, Club.objects.count()],
            'serviceuser': [ServiceUser._meta.verbose_name,
                            ServiceUser.objects.count()],
            'school': [School._meta.verbose_name, School.objects.count()],
            'listright': [ListRight._meta.verbose_name, ListRight.objects.count()],
            'listshell': [ListShell._meta.verbose_name, ListShell.objects.count()],
            'ban': [Ban._meta.verbose_name, Ban.objects.count()],
            'whitelist': [Whitelist._meta.verbose_name, Whitelist.objects.count()]
        },
        _("Subscriptions"): {
            'factures': [
                Facture._meta.verbose_name,
                Facture.objects.count()
            ],
            'vente': [
                Vente._meta.verbose_name,
                Vente.objects.count()
            ],
            'cotisation': [
                Cotisation._meta.verbose_name,
                Cotisation.objects.count()
            ],
            'article': [
                Article._meta.verbose_name,
                Article.objects.count()
            ],
            'banque': [
                Banque._meta.verbose_name,
                Banque.objects.count()
            ],
        },
        _("Machines"): {
            'machine': [Machine._meta.verbose_name,
                        Machine.objects.count()],
            'typemachine': [MachineType._meta.verbose_name,
                            MachineType.objects.count()],
            'typeip': [IpType._meta.verbose_name,
                       IpType.objects.count()],
            'extension': [Extension._meta.verbose_name,
                          Extension.objects.count()],
            'interface': [Interface._meta.verbose_name,
                          Interface.objects.count()],
            'alias': [Domain._meta.verbose_name,
                      Domain.objects.exclude(cname=None).count()],
            'iplist': [IpList._meta.verbose_name,
                       IpList.objects.count()],
            'service': [Service._meta.verbose_name,
                        Service.objects.count()],
            'ouvertureportlist': [
                OuverturePortList._meta.verbose_name,
                OuverturePortList.objects.count()
            ],
            'vlan': [Vlan._meta.verbose_name, Vlan.objects.count()],
            'SOA': [SOA._meta.verbose_name, SOA.objects.count()],
            'Mx': [Mx._meta.verbose_name, Mx.objects.count()],
            'Ns': [Ns._meta.verbose_name, Ns.objects.count()],
            'nas': [Nas._meta.verbose_name, Nas.objects.count()],
        },
        _("Topology"): {
            'switch': [Switch._meta.verbose_name,
                       Switch.objects.count()],
            'bornes': [AccessPoint._meta.verbose_name,
                       AccessPoint.objects.count()],
            'port': [Port._meta.verbose_name, Port.objects.count()],
            'chambre': [Room._meta.verbose_name, Room.objects.count()],
            'stack': [Stack._meta.verbose_name, Stack.objects.count()],
            'modelswitch': [
                ModelSwitch._meta.verbose_name,
                ModelSwitch.objects.count()
            ],
            'constructorswitch': [
                ConstructorSwitch._meta.verbose_name,
                ConstructorSwitch.objects.count()
            ],
        },
        _("Actions performed"):
        {
            'revision': [_("Number of actions"), Revision.objects.count()],
        },
    }
    return render(request, 'logs/stats_models.html', {'stats_list': stats})


@login_required
@can_view_app('users')
def stats_users(request):
    """Affiche les statistiques base de données aggrégées par user :
    nombre de machines par user, d'etablissements par user,
    de moyens de paiements par user, de banque par user,
    de bannissement par user, etc"""
    stats = {
        _("User"): {
            _("Machines"): User.objects.annotate(
                num=Count('machine')
            ).order_by('-num')[:10],
            _("Invoice"): User.objects.annotate(
                num=Count('facture')
            ).order_by('-num')[:10],
            _("Ban"): User.objects.annotate(
                num=Count('ban')
            ).order_by('-num')[:10],
            _("Whitelist"): User.objects.annotate(
                num=Count('whitelist')
            ).order_by('-num')[:10],
            _("Rights"): User.objects.annotate(
                num=Count('groups')
            ).order_by('-num')[:10],
        },
        _("School"): {
            _("User"): School.objects.annotate(
                num=Count('user')
            ).order_by('-num')[:10],
        },
        _("Payment method"): {
            _("User"): Paiement.objects.annotate(
                num=Count('facture')
            ).order_by('-num')[:10],
        },
        _("Bank"): {
            _("User"): Banque.objects.annotate(
                num=Count('facture')
            ).order_by('-num')[:10],
        },
    }
    return render(request, 'logs/stats_users.html', {'stats_list': stats})


@login_required
@can_view_app('users')
def stats_actions(request):
    """Vue qui affiche les statistiques de modifications d'objets par
    utilisateurs.
    Affiche le nombre de modifications aggrégées par utilisateurs"""
    stats = {
        _("User"): {
            _("Action"): User.objects.annotate(
                num=Count('revision')
            ).order_by('-num')[:40],
        },
    }
    return render(request, 'logs/stats_users.html', {'stats_list': stats})


def history(request, application, object_name, object_id):
    """Render history for a model.

    The model is determined using the `HISTORY_BIND` dictionnary if none is
    found, raises a Http404. The view checks if the user is allowed to see the
    history using the `can_view` method of the model.

    Args:
        request: The request sent by the user.
        application: Name of the application.
        object_name: Name of the model.
        object_id: Id of the object you want to acces history.

    Returns:
        The rendered page of history if access is granted, else the user is
        redirected to their profile page, with an error message.

    Raises:
        Http404: This kind of models doesn't have history.
    """
    try:
        model = apps.get_model(application, object_name)
    except LookupError:
        raise Http404(_("No model found."))
    object_name_id = object_name + 'id'
    kwargs = {object_name_id: object_id}
    try:
        instance = model.get_instance(**kwargs)
    except model.DoesNotExist:
        messages.error(request, _("Nonexistent entry."))
        return redirect(reverse(
            'users:profil',
            kwargs={'userid': str(request.user.id)}
        ))
    can, msg = instance.can_view(request.user)
    if not can:
        messages.error(request, msg or _("You don't have the right to access this menu."))
        return redirect(reverse(
            'users:profil',
            kwargs={'userid': str(request.user.id)}
        ))
    pagination_number = GeneralOption.get_cached_value('pagination_number')
    reversions = Version.objects.get_for_object(instance)
    if hasattr(instance, 'linked_objects'):
        for related_object in chain(instance.linked_objects()):
            reversions = (reversions |
                          Version.objects.get_for_object(related_object))
    reversions = re2o_paginator(request, reversions, pagination_number)
    return render(
        request,
        're2o/history.html',
        {'reversions': reversions, 'object': instance}
    )