# Ubisoft Connect client adapted from the Galaxy integration by Rasmus Luund.
# https://github.com/FriendsOfGalaxy/galaxy-integration-uplay

import json
import time
from datetime import datetime
from gettext import gettext as _

import requests

from lutris.util.log import logger
from lutris.util.ubisoft.consts import CHROME_USERAGENT, CLUB_APPID


def parse_date(date_str):
    date_example = "2022-01-25T05:44:59.192453"
    return datetime.strptime(date_str[: len(date_example)], "%Y-%m-%dT%H:%M:%S.%f")


class UbisoftConnectClient:
    def __init__(self, service):
        self._service = service
        self._auth_lost_callback = None
        self.token = None
        self.session_id = None
        self.refresh_token = None
        self.refresh_time = None
        self.user_id = None
        self.user_name = None
        self.__refresh_in_progress = False
        self._session = requests.session()
        self._session.headers.update(
            {"Authorization": None, "Ubi-AppId": CLUB_APPID, "User-Agent": CHROME_USERAGENT, "Ubi-SessionId": None}
        )

    def close(self):
        # If closing is attempted while plugin is inside refresh workflow then give it a chance to finish it.
        if self.__refresh_in_progress:
            time.sleep(1.5)
        self._session.close()

    def request(self, method, url, *args, **kwargs):
        try:
            return self._session.request(method, url, *args, **kwargs)
        except Exception as ex:
            logger.exception(ex)

    def set_auth_lost_callback(self, callback):
        self._auth_lost_callback = callback

    def is_authenticated(self):
        return self.token is not None

    def _do_request(self, method, *args, **kwargs):
        if not kwargs or "headers" not in kwargs:
            kwargs["headers"] = self._session.headers
        if "add_to_headers" in kwargs:
            for header in kwargs["add_to_headers"]:
                kwargs["headers"][header] = kwargs["add_to_headers"][header]
            kwargs.pop("add_to_headers")

        response = self.request(method, *args, **kwargs)
        result = response.json()
        if "errorCode" in result and "message" in result:
            raise RuntimeError(result["message"])
        return result

    def _do_request_safe(self, method, *args, **kwargs):
        result = {}
        try:
            refresh_needed = False
            if self.refresh_token:
                refresh_needed = self.refresh_time is None or datetime.now() > datetime.fromtimestamp(
                    int(self.refresh_time)
                )
            if refresh_needed:
                self._refresh_auth()
                result = self._do_request(method, *args, **kwargs)
            else:
                try:
                    result = self._do_request(method, *args, **kwargs)
                except Exception:
                    # fallback for another reason than expired time or wrong calculation due to changing time zones
                    logger.debug("Fallback refresh")
                    if not self.refresh_token:
                        logger.warning(
                            "No refresh token present, possibly unchecked remember me when connecting plugin"
                        )
                    self._refresh_auth()
                    result = self._do_request(method, *args, **kwargs)
        except Exception as ex:
            logger.error("Unable to refresh authentication calling auth lost: %s", ex)
            if self._auth_lost_callback:
                self._auth_lost_callback()
            raise RuntimeError(_("Ubisoft authentication has been lost: %s") % ex) from ex
        return result

    def _do_options_request(self):
        self._do_request(
            "options",
            "https://public-ubiservices.ubi.com/v3/profiles/sessions",
            headers={
                "Origin": "https://connect.ubisoft.com",
                "Referer": "https://connect.ubisoft.com/",
                "User-Agent": CHROME_USERAGENT,
            },
        )

    def _refresh_auth(self):
        if self.__refresh_in_progress:
            while self.__refresh_in_progress:
                time.sleep(0.2)
        else:
            self.__refresh_in_progress = True
            try:
                self._refresh_ticket()
                self._service.store_credentials(self.get_credentials())
            except:
                self._refresh_remember_me()
                self._refresh_ticket()
                self._service.store_credentials(self.get_credentials())
            finally:
                self.__refresh_in_progress = False

    def _refresh_remember_me(self):
        self._do_options_request()
        j = self._do_request(
            "post",
            "https://public-ubiservices.ubi.com/v3/profiles/sessions",
            headers={
                "Accept": "*/*",
                "Accept-Encoding": "gzip, deflate, br",
                "Accept-Language": "en-US;en;q=0.5",
                "Authorization": f"rm_v1 t={self.refresh_token}",
                "Content-Type": "application/json",
                "Ubi-AppId": CLUB_APPID,
                "User-Agent": CHROME_USERAGENT,
                "Host": "public-ubiservices.ubi.com",
                "Origin": "https://connect.ubisoft.com",
                "Referer": "https://connect.ubisoft.com",
            },
            json={"rememberMe": True},
        )
        self._handle_authorization_response(j)

    def _refresh_ticket(self):
        self._do_options_request()
        j = self._do_request(
            "put",
            "https://public-ubiservices.ubi.com/v3/profiles/sessions",
            headers={
                "Accept": "*/*",
                "Accept-Encoding": "gzip, deflate, br",
                "Accept-Language": "en-US;en;q=0.5",
                "Authorization": f"Ubi_v1 t={self.token}",
                "Content-Type": "application/json",
                "Ubi-AppId": CLUB_APPID,
                "User-Agent": CHROME_USERAGENT,
                "Host": "public-ubiservices.ubi.com",
                "Origin": "https://connect.ubisoft.com",
                "Referer": "https://connect.ubisoft.com",
            },
        )
        self._handle_authorization_response(j)

    def _handle_authorization_response(self, j):
        if "expiration" in j and "serverTime" in j:
            refresh_time = datetime.now() + (parse_date(j["expiration"]) - parse_date(j["serverTime"]))
            j["refreshTime"] = round(refresh_time.timestamp())
            self.restore_credentials(j)

    def restore_credentials(self, data):
        self.token = data["ticket"]
        self.session_id = data["sessionId"]
        self.user_id = data["userId"]
        if data.get("username"):
            self.user_name = data["username"]
        self.refresh_time = data.get("refreshTime", "0")
        if data.get("rememberMeTicket"):
            self.refresh_token = data["rememberMeTicket"]

        self._session.headers.update({"Authorization": f"Ubi_v1 t={self.token}", "Ubi-SessionId": self.session_id})

    def get_credentials(self):
        creds = {
            "ticket": self.token,
            "sessionId": self.session_id,
            "rememberMeTicket": self.refresh_token,
            "userId": self.user_id,
            "refreshTime": self.refresh_time,
        }

        if self.user_name:
            creds["username"] = self.user_name

        return creds

    def authorise_with_stored_credentials(self, credentials):
        self.restore_credentials(credentials)
        if not self.user_name or not self.user_id:
            user_data = self.get_user_data()
        else:
            user_data = {"username": self.user_name, "userId": self.user_id}
        self.post_sessions()
        self._service.store_credentials(self.get_credentials())
        return user_data

    def authorise_with_local_storage(self, storage_jsons):
        user_data = {}
        tasty_storage_values = ["userId", "nameOnPlatform", "ticket", "rememberMeTicket", "sessionId"]
        for json_ in storage_jsons:
            if json_:  # we do get nulls in this list of json dictionaries sometimes
                for key in json_:
                    if key in tasty_storage_values:
                        user_data[key] = json_[key]

        user_data["userId"] = user_data.pop("userId")
        user_data["username"] = user_data.pop("nameOnPlatform")

        self.restore_credentials(user_data)
        self.post_sessions()
        self._service.store_credentials(self.get_credentials())
        return user_data

    # Deprecated 0.39
    def get_user_data(self):
        return self._do_request_safe("get", f"https://public-ubiservices.ubi.com/v3/users/{self.user_id}")

    def get_friends(self):
        return self._do_request_safe("get", "https://api-ubiservices.ubi.com/v2/profiles/me/friends")

    def get_club_titles(self):
        payload = {
            "operationName": "AllGames",
            "variables": {"owned": True},
            "query": "query AllGames {"
            "viewer {"
            "    id"
            "    ...ownedGamesList"
            "  }"
            "}"
            "fragment gameProps on Game {"
            "  id"
            "  spaceId"
            "  name"
            "  coverUrl"
            "  backgroundUrl"
            "  bannerUrl"
            "}"
            "fragment ownedGameProps on Game {"
            "  ...gameProps"
            "  viewer {"
            "    meta {"
            "      id"
            "      ownedPlatformGroups {"
            "        id"
            "        name"
            "        type"
            "      }"
            "    }"
            "  }"
            "}"
            "fragment ownedGamesList on User {"
            "  ownedGames: games(filterBy: {isOwned: true}) {"
            "    totalCount"
            "    nodes {"
            "      ...ownedGameProps"
            "    }"
            "  }"
            "}",
        }
        payload = json.dumps(payload)
        headers = {"Content-Type": "application/json"}
        return self._do_request_safe(
            "post",
            "https://public-ubiservices.ubi.com/v1/profiles/me/uplay/graphql",
            add_to_headers=headers,
            data=payload,
        )

    def get_game_stats(self, space_id):
        url = f"https://public-ubiservices.ubi.com/v1/profiles/{self.user_id}/statscard?spaceId={space_id}"
        headers = {"Ubi-RequestedPlatformType": "uplay", "Ubi-LocaleCode": "en-GB"}
        try:
            return self._do_request("get", url, add_to_headers=headers)
        except Exception as ex:  # 412: no stats available for this user
            logger.debug(ex)
            return {}

    def get_applications(self, spaces):
        space_string = ",".join(space["spaceId"] for space in spaces)
        return self._do_request_safe("get", f"https://api-ubiservices.ubi.com/v2/applications?spaceIds={space_string}")

    def get_configuration(self):
        response = self._do_request_safe("get", "https://uplaywebcenter.ubi.com/v1/configuration")
        return response.json()

    def post_sessions(self):
        headers = {"Content-Type": "application/json"}
        return self._do_request_safe(
            "post", "https://public-ubiservices.ubi.com/v2/profiles/sessions", add_to_headers=headers
        )

    def get_subscription(self):
        try:
            sub_games = self._do_request("get", "https://api-uplayplusvault.ubi.com/v1/games")
        except Exception:
            logger.info("Uplay plus Subscription not active")
            return None
        return sub_games

    def activate_game(self, activation_id):
        response = self._do_request_safe(
            "post", f"https://api-uplayplusvault.ubi.com/v1/games/activate/{activation_id}"
        )
        return "games" in response
