from sqlalchemy import Column as C, ForeignKey
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import relationship, backref
from sqlalchemy.sql import sqltypes as st

import otree.database
from otree.common import (
    get_models_module,
    in_round,
    in_rounds,
    InvalidRoundError,
)
from otree.constants import BaseConstants, get_role, get_roles
from otree.database import db, NoResultFound, MixinSessionFK, SPGModel


class BaseGroup(SPGModel, MixinSessionFK):
    __abstract__ = True

    id_in_subsession = C(st.Integer, index=True)

    round_number = C(st.Integer, index=True)

    @property
    def _Constants(self) -> BaseConstants:
        return get_models_module(self.get_folder_name()).Constants

    def get_players(self):
        return list(self.player_set.order_by('id_in_group'))

    def get_player_by_id(self, id_in_group):
        try:
            return self.player_set.filter_by(id_in_group=id_in_group).one()
        except NoResultFound:
            msg = 'No player with id_in_group {}'.format(id_in_group)
            raise ValueError(msg) from None

    def get_player_by_role(self, role):
        if get_roles(self._Constants):
            try:
                return self.player_set.filter_by(_role=role).one()
            except NoResultFound:
                pass
        else:
            for p in self.get_players():
                if p.role() == role:
                    return p
        msg = f'No player with role "{role}"'
        raise ValueError(msg)

    def set_players(self, players_list):
        """
        don't allow passing in a list of ints, because there are 2 ways of reading it.
        Does set_players([2,3,1]) mean that player 1 gets id_in_group 2,
        or does it mean player 2 gets id_in_group 1?
        """
        Constants = self._Constants
        roles = get_roles(Constants)
        for i, player in enumerate(players_list, start=1):
            player.group = self
            player.id_in_group = i
            player._role = get_role(roles, i)
        db.commit()

    def in_round(self, round_number):
        try:
            return in_round(
                type(self),
                round_number,
                session=self.session,
                id_in_subsession=self.id_in_subsession,
            )
        except InvalidRoundError as exc:
            msg = (
                str(exc)
                + '; '
                + (
                    'Hint: you should not use this '
                    'method if you are rearranging groups between rounds.'
                )
            )
            ExceptionClass = type(exc)
            raise ExceptionClass(msg) from None

    def in_rounds(self, first, last):
        try:
            return in_rounds(
                type(self),
                first,
                last,
                session=self.session,
                id_in_subsession=self.id_in_subsession,
            )
        except InvalidRoundError as exc:
            msg = (
                str(exc)
                + '; '
                + (
                    'Hint: you should not use this '
                    'method if you are rearranging groups between rounds.'
                )
            )
            ExceptionClass = type(exc)
            raise ExceptionClass(msg) from None

    def in_previous_rounds(self):
        return self.in_rounds(1, self.round_number - 1)

    def in_all_rounds(self):
        return self.in_previous_rounds() + [self]

    @declared_attr
    def subsession_id(cls):
        app_name = cls.get_folder_name()
        return C(st.Integer, ForeignKey(f'{app_name}_subsession.id'))

    @declared_attr
    def subsession(cls):
        return relationship(f'{cls.__module__}.Subsession', back_populates='group_set')

    @declared_attr
    def player_set(cls):
        return relationship(
            f'{cls.__module__}.Player', back_populates="group", lazy='dynamic'
        )
