import json
import datetime
import time

from sqlalchemy import Column as Column, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import sqltypes as st
from starlette.exceptions import HTTPException
import otree.common
import otree.database
from otree.common import random_chars_8, ADMIN_SECRET_CODE
import otree.constants
from otree.database import MixinVars, CurrencyType, NoResultFound
from otree.lookup import url_i_should_be_on, get_page_lookup
import otree.channels.utils as channel_utils
from otree import settings


class Participant(MixinVars, otree.database.SSPPGModel):
    __tablename__ = 'otree_participant'

    session_id = Column(st.Integer, ForeignKey('otree_session.id', ondelete='CASCADE'))
    # getting integrityerror, so trying passive_deletes
    # https://stackoverflow.com/questions/5033547/sqlalchemy-cascade-delete
    session = relationship("Session", back_populates="pp_set")

    label = Column(st.String(100), nullable=True)

    id_in_session = Column(st.Integer, nullable=True)

    payoff = Column(CurrencyType, default=0)

    # status could be 'finished', 'dropped out', etc. Empty string means active/in progress
    status = Column(st.String(50), default='', nullable=False)

    # better to use a string so that it doesn't become unofficial API
    time_started_utc = Column(st.String(50), nullable=True)
    mturk_assignment_id = Column(st.String(50), nullable=True)
    mturk_worker_id = Column(st.String(50), nullable=True)

    _index_in_pages = Column(st.Integer, default=0, index=True)

    def _numeric_label(self):
        """the human-readable version."""
        return 'P{}'.format(self.id_in_session)

    _monitor_note_json = Column(st.String(300), default='[]')

    code = Column(
        st.String(16),
        default=random_chars_8,
        # set non-nullable, until we make our CharField non-nullable
        nullable=False,
        # unique implies DB index
        unique=True,
        index=True,
    )

    # useful when we don't want to load the whole session just to get the code
    _session_code = Column(st.String(16))

    visited = Column(
        st.Boolean,
        default=False,
        index=True,
    )

    # stores when the page was first visited
    _last_page_timestamp = Column(st.Integer, nullable=True)

    _last_request_timestamp = Column(st.Integer, nullable=True)

    is_on_wait_page = Column(st.Boolean, default=False)

    # these are both for the admin
    # In the changelist, simply call these "page" and "app"
    _current_page_name = Column(st.String(200), nullable=True)
    _current_app_name = Column(st.String(200), nullable=True)

    # doesn't actually hold any data. just used because we need
    # a column in the monitor table for the presence icon,
    # and the code currently expects an attribute
    _presence = ''  # Column(st.String(1), default=True)

    # only to be displayed in the admin participants changelist
    _round_number = Column(st.Integer, nullable=True)

    _current_form_page_url = Column(st.String(500))

    _max_page_index = Column(
        st.Integer,
    )

    _SETATTR_NO_FIELD_HINT = ' You can define it in the PARTICIPANT_FIELDS setting.'

    _is_bot = Column(st.Boolean, default=False)
    # can't start with an underscore because used in template
    # can't end with underscore because it's a django field (fields.E001)
    is_browser_bot = Column(st.Boolean, default=False)

    _timeout_expiration_time = otree.database.FloatField()
    _timeout_page_index = Column(
        st.Integer,
    )

    _waitpage_is_connected = Column(st.Boolean, default=False)
    _waitpage_tab_hidden = Column(st.Boolean, default=False)
    _gbat_page_index = Column(
        st.Integer,
    )
    _gbat_grouped = Column(
        st.Boolean,
    )

    def set_label(self, label):
        if not label:
            return
        if len(label) > 100:
            raise HTTPException(
                404, f'participant_label is too long or malformed: {label}'
            )
        self.label = label

    def _current_page_of_total(self):
        # don't put 'pages' because that causes wrapping which takes more space
        # since it's longer than the header
        return [self._index_in_pages, self._max_page_index]

    # because variables used in templates can't start with an underscore
    def current_page_(self):
        return f'{self._index_in_pages}/{self._max_page_index}'

    def get_players(self):
        """Used to calculate payoffs"""
        lst = []
        app_sequence = self.session.config['app_sequence']
        for app in app_sequence:
            models_module = otree.common.get_models_module(app)
            players = models_module.Player.objects_filter(participant=self).order_by(
                'round_number'
            )
            lst.extend(list(players))
        return lst

    def get_player(self, app_name, round_number):
        models_module = otree.common.get_models_module(app_name)
        PlayerClass = getattr(models_module, 'Player')
        try:
            return PlayerClass.objects_get(participant=self, round_number=round_number)
        except NoResultFound:
            msg = f"No player found for the given app_name={repr(app_name)} and round_number={repr(round_number)}"
            raise Exception(msg) from None

    def _url_i_should_be_on(self):
        if not self.visited:
            return self._start_url()
        if self._index_in_pages <= self._max_page_index:
            return url_i_should_be_on(
                self.code, self._session_code, self._index_in_pages
            )
        return '/OutOfRangeNotification/' + self.code

    def _start_url(self):
        return otree.common.participant_start_url(self.code)

    def payoff_in_real_world_currency(self):
        return self.payoff.to_real_world_currency(self.session)

    def payoff_plus_participation_fee(self):
        return self.session._get_payoff_plus_participation_fee(self.payoff)

    def _get_current_player(self):
        lookup = get_page_lookup(self._session_code, self._index_in_pages)
        models_module = otree.common.get_models_module(lookup.app_name)
        PlayerClass = getattr(models_module, 'Player')
        return PlayerClass.objects_get(
            participant=self, round_number=lookup.round_number
        )

    def initialize(self, participant_label):
        """in a separate function so that we can call it individually,
        e.g. from advance_last_place_participants"""
        pp = self
        if pp._index_in_pages == 0:
            pp._index_in_pages = 1
            pp.visited = True

            # participant.label might already have been set
            if not pp.label:
                pp.set_label(participant_label)

            # use UTC because daylight savings is not abandoned yet in EU
            # if anything, they will switch to permanent CEST (UTC+2)
            pp.time_started_utc = str(datetime.datetime.utcnow())
            pp._last_page_timestamp = int(time.time())

            from otree import common2

            row = common2.TimeSpentRow(
                session_code=pp._session_code,
                participant_id_in_session=pp.id_in_session,
                participant_code=pp.code,
                page_index=0,
                app_name='',
                page_name='InitializeParticipant',
                epoch_time_completed=int(time.time()),
                round_number=1,
                timeout_happened=0,
                is_wait_page=0,
            )
            common2.write_row_to_page_buffer(row)

    def _update_monitor_table(self):
        from otree import export

        channel_utils.sync_group_send(
            group=channel_utils.session_monitor_group_name(self._session_code),
            data=dict(rows=export.get_rows_for_monitor([self])),
        )

    def _get_page_instance(self):
        if self._index_in_pages > self._max_page_index:
            return
        page = get_page_lookup(
            self._session_code, self._index_in_pages
        ).page_class.instantiate_without_request()
        page.set_attributes(self)
        return page

    def _submit_current_page(self):
        from otree.api import Page

        page = self._get_page_instance()
        if isinstance(page, Page):
            from starlette.datastructures import FormData

            page._form_data = FormData(
                {
                    otree.constants.admin_secret_code: ADMIN_SECRET_CODE,
                    otree.constants.timeout_happened: '1',
                }
            )
            page.post()

    def _visit_current_page(self):
        """
        we need the redirect logic because if it's from ensure_pages_visited,
        then the previous page
        was a wait page, so that will not handle redirects.
        """
        # don't need to handle more redirects than that.
        for i in range(5):
            page = self._get_page_instance()
            if not page:
                return
            # note-to-self
            # it's possible that the slowest user is on a wait page,
            # especially if their browser is closed.
            # because they were waiting for another user who then
            # advanced past the wait page, but they were never
            # advanced themselves.
            resp = page.get()
            if not str(resp.status_code).startswith('3'):
                return
