from django.contrib import admin
from django.db import models
from django.db.models import Q
from django.forms import ModelForm, BaseInlineFormSet
from django.utils import timezone
from django.urls import reverse
from django.utils.html import format_html
from django.conf.urls import url
from django.template.response import TemplateResponse
from django.core.serializers import serialize
from django.http import HttpResponse
from django.db.models.functions import Cast
from django.contrib.postgres.aggregates import StringAgg
from django.db import connection
from django.core.cache import cache
from django.contrib.humanize.templatetags.humanize import naturaltime
from django.contrib.contenttypes.models import ContentType
from django.http import HttpResponseRedirect

from djgeojson.views import GeoJSONLayerView
from urllib.parse import urlencode

from functools import partial, update_wrapper
from datetime import timedelta

from djadhere.utils import get_active_filter
from adhesions.models import Adhesion
from banking.models import PaymentUpdate
from .models import Service, ServiceType, IPPrefix, IPResource, IPResourceState, \
                    ServiceAllocation, Antenna, AntennaAllocation, Allocation, \
                    Route, Tunnel, Switch, Port
from .utils.notifications import notify_allocation
from .forms import AntennaForm


### Filters

class ResourceInUseFilter(admin.SimpleListFilter):
    title = 'disponibilité'
    parameter_name = 'available'

    def lookups(self, request, model_admin):
        return (
            (1, 'Disponible'),
            (0, 'Non disponible'),
        )

    def queryset(self, request, queryset):
        available_filter = Q(reserved=False, in_use=False)
        if self.value() == '0': # non disponible
            return queryset.exclude(available_filter)
        if self.value() == '1': # disponible
            return queryset.filter(available_filter)


class ResourcePingFilter(admin.SimpleListFilter):
    title = 'ping'
    parameter_name = 'ping'

    def lookups(self, request, model_admin):
        return (
            ('up', 'UP'),
            ('down', 'DOWN'),
            ('down-since', 'DOWN depuis…'),
            ('never-up', 'Jamais vu UP'),
        )

    def queryset(self, request, queryset):
        if self.value() == 'up':
            return queryset.filter(last_state__state=IPResourceState.STATE_UP)
        if self.value() == 'down':
            return queryset.exclude(last_state__state=IPResourceState.STATE_UP) # DOWN + UNKNOWN
        if self.value() == 'down-since':
            queryset = queryset.exclude(last_state__state=IPResourceState.STATE_UP)
            return queryset.filter(last_time_up__isnull=False)
        if self.value() == 'never-up':
            queryset = queryset.exclude(last_state__state=IPResourceState.STATE_UP) # DOWN + UNKWON
            return queryset.filter(last_time_up__isnull=True)


class ActiveServiceFilter(admin.SimpleListFilter):
    title = 'actif'
    parameter_name = 'active'

    def lookups(self, request, model_admin):
        return (
            (1, 'Actif'),
            (0, 'Inactif'),
        )

    def queryset(self, request, queryset):
        if self.value() == '0': # inactif
            return queryset.exclude(get_active_filter('allocation'))
        if self.value() == '1': # actif
            return queryset.filter(get_active_filter('allocation'))


class RouteFilter(admin.SimpleListFilter):
    title = 'route'
    parameter_name = 'route'

    def lookups(self, request, model_admin):
        return ServiceAllocation.objects.filter(active=True).values_list('route__pk', 'route__name').distinct()

    def queryset(self, request, queryset):
        try:
            route = int(self.value())
        except (TypeError, ValueError):
            pass
        else:
            allocations = ServiceAllocation.objects.filter(active=True, route__pk=route).values_list('pk', flat=True)
            queryset = queryset.filter(service_allocation__in=allocations)
        return queryset


class AntennaPrefixFilter(admin.SimpleListFilter):
    title = 'préfix'
    parameter_name = 'prefix'

    def lookups(self, request, model_admin):
        resources = AntennaAllocation.objects.filter(active=True).values_list('resource__pk', flat=True)
        prefixes = IPPrefix.objects.filter(ipresource__in=resources).values_list('pk', 'prefix').distinct()
        return prefixes

    def queryset(self, request, queryset):
        try:
            prefix = int(self.value())
        except (TypeError, ValueError):
            pass
        else:
            allocations = AntennaAllocation.objects.filter(active=True, resource__prefixes__pk=prefix).values_list('pk', flat=True)
            queryset = queryset.filter(allocation__in=allocations)
        return queryset


class AntennaPositionFilter(admin.SimpleListFilter):
    title = 'géolocalisation'
    parameter_name = 'position'

    def lookups(self, request, model_admin):
        return (
            ('1', 'Connue'),
            ('0', 'Inconnue'),
        )

    def queryset(self, request, queryset):
        query = Q(position__isnull=True)
        if self.value() == '0':
            return queryset.filter(query)
        if self.value() == '1':
            return queryset.exclude(query)
        return queryset


class ActiveTunnelFilter(admin.SimpleListFilter):
    title = 'status'
    parameter_name = 'active'

    def lookups(self, request, model_admin):
        return (
            ('1', 'Actif'),
            ('0', 'Désactivé'),
        )

    def queryset(self, request, queryset):
        query = Q(ended__isnull=True)
        if self.value() == '0':
            return queryset.exclude(query)
        if self.value() == '1':
            return queryset.filter(query)
        return queryset


### Inlines

class AllocationInlineFormSet(BaseInlineFormSet):
    def save_new(self, form, commit=True):
        obj = super().save_new(form, commit)
        if type(obj) == ServiceAllocation:
            notify_allocation(self.request, obj)
        return obj

    def save_existing(self, form, instance, commit=True):
        old = type(instance).objects.get(pk=instance.pk)
        if type(instance) == ServiceAllocation:
            notify_allocation(self.request, instance, old)
        return super().save_existing(form, instance, commit)


class AllocationInline(admin.TabularInline):
    formset = AllocationInlineFormSet
    extra = 0
    show_change_link = True

    def get_queryset(self, request):
        qs = super().get_queryset(request)
        qs = qs.select_related('resource')
        return qs

    def get_formset(self, request, obj=None, **kwargs):
        formset = super().get_formset(request, obj, **kwargs)
        formset.request = request
        return formset

    def has_delete_permission(self, request, obj=None):
        return False


class ActiveAllocationMixin:
    verbose_name_plural = 'Allocations actives'

    def get_max_num(self, request, obj=None, **kwargs):
        existing = obj.allocations.count() if obj else 0
        # pour simplifier la validation, on ajoute qu’une allocation à la fois
        # il faudrait surcharger la méthode clean du formset pour supprimer cette limite
        return existing + 1

    def get_queryset(self, request):
        return super().get_queryset(request).filter(get_active_filter())


class InactiveAllocationMixin:
    verbose_name_plural = 'Anciennes allocations'
    max_num = 0

    def get_queryset(self, request):
        return super().get_queryset(request).exclude(get_active_filter())


class ServiceAllocationMixin:
    model = ServiceAllocation
    fields = ('id', 'service', 'resource', 'route', 'start', 'end')
    raw_id_fields = ('resource',)
    autocomplete_fields = ('service',)

    def get_queryset(self, request):
        qs = super().get_queryset(request)
        qs = qs.select_related('route')
        return qs


class AntennaAllocationMixin:
    model = AntennaAllocation
    fields = ('id', 'antenna', 'resource', 'start', 'end')
    raw_id_fields = ('resource',)
    autocomplete_fields = ('antenna',)

    def get_queryset(self, request):
        qs = super().get_queryset(request)
        qs = qs.select_related('antenna')
        return qs


class ActiveServiceAllocationInline(ServiceAllocationMixin, ActiveAllocationMixin, AllocationInline):
    pass


class InactiveServiceAllocationInline(ServiceAllocationMixin, InactiveAllocationMixin, AllocationInline):
    pass


class ActiveAntennaAllocationInline(AntennaAllocationMixin, ActiveAllocationMixin, AllocationInline):
    pass


class InactiveAntennaAllocationInline(AntennaAllocationMixin, InactiveAllocationMixin, AllocationInline):
    pass


class IPResourceStateInline(admin.TabularInline):
    model = IPResourceState
    verbose_name_plural = 'Historique des changements d’état'
    fields = ['date']
    ordering = ['-date']

    def has_add_permission(self, request):
        return False

    def has_change_permission(self, request, obj):
        return False

    def has_delete_permission(self, request, obj=None):
        return False


class PortInline(admin.TabularInline):
    model = Port
    max_num = 0

    def has_add_permission(self, request):
        return False

    def has_delete_permission(self, request, obj=None):
        return False


class SwitchPortInline(PortInline):
    fields = ('port', 'up', 'reserved', 'service', 'notes',)
    readonly_fields = ('port', 'up',)
    autocomplete_fields = ('service',)

    def get_queryset(self, request):
        qs = super().get_queryset(request)
        qs = qs.select_related('switch', 'service', 'service__service_type')
        return qs


class ServicePortInline(PortInline):
    fields = ('switch', 'port', 'up', 'notes',)
    readonly_fields = ('switch', 'port', 'up',)


### Actions

def ends_resource(resource, request, queryset):
    now = timezone.now()
    queryset.exclude(start__lte=now, end__isnull=False).update(end=now)
    # TODO: send mail
ends_resource.short_description = 'Terminer les allocations sélectionnées'


### ModelAdmin

class ServiceAdmin(admin.ModelAdmin):
    list_display = ('id', 'get_adhesion_link', 'get_adherent_link', 'service_type', 'label', 'is_active',)
    list_select_related = ('adhesion', 'adhesion__user', 'adhesion__user__profile', 'adhesion__corporation', 'service_type',)
    list_filter = (
        ActiveServiceFilter,
        'loan_equipment',
        ('service_type', admin.RelatedOnlyFieldListFilter),
    )
    search_fields = ('=id', 'service_type__name', 'label', 'notes',)
    fields = ('adhesion', 'service_type', 'label', 'notes', 'loan_equipment', 'get_contribution_link', 'is_active',)
    readonly_fields = ('get_contribution_link', 'is_active',)
    raw_id_fields = ('adhesion',)

    def get_queryset(self, request):
        qs = super().get_queryset(request)
        qs = qs.prefetch_related('allocations',)
        return qs

    get_adhesion_link = lambda self, service: service.adhesion.get_adhesion_link()
    get_adhesion_link.short_description = Adhesion.get_adhesion_link.short_description

    get_adherent_link = lambda self, service: service.adhesion.get_adherent_link()
    get_adherent_link.short_description = Adhesion.get_adherent_link.short_description

    def get_contribution_link(self, obj):
        return format_html(u'<a href="{}">{}</a>', obj.contribution.get_absolute_url(), obj.contribution.get_current_payment_display())
    get_contribution_link.short_description = 'Contribution financière'

    def get_inline_instances(self, request, obj=None):
        inlines = []
        if obj and obj.ports.exists():
            inlines += [ServicePortInline]
        inlines += [ActiveServiceAllocationInline, InactiveServiceAllocationInline]
        return [inline(self.model, self.admin_site) for inline in inlines]

    def get_actions(self, request):
        actions = super().get_actions(request)
        if 'delete_selected' in actions:
            del actions['delete_selected']
        return actions

    def has_delete_permission(self, request, obj=None):
        if not obj:
            return False
        one_year_ago = timezone.now() - timedelta(days=365)
        contribution = obj.contribution.updates.filter(validated=True).first()
        # s’il y avait un paiement actif il y a moins d’un an
        if not contribution or contribution.payment_method != PaymentUpdate.STOP or contribution.start > one_year_ago:
            return False
        # s’il y avait une allocation active il y a moins d’un an
        if any(map(lambda a: a.end is None or a.end > one_year_ago, obj.allocations.all())):
            return False
        return True


class IPPrefixAdmin(admin.ModelAdmin):
    readonly_fields = ('prefix',)

    def has_delete_permission(self, request, obj=None):
        # Interdiction de supprimer le préfix s’il est assigné à une route
        return obj and obj.tunnel_set.exists()

    # pour embêcher de by-passer le check has_delete_permission, on désactive l’action delete
    def get_actions(self, request):
        actions = super().get_actions(request)
        if 'delete_selected' in actions:
            del actions['delete_selected']
        return actions


class IPResourceAdmin(admin.ModelAdmin):
    list_display = ('__str__', 'available_display', 'route', 'last_use', 'ping',)
    list_filter = (
        'category',
        ResourceInUseFilter,
        ResourcePingFilter,
        'reserved',
        ('prefixes', admin.RelatedOnlyFieldListFilter),
        RouteFilter,
    )
    search_fields = ('=ip', 'notes',)
    actions = ['contact_ip_owners']
    ordering = ['ip']
    inlines = [ IPResourceStateInline ]

    def get_fields(self, request, obj=None):
        return self.get_readonly_fields(request, obj)

    def get_readonly_fields(self, request, obj=None):
        fields = ['ip']
        if obj:
            if obj.reserved:
                fields += ['reserved']
            if not obj.in_use:
                fields += ['last_use']
            fields += ['last_state']
            if obj.last_state.state != IPResourceState.STATE_UP:
                fields += ['last_time_up']
            if obj.category == IPResource.CATEGORY_PUBLIC:
                fields += ['password']
            if obj.checkmk_label:
                fields += ['checkmk']
            if obj.notes:
                fields += ['notes']
        return fields

    def get_inline_instances(self, request, obj=None):
        super_inlines = super().get_inline_instances(request, obj)
        if obj:
            if obj.category == 0:
                inlines = (ActiveServiceAllocationInline, InactiveServiceAllocationInline,)
            elif obj.category == 1:
                inlines = (ActiveAntennaAllocationInline, InactiveAntennaAllocationInline,)
        else:
            inlines = ()
        return [inline(self.model, self.admin_site) for inline in inlines] + super_inlines

    def get_queryset(self, request):
        qs = super().get_queryset(request)
        now = timezone.now()
        qs = qs.annotate(
                last_use=models.Case(
                    models.When(in_use=True, then=now),
                    models.When(category=0, then=models.Max('service_allocation__end')),
                    models.When(category=1, then=models.Max('antenna_allocation__end')),
                    default=None,
                ))
        qs = qs.annotate(
                downtime=models.Case(
                    models.When(last_state__state=IPResourceState.STATE_UP, then=models.F('last_state__date')-models.Value(now)),
                    models.When(last_state__state=IPResourceState.STATE_DOWN, then=models.Value(now)-models.F('last_time_up')),
                    default=None,
                    output_field=models.DurationField(),
                ))
        qs = qs.annotate(
                route=models.Case(
                    models.When(
                        in_use_by_service=True,
                        then=models.Subquery(
                            ServiceAllocation.objects.filter(
                                Q(resource=models.OuterRef('pk')) & get_active_filter()
                            ).values('route__name')[:1]
                        ),
                    ),
                    output_field=models.CharField(),
                ))
        return qs

    def available_display(self, obj):
        return not obj.reserved and not obj.in_use
    available_display.short_description = 'Disponible'
    available_display.boolean = True

    def last_use(self, obj):
        if obj.last_use:
            return naturaltime(obj.last_use)
        else:
            return '-'
    last_use.short_description = 'Dernière utilisation'
    last_use.admin_order_field = 'last_use'

    def ping(self, obj):
        if obj.last_state.state == IPResourceState.STATE_UP:
            label = 'UP'
        else:
            if obj.last_time_up:
                label = 'dernier ping : ' + naturaltime(obj.last_time_up)
            else:
                label = 'DOWN'
        if obj.checkmk_url:
            return format_html('<a href="{}">{}</a>', obj.checkmk_url, label)
        else:
            return label
    ping.short_description = 'ping'
    ping.admin_order_field = 'downtime'

    def route(self, obj):
        return obj.route
    route.short_description = 'route'
    route.admin_order_field = 'route'

    def checkmk(self, obj):
        return format_html('<a href="{}">{}</a>', obj.checkmk_url, 'voir')
    checkmk.short_description = 'CheckMK'

    def contact_ip_owners(self, request, queryset):
        selected = request.POST.getlist(admin.ACTION_CHECKBOX_NAME)
        services = ServiceAllocation.objects.filter(resource__ip__in=selected) \
                                            .filter(get_active_filter()) \
                                            .values_list('service__adhesion', flat=True)
        antennas = AntennaAllocation.objects.filter(resource__ip__in=selected) \
                                            .filter(get_active_filter()) \
                                            .values_list('antenna__contact', flat=True)
        pk = ",".join(map(str, set(services) | set(antennas)))
        return HttpResponseRedirect(reverse('admin:contact-adherents') + "?pk=%s" % pk)
    contact_ip_owners.short_description = 'Contacter les adhérents'

    def get_actions(self, request):
        actions = super().get_actions(request)
        if 'delete_selected' in actions:
            del actions['delete_selected']
        return actions

    def has_add_permission(self, request, obj=None):
        return False

    def has_delete_permission(self, request, obj=None):
        return False


class RouteAdmin(admin.ModelAdmin):
    list_display = ('name',)
    search_fields = ('name',)

    def get_fieldsets(self, request, obj=None):
        if obj:
            return (
                (None, {'fields': ['name']}),
                ('Adhérent·e·s', {'fields': ['get_adh'], 'classes': ['collapse']}),
                ('E-mails', {'fields': ['get_email'], 'classes': ['collapse']}),
                ('SMS', {'fields': ['get_sms'], 'classes': ['collapse']}),
                ('IP', {'fields': ['get_ip'], 'classes': ['collapse']}),
            )
        else:
            return (
                (None, {'fields': ['name']}),
            )

    def get_readonly_fields(self, request, obj=None):
        if obj:
            return ('get_email', 'get_sms', 'get_ip', 'get_adh',)
        else:
            return ()

    def get_email(self, route):
        return '\n'.join(route.get_email())
    get_email.short_description = 'E-mails'

    def get_sms(self, route):
        sms_filter = lambda x: x[:2] == '06' or x[:2] == '07' or x[:3] == '+336' or x[:3] == '+337'
        return '\n'.join(filter(sms_filter, route.get_tel()))
    get_sms.short_description = 'SMS'

    def get_ip(self, route):
        return '\n'.join(route.get_ip())
    get_ip.short_description = 'IP'

    def get_adh(self, route):
        return '\n'.join(map(lambda adh: '%s %s' % (adh, adh.adherent), route.get_adh()))
    get_adh.short_description = 'Adhérent·e·s'

    def get_actions(self, request):
        actions = super().get_actions(request)
        if 'delete_selected' in actions:
            del actions['delete_selected']
        return actions

    def has_delete_permission(self, request, obj=None):
        return False


class TunnelAdmin(admin.ModelAdmin):
    list_display = ('name', 'description', 'created', 'active')
    list_filter = (
        ActiveTunnelFilter,
    )

    def active(self, obj):
        return not obj.ended
    active.short_description = 'Actif'
    active.boolean = True


class ServiceTypeAdmin(admin.ModelAdmin):
    fields = ('name', 'contact')

    def get_actions(self, request):
        actions = super().get_actions(request)
        if 'delete_selected' in actions:
            del actions['delete_selected']
        return actions

    def has_delete_permission(self, request, obj=None):
        return False


class ActiveAntennaLayer(GeoJSONLayerView):
    def get_queryset(self):
        return Antenna.objects.filter(get_active_filter('allocation'))


class AntennaAdmin(admin.ModelAdmin):
    inlines = (ActiveAntennaAllocationInline, InactiveAntennaAllocationInline,)
    list_filter = (
        AntennaPrefixFilter,
        AntennaPositionFilter,
        'mode',
        'ssid',
    )
    list_display_links = ('id', 'label')
    search_fields = ('=id', 'label', 'notes', 'ssid')
    raw_id_fields = ('contact',)
    form = AntennaForm

    def get_queryset(self, request):
        qs = super().get_queryset(request)
        if connection.vendor == 'postgresql':
            qs = qs.annotate(
                    ip=StringAgg( # concaténation des IP avec des virgules directement par postgresql
                        Cast( # casting en TextField car StringApp oppère sur des string mais les ip sont des inet
                            models.Case( # seulement les IP des allocations actives
                                models.When(
                                    get_active_filter('allocation'),
                                    then='allocation__resource__ip'
                                ),
                            ),
                            models.TextField()
                        ),
                        delimiter=', '
                    )
                )
        return qs

    def get_list_display(self, request):
        # ssid_display needs request to access query string and preserve filters
        ssid_display = partial(self.ssid_display, request=request)
        update_wrapper(ssid_display, self.ssid_display)
        return ('id', 'label', 'mode', ssid_display, 'position_display', 'ip_display')

    def position_display(self, obj):
        return obj.position is not None
    position_display.short_description = 'Géolocalisé'
    position_display.boolean = True

    def ip_display(self, obj):
        if connection.vendor == 'postgresql':
            return obj.ip
        else:
            # peu efficace car génère une requête par ligne
            allocations = obj.allocations.filter(active=True)
            return ', '.join(allocations.values_list('resource__ip', flat=True)) or '-'
    ip_display.short_description = 'IP'

    def ssid_display(self, obj, request):
        if obj.ssid:
            qs = request.GET.copy()
            qs.update({'ssid': obj.ssid})
            ssid_url = reverse('admin:services_antenna_changelist') + '?' + urlencode(qs)
            return format_html(u'<a href="{}">{}</a>', ssid_url, obj.ssid)
        else:
            return None
    ssid_display.short_description = 'SSID'

    def get_actions(self, request):
        actions = super().get_actions(request)
        if 'delete_selected' in actions:
            del actions['delete_selected']
        return actions

    def has_delete_permission(self, request, obj=None):
        return False

    def get_urls(self):
        my_urls = [
            url(r'^map/$', self.admin_site.admin_view(self.map_view, cacheable=True), name='antenna-map'),
            url(r'^map/data.json$', self.admin_site.admin_view(ActiveAntennaLayer.as_view(
                                        model=Antenna,
                                        geometry_field='position',
                                        properties=('label', 'mode', 'ssid', 'orientation', 'absolute_url',),
                                    )), name='antenna-map-data'),
        ]
        return my_urls + super().get_urls()

    def map_view(self, request):
        return TemplateResponse(request, 'services/antenna_map.html', dict(
            self.admin_site.each_context(request),
            opts=self.model._meta,
            json_url=reverse('admin:antenna-map-data'),
        ))

    def map_data_view(self, request):
        geojson = serialize('geojson', Antenna.objects.all(), geometry_field='point', fields=('position',))
        return HttpResponse(geojson, content_type='application/json')


class SwitchAdmin(admin.ModelAdmin):
    list_display = ('name', 'ports_count', 'active_ports_count', 'inactive_ports_count', 'unknown_ports_count',)
    fields = ('name', 'first_port', 'last_port', 'notes',)
    search_fields = ('name', 'notes', 'ports__notes', 'ports__service__label',)

    def get_queryset(self, request):
        qs = super().get_queryset(request)
        qs = qs.annotate(
            active_ports_count=models.Count(models.Case(models.When(ports__up=True, then=models.Value('1')))),
            unknown_ports_count=models.Count(models.Case(models.When(ports__up__isnull=True, then=models.Value('1')))),
            inactive_ports_count=models.Count(models.Case(models.When(ports__up=False, then=models.Value('1')))),
        )
        return qs

    def ports_count(self, switch):
        return switch.last_port - switch.first_port + 1
    ports_count.short_description = 'Nombre de ports'

    def active_ports_count(self, switch):
        return switch.active_ports_count
    active_ports_count.short_description = 'up'
    active_ports_count.admin_order_field = 'active_ports_count'

    def inactive_ports_count(self, switch):
        return switch.inactive_ports_count
    inactive_ports_count.short_description = 'down'
    inactive_ports_count.admin_order_field = 'inactive_ports_count'

    def unknown_ports_count(self, switch):
        return switch.unknown_ports_count
    unknown_ports_count.short_description = 'inconnus'
    unknown_ports_count.admin_order_field = 'unknown_ports_count'

    def get_inline_instances(self, request, obj=None):
        if obj:
            return [ SwitchPortInline(self.model, self.admin_site) ]
        else:
            return []

    def get_readonly_fields(self, request, obj=None):
        if obj:
            return ('first_port', 'last_port',)
        else:
            return ()

    def get_actions(self, request):
        actions = super().get_actions(request)
        if 'delete_selected' in actions:
            del actions['delete_selected']
        return actions

    def has_delete_permission(self, request, obj=None):
        return False


admin.site.register(ServiceType, ServiceTypeAdmin)
admin.site.register(Service, ServiceAdmin)
admin.site.register(IPPrefix, IPPrefixAdmin)
admin.site.register(IPResource, IPResourceAdmin)
admin.site.register(Route, RouteAdmin)
admin.site.register(Tunnel, TunnelAdmin)
admin.site.register(Antenna, AntennaAdmin)
admin.site.register(Switch, SwitchAdmin)