"""Basketix seasons module"""

from typing import Dict, List, Optional, Tuple, Union

from ..entities import (
    BasketixDate,
    BasketixError,
    LeagueStatus,
    NbaPlayersMotions,
    RankingType,
    Transactions,
)
from ..helpers import DateHelper, uid
from ..tables import BasketixSeasonsTable
from .basketix_weeks import BasketixWeeksHandler


class BasketixSeasonsHandler:
    """Handle basketix seasons"""

    BEGIN_DAY_OF_WEEK: str = "monday"
    DEFAULT_SALARY_CAP = 120
    DEFAULT_DRAFT_PICK_SECOND_DELAY = 5 * 60
    MIN_DRAFT_DELAY_SECONDS = 30
    DEFAULT_RANKING_TYPE = RankingType.POINTS
    DEFAULT_N_SUBSTITUTE = 2
    DEFAULT_POINTS_TABLE = {
        "1": 20,
        "2": 16,
        "3": 13,
        "4": 11,
        "5": 10,
        "6": 9,
        "7": 8,
        "8": 7,
        "9": 6,
        "10": 5,
        "11": 4,
        "12": 3,
        "13": 2,
        "14": 1,
    }
    DEFAULT_BEST_PLAYER_BONUS_POINT = 1
    DEFAULT_BEST_GAME_BONUS_POINT = 1

    def __init__(self, environment: str) -> None:
        """Init basketix season handler"""

        self.basketix_seasons_table = BasketixSeasonsTable(environment)
        self.basketix_weeks_handler = BasketixWeeksHandler(environment)

    def get(self, season_id: str) -> dict:
        """Returns the season corresponding"""

        return self.basketix_seasons_table.query(season_id)[0]

    def get_status(self, season_id: Optional[str] = None, season: Optional[dict] = None) -> str:
        """Returns the status of the season"""

        if not season:
            season_id = season_id if season_id else ""
            season = self.get(season_id)

        if not season["seasonStatus"]["draft"]["isFinished"]:
            return LeagueStatus.DRAFT
        if self._is_regular_season_finished(season.get("regularSeason", {})):
            return LeagueStatus.FINISHED

        return LeagueStatus.REGULAR_SEASON

    def get_number_of_members(self, season_id: str) -> int:
        """Returns the number of users in the season"""

        return len(self.get(season_id)["basketixs"])

    def get_next_season_settings(self, season_id: str) -> dict:
        """Returns the default settings for the next season"""

        season = self.get(season_id)

        return {
            "salaryCap": season["salaryCap"],
            "draftType": season["draft"]["nextDraftType"],
            "isDraftReverse": season["draft"]["nextDraftIsReverse"],
            "nDraftRound": season["draft"]["nRound"],
            "draftPickSecondDelay": season["draft"]["draftPickSecondDelay"],
            "nWeek": season["regularSeason"]["nWeeks"],
            "nSubstitute": season["regularSeason"]["nSubstitute"],
            "rankingType": season["regularSeason"]["rankingType"],
            "pointsTable": season["regularSeason"]["pointsTable"],
        }

    def create_season(
        self,
        league_id: str,
        n_week: int,
        ranking_type: Optional[RankingType],
        draft_order: list,
        n_round_draft: int,
        draft_type: str,
        is_draft_reverse: bool,
        draft_pick_second_delay: Optional[int],
        players: dict,
        salary_cap: Optional[int],
        points_table: Optional[Dict[str, int]],
        next_season_draft_type: str,
        next_season_is_draft_reverse: bool,
        n_substitute: Optional[int],
        best_player_bonus_points: Optional[int],
        best_game_bonus_points: Optional[int],
    ) -> dict:
        """Create a new season"""

        ranking_type = ranking_type if ranking_type else self.DEFAULT_RANKING_TYPE
        salary_cap = salary_cap if salary_cap is not None else self.DEFAULT_SALARY_CAP
        draft_pick_second_delay = draft_pick_second_delay if draft_pick_second_delay else self.MIN_DRAFT_DELAY_SECONDS
        n_substitute = n_substitute if n_substitute is not None else self.DEFAULT_N_SUBSTITUTE
        best_player_bonus_points = (
            best_player_bonus_points if best_player_bonus_points is not None else self.DEFAULT_BEST_PLAYER_BONUS_POINT
        )
        best_game_bonus_points = (
            best_game_bonus_points if best_game_bonus_points is not None else self.DEFAULT_BEST_GAME_BONUS_POINT
        )

        if draft_pick_second_delay < self.MIN_DRAFT_DELAY_SECONDS:
            raise BasketixError("InvalidPickDelay", tokens={"min_delay": self.MIN_DRAFT_DELAY_SECONDS})

        if points_table:
            try:
                [(int(rank), int(pts)) for rank, pts in points_table.items()]
            except ValueError as err:
                raise BasketixError("InvalidPointsTable") from err
        else:
            points_table = self.DEFAULT_POINTS_TABLE

        season = {
            "seasonId": self._generate_season_id(),
            "leagueId": league_id,
            "creationDateTime": DateHelper.now(),
            "basketixIds": draft_order,
            "basketixs": {},
            "salaryCap": salary_cap,
            "draft": {
                "order": self._init_draft(draft_order, is_draft_reverse, n_round_draft),
                "players": players,
                "draftPickSecondDelay": draft_pick_second_delay,
                "draftType": draft_type,
                "isReverse": is_draft_reverse,
                "nRound": n_round_draft,
                "nextDraftType": next_season_draft_type,
                "nextDraftIsReverse": next_season_is_draft_reverse,
            },
            "regularSeason": {
                "rankingType": ranking_type,
                "nWeeks": n_week,
                "nSubstitute": n_substitute,
                "pointsTable": points_table,
                "bonusPoints": {
                    "bestPlayer": best_player_bonus_points,
                    "bestGame": best_game_bonus_points,
                },
            },
            "players": {},
            "seasonStatus": {},
        }

        for basketix_id in draft_order:
            season["basketixs"][basketix_id] = {
                "watchedPalyer": [],
                "transactions": [
                    {
                        "type": Transactions.GAME_CONFIG,
                        "salaryCap": salary_cap,
                        "isSecret": False,
                    }
                ],
            }

        season["seasonStatus"]["draft"] = self._update_draft_status(
            season["draft"]["order"], 0, draft_pick_second_delay
        )

        self.basketix_seasons_table.insert([season])

        return season

    def pick_player(self, season_id: str, pick_number: int, basketix_id: str, player_id: Union[str, None]) -> dict:
        """Pick the player or skip the pick if no player"""

        season = self.basketix_seasons_table.query(season_id)[0]

        draft_status = season["seasonStatus"]["draft"]
        if draft_status["basketixId"] != basketix_id or draft_status["pickNumber"] != pick_number:
            raise BasketixError("CanNotPick")

        draft_pick_second_delay = season["draft"]["draftPickSecondDelay"]

        cost = season["draft"]["players"][player_id]["cost"] if player_id else None
        positions = season["draft"]["players"][player_id]["positions"] if player_id else None

        season = self._pick_player_recursive(
            season, pick_number, basketix_id, player_id, cost, positions, draft_pick_second_delay
        )
        self.basketix_seasons_table.update([season])

        return season["seasonStatus"]["draft"]

    def set_pre_draft_list(
        self, season_id: str, basketix_id: str, pick_number: int, player_ids: list, auto: bool
    ) -> dict:
        """Set the pre-draft list of an user"""

        season = self.basketix_seasons_table.query(season_id)[0]

        if season["draft"]["order"][pick_number - 1]["basketixId"] != basketix_id:
            raise BasketixError("InvalidPickNumber", tokens={"pick_number": pick_number})

        drafted_players = [pick["playerId"] for pick in season["draft"]["order"] if "playerId" in pick]
        already_drafted_players = list(set(drafted_players) & set(player_ids))
        if already_drafted_players:
            raise BasketixError("AlreadyDrafted")

        season["draft"]["order"][pick_number - 1]["predraft"]["choices"] = player_ids
        season["draft"]["order"][pick_number - 1]["predraft"]["auto"] = auto
        update_item = {"seasonId": season_id}
        update_item["draft"] = season["draft"]
        self.basketix_seasons_table.update([update_item])

        return season["draft"]["order"][pick_number - 1]

    def get_roster(self, season_id: str, basketix_id: str, week_id: str) -> dict:
        """Get players positions and costs from basketix rosters"""

        roster = {}
        for player_id, player_history in self._get_players(season_id=season_id).items():
            player_motion = self.get_owner_and_info(week_id, player_history)
            if player_motion and player_motion["basketixId"] == basketix_id:
                roster[player_id] = {
                    "basketixId": basketix_id,
                    "playerId": player_id,
                    "cost": player_motion["cost"],
                    "positions": player_motion["positions"],
                }

        return roster

    def players_in_roster(
        self,
        basketix_id: str,
        week_id: str,
        player_ids: List[str],
        season_id: Optional[str] = None,
        players: Optional[dict] = None,
    ):
        """Returns only the players_ids who are parts of the basketix roster for the weekId"""

        player_ids_in_roster = []
        if not players:
            players = self._get_players(season_id=season_id)

        for player_id in player_ids:
            owner = self.get_owner_and_info(week_id, players[player_id])
            if owner and owner["basketixId"] == basketix_id:
                player_ids_in_roster.append(player_id)

        return player_ids_in_roster

    def get_all_players(self, season_id: str, week_id: str) -> dict:
        """Returns all players with owner"""

        season = self.basketix_seasons_table.query(season_id)[0]
        players = self._get_players(season=season)

        players_with_owner = {}
        for player_id, player_history in players.items():
            player_motion = self.get_owner_and_info(week_id, player_history)
            if player_motion and "basketixId" in player_motion:
                players_with_owner[player_id] = {key: player_motion[key] for key in ["positions", "cost", "basketixId"]}

        return players_with_owner

    def is_season_finished(self, season_id: str) -> bool:
        """Check if season is finished"""

        season = self.basketix_seasons_table.query(season_id)[0]

        is_draft_finished = self._is_draft_finished(season["draft"]["order"])
        is_regular_season_finished = self._is_regular_season_finished(season["regularSeason"])

        return is_draft_finished and is_regular_season_finished

    def get_cap_space(self, transcations: list, with_secrets: bool) -> Tuple[int, Optional[int]]:
        """
        Returns a tuple of :
        - the salary cap based on transactions (with or without secret transactions)
        - the public salary cap if with_secrets = True else None
        """

        salary_cap_transactions = []
        secret_transactions = [0]
        for transaction in transcations:
            if "salaryCap" in transaction and transaction.get("isSecret", False) in (False, with_secrets):
                salary_cap_transactions.append(transaction["salaryCap"])

            if "salaryCap" in transaction and transaction.get("isSecret"):
                secret_transactions.append(transaction["salaryCap"])

        return (
            sum(salary_cap_transactions),
            sum(salary_cap_transactions) - sum(secret_transactions) if with_secrets else None,
        )

    # Deprecated
    def filter_teams_players(self, basketix_teams: List[dict]) -> List[dict]:
        """Delete NBA players who are not in the basketix roster"""

        season_ids = list(set([team["seasonId"] for team in basketix_teams]))

        filtered_teams = []
        for season_id in season_ids:
            teams = [team for team in basketix_teams if team["seasonId"] == season_id]
            season_players = self._get_players(season_id)
            for team in teams:
                filter_team = self._delete_players_not_in_roster(team, season_players)
                filtered_teams.append(filter_team)

        return filtered_teams

    def get_all_next_week_ids(self, week_id: str, season: dict) -> List[str]:
        """Return all the next weeks from <week_id>"""
        try:
            week_ids = season["regularSeason"]["weekIds"]
            return week_ids[
                week_ids.index(
                    week_id,
                ) :
            ]
        except (KeyError, ValueError):
            return []

    def get_n_max_substitute(self, season_id: str) -> int:
        return self.get(season_id=season_id)["regularSeason"]["nSubstitute"]

    # Deprecated
    def _delete_players_not_in_roster(self, team: dict, season_players: dict):
        """Init the position for NBA players who are not in the basketix roster"""

        player_ids = [player_id for player_id, _ in team["players"].items()]
        player_ids_in_roster = self.players_in_roster(
            team["basketixId"], team["weekId"], player_ids, players=season_players
        )
        player_id_not_in_roster = list(set(player_ids) - set(player_ids_in_roster))
        for player_id in player_id_not_in_roster:
            team["players"].pop(player_id)

        return team

    def _init_draft(self, draft_order: list, is_reverse: bool, n_round_draft: int) -> list:
        """Init draft"""

        n_basketix = len(draft_order)
        return [
            self._init_pick(basketix_id, i + 1, n_basketix)
            for i, basketix_id in enumerate(self._get_all_draft_order(draft_order, is_reverse, n_round_draft))
        ]

    def _pick_player_recursive(
        self,
        season: dict,
        pick_number: int,
        basketix_id: str,
        player_id: Optional[str],
        cost: Optional[int],
        positions: Optional[list],
        draft_pick_second_delay: int,
    ) -> dict:
        """Pick a player and the next ones if possible. Update season status"""

        utc_now = DateHelper.now()
        pick = season["draft"]["order"][pick_number - 1]
        pick_update_base = {"engagementDateTime": utc_now, "basketixId": basketix_id, "isPick": True}

        if not player_id or not self._can_pick(basketix_id, season):
            pick.pop("predraft")
            pick.update(pick_update_base)
            return self._next_pick(season, pick_number, draft_pick_second_delay)

        if not self._can_pick_this_player(basketix_id, season, player_id):
            raise BasketixError("CanNotPick")

        pick.pop("predraft")
        pick.update(pick_update_base)
        pick.update({"playerId": player_id, "cost": cost, "positions": positions})

        player_histories = [
            {
                "date": utc_now,
                "type": NbaPlayersMotions.DRAFT,
                "basketixId": basketix_id,
                "cost": cost,
                "positions": positions,
            }
        ]
        season["players"][player_id] = player_histories

        season["basketixs"][basketix_id]["transactions"].append(
            {
                "type": Transactions.DRAFT_PICK,
                "playerId": player_id,
                "salaryCap": -cost,
                "isSecret": False,
            }
        )

        return self._next_pick(season, pick_number, draft_pick_second_delay)

    def _next_pick(self, season: dict, pick_number: int, draft_pick_second_delay: int):
        season["seasonStatus"]["draft"] = self._update_draft_status(
            season["draft"]["order"], pick_number, draft_pick_second_delay
        )

        if season["seasonStatus"]["draft"]["isFinished"]:
            season = self._close_draft(season)
            season = self._init_regular_season(season)
        else:
            next_basketix_id = season["seasonStatus"]["draft"]["basketixId"]
            next_pick_number = season["seasonStatus"]["draft"]["pickNumber"]

            if not self._can_pick(next_basketix_id, season):
                season = self._pick_player_recursive(
                    season, next_pick_number, next_basketix_id, None, None, None, draft_pick_second_delay
                )
                return season

            predraft = season["draft"]["order"][next_pick_number - 1]["predraft"]
            if predraft["auto"]:
                for pick_player_id in predraft["choices"]:
                    if self._can_pick_this_player(next_basketix_id, season, pick_player_id):  # TODO: A tester
                        next_positions = season["draft"]["players"][pick_player_id]["positions"]
                        next_cost = season["draft"]["players"][pick_player_id]["cost"]
                        season = self._pick_player_recursive(
                            season,
                            next_pick_number,
                            next_basketix_id,
                            pick_player_id,
                            next_cost,
                            next_positions,
                            draft_pick_second_delay,
                        )
                        break

        return season

    def _update_draft_status(self, draft: list, actual_pick_number: int, draft_pick_second_delay: int) -> dict:
        """Returns if it's the turn of a specific player to pick a player during the draft"""

        is_finished = self._is_draft_finished(draft)
        draft_status = {"isFinished": is_finished}
        if not is_finished:
            # Next pick number
            draft_status = {
                **draft_status,
                "basketixId": draft[actual_pick_number]["basketixId"],
                "pickNumber": draft[actual_pick_number]["pickNumber"],
                "round": draft[actual_pick_number]["round"],
                "roundPick": draft[actual_pick_number]["roundPick"],
                "limitDateTime": DateHelper.now_delta(seconds=draft_pick_second_delay),
            }

        return draft_status

    def _init_regular_season(self, season: dict) -> dict:
        """Init a regular season after the draft is finished"""

        if self._is_draft_finished(season["draft"]["order"]):
            season_week_ids = self._get_season_week_ids(season["regularSeason"]["nWeeks"])
            season["regularSeason"] = {
                **season["regularSeason"],
                "beginWeekId": season_week_ids[0],
                "endWeekId": season_week_ids[-1],
                "weekIds": season_week_ids,
            }
            season["seasonStatus"]["regularSeason"] = self._update_regular_season_status(season["regularSeason"])

        return season

    def _update_regular_season_status(self, regular_season: dict) -> dict:
        """Update regular season status"""

        return {"isFinished": self._is_regular_season_finished(regular_season)}

    def _is_draft_finished(self, draft: list) -> bool:
        """Check if draft is finished"""
        return draft[-1]["isPick"]

    def _is_regular_season_finished(self, regular_season: dict) -> bool:
        """Check if regular season is finished"""

        if "weekIds" in regular_season:
            return self.basketix_weeks_handler.are_all_finished(regular_season["weekIds"])

        return False

    def get_owner_and_info(self, week_id: str, history: list) -> Optional[dict]:
        """
        Finds basketix owner for a player motion history and a <week_id>
        and returns players main info (cost, positions, owner)
        """

        if "lastWeekId" in history[-1] and "lastWeekId" in history[-1] and week_id > history[-1]["lastWeekId"]:
            return None

        for motion in reversed(history):
            if (
                "lastWeekId" in motion
                and "firstWeekId" in motion
                and motion["firstWeekId"] <= week_id <= motion["lastWeekId"]
            ):
                return motion

            if "lastWeekId" in motion and "firstWeekId" not in motion and week_id <= motion["lastWeekId"]:
                return motion

            if not "lastWeekId" in motion and "firstWeekId" in motion and motion["firstWeekId"] <= week_id:
                return motion

        if history[0]["type"] == "Draft" and (not "lastWeekId" in history[0] or week_id <= history[0]["lastWeekId"]):
            return history[0]

        return None

    def _get_players(self, season_id: Optional[str] = None, season: Optional[dict] = None) -> dict:
        """Get players positions and costs from all basketix rosters"""

        if season_id:
            season = self.basketix_seasons_table.query(season_id)[0]
        elif not season:
            raise BasketixError("InvalidSeason")

        return season["players"]

    def _close_draft(self, season: dict):
        """Close draft when it is finish. Delete none drafted player"""

        season["draft"].pop("players", season)

        return season

    def _generate_season_id(self) -> str:
        """Generate a season id"""

        return uid()

    def _get_all_draft_order(self, draft_order: list, is_reverse: bool, n_round_draft: int) -> list:
        """Returns ordered basketix id list of the draft"""

        draft_orders = [draft_order for _ in range(n_round_draft)]

        if is_reverse:
            for _round, _ in enumerate(draft_orders):
                if _round % 2 == 1:
                    draft_orders[_round] = list(reversed(draft_orders[_round]))

        return [_round for draft_round in draft_orders for _round in draft_round]

    def _init_pick(self, basketix_id: str, position: int, n_basketix: int):
        """Init a draft pick"""

        return {
            "basketixId": basketix_id,
            "isPick": False,
            "pickNumber": position,
            "round": ((position - 1) // n_basketix) + 1,
            "roundPick": position - (n_basketix * (((position - 1) // n_basketix))),
            "predraft": {"auto": True, "choices": []},
        }

    def _can_pick(self, basketix_id: str, season: dict) -> bool:
        """Check if basketix has cap space i.e he can draft a player"""

        return self.get_cap_space(season["basketixs"][basketix_id]["transactions"], with_secrets=True)[0] > 0

    # TODO: A tester
    def _can_pick_this_player(self, basketix_id: str, season: dict, player_id: str) -> bool:
        """
        Check if basketix <basketix_id> can pick player <player_id> dependending
        of the draft history and basketix cap space
        """
        already_drafted_players = [player_id for player_id, _ in season["players"].items()]
        if player_id in already_drafted_players:
            return False

        cap_space, _ = self.get_cap_space(season["basketixs"][basketix_id]["transactions"], with_secrets=True)
        if cap_space == 0 or cap_space < season["draft"]["players"][player_id]["cost"]:
            return False

        return True

    def _get_season_week_ids(self, n_week: int) -> list:
        """Returns the list of weeks id for the regular season based on now"""

        first_week = BasketixDate.tomorrow().week.next()
        return [first_week.next(i).id for i in range(n_week)]
