from time import time
from typing import Type, TypeVar, MutableMapping, Any, Iterable, Generator, Union
import arrow
import datetime
import math

from datapipelines import DataSource, PipelineContext, Query, NotFoundError, validate_query

from .common import RiotAPIService, APINotFoundError
from ...data import Platform, Season, Queue, SEASON_IDS, QUEUE_IDS
from ...dto.match import MatchDto, MatchListDto, TimelineDto
from ..uniquekeys import convert_region_to_platform

T = TypeVar("T")


def _get_current_time(query: MutableMapping[str, Any], context: PipelineContext = None) -> int:
    return int(time()) * 1000


class MatchAPI(RiotAPIService):
    @DataSource.dispatch
    def get(self, type: Type[T], query: MutableMapping[str, Any], context: PipelineContext = None) -> T:
        pass

    @DataSource.dispatch
    def get_many(self, type: Type[T], query: MutableMapping[str, Any], context: PipelineContext = None) -> Iterable[T]:
        pass

    _validate_get_match_query = Query. \
        has("id").as_(int).also. \
        has("platform").as_(Platform)

    @get.register(MatchDto)
    @validate_query(_validate_get_match_query, convert_region_to_platform)
    def get_match(self, query: MutableMapping[str, Any], context: PipelineContext = None) -> MatchDto:
        url = "https://{platform}.api.riotgames.com/lol/match/v4/matches/{id}".format(platform=query["platform"].value.lower(), id=query["id"])
        try:
            app_limiter, method_limiter = self._get_rate_limiter(query["platform"], "matches/id")
            data = self._get(url, {}, app_limiter=app_limiter, method_limiter=method_limiter)
        except APINotFoundError as error:
            raise NotFoundError(str(error)) from error

        data["gameId"] = query["id"]
        data["region"] = query["platform"].region.value
        for p in data["participantIdentities"]:
            aid = p.get("player", {}).get("currentAccountId", None)
            if aid == 0:
                p["player"]["bot"] = True
        return MatchDto(data)

    _validate_get_many_match_query = Query. \
        has("ids").as_(Iterable).also. \
        has("platform").as_(Platform)

    @get_many.register(MatchDto)
    @validate_query(_validate_get_many_match_query, convert_region_to_platform)
    def get_many_match(self, query: MutableMapping[str, Any], context: PipelineContext = None) -> Generator[MatchDto, None, None]:
        def generator():
            for id in query["ids"]:
                url = "https://{platform}.api.riotgames.com/lol/match/v4/matches/{id}".format(platform=query["platform"].value.lower(), id=id)
                try:
                    app_limiter, method_limiter = self._get_rate_limiter(query["platform"], "matches/id")
                    data = self._get(url, {}, app_limiter=app_limiter, method_limiter=method_limiter)
                except APINotFoundError as error:
                    raise NotFoundError(str(error)) from error
                
                for participant in data["participants"]:
                    participant.setdefault("runes", [])
                for p in data["participantIdentities"]:
                    aid = p.get("player", {}).get("currentAccountId", None)
                    if aid == 0:
                        p["player"]["bot"] = True

                data["gameId"] = id
                data["region"] = query["platform"].region.value
                yield MatchDto(data)

        return generator()

    _validate_get_match_list_query = Query. \
        has("accountId").as_(str).also. \
        has("platform").as_(Platform).also. \
        has("beginTime").as_(int).also. \
        can_have("endTime").as_(int).also. \
        has("beginIndex").as_(int).also. \
        has("maxNumberOfMatches").as_(float).also. \
        can_have("seasons").as_(Iterable).also. \
        can_have("champion.ids").as_(Iterable).also. \
        can_have("queues").as_(Iterable)

    @get.register(MatchListDto)
    @validate_query(_validate_get_match_list_query, convert_region_to_platform)
    def get_match_list(self, query: MutableMapping[str, Any], context: PipelineContext = None) -> MatchListDto:
        params = {}

        riot_index_interval = 100
        riot_date_interval = datetime.timedelta(days=7)

        begin_time = query["beginTime"]  # type: arrow.Arrow
        end_time = query.get("endTime", arrow.now())  # type: arrow.Arrow
        if isinstance(begin_time, int):
            begin_time = arrow.get(begin_time / 1000)
        if isinstance(end_time, int):
            end_time = arrow.get(end_time / 1000)

        def determine_calling_method(begin_time, end_time) -> str:
            """Returns either "by_date" or "by_index"."""
            matches_per_date_interval = 10  # This is an assumption
            seconds_per_day = (60 * 60 * 24)
            riot_date_interval_in_days = riot_date_interval.total_seconds() / seconds_per_day  # in units of days
            npulls_by_date = (end_time - begin_time).total_seconds() / seconds_per_day / riot_date_interval_in_days
            npulls_by_index = (arrow.now() - begin_time).total_seconds() / seconds_per_day / riot_date_interval_in_days * matches_per_date_interval / riot_index_interval
            if math.ceil(npulls_by_date) < math.ceil(npulls_by_index):
                by = "by_date"
            else:
                by = "by_index"
            return by

        calling_method = determine_calling_method(begin_time, end_time)

        if calling_method == "by_date":
            params["beginTime"] = begin_time.timestamp * 1000
            if "endTime" in query:
                params["endTime"] = min((begin_time + riot_date_interval).timestamp * 1000, query["endTime"])
            else:
                params["endTime"] = (begin_time + riot_date_interval).timestamp * 1000
        else:
            params["beginIndex"] = query["beginIndex"]
            params["endIndex"] = query["beginIndex"] + min(riot_index_interval, query["maxNumberOfMatches"])
            params["endIndex"] = int(params["endIndex"])

        if "seasons" in query:
            seasons = {Season(season) for season in query["seasons"]}
            params["season"] = {SEASON_IDS[season] for season in seasons}
        else:
            seasons = set()

        if "champion.ids" in query:
            champions = query["champion.ids"]
            params["champion"] = champions
        else:
            champions = set()

        if "queues" in query:
            queues = {Queue(queue) for queue in query["queues"]}
            params["queue"] = {QUEUE_IDS[queue] for queue in queues}
        else:
            queues = set()

        url = "https://{platform}.api.riotgames.com/lol/match/v4/matchlists/by-account/{accountId}".format(platform=query["platform"].value.lower(), accountId=query["accountId"])
        try:
            app_limiter, method_limiter = self._get_rate_limiter(query["platform"], "matchlists/by-account/accountId")
            data = self._get(url, params, app_limiter=app_limiter, method_limiter=method_limiter)
        except APINotFoundError:
            data = {"matches": []}

        data["accountId"] = query["accountId"]
        data["region"] = query["platform"].region.value
        data["season"] = seasons
        data["champion"] = champions
        data["queue"] = queues
        if calling_method == "by_index":
            data["beginIndex"] = params["beginIndex"]
            data["endIndex"] = params["endIndex"]
            data["maxNumberOfMatches"] = query["maxNumberOfMatches"]
        else:
            data["beginTime"] = params["beginTime"]
            data["endTime"] = params["endTime"]
        for match in data["matches"]:
            match["accountId"] = query["accountId"]
            match["region"] = Platform(match["platformId"]).region.value
        return MatchListDto(data)

    _validate_get_many_match_list_query = Query. \
        has("accountIds").as_(Iterable).also. \
        has("platform").as_(Platform).also. \
        can_have("beginTime").as_(int).also. \
        can_have("endTime").as_(int).also. \
        can_have("beginIndex").as_(int).also. \
        can_have("endIndex").as_(int).also. \
        can_have("seasons").as_(Iterable).also. \
        can_have("champion.ids").as_(Iterable).also. \
        can_have("queues").as_(Iterable)

    @get_many.register(MatchListDto)
    @validate_query(_validate_get_many_match_list_query, convert_region_to_platform)
    def get_many_match_list(self, query: MutableMapping[str, Any], context: PipelineContext = None) -> Generator[MatchListDto, None, None]:
        params = {}

        if "beginIndex" in query:
            params["beginIndex"] = query["beginIndex"]

        if "endIndex" in query:
            params["endIndex"] = query["endIndex"]

        if "seasons" in query:
            seasons = {Season(season) for season in query["seasons"]}
            params["season"] = {SEASON_IDS[season] for season in seasons}
        else:
            seasons = set()

        if "champion.ids" in query:
            params["champion"] = {query["champion.ids"]}

        if "queues" in query:
            queues = {Queue(queue) for queue in query["queues"]}
            params["queue"] = {QUEUE_IDS[queue] for queue in queues}
        else:
            queues = set()

        def generator():
            for id in query["accountIds"]:
                url = "https://{platform}.api.riotgames.com/lol/match/v4/matchlists/by-account/{accountId}".format(platform=query["platform"].value.lower(), accountId=id)
                try:
                    app_limiter, method_limiter = self._get_rate_limiter(query["platform"], "matchlists/by-account/accountId")
                    data = self._get(url, params, app_limiter=app_limiter, method_limiter=method_limiter)
                except APINotFoundError as error:
                    raise NotFoundError(str(error)) from error

                data["accountId"] = id
                data["region"] = query["platform"].region.value
                if "beginIndex" in query:
                    data["beginIndex"] = query["beginIndex"]
                if "endIndex" in query:
                    data["endIndex"] = query["endIndex"]
                if "seasons" in query:
                    data["seasons"] = seasons
                if "champion.ids" in query:
                    data["champion"] = params["champion"]
                if "queues" in query:
                    params["queue"] = queues
                yield MatchListDto(data)

        return generator()

    _validate_get_timeline_query = Query. \
        has("id").as_(int).also. \
        has("platform").as_(Platform)

    @get.register(TimelineDto)
    @validate_query(_validate_get_timeline_query, convert_region_to_platform)
    def get_match_timeline(self, query: MutableMapping[str, Any], context: PipelineContext = None) -> TimelineDto:
        url = "https://{platform}.api.riotgames.com/lol/match/v4/timelines/by-match/{id}".format(platform=query["platform"].value.lower(), id=query["id"])
        try:
            app_limiter, method_limiter = self._get_rate_limiter(query["platform"], "timelines/by-match/id")
            data = self._get(url, {}, app_limiter=app_limiter, method_limiter=method_limiter)
        except APINotFoundError as error:
            raise NotFoundError(str(error)) from error

        data["matchId"] = query["id"]
        data["region"] = query["platform"].region.value
        return TimelineDto(data)

    _validate_get_many_timeline_query = Query. \
        has("ids").as_(Iterable).also. \
        has("platform").as_(Platform)

    @get_many.register(TimelineDto)
    @validate_query(_validate_get_many_timeline_query, convert_region_to_platform)
    def get_many_match_timeline(self, query: MutableMapping[str, Any], context: PipelineContext = None) -> Generator[TimelineDto, None, None]:
        def generator():
            for id in query["ids"]:
                url = "https://{platform}.api.riotgames.com/lol/match/v4/timelines/by-match/{id}".format(platform=query["platform"].value.lower(), id=id)
                try:
                    app_limiter, method_limiter = self._get_rate_limiter(query["platform"], "timelines/by-match/id")
                    data = self._get(url, {}, app_limiter=app_limiter, method_limiter=method_limiter)
                except APINotFoundError as error:
                    raise NotFoundError(str(error)) from error

                data["matchId"] = id
                data["region"] = query["platform"].region.value
                yield TimelineDto(data)

        return generator()

[((14, 4, 14, 16), 'typing.TypeVar', 'TypeVar', ({(14, 12, 14, 15): '"""T"""'}, {}), "('T')", False, 'from typing import Type, TypeVar, MutableMapping, Any, Iterable, Generator, Union\n'), ((35, 5, 35, 74), 'datapipelines.validate_query', 'validate_query', ({(35, 20, 35, 45): '_validate_get_match_query', (35, 47, 35, 73): 'convert_region_to_platform'}, {}), '(_validate_get_match_query, convert_region_to_platform)', False, 'from datapipelines import DataSource, PipelineContext, Query, NotFoundError, validate_query\n'), ((57, 5, 57, 79), 'datapipelines.validate_query', 'validate_query', ({(57, 20, 57, 50): '_validate_get_many_match_query', (57, 52, 57, 78): 'convert_region_to_platform'}, {}), '(_validate_get_many_match_query, convert_region_to_platform)', False, 'from datapipelines import DataSource, PipelineContext, Query, NotFoundError, validate_query\n'), ((93, 5, 93, 79), 'datapipelines.validate_query', 'validate_query', ({(93, 20, 93, 50): '_validate_get_match_list_query', (93, 52, 93, 78): 'convert_region_to_platform'}, {}), '(_validate_get_match_list_query, convert_region_to_platform)', False, 'from datapipelines import DataSource, PipelineContext, Query, NotFoundError, validate_query\n'), ((187, 5, 187, 84), 'datapipelines.validate_query', 'validate_query', ({(187, 20, 187, 55): '_validate_get_many_match_list_query', (187, 57, 187, 83): 'convert_region_to_platform'}, {}), '(_validate_get_many_match_list_query, convert_region_to_platform)', False, 'from datapipelines import DataSource, PipelineContext, Query, NotFoundError, validate_query\n'), ((242, 5, 242, 77), 'datapipelines.validate_query', 'validate_query', ({(242, 20, 242, 48): '_validate_get_timeline_query', (242, 50, 242, 76): 'convert_region_to_platform'}, {}), '(_validate_get_timeline_query, convert_region_to_platform)', False, 'from datapipelines import DataSource, PipelineContext, Query, NotFoundError, validate_query\n'), ((260, 5, 260, 82), 'datapipelines.validate_query', 'validate_query', ({(260, 20, 260, 53): '_validate_get_many_timeline_query', (260, 55, 260, 81): 'convert_region_to_platform'}, {}), '(_validate_get_many_timeline_query, convert_region_to_platform)', False, 'from datapipelines import DataSource, PipelineContext, Query, NotFoundError, validate_query\n'), ((98, 29, 98, 55), 'datetime.timedelta', 'datetime.timedelta', (), '', False, 'import datetime\n'), ((18, 15, 18, 21), 'time.time', 'time', ({}, {}), '()', False, 'from time import time\n'), ((101, 40, 101, 51), 'arrow.now', 'arrow.now', ({}, {}), '()', False, 'import arrow\n'), ((103, 25, 103, 53), 'arrow.get', 'arrow.get', ({(103, 35, 103, 52): 'begin_time / 1000'}, {}), '(begin_time / 1000)', False, 'import arrow\n'), ((105, 23, 105, 49), 'arrow.get', 'arrow.get', ({(105, 33, 105, 48): 'end_time / 1000'}, {}), '(end_time / 1000)', False, 'import arrow\n'), ((114, 15, 114, 40), 'math.ceil', 'math.ceil', ({(114, 25, 114, 39): 'npulls_by_date'}, {}), '(npulls_by_date)', False, 'import math\n'), ((114, 43, 114, 69), 'math.ceil', 'math.ceil', ({(114, 53, 114, 68): 'npulls_by_index'}, {}), '(npulls_by_index)', False, 'import math\n'), ((30, 32, 31, 17), 'datapipelines.Query.has', 'Query.has', ({(31, 12, 31, 16): '"""id"""'}, {}), "('id')", False, 'from datapipelines import DataSource, PipelineContext, Query, NotFoundError, validate_query\n'), ((52, 37, 53, 18), 'datapipelines.Query.has', 'Query.has', ({(53, 12, 53, 17): '"""ids"""'}, {}), "('ids')", False, 'from datapipelines import DataSource, PipelineContext, Query, NotFoundError, validate_query\n'), ((237, 35, 238, 17), 'datapipelines.Query.has', 'Query.has', ({(238, 12, 238, 16): '"""id"""'}, {}), "('id')", False, 'from datapipelines import DataSource, PipelineContext, Query, NotFoundError, validate_query\n'), ((255, 40, 256, 18), 'datapipelines.Query.has', 'Query.has', ({(256, 12, 256, 17): '"""ids"""'}, {}), "('ids')", False, 'from datapipelines import DataSource, PipelineContext, Query, NotFoundError, validate_query\n'), ((113, 31, 113, 42), 'arrow.now', 'arrow.now', ({}, {}), '()', False, 'import arrow\n'), ((81, 37, 82, 24), 'datapipelines.Query.has', 'Query.has', ({(82, 12, 82, 23): '"""accountId"""'}, {}), "('accountId')", False, 'from datapipelines import DataSource, PipelineContext, Query, NotFoundError, validate_query\n'), ((175, 42, 176, 25), 'datapipelines.Query.has', 'Query.has', ({(176, 12, 176, 24): '"""accountIds"""'}, {}), "('accountIds')", False, 'from datapipelines import DataSource, PipelineContext, Query, NotFoundError, validate_query\n')]