# This file is part of Indico.
# Copyright (C) 2002 - 2026 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 marshmallow import fields
from sqlalchemy import func, inspect
from sqlalchemy.orm import joinedload, lazyload
from werkzeug.exceptions import BadRequest, Forbidden, NotFound

from indico.core import signals
from indico.core.db import db
from indico.core.logger import Logger
from indico.core.plugins import IndicoPlugin
from indico.modules.events.controllers.base import RHDisplayEventBase
from indico.modules.events.management.controllers import RHManageEventBase
from indico.modules.events.models.events import Event
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.args import use_kwargs
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: IndicoPlugin, event: Event, vc_room: VCRoom, form,
                                event_vc_room: VCRoomEventAssociation | None = None,
                                allow_same_room: bool = False, new_room: bool = 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()
            old_link = None
        else:
            old_link = event_vc_room.link_object

        # set the actual link_object on the association
        assoc_has_changed = plugin.update_data_association(event, vc_room, event_vc_room, form.data)

        # 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:
        flash(_("There is already a videoconference attached to '{link_object_title}'.").format(
            link_object_title=resolve_title(event_vc_room.link_object)), 'error')
        db.session.rollback()
        return None
    elif event_vc_room.link_type == VCRoomLinkType.event and vc_room in existing:
        flash(_('This {plugin_name} videoconference is already attached to the event.')
              .format(plugin_name=plugin.friendly_name), 'error')
        db.session.rollback()
        return None
    elif assoc_has_changed and not new_room:
        if old_link:
            signals.vc.vc_room_detached.send(
                event_vc_room, vc_room=vc_room, old_link=old_link, event=event, data=form.data
            )
        signals.vc.vc_room_attached.send(
            event_vc_room, vc_room=vc_room, event=event, data=form.data, old_link=old_link, new_room=new_room
        )

    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} videoconferences 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, new_room=True)
            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)
                    signals.vc.vc_room_created.send(vc_room, event=self.event, assoc=event_vc_room)
                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} videoconference '{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 {} videoconferences 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('Videoconference %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} videoconference '{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} videoconferences for this event.').format(
                plugin_name=self.plugin.friendly_name), 'error')
            raise Forbidden

        Logger.get('modules.vc').info('Refreshing videoconference %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('Videoconference %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} videoconference '{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."""

    @use_kwargs({'delete_all': fields.Bool(load_default=False)}, location='query')
    def _process(self, delete_all):
        if not self.plugin.can_manage_vc_rooms(session.user, self.event):
            flash(_('You are not allowed to remove {} videoconferences from this event.')
                  .format(self.plugin.friendly_name), 'error')
            raise Forbidden

        if delete_all:
            self.vc_room.delete(session.user, event=self.event)
        else:
            self.event_vc_room.delete(session.user)

        flash(_("{plugin_name} videoconference '{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} videoconferences 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 videoconference '{}'").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 videoconference 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()),  # noqa: SIM118
                          joinedload('events').joinedload('event').joinedload('acl_entries'))
                 .group_by(VCRoom.id)
                 .order_by(func.lower(VCRoom.name) != self.query.lower(), db.desc('event_count'))
                 .limit(10))

        return ((room, count) for room, count in query if room.plugin.can_manage_vc_room(session.user, room))

    def _process(self):
        result = [{'id': room.id, 'name': room.name} for room, count in self._iter_allowed_rooms()]
        return jsonify(result)


class RHVCRoomList(RHProtected):
    """Provide a list of videoconference rooms."""

    def _check_access(self):
        RHProtected._check_access(self)
        if not get_managed_vc_plugins(session.user):
            raise Forbidden

    def _process(self):
        form = VCRoomListFilterForm(request.args, csrf_enabled=False)
        results = None
        if request.args and form.validate():
            reverse = form.direction.data == 'desc'
            from_dt = as_utc(get_day_start(form.start_date.data)) if form.start_date.data else None
            to_dt = as_utc(get_day_end(form.end_date.data)) if form.end_date.data else None
            results = find_event_vc_rooms(from_dt=from_dt, to_dt=to_dt, distinct=True)
            results = group_list((r for r in results if r.event),
                                 key=lambda r: r.event.start_dt.date(),
                                 sort_by=lambda r: r.event.start_dt,
                                 sort_reverse=reverse)
            results = dict(sorted(results.items(), key=itemgetter(0), reverse=reverse))
        return WPVCService.render_template('vc_room_list.html', form=form, results=results,
                                           action=url_for('.vc_room_list'))
