import copy

from sqlalchemy import Table, Column, UniqueConstraint  # type: ignore
from sqlalchemy.types import String, Integer, JSON  # type: ignore
from sqlalchemy.dialects.mysql import BIGINT as BigInteger  # type: ignore
from typing import Optional, Dict, List, Tuple, Any

from bemani.common import GameConstants, ValidatedDict, Time
from bemani.data.mysql.base import BaseData, metadata
from bemani.data.types import UserID

"""
Table for storing logistical information about a player who's session is
live. Mostly, this is used to store IP addresses and such for players that
could potentially match.
"""
playsession = Table(
    'playsession',
    metadata,
    Column('id', Integer, nullable=False, primary_key=True),
    Column('game', String(32), nullable=False),
    Column('version', Integer, nullable=False),
    Column('userid', BigInteger(unsigned=True), nullable=False),
    Column('time', Integer, nullable=False, index=True),
    Column('data', JSON, nullable=False),
    UniqueConstraint('game', 'version', 'userid', name='game_version_userid'),
    mysql_charset='utf8mb4',
)

"""
Table for storing open lobbies for matching between games.
"""
lobby = Table(
    'lobby',
    metadata,
    Column('id', Integer, nullable=False, primary_key=True),
    Column('game', String(32), nullable=False),
    Column('version', Integer, nullable=False),
    Column('userid', BigInteger(unsigned=True), nullable=False),
    Column('time', Integer, nullable=False, index=True),
    Column('data', JSON, nullable=False),
    UniqueConstraint('game', 'version', 'userid', name='game_version_userid'),
    mysql_charset='utf8mb4',
)


class LobbyData(BaseData):

    def get_play_session_info(self, game: GameConstants, version: int, userid: UserID) -> Optional[ValidatedDict]:
        """
        Given a game, version and a user ID, look up play session information for that user.

        Parameters:
            game - Enum value identifying a game series.
            version - Integer identifying the version of the game in the series.
            userid - Integer identifying a user, as possibly looked up by UserData.

        Returns:
            A dictionary representing play session info stored by a game class, or None
            if there is no active session for this game/version/user. The dictionary will
            always contain an 'id' field which is the play session ID, and a 'time' field
            which represents the timestamp when the play session began.
        """
        sql = (
            "SELECT id, time, data FROM playsession "
            "WHERE game = :game AND version = :version AND userid = :userid "
            "AND time > :time"
        )
        cursor = self.execute(
            sql,
            {
                'game': game.value,
                'version': version,
                'userid': userid,
                'time': Time.now() - Time.SECONDS_IN_HOUR,
            },
        )

        if cursor.rowcount != 1:
            # Settings doesn't exist
            return None

        result = cursor.fetchone()
        data = ValidatedDict(self.deserialize(result['data']))
        data['id'] = result['id']
        data['time'] = result['time']
        return data

    def get_all_play_session_infos(self, game: GameConstants, version: int) -> List[Tuple[UserID, ValidatedDict]]:
        """
        Given a game and version, look up all play session information.

        Parameters:
            game - Enum value identifying a game series.
            version - Integer identifying the version of the game in the series.

        Returns:
            A list of Tuples, consisting of a UserID and the dictionary that would be
            returned for that user if get_play_session_info() was called for that user.
        """
        sql = (
            "SELECT id, time, userid, data FROM playsession "
            "WHERE game = :game AND version = :version "
            "AND time > :time"
        )
        cursor = self.execute(
            sql,
            {
                'game': game.value,
                'version': version,
                'time': Time.now() - Time.SECONDS_IN_HOUR,
            },
        )

        ret = []
        for result in cursor.fetchall():
            data = ValidatedDict(self.deserialize(result['data']))
            data['id'] = result['id']
            data['time'] = result['time']
            ret.append((UserID(result['userid']), data))
        return ret

    def put_play_session_info(self, game: GameConstants, version: int, userid: UserID, data: Dict[str, Any]) -> None:
        """
        Given a game, version and a user ID, save play session information for that user.

        Parameters:
            game - Enum value identifying a game series.
            version - Integer identifying the version of the game in the series.
            userid - Integer identifying a user.
            data - A dictionary of play session information to store.
        """
        data = copy.deepcopy(data)
        if 'id' in data:
            del data['id']
        if 'time' in data:
            del data['time']

        # Add json to player session
        sql = (
            "INSERT INTO playsession (game, version, userid, time, data) " +
            "VALUES (:game, :version, :userid, :time, :data) " +
            "ON DUPLICATE KEY UPDATE time=VALUES(time), data=VALUES(data)"
        )
        self.execute(
            sql,
            {
                'game': game.value,
                'version': version,
                'userid': userid,
                'time': Time.now(),
                'data': self.serialize(data),
            },
        )

    def destroy_play_session_info(self, game: GameConstants, version: int, userid: UserID) -> None:
        """
        Given a game, version and a user ID, throw away session info for that play session.

        Parameters:
            game - Enum value identifying a game series.
            version - Integer identifying the version of the game in the series.
            userid - Integer identifying a user, as possibly looked up by UserData.
        """
        # Kill this play session
        sql = (
            "DELETE FROM playsession WHERE game = :game AND version = :version AND userid = :userid"
        )
        self.execute(
            sql,
            {
                'game': game.value,
                'version': version,
                'userid': userid,
            },
        )
        # Prune any orphaned lobbies too
        sql = "DELETE FROM playsession WHERE time <= :time"
        self.execute(sql, {'time': Time.now() - Time.SECONDS_IN_HOUR})

    def get_lobby(self, game: GameConstants, version: int, userid: UserID) -> Optional[ValidatedDict]:
        """
        Given a game, version and a user ID, look up lobby information for that user.

        Parameters:
            game - Enum value identifying a game series.
            version - Integer identifying the version of the game in the series.
            userid - Integer identifying a user, as possibly looked up by UserData.

        Returns:
            A dictionary representing lobby info stored by a game class, or None
            if there is no active session for this game/version/user. The dictionary will
            always contain an 'id' field which is the lobby ID, and a 'time' field representing
            the timestamp the lobby was created.
        """
        sql = (
            "SELECT id, time, data FROM lobby "
            "WHERE game = :game AND version = :version AND userid = :userid "
            "AND time > :time"
        )
        cursor = self.execute(
            sql,
            {
                'game': game.value,
                'version': version,
                'userid': userid,
                'time': Time.now() - Time.SECONDS_IN_HOUR,
            },
        )

        if cursor.rowcount != 1:
            # Settings doesn't exist
            return None

        result = cursor.fetchone()
        data = ValidatedDict(self.deserialize(result['data']))
        data['id'] = result['id']
        data['time'] = result['time']
        return data

    def get_all_lobbies(self, game: GameConstants, version: int) -> List[Tuple[UserID, ValidatedDict]]:
        """
        Given a game and version, look up all active lobbies.

        Parameters:
            game - Enum value identifying a game series.
            version - Integer identifying the version of the game in the series.

        Returns:
            A list of dictionaries representing lobby info stored by a game class.
        """
        sql = (
            "SELECT userid, id, data FROM lobby "
            "WHERE game = :game AND version = :version AND time > :time"
        )
        cursor = self.execute(
            sql,
            {
                'game': game.value,
                'version': version,
                'time': Time.now() - Time.SECONDS_IN_HOUR,
            },
        )

        ret = []
        for result in cursor.fetchall():
            data = ValidatedDict(self.deserialize(result['data']))
            data['id'] = result['id']
            data['time'] = result['time']
            ret.append((UserID(result['userid']), data))
        return ret

    def put_lobby(self, game: GameConstants, version: int, userid: UserID, data: Dict[str, Any]) -> None:
        """
        Given a game, version and a user ID, save lobby information for that user.

        Parameters:
            game - Enum value identifying a game series.
            version - Integer identifying the version of the game in the series.
            userid - Integer identifying a user.
            data - A dictionary of lobby information to store.
        """
        data = copy.deepcopy(data)
        if 'id' in data:
            del data['id']
        if 'time' in data:
            del data['time']

        # Add json to lobby
        sql = (
            "INSERT INTO lobby (game, version, userid, time, data) " +
            "VALUES (:game, :version, :userid, :time, :data) " +
            "ON DUPLICATE KEY UPDATE time=VALUES(time), data=VALUES(data)"
        )
        self.execute(
            sql,
            {
                'game': game.value,
                'version': version,
                'userid': userid,
                'time': Time.now(),
                'data': self.serialize(data),
            },
        )

    def destroy_lobby(self, lobbyid: int) -> None:
        """
        Given a lobby ID, destroy the lobby. The lobby ID can be obtained by reading
        the 'id' field of the get_lobby response.

        Parameters:
            lobbyid: Integer identifying a lobby.
        """
        # Delete this lobby
        sql = "DELETE FROM lobby WHERE id = :id"
        self.execute(sql, {'id': lobbyid})
        # Prune any orphaned lobbies too
        sql = "DELETE FROM lobby WHERE time <= :time"
        self.execute(sql, {'time': Time.now() - Time.SECONDS_IN_HOUR})
