#!/usr/bin/env python
# -*- coding: utf-8 -*-

import json
import logging
import django.db
import django.utils.timezone
import traceback
from datetime import timedelta

from channels import Group
from channels.generic.websockets import JsonWebsocketConsumer

import otree.session
from otree.models import Participant, Session
from otree.models_concrete import (
    CompletedGroupWaitPage, CompletedSubsessionWaitPage)
from otree.common_internal import (
    channels_wait_page_group_name, channels_create_session_group_name,
    channels_group_by_arrival_time_group_name, get_models_module,
    channels_room_participants_group_name
)
from otree.models_concrete import (
    FailedSessionCreation, ParticipantRoomVisit,
    FAILURE_MESSAGE_MAX_LENGTH, BrowserBotsLauncherSessionCode)
from otree.room import ROOM_DICT

logger = logging.getLogger(__name__)


class OTreeJsonWebsocketConsumer(JsonWebsocketConsumer):

    def clean_kwargs(self, **kwargs):
        '''
        subclasses should override if the route receives a comma-separated params arg.
        otherwise, this just passes the route kwargs as is (usually there is just one).
        The output of this method is passed to self.group_name(), self.post_connect,
        and self.pre_disconnect, so within each class, all 3 of those methods must
        accept the same args (or at least take a **kwargs wildcard, if the args aren't used)
        '''
        return kwargs

    def group_name(self, **kwargs):
        raise NotImplementedError()

    def connection_groups(self, **kwargs):
        kwargs = self.clean_kwargs(**kwargs)
        group_name = self.group_name(**kwargs)
        return [group_name]

    def connect(self, message, **kwargs):
        self.message.reply_channel.send({"accept": True})
        kwargs = self.clean_kwargs(**kwargs)
        self.post_connect(**kwargs)

    def post_connect(self, **kwargs):
        pass

    def disconnect(self, message, **kwargs):
        kwargs = self.clean_kwargs(**kwargs)
        self.pre_disconnect(**kwargs)

    def pre_disconnect(self, **kwargs):
        pass


class GroupByArrivalTime(OTreeJsonWebsocketConsumer):

    def clean_kwargs(self, params):
        session_pk, page_index, app_name, player_id = params.split(',')
        return {
            'app_name': app_name,
            'session_pk': int(session_pk),
            'page_index': int(page_index),
            'player_id': int(player_id)
        }

    def group_name(self, app_name, player_id, page_index, session_pk):
        return channels_group_by_arrival_time_group_name(
            session_pk, page_index)

    def post_connect(self, app_name, player_id, page_index, session_pk):
        models_module = get_models_module(app_name)
        player = models_module.Player.objects.get(id=player_id)
        group_id_in_subsession = player.group.id_in_subsession

        ready = CompletedGroupWaitPage.objects.filter(
            page_index=page_index,
            id_in_subsession=int(group_id_in_subsession),
            session_id=session_pk,
            fully_completed=True).exists()
        if ready:
            self.send({'status': 'ready'})


class WaitPage(OTreeJsonWebsocketConsumer):

    def clean_kwargs(self, params):
        session_pk, page_index, group_id_in_subsession = params.split(',')
        return {
            'session_pk': int(session_pk),
            'page_index': int(page_index),
            # don't convert group_id_in_subsession to int yet, it might be null
            'group_id_in_subsession': group_id_in_subsession,
        }

    def group_name(self, session_pk, page_index, group_id_in_subsession):
        return channels_wait_page_group_name(
                session_pk, page_index, group_id_in_subsession)

    def post_connect(self, session_pk, page_index, group_id_in_subsession):
        # in case message was sent before this web socket connects
        if group_id_in_subsession:
            ready = CompletedGroupWaitPage.objects.filter(
                page_index=page_index,
                id_in_subsession=int(group_id_in_subsession),
                session_id=session_pk,
                fully_completed=True).exists()
        else:  # subsession
            ready = CompletedSubsessionWaitPage.objects.filter(
                page_index=page_index,
                session_id=session_pk,
                fully_completed=True).exists()
        if ready:
            self.send({'status': 'ready'})


class AutoAdvance(OTreeJsonWebsocketConsumer):
    def clean_kwargs(self, params):
        participant_code, page_index = params.split(',')
        return {
            'participant_code': participant_code,
            'page_index': int(page_index),
        }

    def group_name(self, page_index, participant_code):
        return 'auto-advance-{}'.format(participant_code)

    def post_connect(self, page_index, participant_code):
        # in case message was sent before this web socket connects
        result = Participant.objects.filter(
                code=participant_code).values_list(
            '_index_in_pages', flat=True)
        try:
            page_should_be_on = result[0]
        except IndexError:
            # doesn't get shown because not yet localized
            self.send({'error': 'Participant not found in database.'})
            return
        if page_should_be_on > page_index:
            self.send({'auto_advanced': True})


def create_session(message):
    group = Group(message['channels_group_name'])

    kwargs = message['kwargs']

    # because it's launched through web UI
    kwargs['honor_browser_bots_config'] = True
    try:
        otree.session.create_session(**kwargs)
    except Exception as e:

        # full error message is printed to console (though sometimes not?)
        error_message = 'Failed to create session: "{}"'.format(e)
        traceback_str = traceback.format_exc()
        group.send(
            {'text': json.dumps(
                {
                    'error': error_message,
                    'traceback': traceback_str,
                })}
        )
        FailedSessionCreation.objects.create(
            pre_create_id=kwargs['_pre_create_id'],
            message=error_message[:FAILURE_MESSAGE_MAX_LENGTH],
            traceback=traceback_str
        )
        raise

    group.send(
        {'text': json.dumps(
            {'status': 'ready'})}
    )

    if 'room_name' in kwargs:
        Group(channels_room_participants_group_name(kwargs['room_name'])).send(
            {'text': json.dumps(
                {'status': 'session_ready'})}
        )


class WaitForSession(OTreeJsonWebsocketConsumer):
    def clean_kwargs(self, **kwargs):
        return kwargs

    def group_name(self, pre_create_id):
        return channels_create_session_group_name(pre_create_id)

    def post_connect(self, pre_create_id):

        group_name = self.group_name(pre_create_id)

        # in case message was sent before this web socket connects
        if Session.objects.filter(
                _pre_create_id=pre_create_id, ready=True).exists():
            self.group_send(group_name,
                {'text': json.dumps(
                    {'status': 'ready'})}
            )
        else:
            failure = FailedSessionCreation.objects.filter(
                pre_create_id=pre_create_id
            ).first()
            if failure:
                self.group_send(group_name,
                    {'text': json.dumps(
                        {'error': failure.message,
                         'traceback': failure.traceback})}
                )


class RoomAdmin(OTreeJsonWebsocketConsumer):

    def group_name(self, room):
        return 'room-admin-{}'.format(room)

    def post_connect(self, room):

        room_object = ROOM_DICT[room]

        now = django.utils.timezone.now()
        stale_threshold = now - timedelta(seconds=15)
        present_list = ParticipantRoomVisit.objects.filter(
            room_name=room_object.name,
            last_updated__gte=stale_threshold,
        ).values_list('participant_label', flat=True)

        # make it JSON serializable
        present_list = list(present_list)

        self.send({
            'status': 'load_participant_lists',
            'participants_present': present_list,
        })

        # prune very old visits -- don't want a resource leak
        # because sometimes not getting deleted on WebSocket disconnect
        very_stale_threshold = now - timedelta(minutes=10)
        ParticipantRoomVisit.objects.filter(
            room_name=room_object.name,
            last_updated__lt=very_stale_threshold,
        ).delete()


class RoomParticipant(OTreeJsonWebsocketConsumer):

    def clean_kwargs(self, params):
        room_name, participant_label, tab_unique_id = params.split(',')
        return {
            'room_name': room_name,
            'participant_label': participant_label,
            'tab_unique_id': tab_unique_id,
        }

    def group_name(self, room_name, participant_label, tab_unique_id):
        return channels_room_participants_group_name(room_name)

    def post_connect(self, room_name, participant_label, tab_unique_id):
        if room_name in ROOM_DICT:
            room = ROOM_DICT[room_name]
        else:
            # doesn't get shown because not yet localized
            self.send({'error': 'Invalid room name "{}".'.format(room_name)})
            return
        if room.has_session():
            self.send({'status': 'session_ready'})
        else:
            try:
                ParticipantRoomVisit.objects.create(
                    participant_label=participant_label,
                    room_name=room_name,
                    tab_unique_id=tab_unique_id
                )
            except django.db.IntegrityError as exc:
                # possible that the tab connected twice
                # without disconnecting in between
                # because of WebSocket failure
                # tab_unique_id is unique=True,
                # so this will throw an integrity error.
                logger.info(
                    'ParticipantRoomVisit: not creating a new record because a '
                    'database integrity error was thrown. '
                    'The exception was: {}: {}'.format(type(exc), exc))
                pass
            self.group_send(
                'room-admin-{}'.format(room_name),
                {
                    'status': 'add_participant',
                    'participant': participant_label
                }
            )

    def pre_disconnect(self, room_name, participant_label, tab_unique_id):

        if room_name in ROOM_DICT:
            room = ROOM_DICT[room_name]
        else:
            # doesn't get shown because not yet localized
            self.send({'error': 'Invalid room name "{}".'.format(room_name)})
            return

        # should use filter instead of get,
        # because if the DB is recreated,
        # the record could already be deleted
        ParticipantRoomVisit.objects.filter(
            participant_label=participant_label,
            room_name=room_name,
            tab_unique_id=tab_unique_id).delete()

        if room.has_participant_labels():
            if not ParticipantRoomVisit.objects.filter(
                participant_label=participant_label,
                room_name=room_name
            ).exists():
                # it's ok if there is a race condition --
                # in JS removing a participant is idempotent
                self.group_send(
                    'room-admin-{}'.format(room_name),
                    {
                        'status': 'remove_participant',
                        'participant': participant_label
                    }
                )
        else:
            self.group_send(
                'room-admin-{}'.format(room_name),
                {
                    'status': 'remove_participant',
                }
            )


class BrowserBotsClient(OTreeJsonWebsocketConsumer):

    def group_name(self, session_code):
        return 'browser-bots-client-{}'.format(self.kwargs['session_code'])


class BrowserBot(OTreeJsonWebsocketConsumer):

    def group_name(self):
        return 'browser_bot_wait'

    def post_connect(self):
        launcher_session_info = BrowserBotsLauncherSessionCode.objects.first()
        if launcher_session_info:
            self.send({'status': 'session_ready'})
