from enum import Enum
from datetime import datetime
from contextlib import suppress
import math
import asyncio
import json
from datetime import timedelta, datetime as dt
from pathlib import Path

import aiofiles
import aiohttp

from ..gametypes import GameType
from ..utils import get_cachedir

RAW_ACHIEVEMENTS_URL = "https://api.hypixel.net/resources/achievements"

__all__ = [
    "OneTimeAchievement", "TieredAchievement", "AchievementTier",
    "PixelAchievements", "PlayerAchievements", "PlayerRank", "Player"
]


class OneTimeAchievement:
    def __init__(self, data: tuple):
        self.game = data[0]
        self.db_name = data[1]
        self.points = data[2]["points"]
        self.name = data[2]["name"]
        self.description = data[2]["description"]

    def __repr__(self):
        return "<OneTimeAchievement game={0.game} db_name={0.db_name} "\
               "name={0.name} points={0.points} description={0.description}"\
               "".format(self)


class TieredAchievement:
    def __init__(self, data: tuple):
        self.game = data[0]
        self.db_name = data[1]
        self.player_value = data[3]
        self.name = data[2]["name"]
        self.description = data[2]["description"]
        completed_tiers = []
        unfinished_tiers = []
        for tier in data[2]["tiers"]:
            if tier["amount"] <= self.player_value:
                completed_tiers.append(AchievementTier(**tier))
            else:
                unfinished_tiers.append(AchievementTier(**tier))
        self.completed_tiers = completed_tiers
        self.unfinished_tiers = unfinished_tiers


class AchievementTier:
    def __init__(self, tier: int, points: int, amount: int):
        self.tier = tier
        self.points = points
        self.amount = amount


class PixelAchievements:
    achievements = None

    def __init__(self):
        loop = asyncio.get_event_loop()
        loop.create_task(self.get_all_achievements())
        self.achievements = None

    async def get_all_achievements(self):
        cachedir = get_cachedir()
        achievements_file = cachedir / "achievements.json"

        try:
            async with aiofiles.open(achievements_file, "r") as fin:
                data = json.loads(await fin.read())
        except FileNotFoundError:
            # Don't have this yet, so we need to cache it
            data = await self._download_achievements_resource()
            self.achievements = data["achievements"]
            async with aiofiles.open(achievements_file, "w") as fout:
                await fout.write(json.dumps(data))
        else:
            if data["last_check_time"] + timedelta(days=7) > dt.utcnow().timestamp():
                self.achievements = data["achievements"]
            else:
                try:
                    achievements_file.unlink()
                except FileNotFoundError:
                    # If this ever happens, it means the file was 
                    # removed between opening it and now
                    raise
                else:
                    await self.get_all_achievements()
    
    async def _download_achievements_resource(self, backoff_cnt: int=0) -> dict:
        backoff = 2
        async with aiohttp.ClientSession() as session:
            async with session.get(RAW_ACHIEVEMENTS_URL) as r:
                if r.status == 200:
                    try:
                        data = json.loads(await r.text())
                    except aiohttp.ClientResponseError as e:
                        if backoff_cnt == 8:
                            # At this point, we've waited over 8 minutes in total and 
                            # are not getting a parseable response, so just stop trying 
                            # and raise the error
                            raise
                        await asyncio.sleep(backoff**backoff_cnt)
                        backoff_cnt += 1
                        return await self._download_achievements_resource()
                    else:
                        self.achievements = data["achievements"]
                        data["last_check_time"] = dt.utcnow().timestamp()
                        return data

    def get_one_time_achievement(self, name: str):
        game, achievement_name = name.split("_", 1)
        if game not in self.achievements:
            return None
        if not achievement_name.upper() in self.achievements[game]["one_time"]:
            return None
        raw_achievement = \
            self.achievements[game]["one_time"][achievement_name.upper()]
        return OneTimeAchievement((game, achievement_name, raw_achievement))

    def get_tiered_achievement(self, name: str, player_value: int):
        game, achievement_name = name.split("_", 1)
        if game not in self.achievements:
            return None
        if not achievement_name.upper() in self.achievements[game]["tiered"]:
            return None
        raw_achv = self.achievements[game]["tiered"][achievement_name.upper()]
        return TieredAchievement(
            (game, achievement_name, raw_achv, player_value)
        )


class PlayerAchievements:

    def __init__(self, data, all_achievements: PixelAchievements):
        self.all_achievements = all_achievements
        one_time = []
        tiered = []
        if "achievementsOneTime" in data:
            for item in data["achievementsOneTime"]:
                one_time.append(
                    self.all_achievements.get_one_time_achievement(item)
                )
        if "achievements" in data:
            for k, v in data["achievements"].items():
                tiered.append(
                    self.all_achievements.get_tiered_achievement(k, v)
                )
        self.one_time = one_time
        self.tiered = tiered


class PlayerRank(Enum):
    """
    An enum representing player ranks
    """
    ADMIN = "Admin"
    MODERATOR = "Moderator"
    HELPER = "Helper"
    JR_HELPER = "Jr. Helper"
    YOUTUBER = "Youtuber"
    BUILD_TEAM = "Build Team"
    SUPERSTAR = "MVP++"
    MVP_PLUS = "MVP+"
    MVP = "MVP"
    VIP_PLUS = "VIP+"
    VIP = "VIP"
    NONE = ""

    def __init__(self, pretty_name: str):
        self.pretty_name = pretty_name

    @classmethod
    def from_player_data(cls, data: dict):
        """
        Gets the player's rank from their player data
        """
        if "rank" in data:
            try:
                return getattr(cls, data["rank"])
            except AttributeError:
                # if there's some value for data["rank"] that 
                # isn't here, skip and find the next valid rank
                pass
        elif data.get("buildTeam") is True:
            return cls.BUILD_TEAM
        elif data.get("monthlyPackageRank") == "SUPERSTAR":
            return cls.SUPERSTAR
        elif "newPackageRank" in data:
            return getattr(cls, data["newPackageRank"])
        elif "packageRank" in data:
            return getattr(cls, data["packageRank"])
        else:
            return cls.NONE


class Player:
    def __init__(self, _id: str, displayname: str, first_login: int,
                 last_login: int, karma: int, network_exp: float,
                 rank: PlayerRank, last_logout: int,
                 achievements: PlayerAchievements, stats: list,
                 most_recent_game_type: GameType):
        self._id = _id
        self.displayname = displayname
        if first_login:
            self.first_login = datetime.utcfromtimestamp(first_login/1000)
        else:
            self.first_login = first_login
        if last_login:
            self.last_login = datetime.utcfromtimestamp(last_login/1000)
        else:
            self.last_login = last_login
        self.karma = karma
        self.network_exp = network_exp
        self.rank = rank
        if last_logout:
            self.last_logout = datetime.utcfromtimestamp(last_logout/1000)
        else:
            self.last_logout = last_logout
        self.achievements = achievements
        self.stats = stats
        self.most_recent_game_type = most_recent_game_type

    def network_level(self) -> float:
        """
        Calculate the player's network level

        Returns
        -------
        float
            The player's network level
        """
        base = 10000
        growth = 2500

        reverse_pq_prefix = -(base - 0.5 * growth) / growth
        reverse_const = math.pow(reverse_pq_prefix, 2)
        growth_divides_2 = 2 / growth

        return 1.0 if self.network_exp < 0 \
            else 1 + reverse_pq_prefix + math.sqrt(
                reverse_const + growth_divides_2 * self.network_exp
            )
    
    def online(self) -> bool:
        """
        Show whether the player is online or not
        """
        return self.last_logout < self.last_login
