"""
MTGJSON support core
"""

import json
import logging
from enum import Enum
from typing import (
    Any,
    AsyncGenerator,
    Callable,
    Dict,
    List,
    Literal,
    Optional,
    Tuple,
    Type,
    TypeVar,
    Union,
    overload,
)

import asyncstdlib
from hishel import AsyncCacheTransport
from httpx import HTTPStatusError
from injector import inject, noninjectable
from pydantic import ValidationError

from mightstone.ass import compressor, synchronize
from mightstone.core import MightstoneModel
from mightstone.services import MightstoneHttpClient, ServiceError
from mightstone.services.mtgjson.models import (
    Card,
    CardAtomic,
    CardFace,
    CardPrices,
    CardTypes,
    Deck,
    DeckList,
    Keywords,
    Meta,
    Set,
    SetList,
    TcgPlayerSKU,
    TcgPlayerSKUs,
)
from mightstone.types import MightstoneIjsonBackend

DictOfListOfKey = Tuple[str, int]
DictOfListOfModel = Tuple[DictOfListOfKey, Any]
ListOfKey = int
ListOfModel = Tuple[ListOfKey, Any]
DictOfKey = str
DictOfModel = Tuple[DictOfKey, Any]
GeneratorModel = Union[DictOfListOfModel, ListOfModel, DictOfModel]
GeneratorKey = Union[DictOfListOfKey, ListOfKey, DictOfKey]

logger = logging.getLogger("mightstone")


class MtgJsonMode(Enum):
    """
    Available data parse mode

    MTGJSON model is not consistent, this enum describe the expected data structure
    of a MTGJSON response
    """

    LIST_OF_MODEL = 0
    """
    In this mode, we expect a structure similar to
     .. code-block:: json
     {"data": [{"prop": 1}, "b": {"prop": 2}]}
    """
    DICT_OF_MODEL = 1
    """
    In this mode, we expect a structure similar to
     .. code-block:: json
     {"data": {"a": {"prop": 1}, "b": {"prop": 2}}}
    """
    DICT_OF_LIST_OF_MODEL = 2
    """
    In this mode, we expect a structure similar to
     .. code-block:: json
     {"data": {"a": [{"prop": 1}], "b": [{"prop": 2}]}}
    """


class MtgJsonCompression(str, Enum):
    """
    Available compression mode enumerator

    MTGJSON provide 5 compression formats, Mightstone support 4 of them.
    """

    NONE = ""
    """ No compression, use raw JSON """
    XZ = "xz"
    """ LZMA compression, use .xz files """
    ZIP = "zip"
    """ ZIP compression, use .zip files (not supported)"""
    GZIP = "gz"
    """ GZIP compression, use .gz files"""
    BZ2 = "bz2"
    """ BZIP2 compression, use .bz2 files"""

    def to_stream_compression(self):
        """
        Compute the compression type to a python module

        :return: the name of the python module
        """
        if self.value == "":
            return None
        elif self.value == "xz":
            return "lzma"
        elif self.value == "gz":
            return "gzip"
        elif self.value == "bz2":
            return "bzip2"
        raise ValueError(f"{self.name} compression protocol cannot be read as a stream")


_T = TypeVar("_T", bound=MightstoneModel)


class MtgJson(MightstoneHttpClient):
    """
    MTGJSON client

    Supports compression and will get gzip versions by default.
    """

    base_url = "https://mtgjson.com"

    @inject
    @noninjectable("compression", "version")
    def __init__(
        self,
        transport: Optional[AsyncCacheTransport] = None,
        ijson: Optional[MightstoneIjsonBackend] = None,
        compression: Optional[MtgJsonCompression] = MtgJsonCompression.GZIP,
        version: int = 5,
    ):
        super().__init__(transport=transport, ijson=ijson)
        self.version = int(version)
        if compression is None:
            compression = MtgJsonCompression.GZIP
        self.compression = MtgJsonCompression(compression)

    def set_compression(self, compression=MtgJsonCompression):
        self.compression = MtgJsonCompression(compression)

    async def all_printings_async(self) -> AsyncGenerator[Set, None]:
        """
        all Card (Set) cards, including all printings and variations, categorized by
        set.

        :return: An async iterator of CardSet
        """
        async for k, item in self._iterate_model(
            kind="AllPrintings", model=Set, mode=MtgJsonMode.DICT_OF_MODEL
        ):
            yield item

    all_printings = synchronize(all_printings_async)

    async def all_identifiers_async(self) -> AsyncGenerator[Card, None]:
        """
        all Card (Set) cards organized by card UUID.

        :return: An async iterator of Card object (either CardToken, or CardSet)
        """
        async for k, item in self._iterate_model(  # type: ignore # no-qa
            kind="AllIdentifiers", model=Card
        ):
            yield item

    all_identifiers = synchronize(all_identifiers_async)

    async def all_prices_async(self) -> AsyncGenerator[CardPrices, None]:
        """
        all prices of cards in various formats.

        :return: An async iterator of CardPrices
        """
        async for k, item in self._iterate_model(kind="AllPrices"):
            yield CardPrices(uuid=k, **item)  # type: ignore

    all_prices = synchronize(all_prices_async)

    async def atomic_cards_async(self) -> AsyncGenerator[CardAtomic, None]:
        """
        every Card (Atomic) card.

        :return: An async iterator of ``CardAtomic``
        """
        async for item in self._atomic(kind="AtomicCards"):
            yield item

    atomic_cards = synchronize(atomic_cards_async)

    async def card_types_async(self) -> CardTypes:
        """
        every card type of any type of card.

        :return: A ``CardTypes`` object
        """
        return await self._get_item("CardTypes", model=CardTypes)

    card_types = synchronize(card_types_async)

    async def compiled_list_async(self) -> List[str]:
        """
        all individual outputs from MTGJSON, such as AllPrintings, CardTypes, etc.

        :return: A list of string
        """
        return await self._get_item("CompiledList", model=list)

    compiled_list = synchronize(compiled_list_async)

    async def deck_list_async(self) -> AsyncGenerator[DeckList, None]:
        """
        all individual Deck data.

        :return: An async iterator of DeckList
        """
        async for i, item in self._iterate_model(
            kind="DeckList", model=DeckList, mode=MtgJsonMode.LIST_OF_MODEL
        ):
            yield item

    deck_list = synchronize(deck_list_async)

    async def deck_async(self, file_name: str) -> Deck:
        """
        Recovers a deck data

        :param file_name: the deck file_name
        :return: A ``Deck`` object
        """
        return await self._get_item(f"decks/{file_name}", model=Deck)

    deck = synchronize(deck_async)

    async def enum_values_async(self) -> dict:
        """
        All known property values for various Data Models.

        :return: a ``dict`` object
        """
        return await self._get_item("EnumValues", model=dict)

    enum_values = synchronize(enum_values_async)

    async def keywords_async(self) -> Keywords:
        """
        a list of possible all keywords used on all cards.

        :return: A ``Keywords`` object
        """
        return await self._get_item("Keywords", model=Keywords)

    keywords = synchronize(keywords_async)

    async def legacy_async(self) -> AsyncGenerator[Set, None]:
        """
        all Card (Set) cards organized by Set, restricted to sets legal in the
        Legacy format.

        :return: An async iterator of ``Set``
        """
        async for k, item in self._iterate_model(kind="Legacy", model=Set):
            yield item

    legacy = synchronize(legacy_async)

    async def legacy_atomic_async(self) -> AsyncGenerator[CardAtomic, None]:
        """
        all Card (Set) cards organized by Set, restricted to sets legal in the
        Legacy format.

        :return: An async iterator of ``CardAtomic``
        """
        async for item in self._atomic(kind="LegacyAtomic"):
            yield item

    legacy_atomic = synchronize(legacy_atomic_async)

    async def meta_async(self) -> Meta:
        """
        the metadata object with ISO 8601 dates for latest build and SemVer
        specifications of the MTGJSON release.

        :return: A Meta object
        """
        return await self._get_item("Meta", model=Meta)

    meta = synchronize(meta_async)

    async def modern_async(self) -> AsyncGenerator[Set, None]:
        """
        all Card (Set) cards organized by Set, restricted to sets legal in the
        Modern format.

        :return: An async iterator of ``Set``
        """
        async for k, item in self._iterate_model(kind="Modern", model=Set):
            yield item

    modern = synchronize(modern_async)

    async def modern_atomic_async(self) -> AsyncGenerator[CardAtomic, None]:
        """
        all Card (Atomic) cards, restricted to cards legal in the Modern format.

        :return: An async iterator of ``CardAtomic``
        """
        async for item in self._atomic(kind="ModernAtomic"):
            yield item

    modern_atomic = synchronize(modern_atomic_async)

    async def pauper_atomic_async(self) -> AsyncGenerator[CardAtomic, None]:
        """
        all Card (Atomic) cards, restricted to cards legal in the Pauper format.

        :return: An async iterator of ``CardAtomic``
        """
        async for item in self._atomic(kind="PauperAtomic"):
            yield item

    pauper_atomic = synchronize(pauper_atomic_async)

    async def pioneer_async(self) -> AsyncGenerator[Set, None]:
        """
        all Card (Set) cards organized by Set, restricted to cards legal in the
        Pioneer format.

        :return: An async iterator of ``Set``
        """
        async for k, item in self._iterate_model(kind="Pioneer", model=Set):
            yield item

    pioneer = synchronize(pioneer_async)

    async def pioneer_atomic_async(self) -> AsyncGenerator[CardAtomic, None]:
        """
        all Card (Atomic) cards, restricted to cards legal in the Pioneer format.

        :return: An async iterator of ``CardAtomic``
        """
        async for item in self._atomic(kind="PioneerAtomic"):
            yield item

    pioneer_atomic = synchronize(pioneer_atomic_async)

    async def set_list_async(self) -> AsyncGenerator[SetList, None]:
        """
        a list of meta data for all Set data.

        :return: An async iterator of ``SetList``
        """
        async for k, item in self._iterate_model(
            kind="SetList", model=SetList, mode=MtgJsonMode.LIST_OF_MODEL
        ):
            yield item

    set_list = synchronize(set_list_async)

    async def set_async(self, code: str) -> SetList:
        """
        Get a Set data

        :param code: The set identifier, such as "IKO" for "Ikoria, lair of the
                     monsters"

        :return: The set representation
        """
        return await self._get_item(code.upper(), SetList)

    set = synchronize(set_async)

    async def standard_async(self) -> AsyncGenerator[Set, None]:
        """
        all Card (Set) cards organized by Set, restricted to cards legal in the
        Standard format.

        :return: An async iterator of ``Set``
        """
        async for k, item in self._iterate_model(kind="Standard", model=Set):
            yield item

    standard = synchronize(standard_async)

    async def standard_atomic_async(self) -> AsyncGenerator[CardAtomic, None]:
        """
        all Card (Atomic) cards, restricted to cards legal in the Standard format.

        :return: An async iterator of ``CardAtomic``
        """
        async for item in self._atomic(kind="StandardAtomic"):
            yield item

    standard_atomic = synchronize(standard_atomic_async)

    async def tcg_player_skus_async(self) -> AsyncGenerator[TcgPlayerSKUs, None]:
        """
        TCGplayer SKU information based on card UUIDs.

        :return: an async iterator of ``TcgPlayerSKUs``
        """
        group: Optional[TcgPlayerSKUs] = None
        async for (k, i), item in self._iterate_model(
            kind="TcgplayerSkus",
            model=TcgPlayerSKU,
            mode=MtgJsonMode.DICT_OF_LIST_OF_MODEL,
        ):
            if not group or k != group.id:
                if group:
                    yield group
                group = TcgPlayerSKUs(uuid=k, skus=[])  # type: ignore
            group.skus.append(item)

            yield group

    tcg_player_skus = synchronize(tcg_player_skus_async)

    async def vintage_async(self) -> AsyncGenerator[Set, None]:
        """
        all Card (Set) cards organized by Set, restricted to sets legal in the
        Vintage format.

        :return: An async iterator of ``Set``
        """
        async for k, item in self._iterate_model(kind="Vintage", model=Set):
            yield item

    vintage = synchronize(vintage_async)

    async def vintage_atomic_async(self) -> AsyncGenerator[CardAtomic, None]:
        """
        all Card (Atomic) cards, restricted to sets legal in the Vintage format.

        :return: An async iterator of ``CardAtomic``
        """
        async for item in self._atomic(kind="VintageAtomic"):
            yield item

    vintage_atomic = synchronize(vintage_atomic_async)

    async def _atomic(self, kind: str) -> AsyncGenerator[CardAtomic, None]:
        card: Optional[CardAtomic] = None
        async for (k, i), item in self._iterate_model(
            kind=kind, model=CardFace, mode=MtgJsonMode.DICT_OF_LIST_OF_MODEL
        ):
            if not card or k != card.ascii_name:
                if card:
                    yield card
                card = CardAtomic(asciiName=k, faces=[])  # type: ignore
            card.faces.append(item)

        if card:
            yield card

    async def _get_item(
        self,
        kind: str,
        model: Union[Type[_T], Type[Dict], Type[List]] = dict,
        **kwargs,
    ) -> _T:
        path = f"/api/v{self.version}/{kind}.json"
        if self.compression != MtgJsonCompression.NONE:
            path += "." + self.compression.value

        try:
            async with self.client.stream("GET", path, **kwargs) as f:
                f.raise_for_status()
                async with compressor.open(
                    f.aiter_bytes(),
                    compression=self.compression.to_stream_compression(),
                ) as f2:
                    data = json.loads(await f2.read())
                    data = data.get("data")

                    if issubclass(model, MightstoneModel):
                        return model.model_validate(data)  # type: ignore

                    return model(data)  # type: ignore
        except ValidationError as e:
            raise ServiceError(
                message=f"Failed to validate {model} data, {e.errors()}",
                url=path,
                method="GET",
                status=None,
                data=e,
            )
        except HTTPStatusError as e:
            raise ServiceError(
                message="Failed to fetch data from MTG Json",
                url=e.request.url,
                method=e.request.method,
                status=e.response.status_code,
                data=None,
            )

    @overload
    def _iterate_model(
        self, kind: str
    ) -> AsyncGenerator[Tuple[DictOfKey, Dict], None]: ...

    @overload
    def _iterate_model(
        self, kind: str, model: None
    ) -> AsyncGenerator[Tuple[DictOfKey, Dict], None]: ...

    @overload
    def _iterate_model(
        self, kind: str, model: Type[_T]
    ) -> AsyncGenerator[Tuple[DictOfKey, _T], None]: ...

    @overload
    def _iterate_model(
        self, kind: str, model: None, mode: Literal[MtgJsonMode.DICT_OF_MODEL]
    ) -> AsyncGenerator[Tuple[DictOfKey, Dict], None]: ...

    @overload
    def _iterate_model(
        self, kind: str, model: Type[_T], mode: Literal[MtgJsonMode.DICT_OF_MODEL]
    ) -> AsyncGenerator[Tuple[DictOfKey, _T], None]: ...

    @overload
    def _iterate_model(
        self, kind: str, model: None, mode: Literal[MtgJsonMode.LIST_OF_MODEL]
    ) -> AsyncGenerator[Tuple[ListOfKey, Dict], None]: ...

    @overload
    def _iterate_model(
        self, kind: str, model: Type[_T], mode: Literal[MtgJsonMode.LIST_OF_MODEL]
    ) -> AsyncGenerator[Tuple[ListOfKey, _T], None]: ...

    @overload
    def _iterate_model(
        self, kind: str, model: None, mode: Literal[MtgJsonMode.DICT_OF_LIST_OF_MODEL]
    ) -> AsyncGenerator[Tuple[DictOfListOfKey, Dict], None]: ...

    @overload
    def _iterate_model(
        self,
        kind: str,
        model: Type[_T],
        mode: Literal[MtgJsonMode.DICT_OF_LIST_OF_MODEL],
    ) -> AsyncGenerator[Tuple[DictOfListOfKey, _T], None]: ...

    async def _iterate_model(
        self,
        kind: str,
        model: Optional[Type[_T]] = None,
        mode: MtgJsonMode = MtgJsonMode.DICT_OF_MODEL,
        error_threshold: int = 10,
        **kwargs,
    ) -> AsyncGenerator[Tuple[GeneratorKey, Union[_T, Dict]], None]:
        error = 0
        async for k, v in self._iterate_raw(kind, mode, **kwargs):
            try:
                if model:
                    yield k, model.model_validate(v)
                else:
                    yield k, dict(v)
            except ValidationError as e:
                error += 1
                logger.warning(
                    "Failed to validate %s data, for item %s, %s", model, k, e.errors
                )

                if error > error_threshold:
                    raise RuntimeError(
                        "Too many model validation error, something is wrong"
                    )

    # @overload
    # def _iterate_raw(
    #     self,
    #     kind: str,
    #     mode: Literal[MtgJsonMode.DICT_OF_MODEL],
    # ) -> AsyncGenerator[DictOfModel, None]: ...
    #
    # @overload
    # def _iterate_raw(
    #     self,
    #     kind: str,
    #     mode: Literal[MtgJsonMode.DICT_OF_MODEL],
    #     ijson_path: str
    # ) -> AsyncGenerator[DictOfModel, None]: ...
    #
    # @overload
    # def _iterate_raw(
    #     self,
    #     kind: str,
    #     mode: Literal[MtgJsonMode.DICT_OF_LIST_OF_MODEL],
    # ) -> AsyncGenerator[DictOfListOfModel, None]: ...
    #
    # @overload
    # def _iterate_raw(
    #     self,
    #     kind: str,
    #     mode: Literal[MtgJsonMode.DICT_OF_LIST_OF_MODEL],
    #     ijson_path: str
    # ) -> AsyncGenerator[DictOfListOfModel, None]: ...
    #
    # @overload
    # def _iterate_raw(
    #     self,
    #     kind: str,
    #     mode: Literal[MtgJsonMode.LIST_OF_MODEL],
    # ) -> AsyncGenerator[ListOfModel, None]: ...
    #
    # @overload
    # def _iterate_raw(
    #     self,
    #     kind: str,
    #     mode: Literal[MtgJsonMode.LIST_OF_MODEL],
    #     ijson_path: str
    # ) -> AsyncGenerator[ListOfModel, None]: ...

    async def _iterate_raw(
        self,
        kind: str,
        mode: MtgJsonMode,
        ijson_path: Optional[str] = None,
    ) -> AsyncGenerator[GeneratorModel, None]:
        path = f"/api/v{self.version}/{kind}.json"
        if self.compression != MtgJsonCompression.NONE:
            path += "." + self.compression.value

        compression = self.compression.to_stream_compression()

        generator: Callable[
            [Any, Any, Optional[str]], AsyncGenerator[GeneratorModel, None]
        ] = dict_of_model_generator
        if mode == MtgJsonMode.LIST_OF_MODEL:
            generator = list_of_model_generator
        elif mode == MtgJsonMode.DICT_OF_LIST_OF_MODEL:
            generator = dict_of_list_of_model_generator

        async with self.client.stream("GET", path) as f:
            try:
                f.raise_for_status()
            except HTTPStatusError as e:
                raise ServiceError(
                    message="Failed to fetch data from Mtg JSON",
                    url=e.request.url,
                    method=e.request.method,
                    status=e.response.status_code,
                    data=e.response.content,
                )

            async with compressor.open(f.aiter_bytes(), compression=compression) as bit:
                async for item in generator(self.ijson, bit, ijson_path):
                    yield item


async def dict_of_list_of_model_generator(
    ijson, bytes_iterator, ijson_path=None
) -> AsyncGenerator[DictOfListOfModel, None]:
    if not ijson_path:
        ijson_path = "data"
    async for k, line in ijson.kvitems_async(bytes_iterator, ijson_path):
        for i, value in enumerate(line, start=1):
            yield (k, i), value


async def list_of_model_generator(
    ijson, bytes_iterator, ijson_path=None
) -> AsyncGenerator[ListOfModel, None]:
    if not ijson_path:
        ijson_path = "data.item"
    async for i, v in asyncstdlib.enumerate(
        ijson.items_async(bytes_iterator, ijson_path)
    ):
        yield i, v


async def dict_of_model_generator(
    ijson, bytes_iterator, ijson_path=None
) -> AsyncGenerator[DictOfModel, None]:
    if not ijson_path:
        ijson_path = "data"
    async for k, v in ijson.kvitems_async(bytes_iterator, ijson_path):
        yield k, v
