from typing import List, Dict, Tuple

import utils
from database import conn_manager
import psycopg2.extras

from log_setup import logger


def rating_change(user_ratings: Dict[int, float], variant_rating: float, win: bool) -> Tuple[Dict[int, float], float]:
    """
    @param user_ratings: Mapping of user ids to ratings that played this game.
    @param variant_rating: Rating of the variant that was played
    @param win: Whether the team won the game
    @return: Mapping of user ids to their rating *changes* and *change* in variant rating
    """
    # TODO: Implement this properly (We have not decided how this will work exactly)
    # For now, return +1 elo for players and -1 elo for variants
    return {user_id: 1 for user_id in user_ratings.keys()}, -1


def next_game_to_rate():
    cur = conn_manager.get_new_cursor()
    cur.execute("SELECT games.id FROM games "
                "LEFT OUTER JOIN user_ratings"
                "  ON games.league_id = user_ratings.league_id "
                "WHERE user_ratings.league_id IS NULL "
                "ORDER BY games.league_id ASC "
                "LIMIT 1"
                )
    query_result = cur.fetchone()
    if query_result is None:
        return

    (game_id,) = query_result
    return game_id


def get_current_user_ratings(user_ids: List[int], rating_type: int) -> Dict[int, float]:
    """
    Fetches the current ratings for specified players and rating type from DB
    @return: Mapping user_id -> current rating
    """
    cur = conn_manager.get_new_cursor()
    cur.execute("SELECT user_id, rating FROM user_base_ratings "
                "WHERE user_id IN ({}) AND type = %s".format(", ".join("%s" for _ in user_ids)),
                user_ids + [rating_type]
                )
    base_ratings = cur.fetchall()
    cur.execute("SELECT user_ratings.user_id, value_after FROM user_ratings "
                "INNER JOIN ("
                "    SELECT user_id, type, MAX(league_id) AS max_league_id"
                "    FROM user_ratings "
                "    GROUP BY (user_id, type)"
                "  ) AS latest_user_ratings "
                "  ON user_ratings.league_id = latest_user_ratings.max_league_id "
                "WHERE user_ratings.user_id IN ({}) AND user_ratings.type = %s".format(", ".join("%s" for _ in user_ids)),
                user_ids + [rating_type]
                )
    current_ratings = cur.fetchall()

    ratings: Dict[int, float] = {}
    for user_id, base_rating in base_ratings:
        ratings[user_id] = base_rating
    for user_id, rating in current_ratings:
        ratings[user_id] = rating

    return ratings


def get_current_variant_rating(variant_id: int, player_count: int) -> float:
    cur = conn_manager.get_new_cursor()
    cur.execute("SELECT value_after FROM variant_ratings "
                "INNER JOIN ("
                "    SELECT variant_id, player_count, MAX(league_id) AS max_league_id"
                "    FROM variant_ratings "
                "    GROUP BY (variant_id, player_count)"
                "  ) AS latest_variant_ratings "
                "  ON variant_ratings.league_id = latest_variant_ratings.max_league_id "
                "WHERE variant_ratings.variant_id = %s AND variant_ratings.player_count = %s",
                (variant_id, player_count)
                )
    query_result = cur.fetchone()
    if query_result is not None:
        (current_rating, ) = query_result
        return current_rating

    # Reaching this point of code execution just means this is the first game for this variant rating
    cur.execute("SELECT rating FROM variant_base_ratings "
                "WHERE variant_id = %s AND player_count = %s",
                (variant_id, player_count)
                )
    query_result = cur.fetchone()
    if query_result is None:
        err_msg = "Failed to get current variant rating for variant {}.".format(variant_id)
        logger.error(err_msg)
        raise ValueError(err_msg)

    (base_rating, ) = query_result
    return base_rating


def process_rating_of_next_game():
    game_id = next_game_to_rate()
    if game_id is None:
        logger.verbose("All games already processed for rating changes.")
        return
    logger.verbose("Processing rating for game {}".format(game_id))
    cur = conn_manager.get_new_cursor()

    # Fetch data on the game played
    cur.execute(
        "SELECT games.league_id, games.num_players, games.score, variants.num_suits, variants.clue_starved, variants.id "
        "FROM games "
        "INNER JOIN variants "
        "  ON games.variant_id = variants.id "
        "WHERE games.id = %s",
        (game_id,)
        )
    league_id, num_players, score, num_suits, clue_starved, variant_id = cur.fetchone()

    # Fetch game participants
    cur.execute("SELECT game_participants.user_id FROM games "
                "INNER JOIN game_participants "
                "  ON games.id = game_participants.game_id "
                "WHERE games.id = %s",
                (game_id,)
                )
    user_ids = cur.fetchall()
    if len(user_ids) != num_players:
        err_msg = "Player number mismatch: Expected {} participants for game {}, but only found {} in DB: [{}]".format(
            num_players, game_id, len(user_ids), ", ".join(user_ids)
        )
        logger.error(err_msg)
        raise ValueError(err_msg)

    # Fetch current ratings of variant and players involved
    rating_type = utils.get_rating_type(clue_starved)
    user_ratings = get_current_user_ratings(user_ids, rating_type)
    variant_rating = get_current_variant_rating(variant_id, num_players)

    # Calculate changes in rating
    # TODO: If we want to use, we still have to think about how to define the K-factor and add it here
    user_changes, variant_change = rating_change(user_ratings, variant_rating, score == 5 * num_suits)

    # Update database for variants
    cur.execute("INSERT INTO variant_ratings (league_id, variant_id, player_count, change, value_after) "
                "VALUES (%s, %s, %s, %s, %s)",
                (league_id, variant_id, num_players, variant_change, variant_rating + variant_change)
                )
    # Note: We do not commit here, only after players have been processed as well

    user_ratings_vals = []
    for user_id, change in user_changes.items():
        user_ratings_vals.append((league_id, user_id, rating_type, change, user_ratings[user_id] + change))

    # This updates the player rating.
    psycopg2.extras.execute_values(
        cur,
        "INSERT INTO user_ratings (league_id, user_id, type, change, value_after) "
        "VALUES %s",
        user_ratings_vals
    )
    conn_manager.get_connection().commit()