"""Epic Games Store service"""

import json
import os
from gettext import gettext as _
from typing import Any, Dict, Optional

import requests
from gi.repository import Gio

from lutris import settings
from lutris.config import LutrisConfig, write_game_config
from lutris.database.games import add_game, get_game_by_field
from lutris.database.services import ServiceGameCollection
from lutris.game import Game
from lutris.gui.widgets.utils import Image, paste_overlay, thumbnail_image
from lutris.services.base import SERVICE_LOGIN, AuthTokenExpiredError, OnlineService
from lutris.services.lutris import sync_media
from lutris.services.service_game import ServiceGame
from lutris.services.service_media import ServiceMedia
from lutris.util import system
from lutris.util.egs.egs_launcher import EGSLauncher
from lutris.util.log import logger
from lutris.util.strings import slugify

EGS_GAME_ART_PATH = os.path.expanduser("~/.cache/lutris/egs/game_box")
EGS_GAME_BOX_PATH = os.path.expanduser("~/.cache/lutris/egs/game_box_tall")
EGS_LOGO_PATH = os.path.expanduser("~/.cache/lutris/egs/game_logo")
EGS_BANNERS_PATH = os.path.expanduser("~/.cache/lutris/egs/banners")
EGS_BOX_ART_PATH = os.path.expanduser("~/.cache/lutris/egs/boxart")
BANNER_SIZE = (316, 178)
BOX_ART_SIZE = (200, 267)


class DieselGameMedia(ServiceMedia):
    service = "egs"
    remote_size = (200, 267)
    file_patterns = ["%s.jpg"]
    min_logo_x = 300
    min_logo_y = 150

    def _render_filename(self, filename):
        game_box_path = os.path.join(self.dest_path, filename)
        logo_path = os.path.join(EGS_LOGO_PATH, filename.replace(".jpg", ".png"))
        has_logo = os.path.exists(logo_path)
        thumb_image = Image.open(game_box_path)
        thumb_image = thumb_image.convert("RGBA")
        thumb_image = thumbnail_image(thumb_image, self.remote_size)
        if has_logo:
            logo_image = Image.open(logo_path)
            logo_image = logo_image.convert("RGBA")
            logo_width, logo_height = logo_image.size
            if logo_width > self.min_logo_x:
                logo_image = logo_image.resize(
                    (self.min_logo_x, int(logo_height * (self.min_logo_x / logo_width))),
                    resample=Image.Resampling.BICUBIC,
                )
            elif logo_height > self.min_logo_y:
                logo_image = logo_image.resize(
                    (int(logo_width * (self.min_logo_y / logo_height)), self.min_logo_y),
                    resample=Image.Resampling.BICUBIC,
                )
            thumb_image = paste_overlay(thumb_image, logo_image)
        thumb_path = os.path.join(self.dest_path, filename)
        thumb_image = thumb_image.convert("RGB")
        thumb_image.save(thumb_path)

    def get_media_url(self, details: Dict[str, Any]) -> Optional[str]:
        for image in details.get("keyImages", []):
            if image["type"] == self.api_field:
                return image["url"] + "?w=%s&resize=1&h=%s" % (self.remote_size[0], self.remote_size[1])
        return None


class DieselGameBoxTall(DieselGameMedia):
    """EGS tall game box"""

    size = (200, 267)
    remote_size = size
    min_logo_x = 100
    min_logo_y = 100
    dest_path = os.path.join(settings.CACHE_DIR, "egs/game_box_tall")
    api_field = "DieselGameBoxTall"

    def render(self):
        for filename in os.listdir(self.dest_path):
            self._render_filename(filename)


class DieselGameBoxSmall(DieselGameBoxTall):
    size = (100, 133)
    remote_size = (200, 267)


class DieselGameBox(DieselGameBoxTall):
    """EGS game box"""

    size = (316, 178)
    remote_size = size
    min_logo_x = 300
    min_logo_y = 150
    dest_path = os.path.join(settings.CACHE_DIR, "egs/game_box")
    api_field = "DieselGameBox"


class DieselGameBannerSmall(DieselGameBox):
    size = (158, 89)
    remote_size = (316, 178)


class DieselGameBoxLogo(DieselGameMedia):
    """EGS game box"""

    size = (200, 100)
    remote_size = size
    file_patterns = ["%s.png"]
    visible = False
    dest_path = os.path.join(settings.CACHE_DIR, "egs/game_logo")
    api_field = "DieselGameBoxLogo"


class EGSGame(ServiceGame):
    """Service game for Epic Games Store"""

    service = "egs"

    @classmethod
    def new_from_api(cls, egs_game):
        """Convert an EGS game to a service game"""
        service_game = cls()
        service_game.appid = egs_game["appName"]
        service_game.slug = slugify(egs_game["title"])
        service_game.name = egs_game["title"]
        service_game.details = json.dumps(egs_game)
        return service_game


class EpicGamesStoreService(OnlineService):
    """Service class for Epic Games Store"""

    id = "egs"
    name = _("Epic Games Store")
    login_window_width = 500
    login_window_height = 850
    icon = "egs"
    online = True
    runner = "wine"
    client_installer = "epic-games-store"
    medias = {
        "game_box_small": DieselGameBoxSmall,
        "game_banner_small": DieselGameBannerSmall,
        "game_box": DieselGameBox,
        "box_tall": DieselGameBoxTall,
    }
    extra_medias = {
        "logo": DieselGameBoxLogo,
    }
    default_format = "game_banner_small"
    requires_login_page = True
    cookies_path = os.path.join(settings.CACHE_DIR, ".egs.auth")
    token_path = os.path.join(settings.CACHE_DIR, ".egs.token")
    cache_path = os.path.join(settings.CACHE_DIR, "egs-library.json")
    login_url = (
        "https://www.epicgames.com/id/login?redirectUrl="
        "https%3A//www.epicgames.com/id/api/redirect%3F"
        "clientId%3D34a02cf8f4414e29b15921876da36f9a%26responseType%3Dcode"
    )
    redirect_uris = ["https://www.epicgames.com/id/api/redirect"]
    oauth_url = "https://account-public-service-prod03.ol.epicgames.com"
    catalog_url = "https://catalog-public-service-prod06.ol.epicgames.com"
    library_url = "https://library-service.live.use1a.on.epicgames.com"

    user_agent = (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "EpicGamesLauncher/11.0.1-14907503+++Portal+Release-Live "
        "UnrealEngine/4.23.0-14907503+++Portal+Release-Live "
        "Chrome/84.0.4147.38 Safari/537.36"
    )

    def __init__(self):
        super().__init__()
        self.session = requests.session()
        self.session.headers["User-Agent"] = self.user_agent
        self.session_data = {}
        if os.path.exists(self.token_path):
            try:
                with open(self.token_path, encoding="utf-8") as token_file:
                    self.session_data = json.loads(token_file.read())
            except Exception as ex:
                logger.exception("Unable to load token file '%s': %s", token_file, ex)

    @property
    def http_basic_auth(self):
        return requests.auth.HTTPBasicAuth("34a02cf8f4414e29b15921876da36f9a", "daafbccc737745039dffe53d94fc76cf")

    def is_connected(self):
        return self.is_authenticated()

    def login_callback(self, content):
        """Once the user logs in in a browser window, Epic redirects
        to a page containing a Session ID which we can use to finish the authentication.
        Store session ID and exchange token to auth file"""
        logger.debug("Login to EGS successful")
        logger.debug(content)
        content_json = json.loads(content.decode())
        session_id = content_json["authorizationCode"]

        self.start_session(authorization_code=session_id)
        SERVICE_LOGIN.fire(self)

    def resume_session(self):
        self.session.headers["Authorization"] = "bearer %s" % self.session_data["access_token"]
        response = self.session.get("%s/account/api/oauth/verify" % self.oauth_url)
        if response.status_code >= 500:
            response.raise_for_status()

        response_content = response.json()
        if "errorMessage" in response_content:
            raise RuntimeError(response_content)
        return response_content

    def start_session(self, exchange_code=None, authorization_code=None):
        if exchange_code:
            params = {"grant_type": "exchange_code", "exchange_code": exchange_code, "token_type": "eg1"}
        elif authorization_code:
            params = {"grant_type": "authorization_code", "code": authorization_code, "token_type": "eg1"}
        else:
            params = {
                "grant_type": "refresh_token",
                "refresh_token": self.session_data["refresh_token"],
                "token_type": "eg1",
            }

        response = self.session.post(
            "https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token",
            data=params,
            auth=self.http_basic_auth,
        )
        if response.status_code >= 500:
            response.raise_for_status()

        response_content = response.json()
        if "error" in response_content:
            raise RuntimeError(response_content)
        with open(self.token_path, "w", encoding="utf-8") as auth_file:
            auth_file.write(json.dumps(response_content, indent=2))
        self.session_data = response_content

    def get_game_details(self, asset):
        namespace = asset["namespace"]
        catalog_item_id = asset["catalogItemId"]
        response = self.session.get(
            "%s/catalog/api/shared/namespace/%s/bulk/items" % (self.catalog_url, namespace),
            params={
                "id": catalog_item_id,
                "includeDLCDetails": True,
                "includeMainGameDetails": True,
                "country": "US",
                "locale": "en",
            },
        )
        response.raise_for_status()
        # Merge the details with the initial asset to keep 'appName'
        asset.update(response.json()[catalog_item_id])
        return asset

    def get_library(self):
        self.resume_session()
        response = self.session.get(
            "%s/library/api/public/items" % self.library_url, params={"includeMetadata": "true"}
        )
        response.raise_for_status()
        resData = response.json()
        records = resData["records"]
        cursor = resData["responseMetadata"].get("nextCursor", None)
        while cursor:
            response = self.session.get(
                "%s/library/api/public/items" % self.library_url, params={"includeMetadata": "true", "cursor": cursor}
            )
            response.raise_for_status()
            resData = response.json()
            records.extend(resData["records"])
            cursor = resData["responseMetadata"].get("nextCursor", None)

        games = []
        for record in records:
            if record["namespace"] == "ue":
                continue
            game_details = self.get_game_details(record)
            games.append(game_details)
        return games

    def load(self):
        """Load the list of games"""
        try:
            library = self.get_library()
        except Exception as ex:  # pylint=disable:broad-except
            logger.warning("EGS Token expired")
            raise AuthTokenExpiredError("EGS Token expired") from ex
        egs_games = []
        for game in library:
            try:
                # Subscriptions turn up as 'games' that have no 'appName'; these
                # are not really games, so we skip them.
                if "appName" in game:
                    egs_game = EGSGame.new_from_api(game)
                else:
                    continue
            except Exception as ex:
                logger.exception("Unable to interpret EGS game: %s", ex)
                logger.info("EGS game skipped: %s", game)
                continue
            egs_game.save()
            egs_games.append(egs_game)
        return egs_games

    def install_from_egs(self, egs_game, manifest):
        """Create a new Lutris game based on an existing EGS install"""
        app_name = manifest["AppName"]
        logger.debug("Installing EGS game %s", app_name)
        service_game = ServiceGameCollection.get_game("egs", app_name)
        if not service_game:
            logger.error("Aborting install, %s is not present in the game library.", app_name)
            return
        lutris_game_id = slugify(service_game["name"]) + "-" + self.id
        existing_game = get_game_by_field(lutris_game_id, "installer_slug")
        if existing_game:
            return
        game_config = LutrisConfig(game_config_id=egs_game["configpath"]).game_level
        game_config["game"]["args"] = get_launch_arguments(app_name)
        configpath = write_game_config(lutris_game_id, game_config)
        slug = self.get_installed_slug(service_game)
        add_game(
            name=service_game["name"],
            runner=egs_game["runner"],
            slug=slug,
            directory=egs_game["directory"],
            installed=1,
            installer_slug=lutris_game_id,
            configpath=configpath,
            service=self.id,
            service_id=app_name,
        )
        return slug

    def add_installed_games(self):
        """Scan an existing EGS install for games"""
        egs_game = get_game_by_field("epic-games-store", "slug")
        if not egs_game:
            logger.error("EGS is not installed in Lutris")
            return

        egs_prefix = egs_game["directory"].split("drive_c")[0]
        logger.info("EGS detected in %s", egs_prefix)
        if not system.path_exists(os.path.join(egs_prefix, "drive_c")):
            logger.error("Invalid install of EGS at %s", egs_prefix)
            return
        egs_launcher = EGSLauncher(egs_prefix)
        installed_slugs = []
        for manifest in egs_launcher.iter_manifests():
            slug = self.install_from_egs(egs_game, manifest)
            if slug:
                installed_slugs.append(slug)
        sync_media(installed_slugs)
        logger.debug("All EGS games imported")

    def generate_installer(self, db_game, egs_db_game):
        egs_game = Game(egs_db_game["id"])
        egs_exe = egs_game.config.game_config["exe"]
        if not os.path.isabs(egs_exe):
            egs_exe = os.path.join(egs_game.config.game_config["prefix"], egs_exe)
        return {
            "name": db_game["name"],
            "version": self.name,
            "slug": slugify(db_game["name"]) + "-" + self.id,
            "game_slug": self.get_installed_slug(db_game),
            "runner": self.get_installed_runner_name(db_game),
            "appid": db_game["appid"],
            "script": {
                "requires": self.client_installer,
                "game": {
                    "args": get_launch_arguments(db_game["appid"]),
                },
                "installer": [
                    {
                        "task": {
                            "name": "wineexec",
                            "executable": egs_exe,
                            "args": get_launch_arguments(db_game["appid"], "install"),
                            "prefix": egs_game.config.game_config["prefix"],
                            "description": (
                                "The Epic Game Store will now open. Please launch "
                                "the installation of %s then close the EGS client "
                                "once the game has been downloaded." % db_game["name"]
                            ),
                        }
                    }
                ],
            },
        }

    def get_installed_runner_name(self, db_game):
        return self.runner

    def install(self, db_game):
        egs_game = get_game_by_field(self.client_installer, "slug")
        application = Gio.Application.get_default()
        if not egs_game or not egs_game["installed"]:
            logger.warning("EGS (%s) not installed", self.client_installer)
            application.show_lutris_installer_window(game_slug=self.client_installer)
        else:
            application.show_installer_window(
                [self.generate_installer(db_game, egs_game)], service=self, appid=db_game["appid"]
            )


def get_launch_arguments(app_name, action="launch"):
    return ("-opengl -SkipBuildPatchPrereq -com.epicgames.launcher://apps/%s?action=%s") % (app_name, action)
