from typing import Callable, Optional, List
import logging
import requests
import pandas as pd

logging.basicConfig(encoding='utf-8', level=logging.INFO)
logger = logging.getLogger(__name__)


BASE_URL = "https://api-live.euroleague.net"
version = "v3"
competition = "E"
URL = f"{BASE_URL}/{version}/competitions/{competition}"


def make_season_game_url(
    season: int,
    game_code: int,
    endpoint: str
) -> str:
    """
    Concatenates the base URL and makes the game url.

    Args:

        season (int): The start year of the season

        game_code (int): The code of the game. Find the code from
            Euroleague's website

        endpoint (str): The endpoint of the API

    Returns:

        str: the full URL
    """
    FULL_URL = f"{URL}/seasons/E{season}/games/{game_code}/{endpoint}"
    return FULL_URL


def get_requests(
    url: str,
    params={},
    headers={"Accept": "application/json"}
) -> requests.models.Response:
    """
    A wrapper to `requests.get()` which handles unsuccesful requests too.

    Args:

        url (str): _description_

        params (dict, optional): The `params` variables in get requests.
            Defaults to {}.

        headers (dict, optional): the `header` variable in get requests.
            Defaults to {"Accept": "application/json"}.

    Raises:

        Requests Error: If get request was not succesful

    Returns:

        requests.models.Response: The response object.
    """
    r = requests.get(url, params=params, headers=headers)

    if r.status_code != 200:
        r.raise_for_status()

    return r


def get_game_data(season: int, game_code: int, endpoint: str) -> pd.DataFrame:
    """
    A wrapper function for getting game-level data.

    Args:

        season (int): The start year of the season

        game_code (int): The game code of the game of interest.
            Find the game code from Euroleague's website

        endpoint (str): The type of game data, available variables:
            - report
            - stats
            - teamsComparison

    Raises:

        ValueError: If input endpoint is not applicable.

    Returns:

        pd.DataFrame: A dataframe with the game data.
    """
    game_endpoints = ["report", "stats", "teamsComparison"]

    raise_error(endpoint, "Statistic type", game_endpoints, False)

    url_ = make_season_game_url(season, game_code, endpoint)
    r = get_requests(url_)

    data = r.json()
    df = pd.json_normalize(data)
    return df


def get_season_data_from_game_data(
    season: int,
    fun: Callable[[int, int], pd.DataFrame]
) -> pd.DataFrame:
    """
    A wrapper function for getting game data for all games in a single season.

    Args:

        season (int, optional): The start year of the season.

        fun (Callable[[int, int], pd.DataFrame]): A callable function that
            determines that type of data to be collected. Available values:
            - get_game_report
            - get_game_stats
            - get_game_teams_comparison

    Returns:

        pd.DataFrame: A dataframe with the game data.
    """
    data_list = []
    game_code = 0
    attempt = 0
    while True:
        game_code += 1
        try:
            shots_df = fun(season, game_code)
            attempt = 0
            data_list.append(shots_df)
        except ValueError:
            attempt += 1

        # Due to the ban of Russian teams from Euroleague in 2021
        # this is a hack for not breaking in the first
        # "empty" game, but only after 5 *concecutive*
        # "empty" games
        if attempt > 5:
            logger.debug(
                "No more available game data for this season, break and exit"
            )
            break

    if data_list:
        data_df = pd.concat(data_list, axis=0)
    else:
        data_df = pd.DataFrame([])
    return data_df


def get_range_seasons_data(
    start_season: int,
    end_season: int,
    fun: Callable[[int, int], pd.DataFrame]
) -> pd.DataFrame:
    """
    A wrapper function with the all game data in a range of seasons

    Args:

        season (int, optional): The start year of the season.

        end_season (int): The start year of teh end season

        fun (Callable[[int, int], pd.DataFrame]): A callable function that
            determines that type of data to be collected. Available values:
            - get_game_report
            - get_game_stats
            - get_game_teams_comparison

    Returns:

        pd.DataFrame: A dataframe with the game data
    """
    data = []
    for season in range(start_season, end_season + 1):
        data_df = get_season_data_from_game_data(season, fun)
        data.append(data_df)
    df = pd.concat(data)
    return df


def get_player_stats(
    endpoint: str,
    params={},
    phase_type_code: Optional[str] = None,
    statistic_mode: str = "PerGame"
) -> pd.DataFrame:
    """
    A wrapper function for getting the players' stats for
    - all seasons
    - a single season
    - a range of seasons

    Args:

        endpoint (str): The type of stats, available variables:
            - traditional
            - advanced
            - misc
            - scoring

        params (Dict[str, Union[str, int]]): A dictionary of parameters for the
            get request.

        phase_type_code (Optional[str], optional): The phase of the season,
            available variables:
            - "RS" (regular season)
            - "PO" (play-off)
            - "FF" (final four)
            Defaults to None, which includes all phases.

        statistic_mode (str, optional): The aggregation of statistics,
            available variables:
            - PerGame
            - Accumulated
            - Per100Possesions
            Defaults to "PerGame".

    Raises:

        ValueError: If the endpoint is not applicable

        ValueError: If the phase_type_code is not applicable

        ValueError: If the statistic_mode is not applicable

    Returns:

        pd.DataFrame: A dataframe with the players' stats.
    """

    available_endpoints = ["traditional", "advanced", "misc", "scoring"]
    available_phase_type_code = ["RS", "PO", "FF"]
    available_stat_mode = ["PerGame", "Accumulated", "Per100Possesions"]

    raise_error(
        endpoint, "Statistic type", available_endpoints, False)
    raise_error(
        statistic_mode, "Statistic Aggregation", available_stat_mode, False)
    raise_error(
        phase_type_code, "Phase type code", available_phase_type_code, True)

    params["statisticMode"] = statistic_mode
    params["phaseTypeCode"] = phase_type_code
    params["limit"] = 400

    url_ = f"{URL}/statistics/players/{endpoint}"

    r = get_requests(url_, params=params)
    data = r.json()
    if data["total"] < len(data["players"]):
        params["limit"] = len(data["players"]) + 1
        r = get_requests(url_, params=params)
        data = r.json()
    df = pd.json_normalize(data["players"])
    return df


def get_team_stats(
    endpoint: str,
    params={},
    phase_type_code: Optional[str] = None,
    statistic_mode: str = "PerGame"
) -> pd.DataFrame:
    """
    A wrapper functions for collecting teams' stats.
    Allows for three types of data:
    - all seasons
    - single season
    - range of seasons

    Args:

        endpoint (str): The type of stats to fetch. Available values:
            - traditional
            - advanced
            - opponentsTraditional
            - opponentsAdvanced

        params (Dict[str, Union[str, int]]): A dictionary of the parmaters for
            the get request.

        phase_type_code (Optional[str], optional): The phase of the season,
            available variables:
            - "RS" (regular season)
            - "PO" (play-off)
            - "FF" (final four)
            Defaults to None, which includes all phases.

        statistic_mode (str, optional): The aggregation of statistics,
            available variables:
            - PerGame
            - Accumulated
            Defaults to "PerGame".

    Raises:

        ValueError: If the endpoint is not applicable

        ValueError: If the phase_type_code is not applicable

        ValueError: If the statistic_mode is not applicable

    Returns:

        pd.DataFrame: A dataframe with the teams' stats.
    """

    available_endpoints = [
        "traditional", "advanced",
        "opponentsTraditional", "opponentsAdvanced"
    ]
    available_phase_type_code = ["RS", "PO", "FF"]
    available_stat_mode = ["PerGame", "Accumulated"]

    raise_error(
        endpoint, "Statistic type", available_endpoints, False)
    raise_error(
        statistic_mode, "Statistic Aggregation", available_stat_mode, False)
    raise_error(
        phase_type_code, "Phase type code", available_phase_type_code, True)

    params["statisticMode"] = statistic_mode
    params["phaseTypeCode"] = phase_type_code
    params["limit"] = 400

    url_ = f"{URL}/statistics/teams/{endpoint}"

    r = get_requests(url_, params=params)
    data = r.json()
    if data["total"] < len(data["teams"]):
        params["limit"] = len(data["teams"]) + 1
        r = get_requests(url_, params=params)
        data = r.json()
    df = pd.json_normalize(data["teams"])
    return df


def get_player_stats_leaders(
    params={},
    stat_category: str = "Score",
    top_n: int = 200,
    phase_type_code: Optional[str] = None,
    statistic_mode: str = "PerGame",
    game_type: Optional[str] = None,
    position: Optional[str] = None
) -> pd.DataFrame:
    """
    A wrapper function for collecting the leading players in a given
    stat category.

    Args:

        params (Dict[str, Union[str, int]]): A dictionary of parameters for the
            get request.

        top_n (int): The number of top N players to return.  Defaults to 200.

        stat_category (str): The stat category. Available values:
            - Valuation
            - Score
            - TotalRebounds
            - OffensiveRebounds
            - Assistances
            - Steals
            - BlocksFavour
            - BlocksAgainst
            - Turnovers
            - FoulsReceived
            - FoulsCommited
            - FreeThrowsMade
            - FreeThrowsAttempted
            - FreeThrowsPercent
            - FieldGoalsMade2
            - FieldGoalsAttempted2
            - FieldGoals2Percent
            - FieldGoalsMade3
            - FieldGoalsAttempted3
            - FieldGoals3Percent
            - FieldGoalsMadeTotal
            - FieldGoalsAttemptedTotal
            - FieldGoalsPercent
            - AccuracyMade
            - AccuracyAttempted
            - AccuracyPercent
            - AssitancesTurnoversRation
            - GamesPlayed
            - GamesStarted
            - TimePlayed
            - Contras
            - Dunks
            - OffensiveReboundPercentage
            - DefensiveReboundPercentage
            - ReboundPercentage
            - EffectiveFeildGoalPercentage
            - TrueShootingPercentage
            - AssistRatio
            - TurnoverRatio
            - FieldGoals2AttemptedRatio
            - FieldGoals3AttemptedRatio
            - FreeThrowRate
            - Possessions
            - GamesWon
            - GamesLost
            - DoubleDoubles
            - TripleDoubles
            - FieldGoalsAttempted2Share
            - FieldGoalsAttempted3Share
            - FreeThrowsAttemptedShare
            - FieldGoalsMade2Share
            - FieldGoalsMade3Share
            - FreeThrowsMadeShare
            - PointsMade2Rate
            - PointsMade3Rate
            - PointsMadeFreeThrowsRate
            - PointsAttempted2Rate
            - PointsAttempted3Rate
            - Age

        phase_type_code (Optional[str], optional): The phase of the season,
            available variables:
            - "RS" (regular season)
            - "PO" (play-off)
            - "FF" (final four)
            Defaults to None, which includes all phases.

        statistic_mode (str, optional): The aggregation of statistics,
            available variables:
            - PerGame
            - Accumulated
            - Per100Possesions
            - PerGameReverse
            - AccumulatedReverse
            Defaults to "PerGame".

        game_type (Optional[str], optional): The type of games to draw the top
            stats from. Available values:
            - HomeGames
            - AwayGames
            - GamesWon
            - GamesLost
            Defaults to None, meaning all games

        position (Optional[str], optional): The position of the player to draw
            the top stats from. Available values:
            - Guards
            - Forwards
            - Centers
            - RisingStars
            Defaults to None, meaning all positions.

    Raises:

        ValueError: If the stat_category is not applicable

        ValueError: If the phase_type_code is not applicable

        ValueError: If the statistic_mode is not applicable

        ValueError: If the game_type is not applicable

        ValueError: If the position is not applicable

    Returns:

        pd.DataFrame: A dataframe with the top players' stats
    """
    avaiable_stat_category = [
        "Valuation",
        "Score",
        "TotalRebounds",
        "OffensiveRebounds",
        "Assistances",
        "Steals",
        "BlocksFavour",
        "BlocksAgainst",
        "Turnovers",
        "FoulsReceived",
        "FoulsCommited",
        "FreeThrowsMade",
        "FreeThrowsAttempted",
        "FreeThrowsPercent",
        "FieldGoalsMade2",
        "FieldGoalsAttempted2",
        "FieldGoals2Percent",
        "FieldGoalsMade3",
        "FieldGoalsAttempted3",
        "FieldGoals3Percent",
        "FieldGoalsMadeTotal",
        "FieldGoalsAttemptedTotal",
        "FieldGoalsPercent",
        "AccuracyMade",
        "AccuracyAttempted",
        "AccuracyPercent",
        "AssitancesTurnoversRation",
        "GamesPlayed",
        "GamesStarted",
        "TimePlayed",
        "Contras",
        "Dunks",
        "OffensiveReboundPercentage",
        "DefensiveReboundPercentage",
        "ReboundPercentage",
        "EffectiveFeildGoalPercentage",
        "TrueShootingPercentage",
        "AssistRatio",
        "TurnoverRatio",
        "FieldGoals2AttemptedRatio",
        "FieldGoals3AttemptedRatio",
        "FreeThrowRate",
        "Possessions",
        "GamesWon",
        "GamesLost",
        "DoubleDoubles",
        "TripleDoubles",
        "FieldGoalsAttempted2Share",
        "FieldGoalsAttempted3Share",
        "FreeThrowsAttemptedShare",
        "FieldGoalsMade2Share",
        "FieldGoalsMade3Share",
        "FreeThrowsMadeShare",
        "PointsMade2Rate",
        "PointsMade3Rate",
        "PointsMadeFreeThrowsRate",
        "PointsAttempted2Rate",
        "PointsAttempted3Rate",
        "Age"
    ]
    available_phase_type_code = ["RS", "PO", "FF"]
    available_stat_mode = [
        "PerGame",
        "Accumulated",
        "Per100Possesions",
        "PerGameReverse",
        "AccumulatedReverse"
    ]
    available_game_types = [
        "HomeGames",
        "AwayGames",
        "GamesWon",
        "GamesLost",
    ]
    availabe_positions = [
        "Guards",
        "Forwards",
        "Centers",
        "RisingStars"
    ]

    raise_error(stat_category, "Stat category", avaiable_stat_category, False)
    raise_error(
        statistic_mode, "Statistic Aggregation", available_stat_mode, False)
    raise_error(
        phase_type_code, "Phase type code", available_phase_type_code, True)
    raise_error(game_type, "Game type", available_game_types, True)
    raise_error(position, "Position", availabe_positions, True)

    params["category"] = stat_category
    params["phaseTypeCode"] = phase_type_code
    params["statisticMode"] = statistic_mode
    params["limit"] = top_n
    if game_type is not None and position is not None:
        raise ValueError(
            "Cannot select a game_type and position at the same type. "
            "One of the variables must be None"
        )
    elif game_type is not None and position is None:
        params["misc"] = game_type
    elif game_type is None and position is not None:
        params["misc"] = position

    url_ = f"{URL}/stats/players/leaders"

    r = get_requests(url_, params=params)
    data = r.json()
    df = pd.json_normalize(data["data"])
    return df


def raise_error(
    var: Optional[str],
    descripitve_var: str,
    available_vals: List,
    allow_none: bool = False,
) -> None:
    """
    A function that raises a ValueError with specific message.

    Args:

        var (str): The input variable by the user

        descripitve_var (str): A description in plain English of this variable

        available_vals (List): The available variables

        allow_none (bool, optional): If `var` can take None value.
            Defaults to False.

    Raises:

        ValueError: if `var` not applicable
    """

    if allow_none:
        available_vals.append(None)

    if var not in available_vals:
        raise ValueError(
            f"{descripitve_var}, {var}, is not applicable. "
            f"Available values: {available_vals}"
        )
    return
