# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2022  Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import hashlib
import time
import uuid

from django.contrib.postgres.fields import JSONField
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.db import models, transaction
from django.http import HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _

from passerelle.base.models import BaseResource, HTTPResource
from passerelle.utils.api import endpoint
from passerelle.utils.jsonresponse import APIError

from . import utils


def complete_url(request, data):
    data = dict(data)
    for key in data:
        if key == 'url' or key.endswith('_url') and data[key]:
            data[key] = request.build_absolute_uri(data[key])
    return data


class Resource(BaseResource, HTTPResource):
    bbb_url = models.URLField(
        max_length=400,
        verbose_name=_('BBB URL'),
        help_text=_('Base URL of Big Blue Button (use "bbb-conf --secret" to get it)'),
    )
    shared_secret = models.CharField(
        max_length=128, verbose_name=_('Shared secret'), help_text=_('Use "bbb-conf --secret" to get it.')
    )
    category = _('Business Process Connectors')

    class Meta:
        verbose_name = _('Big Blue Button')

    @cached_property
    def bbb(self):
        return utils.BBB(url=self.bbb_url, shared_secret=self.shared_secret, session=self.requests)

    def check_status(self):
        self.bbb.get_meetings()

    @endpoint(
        methods=['post'],
        name='meeting',
        perm='can_access',
        description_post=_('Create a meeting'),
        post={
            'request_body': {
                'schema': {
                    'application/json': {
                        'type': 'object',
                        'properties': {
                            'name': {
                                'type': 'string',
                            },
                            'idempotent_id': {
                                'type': 'string',
                            },
                            'create_parameters': {
                                'type': 'object',
                                'properties': {
                                    'logoutURL': {'type': 'string'},
                                },
                                'additionalProperties': True,
                            },
                            'user_full_name': {'type': 'string'},
                            'join_user_parameters': {
                                'type': 'object',
                                'properties': {
                                    'guest': {'type': 'string'},
                                },
                                'additionalProperties': True,
                            },
                            'agent_full_name': {'type': 'string'},
                            'join_agent_parameters': {
                                'type': 'object',
                                'properties': {
                                    'guest': {'type': 'string'},
                                },
                                'additionalProperties': True,
                            },
                            'metadata': {
                                'type': 'object',
                            },
                        },
                        'required': ['name', 'idempotent_id'],
                        'unflatten': True,
                    }
                }
            }
        },
    )
    @transaction.atomic
    def meetings_endpoint(self, request, post_data):
        defaults = {
            key: post_data.get(key)
            for key in [
                'name',
                'create_parameters',
                'user_full_name',
                'join_user_parameters',
                'agent_full_name',
                'join_agent_parameters',
                'metadata',
            ]
        }
        meeting, created = Meeting.objects.select_for_update().get_or_create(
            resource=self, idempotent_id=post_data['idempotent_id'], defaults=defaults
        )
        if not created:
            for key, value in defaults.items():
                if getattr(meeting, key, None) != value:
                    raise APIError('meeting already exists with different parameters')
        try:
            meeting.create()
        except utils.BBB.BBBError as e:
            raise APIError(e)
        return {'data': complete_url(request, meeting.to_json())}

    @endpoint(
        methods=['get', 'delete'],
        name='meeting',
        perm='can_access',
        pattern=r'^(?P<guid>[0-9a-f]{32})/?$',
        example_pattern='{guid}/',
        description_post=_('Get a meeting'),
        parameters={
            'guid': {
                'description': _('Meeting guid'),
                'example_value': '7edb43abf2004f55a8a526ac4b1403e4',
            },
        },
    )
    def meeting_endpoint(self, request, guid):
        if request.method == 'DELETE':
            with transaction.atomic():
                meeting = get_object_or_404(Meeting.objects.select_for_update(), guid=guid)
                try:
                    meeting.create().end()
                except Exception:
                    pass
                meeting.delete()
                return {}
        else:
            meeting = get_object_or_404(Meeting.objects, guid=guid)
        return {'data': complete_url(request, meeting.to_json())}

    @endpoint(
        methods=['get'],
        name='meeting',
        pattern=r'^(?P<guid>[0-9a-f]{32})/is-running/?$',
        example_pattern='{guid}/is-running/',
        description_post=_('Report if meeting is running'),
        parameters={
            'guid': {
                'description': _('Meeting guid'),
                'example_value': '7edb43abf2004f55a8a526ac4b1403e4',
            },
        },
    )
    @transaction.atomic
    def is_running(self, request, guid):
        meeting = get_object_or_404(Meeting.objects.select_for_update(), guid=guid)
        if (now() - meeting.updated).total_seconds() > 5:
            meeting.update_is_running()
        response = JsonResponse({'err': 0, 'data': meeting.running})
        response['Access-Control-Allow-Origin'] = '*'
        return response

    @endpoint(
        methods=['get'],
        name='meeting',
        pattern=r'^(?P<guid>[0-9a-f]{32})/join/agent/(?P<key>[^/]*)/?$',
        example_pattern='{guid}/join/agent/',
        description_post=_('Get a meeting'),
        parameters={
            'guid': {
                'description': _('Meeting guid'),
                'example_value': '7edb43abf2004f55a8a526ac4b1403e4',
            },
            'full_name': {
                'description': _('Agent full name'),
                'example_value': 'John Doe',
            },
            'key': {
                'description': _('Secret key'),
                'example_value': '1234',
            },
        },
    )
    def join_agent(self, request, guid, key, full_name=None):
        meeting = get_object_or_404(Meeting, guid=guid)
        if key != meeting.agent_key:
            raise PermissionDenied
        return HttpResponseRedirect(meeting.make_join_agent_url(full_name))

    @endpoint(
        methods=['get'],
        name='meeting',
        pattern=r'^(?P<guid>[0-9a-f]{32})/join/user/(?P<key>[^/]*)/?$',
        example_pattern='{guid}/join/user/',
        description_post=_('Get a meeting'),
        parameters={
            'guid': {
                'description': _('Meeting guid'),
                'example_value': '7edb43abf2004f55a8a526ac4b1403e4',
            },
            'full_name': {
                'description': _('User full name'),
                'example_value': 'John Doe',
            },
            'key': {
                'description': _('Secret key'),
                'example_value': '1234',
            },
        },
    )
    def join_user(self, request, guid, key, full_name=None):
        meeting = get_object_or_404(Meeting, guid=guid)
        if key != meeting.user_key:
            raise PermissionDenied
        return HttpResponseRedirect(meeting.make_join_user_url(full_name))

    def _make_endpoint_url(self, endpoint, rest):
        return reverse(
            'generic-endpoint',
            kwargs={'connector': 'bbb', 'endpoint': endpoint, 'slug': self.slug, 'rest': rest},
        )


class Meeting(models.Model):
    created = models.DateTimeField(verbose_name=_('Created'), auto_now_add=True)
    updated = models.DateTimeField(verbose_name=_('Updated'), auto_now=True)
    resource = models.ForeignKey(
        Resource, verbose_name=_('Resource'), on_delete=models.CASCADE, related_name='meetings'
    )
    guid = models.UUIDField(verbose_name=_('UUID'), unique=True, default=uuid.uuid4)
    name = models.TextField(verbose_name=_('Name'))
    idempotent_id = models.TextField(verbose_name=_('Idempotent ID'), unique=True)
    running = models.BooleanField(verbose_name=_('Is running?'), default=False)
    last_time_running = models.DateTimeField(verbose_name=_('Last time running'), null=True)
    create_parameters = JSONField('Create parameters', null=True)
    user_full_name = models.TextField(_('User full name'), null=True)
    agent_full_name = models.TextField(_('Agent full name'), null=True)
    join_user_parameters = JSONField('Join user parameters', null=True)
    join_agent_parameters = JSONField('Join agent parameters', null=True)
    metadata = JSONField('Metadata', null=True)

    def _make_key(self, _for):
        return hashlib.sha1((self.resource.shared_secret + _for + self.meeting_id).encode()).hexdigest()[:6]

    @property
    def bbb(self):
        return self.resource.bbb

    @property
    def user_key(self):
        return self._make_key('user')

    @property
    def join_user_url(self):
        return self.resource._make_endpoint_url(
            endpoint='meeting', rest=f'{self.guid.hex}/join/user/{self.user_key}/'
        )

    @property
    def agent_key(self):
        return self._make_key('agent')

    @property
    def join_agent_url(self):
        return self.resource._make_endpoint_url(
            endpoint='meeting', rest=f'{self.guid.hex}/join/agent/{self.agent_key}/'
        )

    @property
    def url(self):
        return self.resource._make_endpoint_url(endpoint='meeting', rest=f'{self.guid.hex}/')

    @property
    def is_running_url(self):
        return self.resource._make_endpoint_url(endpoint='meeting', rest=f'{self.guid.hex}/is-running/')

    def to_json(self):
        return {
            'guid': self.guid.hex,
            'created': self.created,
            'updated': self.updated,
            'name': self.name,
            'idempotent_id': self.idempotent_id,
            'running': self.running,
            'last_time_running': self.last_time_running,
            'url': self.url,
            'join_user_url': self.join_user_url,
            'join_agent_url': self.join_agent_url,
            'is_running_url': self.is_running_url,
            'bbb_meeting_info': self.meeting_info(),
            'create_parameters': self.create_parameters,
            'join_user_parameters': self.join_user_parameters,
            'join_agent_parameters': self.join_agent_parameters,
            'metadata': self.metadata,
        }

    @property
    def meeting_id(self):
        return self.guid.hex

    def update_is_running(self):
        try:
            running = self.resource.bbb.is_meeting_running(self.meeting_id)
        except self.resource.bbb.BBBError:
            return
        if self.running != running:
            self.running = running
            if running:
                self.last_time_running = now()
                self.save(update_fields=['updated', 'last_time_running', 'running'])
            else:
                self.save(update_fields=['updated', 'running'])

    def create(self):
        return self.resource.bbb.meetings.create(
            name=self.name, meeting_id=self.meeting_id, **(self.create_parameters or {})
        )

    @property
    def cache_key(self):
        return f'bbb_{self.resource.slug}_{self.guid.hex}'

    def meeting_info(self):
        start = time.time()
        try:
            data, timestamp = cache.get(self.cache_key)
        except TypeError:
            data = None
        if data is None or (start - timestamp) > 30:
            try:
                data = self.resource.bbb.meetings.get(meeting_id=self.meeting_id).attributes
            except utils.BBB.BBBError:
                data = data or {}
            cache.set(self.cache_key, (data, start))
        return data

    def make_join_agent_url(self, full_name):
        return self.create().join_url(
            str(full_name or self.agent_full_name or _('Agent')),
            self.bbb.ROLE_MODERATOR,
            **(self.join_agent_parameters or {}),
        )

    def make_join_user_url(self, full_name):
        return self.create().join_url(
            str(full_name or self.user_full_name or _('User')),
            self.bbb.ROLE_VIEWER,
            **(self.join_user_parameters or {}),
        )
