import datetime
import logging
import os
from collections import OrderedDict, defaultdict
from pathlib import Path
from typing import List

from django.conf import settings

import otree.common
import otree.export
import otree.session
from otree.constants import AUTO_NAME_BOTS_EXPORT_FOLDER
from otree.models import Session, Participant
from otree.session import SESSION_CONFIGS_DICT
from .bot import ParticipantBot
from otree.common import get_bots_module, get_models_module

logger = logging.getLogger(__name__)


class SessionBotRunner:
    def __init__(self, bots: List[ParticipantBot]):
        self.bots = {}

        for bot in bots:
            self.bots[bot.participant_code] = bot

    def play(self):
        '''round-robin'''
        self.open_start_urls()
        loops_without_progress = 0
        while True:
            if len(self.bots) == 0:
                return
            # bots got stuck if there's 2 wait pages in a row
            if loops_without_progress > 10:
                raise AssertionError('Bots got stuck')
            # store in a separate list so we don't mutate the iterable
            playable_ids = list(self.bots.keys())
            progress_made = False
            for pk in playable_ids:
                bot = self.bots[pk]
                if bot.on_wait_page():
                    pass
                else:
                    try:
                        submission = next(bot.submits_generator)
                    except StopIteration:
                        # this bot is finished
                        self.bots.pop(pk)
                    else:
                        bot.submit(**submission)
                    progress_made = True
                    # need to set this so that we only count *consecutive* unsuccessful loops
                    # see Manu's error on 2019-12-19.
                    loops_without_progress = 0
            if not progress_made:
                loops_without_progress += 1

    def open_start_urls(self):
        for bot in self.bots.values():
            bot.open_start_url()


def make_bots(*, session_pk, case_number, use_browser_bots) -> List[ParticipantBot]:
    update_kwargs = {'_is_bot': True}
    if use_browser_bots:
        update_kwargs['is_browser_bot'] = True

    Participant.objects.filter(session_id=session_pk).update(**update_kwargs)
    bots = []

    # can't use .distinct('player_pk') because it only works on Postgres
    # this implicitly orders by round also
    session = Session.objects.get(pk=session_pk)

    participant_codes = session.participant_set.order_by('id').values_list(
        'code', flat=True
    )

    player_bots_dict = {pcode: [] for pcode in participant_codes}

    for app_name in session.config['app_sequence']:
        bots_module = get_bots_module(app_name)
        models_module = get_models_module(app_name)
        players = (
            models_module.Player.objects.filter(session_id=session_pk)
            .order_by('round_number')
            .values('id', 'participant_id', 'participant__code', 'subsession_id')
        )
        for player in players:
            participant_code = player['participant__code']
            player_bot = bots_module.PlayerBot(
                case_number=case_number,
                app_name=app_name,
                player_pk=player['id'],
                subsession_pk=player['subsession_id'],
                participant_code=participant_code,
                session_pk=session_pk,
            )
            player_bots_dict[participant_code].append(player_bot)

    executed_live_methods = set()

    for participant_code, player_bots in player_bots_dict.items():
        bot = ParticipantBot(
            participant_code,
            player_bots=player_bots,
            executed_live_methods=executed_live_methods,
        )
        bots.append(bot)

    return bots


def run_bots(session: Session, case_number=None):
    bot_list = make_bots(
        session_pk=session.pk, case_number=case_number, use_browser_bots=False
    )
    if session.get_room() is None:
        session.mock_exogenous_data()
    session.save()
    runner = SessionBotRunner(bots=bot_list)
    runner.play()


def run_all_bots_for_session_config(session_config_name, num_participants, export_path):
    """
    this means all test cases are in 1 big test case.
    so if 1 fails, the others will not get run.
    """
    if session_config_name:
        session_config_names = [session_config_name]
    else:
        session_config_names = SESSION_CONFIGS_DICT.keys()

    for config_name in session_config_names:
        try:
            config = SESSION_CONFIGS_DICT[config_name]
        except KeyError:
            # important to alert the user, since people might be trying to enter app names.
            msg = f"No session config with name '{config_name}'."
            raise Exception(msg) from None

        num_bot_cases = config.get_num_bot_cases()
        for case_number in range(num_bot_cases):
            logger.info(
                "Creating '{}' session (test case {})".format(config_name, case_number)
            )

            session = otree.session.create_session(
                session_config_name=config_name,
                num_participants=(num_participants or config['num_demo_participants']),
            )
            run_bots(session, case_number=case_number)

            logger.info('Bots completed session')
    if export_path:

        now = datetime.datetime.now()

        if export_path == AUTO_NAME_BOTS_EXPORT_FOLDER:
            # oTree convention to prefix __temp all temp folders.
            export_path = now.strftime('__temp_bots_%b%d_%Hh%Mm%S.%f')[:-5] + 's'

        os.makedirs(export_path, exist_ok=True)

        for app in settings.INSTALLED_OTREE_APPS:
            model_module = otree.common.get_models_module(app)
            if model_module.Player.objects.exists():
                fpath = Path(export_path, "{}.csv".format(app))
                with fpath.open("w", encoding="utf8") as fp:
                    otree.export.export_app(app, fp, file_extension='csv')
        fpath = Path(export_path, "all_apps_wide.csv")
        with fpath.open("w", encoding="utf8") as fp:
            otree.export.export_wide(fp, 'csv')

        logger.info('Exported CSV to folder "{}"'.format(export_path))
    else:
        logger.info(
            'Tip: Run this command with the --export flag'
            ' to save the data generated by bots.'
        )
