import asyncio
import json

import aiohttp
from urllib3.util import response

from ..constants import API_URLS, MIN_GAMEWEEK, MAX_GAMEWEEK
from ..utils import fetch, logged_in, post, get_headers

is_c = "is_captain"
is_vc = "is_vice_captain"


def valid_gameweek(gameweek):
    """Returns True if the gameweek is valid.

    :param gameweek: The gameweek.
    :type gameweek: int or string
    :raises ValueError: if gameweek is not a number between 1 and 38
    """
    gameweek = int(gameweek)
    if (gameweek < MIN_GAMEWEEK) or (gameweek > MAX_GAMEWEEK):
        raise ValueError(f"Gameweek must be a number between {MIN_GAMEWEEK} and {MAX_GAMEWEEK}.")
    return True


def _ids_to_lineup(player_ids, user_team):
    """Helper for converting list of player IDs to usable lineup.

    :param player_ids: List of player IDS.
    :type player_ids: list
    :param user_team: The user's current team.
    :type user_team: list
    :return: A usable lineup.
    :rtype: list
    """
    return [next(player for player in user_team
                 if player["element"] == player_id)
            for player_id in player_ids]


def _id_to_element_type(player_id, players):
    """Helper for converting a player's ID to their respective element type:
    1, 2, 3 or 4.

    :param player_id: A player's ID.
    :type player_id: int
    :param players: List of all players in the Fantasy Premier League.
    :type players: list
    :return: The player's element type.
    :rtype: int
    """
    player = next(player for player in players
                  if player["id"] == player_id)
    return player["element_type"]


def _set_element_type(lineup, players):
    """Helper for setting the players' element types.

    :param lineup: The user's current lineup.
    :type lineup: list
    :param players: List of all players in the Fantasy Premier League.
    :type players: list
    """
    for player in lineup:
        element_type = _id_to_element_type(player["element"], players)
        player["element_type"] = element_type


def _set_captain(lineup, captain, captain_type, player_ids):
    """Sets the given captain's captain_type to True.

    :param lineup: List of players.
    :type lineup: list
    :param captain: ID of the captain.
    :type captain: int or str
    :param captain_type: The captain type: 'is_captain' or 'is_vice_captain'.
    :type captain_type: string
    :param player_ids: List of the team's players' IDs.
    :type player_ids: list
    """
    if captain and captain not in player_ids:
        raise ValueError(
            "Cannot (vice) captain player who isn't in user's team.")

    current_captain = next(player for player in lineup if player[captain_type])
    chosen_captain = next(player for player in lineup
                          if player["element"] == captain)

    # If the chosen captain is already a (vice) captain, then give his previous
    # role to the current (vice) captain.
    if chosen_captain[is_c] or chosen_captain[is_vc]:
        current_captain[is_c], chosen_captain[is_c] = (
            chosen_captain[is_c], current_captain[is_c])
        current_captain[is_vc], chosen_captain[is_vc] = (
            chosen_captain[is_vc], current_captain[is_vc])

    for player in lineup:
        player[captain_type] = False

        if player["element"] == captain:
            player[captain_type] = True


class User():
    """A class representing a user of the Fantasy Premier League.

    Basic usage::

      from fpl import FPL
        import aiohttp
        import asyncio
      
        async def main():
            async with aiohttp.ClientSession() as session:
                fpl = FPL(session)
                user = await fpl.get_user(3808385)
            print(user)
        ...
        # Python 3.7+
        asyncio.run(main())
        ...
        # Python 3.6
        loop = asyncio.get_event_loop()
        loop.run_until_complete(main())
        Amos Bastian - Netherlands
    """

    def __init__(self, user_information, session):
        self._session = session
        for k, v in user_information.items():
            setattr(self, k, v)

    async def get_gameweek_history(self, gameweek=None):
        """Returns a list containing the gameweek history of the user.

        Information is taken from e.g.:
            https://fantasy.premierleague.com/api/entry/91928/history

        :param gameweek: (optional): The gameweek. Defaults to ``None``.
        :rtype: list if gameweek is ``None``, otherwise dict.
        """
        if hasattr(self, "_history"):
            history = self._history
        else:
            history = await fetch(
                self._session, API_URLS["user_history"].format(self.id))

        self._history = history

        if gameweek is not None:
            valid_gameweek(gameweek)
            return next(gw for gw in history["current"]
                        if gw["event"] == gameweek)

        return history["current"]

    async def get_season_history(self):
        """Returns a list containing the seasonal history of the user.

        Information is taken from e.g.:
            https://fantasy.premierleague.com/api/entry/91928/history

        :rtype: list
        """
        if hasattr(self, "_history"):
            history = self._history
        else:
            history = await fetch(
                self._session, API_URLS["user_history"].format(self.id))

        self._history = history
        return history["past"]

    async def get_chips_history(self, gameweek=None):
        """Returns a list containing the chip history of the user.

        Information is taken from e.g.:
            https://fantasy.premierleague.com/api/entry/91928/history

        :param gameweek: (optional): The gameweek. Defaults to ``None``.
        :rtype: list
        """
        if hasattr(self, "_history"):
            history = self._history
        else:
            history = await fetch(
                self._session, API_URLS["user_history"].format(self.id))

        self._history = history

        if gameweek is not None:
            valid_gameweek(gameweek)
            try:
                return next(chip for chip in history["chips"]
                            if chip["event"] == gameweek)
            except StopIteration:
                return None

        return history["chips"]

    async def get_picks(self, gameweek=None):
        """Returns a dict containing the user's picks each gameweek.

        Key is the gameweek number, value contains picks of the gameweek.

        Information is taken from e.g.:
            https://fantasy.premierleague.com/api/entry/91928/event/1/picks/

        :param gameweek: (optional): The gameweek. Defaults to ``None``.
        :rtype: dict
        """
        if hasattr(self, "_picks"):
            picks = self._picks
        else:
            tasks = [asyncio.ensure_future(
                     fetch(self._session,
                           API_URLS["user_picks"].format(self.id, gameweek)))
                     for gameweek in range(self.started_event,
                                           self.current_event + 1)]
            picks = await asyncio.gather(*tasks)
            self._picks = picks

        if gameweek is not None:
            valid_gameweek(gameweek)
            try:
                pick = next(pick for pick in picks
                            if pick["entry_history"]["event"] == gameweek)
            except StopIteration:
                return {}
            else:
                return {pick["entry_history"]["event"]: pick["picks"]}

        picks_out = {}
        for pick in picks:
            try:
                picks_out[pick["entry_history"]["event"]] = pick["picks"]
            except KeyError:
                pass
        return picks_out

    async def get_cup_matches(self, gameweek=None):
        """Returns either a list of all the user's cup matches, dictionary
        of the cup match in the given gameweek (gameweek 17 and onwards).

        Information is taken from e.g.:
            https://fantasy.premierleague.com/api/entry/91928/cup/

        :param gameweek: (optional): The gameweek. Defaults to ``None``.
        :rtype: list or dict
        """
        cup = getattr(self, "_cup", None)
        if not cup:
            cup = await fetch(
                self._session, API_URLS["user_cup"].format(self.id))
            self._cup = cup

        if gameweek is not None:
            valid_gameweek(gameweek)
            return [cup_match for cup_match in cup["cup_matches"]
                    if cup_match["event"] == gameweek]

        return cup["cup_matches"]

    async def get_cup_status(self):
        """Returns the user's cup status.

        Information is taken from e.g.:
            https://fantasy.premierleague.com/api/entry/91928/cup/

        :rtype: dict
        """
        cup = getattr(self, "_cup", None)
        if not cup:
            cup = await fetch(
                self._session, API_URLS["user_cup"].format(self.id))
            self._cup = cup

        return cup["cup_status"]

    async def get_active_chips(self, gameweek=None):
        """Returns a list containing the user's active chip for each gameweek,
        or the active chip of the given gameweek.

        Information is taken from e.g.:
            https://fantasy.premierleague.com/api/entry/91928/event/1/picks/

        :param gameweek: (optional): The gameweek. Defaults to ``None``.
        :rtype: list
        """
        if hasattr(self, "_picks"):
            picks = self._picks
        else:
            tasks = [asyncio.ensure_future(
                     fetch(self._session,
                           API_URLS["user_picks"].format(self.id, gameweek)))
                     for gameweek in range(1, self.current_event + 1)]
            picks = await asyncio.gather(*tasks)
            self._picks = picks

        if gameweek is not None:
            valid_gameweek(gameweek)
            try:
                return [next(pick["active_chip"] for pick in picks
                             if pick["entry_history"]["event"] == gameweek)][0]
            except StopIteration:
                return None

        return [pick["active_chip"] for pick in picks]

    async def get_automatic_substitutions(self, gameweek=None):
        """Returns a list containing the user's automatic substitutions each
        gameweek.

        Information is taken from e.g.:
            https://fantasy.premierleague.com/api/entry/91928/event/1/picks/

        :param gameweek: (optional): The gameweek. Defaults to ``None``.
        :rtype: list
        """
        if hasattr(self, "_picks"):
            picks = self._picks
        else:
            tasks = [asyncio.ensure_future(
                     fetch(self._session,
                           API_URLS["user_picks"].format(self.id, gameweek)))
                     for gameweek in range(1, self.current_event + 1)]
            picks = await asyncio.gather(*tasks)
            self._picks = picks

        if gameweek is not None:
            valid_gameweek(gameweek)
            try:
                return next(pick["automatic_subs"] for pick in picks
                            if pick["entry_history"]["event"] == gameweek)
            except StopIteration:
                return None

        return [p for pick in picks for p in pick["automatic_subs"]]

    async def get_user_history(self, gameweek=None):
        """Returns a list containing the user's history for each gameweek,
        or a dictionary of the user's history for the given gameweek.

        :rtype: list or dict
        """
        if hasattr(self, "_picks"):
            picks = self._picks
        else:
            tasks = [asyncio.ensure_future(
                     fetch(self._session,
                           API_URLS["user_picks"].format(self.id, gameweek)))
                     for gameweek in range(self.started_event, 
                                           self.current_event + 1)]
            picks = await asyncio.gather(*tasks)
            self._picks = picks

        if gameweek is not None:
            valid_gameweek(gameweek)
            try:
                return next(pick["entry_history"] for pick in picks
                            if pick["entry_history"]["event"] == gameweek)
            except StopIteration:
                return None

        return [history["entry_history"] for history in picks]

    async def get_team(self):
        """Returns a logged in user's current team. Requires the user to have
        logged in using ``fpl.login()``.

        Information is taken from e.g.:
            https://fantasy.premierleague.com/api/my-team/91928/

        :rtype: list
        """
        if not logged_in(self._session):
            raise Exception("User must be logged in.")

        try:
            response = await fetch(
                self._session, API_URLS["user_team"].format(self.id))
        except aiohttp.client_exceptions.ClientResponseError:
            raise Exception("User ID does not match provided email address!")

        if response.get("detail") == "Not found.":
            raise Exception("Data not found. Please ensure user ID matches provided email address.")
        
        return response["picks"]

    async def get_chips(self):
        """Returns a logged in user's list of chips. Requires the user to have
        logged in using ``fpl.login()``.

        Information is taken from e.g.:
            https://fantasy.premierleague.com/api/my-team/91928/

        :rtype: list
        """
        if not logged_in(self._session):
            raise Exception("User must be logged in.")

        try:
            response = await fetch(
                self._session, API_URLS["user_team"].format(self.id))
        except aiohttp.client_exceptions.ClientResponseError:
            raise Exception("User ID does not match provided email address!")

        return response["chips"]

    async def get_transfers_status(self):
        """Returns a logged in user's transfer status, which is a dictionary
        containing their bank value, how many free transfers they have left
        and so on. Requires the user to have logged in using ``fpl.login()``.

        Information is taken from e.g.:
            https://fantasy.premierleague.com/api/my-team/91928/

        :rtype: dict
        """
        if not logged_in(self._session):
            raise Exception("User must be logged in.")

        try:
            response = await fetch(
                self._session, API_URLS["user_team"].format(self.id))
        except aiohttp.client_exceptions.ClientResponseError:
            raise Exception("User ID does not match provided email address!")

        return response["transfers"]

    async def get_transfers(self, gameweek=None):
        """Returns either a list of all the user's transfers, or a list of
        transfers made in the given gameweek.

        Information is taken from e.g.:
            https://fantasy.premierleague.com/api/entry/91928/transfers/

        :param gameweek: (optional): The gameweek. Defaults to ``None``.
        :rtype: list
        """
        transfers = getattr(self, "_transfers", None)
        if not transfers:
            transfers = await fetch(
                self._session, API_URLS["user_transfers"].format(self.id))
            self._transfers = transfers

        if gameweek is not None:
            valid_gameweek(gameweek)
            return [transfer for transfer in transfers
                    if transfer["event"] == gameweek]

        return transfers

    async def get_latest_transfers(self):
        """Returns a list of transfers made by the user in the current
        gameweek. Requires the user to have logged in using ``fpl.login()``.

        Information is taken from e.g.:
            https://fantasy.premierleague.com/api/entry/91928/transfers-latest/

        :rtype: list
        """
        if not logged_in(self._session):
            raise Exception("User must be logged in.")

        try:
            transfers = await fetch(
                self._session, API_URLS["user_latest_transfers"].format(self.id))
        except aiohttp.client_exceptions.ClientResponseError:
            raise Exception("User ID does not match provided email address!")

        return transfers

    # async def get_wildcards(self):
    #     """Returns a list containing information about when (and if) the user
    #     has played their wildcard(s).

    #     Information is taken from e.g.:
    #         https://fantasy.premierleague.com/drf/entry/3808385/transfers

    #     :rtype: list
    #     """
    #     if hasattr(self, "_transfers"):
    #         return self._transfers["wildcards"]

    #     transfers = await fetch(
    #         self._session, API_URLS["user_transfers"].format(self.id))

    #     self._transfers = transfers
    #     return transfers["wildcards"]

    async def get_watchlist(self):
        """Returns the user's watchlist. Requires the user to have logged in
        using ``fpl.login()``.

        Information is taken from here:
            https://fantasy.premierleague.com/api/me/

        :rtype: list
        """
        if not logged_in(self._session):
            raise Exception("User must be logged in.")

        me = await fetch(self._session, API_URLS["me"])
        return me["watched"]

    def _get_transfer_payload(
            self, players_out, players_in, user_team, players, wildcard,
            free_hit):
        """Returns the payload needed to make the desired transfers."""
        event = 0
        if (self.current_event):
            event = self.current_event
        payload = {
            "confirmed": False,
            "entry": self.id,
            "event": event + 1,
            "transfers": [],
            "wildcard": wildcard,
            "freehit": free_hit
        }

        for player_out_id, player_in_id in zip(players_out, players_in):
            player_out = next(player for player in user_team
                              if player["element"] == player_out_id)
            player_in = next(player for player in players
                             if player["id"] == player_in_id)
            payload["transfers"].append({
                "element_in": player_in["id"],
                "element_out": player_out["element"],
                "purchase_price": player_in["now_cost"],
                "selling_price": player_out["selling_price"]
            })

        return payload

    async def transfer(self, players_out, players_in, max_hit=60,
                       wildcard=False, free_hit=False):
        """Transfers given players out and transfers given players in.

        :param players_out: List of IDs of players who will be transferred out.
        :type players_out: list
        :param players_in: List of IDs of players who will be transferred in.
        :type players_in: list
        :param max_hit: Maximum hit that should be taken by making the
            transfer(s), defaults to 60
        :param max_hit: int, optional
        :param wildcard: Boolean for playing wildcard, defaults to False
        :param wildcard: bool, optional
        :param free_hit: Boolean for playing free hit, defaults to False
        :param free_hit: bool, optional
        :return: Returns the response given by a succesful transfer.
        :rtype: dict
        """
        if wildcard and free_hit:
            raise Exception("Can only use 1 of wildcard and free hit.")

        if not logged_in(self._session):
            raise Exception("User must be logged in.")

        if not players_out or not players_in:
            raise Exception(
                "Lists must both contain at least one player's ID.")

        if len(players_out) != len(players_in):
            raise Exception("Number of players transferred in must be same as "
                            "number transferred out.")

        if not set(players_in).isdisjoint(players_out):
            raise Exception("Player ID can't be in both lists.")

        user_team = await self.get_team()
        team_ids = [player["element"] for player in user_team]

        if not set(team_ids).isdisjoint(players_in):
            raise Exception(
                "Cannot transfer a player in who is already in the user's team.")

        if set(team_ids).isdisjoint(players_out):
            raise Exception(
                "Cannot transfer a player out who is not in the user's team.")

        players = await fetch(self._session, API_URLS["static"])
        players = players["elements"]
        player_ids = [player["id"] for player in players]

        if set(player_ids).isdisjoint(players_in):
            raise Exception("Player ID in `players_in` does not exist.")

        # Send POST requests with `confirmed` set to False; this basically
        # checks if there are any errors from FPL's side for this transfer,
        # e.g. too many players from the same team, or not enough money.
        payload = self._get_transfer_payload(
            players_out, players_in, user_team, players, wildcard, free_hit)
        headers = get_headers(
            "https://fantasy.premierleague.com/a/squad/transfers")
        post_response = await post(
            self._session, API_URLS["transfers"], json.dumps(payload), headers)

        if "non_form_errors" in post_response:
            raise Exception(post_response["non_form_errors"])

        if post_response["spent_points"] > max_hit:
            raise Exception(
                f"Point hit for transfer(s) [-{post_response['spent_points']}]"
                f" exceeds max_hit [{max_hit}].")

        # Everything is okay, so push the transfer through!
        payload["confirmed"] = True
        post_response = await post(
            self._session, API_URLS["transfers"], json.dumps(payload), headers)
        return post_response

    async def _create_new_lineup(self, players_in, players_out, lineup):
        """Helper for creating the new lineup of players.

        :param players_in: List of IDs of players who will be substituted in.
        :type players_in: list
        :param players_out: List of IDs of players who will be substituted out.
        :type players_out: list
        :param lineup: List containing the user's current lineup.
        :type lineup: list
        :return: Returns the new lineup.
        :rtype: list
        """

        players = await fetch(self._session, API_URLS["static"])
        players = players["elements"]
        _set_element_type(lineup, players)

        subs_in = _ids_to_lineup(players_in, lineup)
        subs_out = _ids_to_lineup(players_out, lineup)

        for sub_out, sub_in in zip(subs_out, subs_in):
            # Get indices of sub out and sub in, then swap their position in
            # the lineup
            out_i, in_i = lineup.index(sub_out), lineup.index(sub_in)
            lineup[out_i], lineup[in_i] = lineup[in_i], lineup[out_i]

            # Swap position and (vice) captaincy
            lineup[out_i]["position"], lineup[in_i]["position"] = (
                lineup[in_i]["position"], lineup[out_i]["position"])
            lineup[out_i][is_c], lineup[in_i][is_c] = (
                lineup[in_i][is_c], lineup[out_i][is_c])
            lineup[out_i][is_vc], lineup[in_i][is_vc] = (
                lineup[in_i][is_vc], lineup[out_i][is_vc])

            starters, subs = lineup[:11], lineup[11:]
            new_starters = sorted(starters, key=lambda x: (
                x["element_type"] - 1) * 100 + x["position"])
            lineup = new_starters + subs

            for position, player in enumerate(lineup):
                player["position"] = position + 1

        new_lineup = [{
            "element": player["element"],
            "position": player["position"],
            "is_captain": player[is_c],
            "is_vice_captain": player[is_vc]
        } for player in lineup]

        return new_lineup

    async def _post_substitutions(self, lineup):
        """Helper for sending the POST requests with the new lineup.

        :param lineup: The new lineup.
        :type lineup: list
        """
        # Get CSRF token and create payload + headers
        payload = json.dumps({"chip": None, "picks": lineup})
        headers = get_headers("https://fantasy.premierleague.com/a/team/my")

        await post(
            self._session, API_URLS["user_team"].format(self.id) + "/",
            payload=payload, headers=headers)

    async def _captain_helper(self, captain, captain_type):
        """Helper for setting the (vice) captain of the user's team."""
        if not logged_in(self._session):
            raise Exception("User must be logged in.")

        user_team = await self.get_team()
        team_ids = [player["element"] for player in user_team]
        _set_captain(user_team, captain, captain_type, team_ids)
        lineup = await self._create_new_lineup([], [], user_team)

        await self._post_substitutions(lineup)

    async def captain(self, captain):
        """Set the captain of the user's team.

        :param captain: ID of the captain.
        :type captain: int
        """
        await self._captain_helper(captain, is_c)

    async def vice_captain(self, vice_captain):
        """Set the vice captain of the user's team.

        :param vice_captain: ID of the vice captain.
        :type vice_captain: int
        """
        await self._captain_helper(vice_captain, is_vc)

    async def substitute(self, players_in, players_out, captain=None,
                         vice_captain=None):
        """Substitute players on the bench for players in the starting eleven.
        Also allows the user to simultaneously set the new (vice) captain(s).
        A maximum of 4 substitutes is set to force proper usage.

        :param players_in: List of IDs of players who will be substituted in.
        :type players_in: list
        :param players_out: List of IDS of players who will be substituted out.
        :type players_out: list
        :param captain: ID of the captain, defaults to None.
        :param captain: int, optional
        :param vice_captain: ID of the vice captain, defaults to None.
        :param vice_captain: int, optional
        """
        if not logged_in(self._session):
            raise Exception("User must be logged in.")

        if len(players_out) > 4 or len(players_in) > 4:
            raise Exception("Can only substitute a maximum of 4 players.")

        if len(players_out) != len(players_in):
            raise Exception("Number of players substituted in must be same as "
                            "number substituted out.")

        if not set(players_in).isdisjoint(players_out):
            raise Exception("Player ID can't be in both lists.")

        user_team = await self.get_team()
        team_ids = [player["element"] for player in user_team]
        substitution_ids = players_out + players_in

        if not set(substitution_ids).issubset(team_ids):
            raise Exception(
                "Cannot substitute players who aren't in the user's team.")

        # Set new captain or vice captain if applicable
        if captain:
            _set_captain(user_team, captain, is_c, team_ids)

        if vice_captain:
            _set_captain(user_team, vice_captain, is_vc, team_ids)

        lineup = await self._create_new_lineup(
            players_in, players_out, user_team)

        await self._post_substitutions(lineup)

    def __str__(self):
        return (f"{self.player_first_name} {self.player_last_name} - "
                f"{self.player_region_name}")
