import json
from abc import abstractmethod
from hashlib import sha256
from json import JSONDecodeError
from pathlib import Path
from time import time
from typing import Generator, Optional, TypedDict

from src import shared
from src.game import Game
from src.importer.sources.source import Source, SourceIterator
from src.utils.decorators import (
    replaced_by_env_path,
    replaced_by_path,
    replaced_by_schema_key,
)
from src.utils.save_cover import resize_cover, save_cover


class HeroicLibraryEntry(TypedDict):
    app_name: str
    installed: Optional[bool]
    runner: str
    title: str
    developer: str
    art_square: str


class HeroicSubSource(TypedDict):
    service: str
    path: tuple[str]


class HeroicSourceIterator(SourceIterator):
    source: "HeroicSource"
    generator: Generator = None
    sub_sources: dict[str, HeroicSubSource] = {
        "sideload": {
            "service": "sideload",
            "path": ("sideload_apps", "library.json"),
        },
        "legendary": {
            "service": "epic",
            "path": ("store_cache", "legendary_library.json"),
        },
        "gog": {
            "service": "gog",
            "path": ("store_cache", "gog_library.json"),
        },
    }

    def game_from_library_entry(self, entry: HeroicLibraryEntry) -> Optional[Game]:
        """Helper method used to build a Game from a Heroic library entry"""

        # Skip games that are not installed
        if not entry["installed"]:
            return None

        # Build game
        app_name = entry["app_name"]
        runner = entry["runner"]
        service = self.sub_sources[runner]["service"]
        values = {
            "version": shared.SPEC_VERSION,
            "hidden": False,
            "source": self.source.id,
            "added": int(time()),
            "name": entry["title"],
            "developer": entry["developer"],
            "game_id": self.source.game_id_format.format(
                service=service, game_id=app_name
            ),
            "executable": self.source.executable_format.format(app_name=app_name),
        }

        # Save image from the heroic cache
        # Filenames are derived from the URL that heroic used to get the file
        uri: str = entry["art_square"]
        if service == "epic":
            uri += "?h=400&resize=1&w=300"
        digest = sha256(uri.encode()).hexdigest()
        image_path = self.source.location / "images-cache" / digest
        if image_path.is_file():
            save_cover(values["game_id"], resize_cover(image_path))

        return Game(values, allow_side_effects=False)

    def sub_sources_generator(self):
        """Generator method producing games from all the Heroic sub-sources"""
        for key, sub_source in self.sub_sources.items():
            # Skip disabled sub-sources
            if not shared.schema.get_boolean("heroic-import-" + key):
                continue
            # Load games from JSON
            try:
                file = self.source.location.joinpath(*sub_source["path"])
                library = json.load(file.open())["library"]
            except (JSONDecodeError, OSError, KeyError):
                # Invalid library.json file, skip it
                continue
            for entry in library:
                try:
                    game = self.game_from_library_entry(entry)
                except KeyError:
                    # Skip invalid games
                    continue
                yield game

    def __init__(self, source: "HeroicSource") -> None:
        self.source = source
        self.generator = self.sub_sources_generator()

    def __next__(self) -> Optional[Game]:
        try:
            game = next(self.generator)
        except StopIteration:
            raise
        return game


class HeroicSource(Source):
    """Generic heroic games launcher source"""

    name = "Heroic"
    executable_format = "xdg-open heroic://launch/{app_name}"

    @property
    @abstractmethod
    def location(self) -> Path:
        pass

    @property
    def game_id_format(self) -> str:
        """The string format used to construct game IDs"""
        return self.name.lower() + "_{service}_{game_id}"

    @property
    def is_installed(self):
        # pylint: disable=pointless-statement
        try:
            self.location
        except FileNotFoundError:
            return False
        return True

    def __iter__(self):
        return HeroicSourceIterator(source=self)


class HeroicNativeSource(HeroicSource):
    variant = "native"

    @replaced_by_schema_key("heroic-location")
    @replaced_by_env_path("XDG_CONFIG_HOME", "heroic/")
    @replaced_by_path("~/.config/heroic/")
    def location(self) -> Path:
        raise FileNotFoundError()


class HeroicFlatpakSource(HeroicSource):
    variant = "flatpak"

    @replaced_by_schema_key("heroic-flatpak-location")
    @replaced_by_path("~/.var/app/com.heroicgameslauncher.hgl/config/heroic/")
    def location(self) -> Path:
        raise FileNotFoundError()


class HeroicWindowsSource(HeroicSource):
    variant = "windows"
    executable_format = "start heroic://launch/{app_name}"

    @replaced_by_schema_key("heroic-windows-location")
    @replaced_by_env_path("appdata", "heroic/")
    def location(self) -> Path:
        raise FileNotFoundError()
