import datetime
import typing
from enum import Enum
from typing import List, Union

from .aes import AES
from .banner import Banner, BannerColor
from .cosmetics import BrCosmetic, NewBrCosmetics, CarCosmetic, InstrumentCosmetic, LegoCosmeticVariant, JamTrack, \
    NewCosmetics, LegoKitCosmetic
from .creator_code import CreatorCode
from .enums import GameLanguage, MatchMethod, NewsType, KeyFormat, AccountType, TimeWindow, StatsImageType, \
    BrCosmeticRarity, BrCosmeticType
from .errors import MissingSearchParameter, MissingIDParameter, NotFound
from .map import Map
from .news import GameModeNews, News
from .playlist import Playlist
from .shop import BrShop, Shop
from .stats import BrPlayerStats

_SEARCH_PARAMETERS = {
    'language': [None, [GameLanguage]],
    'search_language': ['searchLanguage', [GameLanguage]],
    'match_method': ['matchMethod', [MatchMethod]],
    'id': [None, [str, None]],
    'name': [None, [str, None]],
    'description': [None, [str, None]],
    'type': [None, [BrCosmeticType, None]],
    'display_type': [None, [str, None]],
    'backend_type': ['backendType', [str, None]],
    'rarity': [None, [BrCosmeticRarity, None]],
    'display_rarity': ['displayRarity', [str, None]],
    'backend_rarity': ['backendRarity', [str, None]],
    'has_series': ['hasSeries', [bool, None]],
    'series': [None, [str, None]],
    'backend_series': ['backendSeries', [str, None]],
    'has_set': ['hasSet', [bool, None]],
    'set': ['set', [str, None]],
    'set_text': ['setText', [str, None]],
    'backend_set': ['backendSet', [str, None]],
    'has_introduction': ['hasIntroduction', [bool, None]],
    'backendIntroduction': ['backendIntroduction', [int, None]],
    'introduction_chapter': ['introductionChapter', [str, None]],
    'introduction_season': ['introductionSeason', [str, None]],
    'has_small_icon': ['hasSmallIcon', [bool, None]],
    'has_icon': ['hasIcon', [bool, None]],
    'has_featured_image': ['hasFeaturedImage', [bool, None]],
    'has_variants': ['hasVariants', [bool, None]],
    'has_gameplay_tags': ['hasGameplayTags', [bool, None]],
    'gameplay_tag': ['gameplayTag', [str, None]],
    'has_meta_tags': ['hasMetaTags', [bool, None]],
    'meta_tag': ['metaTag', [str, None]],
    'has_dynamic_pak_id': ['hasDynamicPakId', [bool, None]],
    'dynamic_pak_id': ['dynamicPakId', [str, None]],
    'added': [None, [datetime.datetime, None]],
    'added_since': ['addedSince', [datetime.datetime, None]],
    'unseen_for': ['unseenFor', [int, None]],
    'last_appearance': ['lastAppearance', [datetime.datetime, None]]
}


def _parse_search_parameter(**search_parameters):
    parameters = {}  # TODO: Empty string as search parameter
    for key, value in search_parameters.items():
        search_parameter_data = _SEARCH_PARAMETERS.get(key)
        if search_parameter_data is None:
            continue
        if type(value) not in search_parameter_data[1]:
            types = ' or '.join([str(t) for t in search_parameter_data[1]])
            raise TypeError('{0} require a value of type {1}'.format(key, types))
        key = search_parameter_data[0] if search_parameter_data[0] else key
        if value is None:
            value = '<null>'
        elif isinstance(value, Enum):
            value = value.value
        elif isinstance(value, datetime.datetime):
            value = int(value.timestamp())
        parameters[key] = value
    if len(parameters) == 0:
        raise MissingSearchParameter('at least one search parameter is required')
    return parameters


class SyncAESEndpoints:

    def __init__(self, client):
        self._client = client

    def fetch(self, key_format: KeyFormat = KeyFormat.HEX) -> AES:
        params = {'keyFormat': key_format.value}
        data = self._client.http.get('v2/aes', params=params)
        return AES(data['data'])


class AsyncAESEndpoints:

    def __init__(self, client):
        self._client = client

    async def fetch(self, key_format: KeyFormat = KeyFormat.HEX) -> AES:
        params = {'keyFormat': key_format.value}
        data = await self._client.http.get('v2/aes', params=params)
        return AES(data['data'])


class SyncBannerEndpoints:

    def __init__(self, client):
        self._client = client

    def fetch(self, language: GameLanguage = GameLanguage.ENGLISH) -> typing.List[Banner]:
        params = {'language': language.value}
        data = self._client.http.get('v1/banners', params=params)
        return [Banner(item_data) for item_data in data['data']]

    def fetch_colors(self) -> typing.List[BannerColor]:
        data = self._client.http.get('v1/banners/colors')
        return [BannerColor(item_data) for item_data in data['data']]


class AsyncBannerEndpoints:

    def __init__(self, client):
        self._client = client

    async def fetch(self, language: GameLanguage = GameLanguage.ENGLISH) -> typing.List[Banner]:
        params = {'language': language.value}
        data = await self._client.http.get('v1/banners', params=params)
        return [Banner(item_data) for item_data in data['data']]

    async def fetch_colors(self) -> typing.List[BannerColor]:
        data = await self._client.http.get('v1/banners/colors')
        return [BannerColor(item_data) for item_data in data['data']]


class SyncCosmeticsEndpoints:

    def __init__(self, client):
        self._client = client

    def fetch(
            self,
            language=GameLanguage.ENGLISH
    ) -> List[Union[BrCosmetic, CarCosmetic, InstrumentCosmetic, LegoKitCosmetic, JamTrack, LegoCosmeticVariant]]:
        params = {'language': language.value}
        data = self._client.http.get('v2/cosmetics', params=params)
        data = data['data']
        cosmetics = []
        cosmetics.extend([BrCosmetic(item) for item in data['br']])
        cosmetics.extend([CarCosmetic(item) for item in data['cars']])
        cosmetics.extend([InstrumentCosmetic(item) for item in data['instruments']])
        cosmetics.extend([LegoKitCosmetic(item) for item in data['legoKits']])
        cosmetics.extend([JamTrack(item) for item in data['tracks']])
        cosmetics.extend([LegoCosmeticVariant(item) for item in data['lego']])
        return cosmetics

    def fetch_br(self, language=GameLanguage.ENGLISH):
        """Returns an list of all battle royale cosmetics.

        Parameters
        -----------
        language: :class:`GameLanguage`
            The language in which the cosmetics should be returned. Default is set to english.

        Returns
        --------
        List[:class:`BrCosmetic`]
            A list of all cosmetics.

        Raises
        -------
        Unauthorized
            You have not used a valid API key.
        RateLimited
            You reached the rate limit.
        ServiceUnavailable
            The Fortnite-API.com server is currently not available.
        """
        params = {'language': language.value}
        data = self._client.http.get('v2/cosmetics/br', params=params)
        return [BrCosmetic(item_data) for item_data in data['data']]

    fetch_all = fetch_br  # Backwards compatibility

    def fetch_cars(self, language=GameLanguage.ENGLISH):
        params = {'language': language.value}
        data = self._client.http.get('v2/cosmetics/cars', params=params)
        return [CarCosmetic(item_data) for item_data in data['data']]

    def fetch_instruments(self, language=GameLanguage.ENGLISH):
        params = {'language': language.value}
        data = self._client.http.get('v2/cosmetics/instruments', params=params)
        return [InstrumentCosmetic(item_data) for item_data in data['data']]

    def fetch_lego_kits(self, language=GameLanguage.ENGLISH):
        params = {'language': language.value}
        data = self._client.http.get('v2/cosmetics/lego/kits', params=params)
        return [LegoKitCosmetic(item_data) for item_data in data['data']]

    def fetch_jam_tracks(self, language=GameLanguage.ENGLISH):
        params = {'language': language.value}
        data = self._client.http.get('v2/cosmetics/tracks', params=params)
        return [JamTrack(item_data) for item_data in data['data']]

    def fetch_lego_variants(self, language=GameLanguage.ENGLISH):
        params = {'language': language.value}
        data = self._client.http.get('v2/cosmetics/lego', params=params)
        return [LegoCosmeticVariant(item_data) for item_data in data['data']]

    def fetch_new_all(self, language=GameLanguage.ENGLISH):
        params = {'language': language.value}
        data = self._client.http.get('v2/cosmetics/new', params=params)
        return NewCosmetics(data['data'])

    def fetch_new_br(self, language=GameLanguage.ENGLISH):
        params = {'language': language.value}
        data = self._client.http.get('v2/cosmetics/br/new', params=params)
        return NewBrCosmetics(data['data'])

    fetch_new = fetch_new_br  # Backwards compatibility

    def search_by_id(self, *cosmetic_ids, language=GameLanguage.ENGLISH):
        """Returns an list of the requested battle royale cosmetic ids.

        Parameters
        -----------
        \*cosmetic_ids: :class:`str`
            The id of the cosmetic

        Returns
        --------
        List[:class:`BrCosmetic`]
            A list of all cosmetics.

        Raises
        -------
        Unauthorized
            You have not used a valid API key.
        RateLimited
            You reached the rate limit.
        ServiceUnavailable
            The Fortnite-API.com server is currently not available.
        """
        cosmetic_ids = list(cosmetic_ids)
        params = {'language': language.value}

        if len(cosmetic_ids) == 0:
            raise MissingIDParameter('at least one cosmetic id is required')

        endpoint = 'v2/cosmetics/br/search/ids'
        endpoint += '?id=' + '&id='.join(cosmetic_ids)
        data = self._client.http.get(endpoint, params=params)
        return [BrCosmetic(item_data) for item_data in data['data']]

    def search_all(self, **search_parameters) -> typing.List[BrCosmetic]:
        data = self._client.http.get('v2/cosmetics/br/search/all',
                                     params=_parse_search_parameter(**search_parameters))
        return [BrCosmetic(item_data) for item_data in data['data']]

    def search_first(self, **search_parameters) -> BrCosmetic:
        data = self._client.http.get('v2/cosmetics/br/search',
                                     params=_parse_search_parameter(**search_parameters))
        return BrCosmetic(data['data'])


class AsyncCosmeticsEndpoints:

    def __init__(self, client):
        self._client = client

    async def fetch(
            self,
            language=GameLanguage.ENGLISH
    ) -> List[Union[BrCosmetic, CarCosmetic, InstrumentCosmetic, LegoKitCosmetic, JamTrack, LegoCosmeticVariant]]:
        params = {'language': language.value}
        data = await self._client.http.get('v2/cosmetics', params=params)
        data = data['data']
        cosmetics = []
        cosmetics.extend([BrCosmetic(item) for item in data['br']])
        cosmetics.extend([CarCosmetic(item) for item in data['cars']])
        cosmetics.extend([InstrumentCosmetic(item) for item in data['instruments']])
        cosmetics.extend([LegoKitCosmetic(item) for item in data['legoKits']])
        cosmetics.extend([JamTrack(item) for item in data['tracks']])
        cosmetics.extend([LegoCosmeticVariant(item) for item in data['lego']])
        return cosmetics

    async def fetch_br(self, language: GameLanguage = GameLanguage.ENGLISH) -> typing.List[BrCosmetic]:
        params = {'language': language.value}
        data = await self._client.http.get('v2/cosmetics/br', params=params)
        return [BrCosmetic(item_data) for item_data in data['data']]

    fetch_all = fetch_br  # Backwards compatibility

    async def fetch_cars(self, language=GameLanguage.ENGLISH):
        params = {'language': language.value}
        data = await self._client.http.get('v2/cosmetics/cars', params=params)
        return [CarCosmetic(item_data) for item_data in data['data']]

    async def fetch_instruments(self, language=GameLanguage.ENGLISH):
        params = {'language': language.value}
        data = await self._client.http.get('v2/cosmetics/instruments', params=params)
        return [InstrumentCosmetic(item_data) for item_data in data['data']]

    async def fetch_lego_kits(self, language=GameLanguage.ENGLISH):
        params = {'language': language.value}
        data = await self._client.http.get('v2/cosmetics/lego/kits', params=params)
        return [LegoKitCosmetic(item_data) for item_data in data['data']]

    async def fetch_jam_tracks(self, language=GameLanguage.ENGLISH):
        params = {'language': language.value}
        data = await self._client.http.get('v2/cosmetics/tracks', params=params)
        return [JamTrack(item_data) for item_data in data['data']]

    async def fetch_lego_variants(self, language=GameLanguage.ENGLISH):
        params = {'language': language.value}
        data = await self._client.http.get('v2/cosmetics/lego', params=params)
        return [LegoCosmeticVariant(item_data) for item_data in data['data']]

    async def fetch_new_all(self, language=GameLanguage.ENGLISH):
        params = {'language': language.value}
        data = await self._client.http.get('v2/cosmetics/new', params=params)
        return NewCosmetics(data['data'])

    async def fetch_new_br(self, language=GameLanguage.ENGLISH):
        params = {'language': language.value}
        data = await self._client.http.get('v2/cosmetics/br/new', params=params)
        return NewBrCosmetics(data['data'])

    fetch_new = fetch_new_br  # Backwards compatibility

    async def search_by_id(self, *cosmetic_id: str, language: GameLanguage = GameLanguage.ENGLISH) -> typing.List[
        BrCosmetic]:
        cosmetic_ids = list(cosmetic_id)
        params = {'language': language.value}

        if len(cosmetic_ids) == 0:
            raise MissingIDParameter('at least one cosmetic id is required')

        endpoint = 'v2/cosmetics/br/search/ids'
        endpoint += '?id=' + '&id='.join(cosmetic_ids)
        data = await self._client.http.get(endpoint, params=params)
        return [BrCosmetic(item_data) for item_data in data['data']]

    async def search_all(self, **search_parameters) -> typing.List[BrCosmetic]:
        data = await self._client.http.get('v2/cosmetics/br/search/all',
                                           params=_parse_search_parameter(**search_parameters))
        return [BrCosmetic(item_data) for item_data in data['data']]

    async def search_first(self, **search_parameters) -> BrCosmetic:
        data = await self._client.http.get('v2/cosmetics/br/search',
                                           params=_parse_search_parameter(**search_parameters))
        return BrCosmetic(data['data'])


class SyncCreatorCodeEndpoints:

    def __init__(self, client):
        self._client = client

    def fetch(self, name: str) -> CreatorCode:
        params = {'name': name}
        data = self._client.http.get('v2/creatorcode', params=params)
        return CreatorCode(data['data'])

    def exists(self, name: str) -> bool:
        try:
            self.fetch(name)
            return True
        except NotFound:
            return False

    def search_first(self, name: str) -> CreatorCode:
        params = {'name': name}
        data = self._client.http.get('v2/creatorcode/search', params=params)
        return CreatorCode(data['data'])

    def search_all(self, name: str) -> typing.List[CreatorCode]:
        params = {'name': name}
        data = self._client.http.get('v2/creatorcode/search/all', params=params)
        return [CreatorCode(creator_code_data) for creator_code_data in data['data']]


class AsyncCreatorCodeEndpoints:

    def __init__(self, client):
        self._client = client

    async def fetch(self, name: str) -> CreatorCode:
        params = {'name': name}
        data = await self._client.http.get('v2/creatorcode', params=params)
        return CreatorCode(data['data'])

    async def exists(self, name: str) -> bool:
        try:
            await self.fetch(name)
            return True
        except NotFound:
            return False

    async def search_first(self, name: str) -> CreatorCode:
        params = {'name': name}
        data = await self._client.http.get('v2/creatorcode/search', params=params)
        return CreatorCode(data['data'])

    async def search_all(self, name: str) -> typing.List[CreatorCode]:
        params = {'name': name}
        data = await self._client.http.get('v2/creatorcode/search/all', params=params)
        return [CreatorCode(creator_code_data) for creator_code_data in data['data']]


class SyncMapEndpoints:

    def __init__(self, client):
        self._client = client

    def fetch(self, language: GameLanguage = GameLanguage.ENGLISH) -> Map:
        params = {'language': language.value}
        data = self._client.http.get('v1/map', params=params)
        return Map(data['data'])


class AsyncMapEndpoints:

    def __init__(self, client):
        self._client = client

    async def fetch(self, language: GameLanguage = GameLanguage.ENGLISH) -> Map:
        params = {'language': language.value}
        data = await self._client.http.get('v1/map', params=params)
        return Map(data['data'])


class SyncNewsEndpoints:

    def __init__(self, client):
        self._client = client

    def fetch(self, language: GameLanguage = GameLanguage.ENGLISH) -> News:
        params = {'language': language.value}
        data = self._client.http.get('v2/news', params=params)
        return News(data['data'])

    def fetch_by_type(self, news_type: NewsType, language=GameLanguage.ENGLISH) -> GameModeNews:
        params = {'language': language.value}
        data = self._client.http.get('v2/news/' + news_type.value, params=params)
        return GameModeNews(data['data'])


class AsyncNewsEndpoints:

    def __init__(self, client):
        self._client = client

    async def fetch(self, language: GameLanguage = GameLanguage.ENGLISH) -> News:
        params = {'language': language.value}
        data = await self._client.http.get('v2/news', params=params)
        return News(data['data'])

    async def fetch_by_type(self, news_type: NewsType, language: GameLanguage = GameLanguage.ENGLISH) -> GameModeNews:
        params = {'language': language.value}
        data = await self._client.http.get('v2/news/' + news_type.value, params=params)
        return GameModeNews(data['data'])


class SyncPlaylistEndpoints:

    def __init__(self, client):
        self._client = client

    def fetch_all(self, language: GameLanguage = GameLanguage.ENGLISH) -> List[Playlist]:
        params = {'language': language.value}
        data = self._client.http.get('v1/playlists', params=params)
        return [Playlist(playlist) for playlist in data['data']]

    def fetch_by_id(self, playlist_id, language: GameLanguage = GameLanguage.ENGLISH) -> Playlist:
        params = {'language': language.value}
        data = self._client.http.get(f'v1/playlists/{playlist_id}', params=params)
        return Playlist(data['data'])


class AsyncPlaylistEndpoints:

    def __init__(self, client):
        self._client = client

    async def fetch_all(self, language: GameLanguage = GameLanguage.ENGLISH) -> List[Playlist]:
        params = {'language': language.value}
        data = await self._client.http.get('v1/playlists', params=params)
        return [Playlist(playlist) for playlist in data['data']]

    async def fetch_by_id(self, playlist_id, language: GameLanguage = GameLanguage.ENGLISH) -> Playlist:
        params = {'language': language.value}
        data = await self._client.http.get(f'v1/playlists/{playlist_id}', params=params)
        return Playlist(data['data'])


class SyncShopEndpoints:

    def __init__(self, client):
        self._client = client

    def fetch_all(self, language: GameLanguage = GameLanguage.ENGLISH) -> Shop:
        params = {'language': language.value}
        data = self._client.http.get('v2/shop', params=params)
        return Shop(data['data'])

    def fetch_br(self, language: GameLanguage = GameLanguage.ENGLISH, combined: bool = False) -> BrShop:
        params = {'language': language.value}
        data = self._client.http.get('v2/shop/br' if not combined else 'v2/shop/br/combined', params=params)
        return BrShop(data['data'])

    fetch = fetch_br


class AsyncShopEndpoints:

    def __init__(self, client):
        self._client = client

    async def fetch_all(self, language: GameLanguage = GameLanguage.ENGLISH) -> Shop:
        params = {'language': language.value}
        data = await self._client.http.get('v2/shop', params=params)
        return Shop(data['data'])

    async def fetch_br(self, language: GameLanguage = GameLanguage.ENGLISH, combined: bool = False) -> BrShop:
        params = {'language': language.value}
        data = await self._client.http.get('v2/shop/br' if not combined else 'v2/shop/br/combined', params=params)
        return BrShop(data['data'])

    fetch = fetch_br


class SyncStatsEndpoints:

    def __init__(self, client):
        self._client = client

    def fetch_by_name(self, name: str,
                      account_type: AccountType = AccountType.EPIC,
                      time_window: TimeWindow = TimeWindow.LIFETIME,
                      image: StatsImageType = StatsImageType.NONE) -> BrPlayerStats:
        params = {'name': name, 'accountType': account_type.value, 'timeWindow': time_window.value,
                  'image': image.value}
        data = self._client.http.get('v2/stats/br/v2', params=params)
        return BrPlayerStats(data['data'])

    def fetch_by_id(self, account_id: str,
                    time_window: TimeWindow = TimeWindow.LIFETIME,
                    image: StatsImageType = StatsImageType.NONE) -> BrPlayerStats:
        params = {'timeWindow': time_window.value, 'image': image.value}
        data = self._client.http.get('v2/stats/br/v2/{account_id}'.format(account_id=account_id), params=params)
        return BrPlayerStats(data['data'])


class AsyncStatsEndpoints:

    def __init__(self, client):
        self._client = client

    async def fetch_by_name(self, name: str,
                            account_type: AccountType = AccountType.EPIC,
                            time_window: TimeWindow = TimeWindow.LIFETIME,
                            image: StatsImageType = StatsImageType.NONE) -> BrPlayerStats:
        params = {'name': name, 'accountType': account_type.value, 'timeWindow': time_window.value,
                  'image': image.value}
        data = await self._client.http.get('v2/stats/br/v2', params=params)
        return BrPlayerStats(data['data'])

    async def fetch_by_id(self, account_id: str,
                          time_window: TimeWindow = TimeWindow.LIFETIME,
                          image: StatsImageType = StatsImageType.NONE) -> BrPlayerStats:
        params = {'timeWindow': time_window.value, 'image': image.value}
        data = await self._client.http.get('v2/stats/br/v2/{account_id}'.format(account_id=account_id), params=params)
        return BrPlayerStats(data['data'])
