# This file is part of Indico.
# Copyright (C) 2002 - 2021 CERN
#
# Indico is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see the
# LICENSE file for more details.

from collections import defaultdict
from operator import itemgetter

from flask import flash, jsonify, redirect, request, session
from sqlalchemy import func, inspect
from sqlalchemy.orm import joinedload, lazyload
from werkzeug.exceptions import BadRequest, Forbidden, NotFound

from indico.core.db import db
from indico.core.logger import Logger
from indico.modules.events.controllers.base import RHDisplayEventBase
from indico.modules.events.management.controllers import RHManageEventBase
from indico.modules.vc.exceptions import VCRoomError, VCRoomNotFoundError
from indico.modules.vc.forms import VCRoomListFilterForm
from indico.modules.vc.models.vc_rooms import VCRoom, VCRoomEventAssociation, VCRoomLinkType, VCRoomStatus
from indico.modules.vc.notifications import notify_created
from indico.modules.vc.util import find_event_vc_rooms, get_managed_vc_plugins, get_vc_plugins, resolve_title
from indico.modules.vc.views import WPVCEventPage, WPVCManageEvent, WPVCService
from indico.util.date_time import as_utc, get_day_end, get_day_start, now_utc
from indico.util.i18n import _
from indico.util.iterables import group_list
from indico.web.flask.util import url_for
from indico.web.forms.base import FormDefaults
from indico.web.rh import RHProtected
from indico.web.util import _pop_injected_js, jsonify_data, jsonify_template


def process_vc_room_association(plugin, event, vc_room, form, event_vc_room=None, allow_same_room=False):
    # disable autoflush, so that the new event_vc_room does not influence the result
    with db.session.no_autoflush:
        if event_vc_room is None:
            event_vc_room = VCRoomEventAssociation()

        plugin.update_data_association(event, vc_room, event_vc_room, form.data)

        existing = set()
        if event_vc_room.link_object is not None:
            # check whether there is a room-event association already present
            # for the given event, room and plugin
            q = (VCRoomEventAssociation.query
                 .filter(VCRoomEventAssociation.event == event,
                         VCRoomEventAssociation.link_object == event_vc_room.link_object)
                 .join(VCRoom))
            if allow_same_room:
                q = q.filter(VCRoom.id != vc_room.id)
            existing = {x.vc_room for x in q}

    if event_vc_room.link_type != VCRoomLinkType.event and existing:
        db.session.rollback()
        flash(_("There is already a VC room attached to '{link_object_title}'.").format(
            link_object_title=resolve_title(event_vc_room.link_object)), 'error')
        return None
    elif event_vc_room.link_type == VCRoomLinkType.event and vc_room in existing:
        db.session.rollback()
        flash(_("This {plugin_name} room is already attached to the event.").format(plugin_name=plugin.friendly_name),
              'error')
        return None
    else:
        return event_vc_room


class RHVCManageEventBase(RHManageEventBase):
    pass


class RHEventVCRoomMixin:
    normalize_url_spec = {
        'locators': {
            lambda self: self.event_vc_room
        }
    }

    def _process_args(self):
        self.event_vc_room = VCRoomEventAssociation.get_or_404(request.view_args['event_vc_room_id'])
        self.vc_room = self.event_vc_room.vc_room


class RHVCManageEvent(RHVCManageEventBase):
    """List the available videoconference rooms."""

    def _process(self):
        room_event_assocs = VCRoomEventAssociation.find_for_event(self.event, include_hidden=True,
                                                                  include_deleted=True).all()
        event_vc_rooms = [event_vc_room for event_vc_room in room_event_assocs if event_vc_room.vc_room.plugin]
        return WPVCManageEvent.render_template('manage_event.html', self.event,
                                               event_vc_rooms=event_vc_rooms, plugins=list(get_vc_plugins().values()))


class RHVCManageEventSelectService(RHVCManageEventBase):
    """
    List available videoconference plugins to create a new
    videoconference room.
    """

    def _process(self):
        action = request.args.get('vc_room_action', '.manage_vc_rooms_create')
        attach = request.args.get('attach', '')
        return jsonify_template('vc/manage_event_select.html', event=self.event, vc_room_action=action,
                                plugins=list(get_vc_plugins().values()), attach=attach)


class RHVCManageEventCreateBase(RHVCManageEventBase):
    def _process_args(self):
        RHVCManageEventBase._process_args(self)
        try:
            self.plugin = get_vc_plugins()[request.view_args['service']]
        except KeyError:
            raise NotFound


class RHVCManageEventCreate(RHVCManageEventCreateBase):
    """Load the form for the selected VC plugin."""

    def _process(self):
        if not self.plugin.can_manage_vc_rooms(session.user, self.event):
            flash(_('You are not allowed to create {plugin_name} rooms for this event.').format(
                plugin_name=self.plugin.friendly_name), 'error')
            raise Forbidden

        form = self.plugin.create_form(event=self.event)

        if form.validate_on_submit():
            vc_room = VCRoom(created_by_user=session.user)
            vc_room.type = self.plugin.service_name
            vc_room.status = VCRoomStatus.created

            event_vc_room = process_vc_room_association(self.plugin, self.event, vc_room, form)
            if not event_vc_room:
                return jsonify_data(flash=False)

            with db.session.no_autoflush:
                self.plugin.update_data_vc_room(vc_room, form.data, is_new=True)

            try:
                # avoid flushing the incomplete vc room to the database
                with db.session.no_autoflush:
                    self.plugin.create_room(vc_room, self.event)
                notify_created(self.plugin, vc_room, event_vc_room, self.event, session.user)
            except VCRoomError as err:
                if err.field is None:
                    raise
                field = getattr(form, err.field)
                field.errors.append(str(err))
                db.session.rollback()  # otherwise the incomplete vc room would be added to the db!
            else:
                db.session.add(vc_room)

                flash(_("{plugin_name} room '{room.name}' created").format(
                    plugin_name=self.plugin.friendly_name, room=vc_room), 'success')
                return jsonify_data(flash=False)

        form_html = self.plugin.render_form(plugin=self.plugin, event=self.event, form=form,
                                            skip_fields=form.skip_fields | {'name'})

        return jsonify(html=form_html, js=_pop_injected_js())


class RHVCSystemEventBase(RHEventVCRoomMixin, RHVCManageEventBase):
    def _process_args(self):
        RHVCManageEventBase._process_args(self)
        RHEventVCRoomMixin._process_args(self)
        if self.vc_room.type != request.view_args['service']:
            raise NotFound
        self.plugin = self.vc_room.plugin


class RHVCManageEventModify(RHVCSystemEventBase):
    """Modify an existing VC room."""

    def _process(self):
        if not self.plugin.can_manage_vc_rooms(session.user, self.event):
            flash(_('You are not allowed to modify {} rooms for this event.').format(self.plugin.friendly_name),
                  'error')
            raise Forbidden

        form = self.plugin.create_form(self.event,
                                       existing_vc_room=self.vc_room,
                                       existing_event_vc_room=self.event_vc_room)

        if form.validate_on_submit():

            self.plugin.update_data_vc_room(self.vc_room, form.data)

            event_vc_room = process_vc_room_association(
                self.plugin, self.event, self.vc_room, form, event_vc_room=self.event_vc_room, allow_same_room=True)
            if not event_vc_room:
                return jsonify_data(flash=False)

            self.vc_room.modified_dt = now_utc()

            try:
                self.plugin.update_room(self.vc_room, self.event)
            except VCRoomNotFoundError as err:
                Logger.get('modules.vc').warning("VC room %r not found. Setting it as deleted.", self.vc_room)
                self.vc_room.status = VCRoomStatus.deleted
                flash(str(err), 'error')
                return jsonify_data(flash=False)
            except VCRoomError as err:
                if err.field is None:
                    raise
                field = getattr(form, err.field)
                field.errors.append(str(err))
                db.session.rollback()
            else:
                # TODO
                # notify_modified(self.vc_room, self.event, session.user)

                flash(_("{plugin_name} room '{room.name}' updated").format(
                    plugin_name=self.plugin.friendly_name, room=self.vc_room), 'success')
                return jsonify_data(flash=False)

        form_html = self.plugin.render_form(plugin=self.plugin, event=self.event, form=form,
                                            existing_vc_room=self.vc_room,
                                            skip_fields=form.skip_fields | {'name'})

        return jsonify(html=form_html, js=_pop_injected_js())


class RHVCManageEventRefresh(RHVCSystemEventBase):
    """Refresh an existing VC room, fetching information from the VC system."""

    def _process(self):
        if not self.plugin.can_manage_vc_rooms(session.user, self.event):
            flash(_('You are not allowed to refresh {plugin_name} rooms for this event.').format(
                plugin_name=self.plugin.friendly_name), 'error')
            raise Forbidden

        Logger.get('modules.vc').info("Refreshing VC room %r from event %r", self.vc_room, self.event)

        try:
            self.plugin.refresh_room(self.vc_room, self.event)
        except VCRoomNotFoundError as err:
            Logger.get('modules.vc').warning("VC room %r not found. Setting it as deleted.", self.vc_room)
            self.vc_room.status = VCRoomStatus.deleted
            flash(str(err), 'error')
            return redirect(url_for('.manage_vc_rooms', self.event))

        flash(_("{plugin_name} room '{room.name}' refreshed").format(
            plugin_name=self.plugin.friendly_name, room=self.vc_room), 'success')
        return redirect(url_for('.manage_vc_rooms', self.event))


class RHVCManageEventRemove(RHVCSystemEventBase):
    """Remove an existing VC room."""

    def _process(self):
        if not self.plugin.can_manage_vc_rooms(session.user, self.event):
            flash(_('You are not allowed to remove {} rooms from this event.').format(self.plugin.friendly_name),
                  'error')
            raise Forbidden

        delete_all = request.args.get('delete_all') == '1'
        self.event_vc_room.delete(session.user, delete_all=delete_all)
        flash(_("{plugin_name} room '{room.name}' removed").format(
            plugin_name=self.plugin.friendly_name, room=self.vc_room), 'success')
        return redirect(url_for('.manage_vc_rooms', self.event))


class RHVCEventPage(RHDisplayEventBase):
    """List the VC rooms in an event page."""

    def _process(self):
        event_vc_rooms = [event_vc_room
                          for event_vc_room in VCRoomEventAssociation.find_for_event(self.event).all()
                          if event_vc_room.vc_room.plugin]
        vc_plugins_available = bool(get_vc_plugins())
        linked_to = defaultdict(lambda: defaultdict(list))
        for event_vc_room in event_vc_rooms:
            linked_to[event_vc_room.link_type.name][event_vc_room.link_object].append(event_vc_room)
        return WPVCEventPage.render_template('event_vc.html', self.event,
                                             event_vc_rooms=event_vc_rooms, linked_to=linked_to,
                                             vc_plugins_available=vc_plugins_available)


class RHVCManageAttach(RHVCManageEventCreateBase):
    """Attach a room to the event."""

    def _process(self):
        defaults = FormDefaults(self.plugin.get_vc_room_attach_form_defaults(self.event))
        form = self.plugin.vc_room_attach_form(prefix='vc-', obj=defaults, event=self.event,
                                               service=self.plugin.service_name)

        if form.validate_on_submit():
            vc_room = form.data['room']
            if not self.plugin.can_manage_vc_rooms(session.user, self.event):
                flash(_("You are not allowed to attach {plugin_name} rooms to this event.").format(
                    plugin_name=self.plugin.friendly_name), 'error')
            elif not self.plugin.can_manage_vc_room(session.user, vc_room):
                flash(_("You are not authorized to attach the room '{0}'").format(vc_room.name), 'error')
            else:
                event_vc_room = process_vc_room_association(self.plugin, self.event, vc_room, form)
                if event_vc_room:
                    flash(_("The room has been attached to the event."), 'success')
                    db.session.add(event_vc_room)
            return jsonify_data(flash=False)

        return jsonify_template('vc/attach_room.html', event=self.event, form=form,
                                skip_fields=form.conditional_fields | {'room'},
                                plugin=self.plugin)


class RHVCManageSearch(RHVCManageEventCreateBase):
    """Search for a room based on its name."""

    def _process_args(self):
        RHVCManageEventCreateBase._process_args(self)

        self.query = request.args.get('q', '')
        if len(self.query) < 3:
            raise BadRequest("A query has to be provided, with at least 3 characters")

    def _iter_allowed_rooms(self):
        query = (db.session.query(VCRoom, func.count(VCRoomEventAssociation.id).label('event_count'))
                 .filter(func.lower(VCRoom.name).contains(self.query.lower()), VCRoom.status != VCRoomStatus.deleted,
                         VCRoom.type == self.plugin.service_name)
                 .join(VCRoomEventAssociation)
                 # Plugins might add eager-loaded extensions to the table - since we cannot group by them
                 # we need to make sure everything is lazy-loaded here.
                 .options((lazyload(r) for r in inspect(VCRoom).relationships.keys()),
                          joinedload('events').joinedload('event').joinedload('acl_entries'))
                 .group_by(VCRoom.id)
                 .order_by(db.desc('event_count'))