from base64 import b64encode
from http import HTTPStatus

from requests import Response, Session
from rsa import PublicKey, encrypt

from .exceptions import (
    ApiException,
    CaptchaRequired,
    InvalidCredentials,
    SteampieException,
)
from .guard import generate_one_time_code
from .models import SteamUrl
from .utils import create_cookie


class LoginExecutor:
    def __init__(
        self, username: str, password: str, shared_secret: str, session: Session
    ) -> None:
        self.username = username
        self.password = password
        self.one_time_code = ""
        self.shared_secret = shared_secret
        self.session = session
        self.refresh_token = ""

    def _api_call(
        self,
        method: str,
        service: str,
        endpoint: str,
        version: str = "v1",
        params: dict | None = None,
    ) -> Response:
        url = f"{SteamUrl.API_URL}/{service}/{endpoint}/{version}"
        # All requests from the login page use the same 'Referer' and 'Origin' values
        headers = {
            "Referer": f"{SteamUrl.COMMUNITY_URL}/",
            "Origin": SteamUrl.COMMUNITY_URL,
        }

        if method.upper() == "GET":
            return self.session.get(url, params=params, headers=headers)

        if method.upper() == "POST":
            return self.session.post(url, data=params, headers=headers)

        raise ValueError("Method must be either GET or POST")

    def login(self) -> Session:
        login_response = self._send_login_request()

        if not login_response.json()["response"]:
            raise ApiException(
                "No response received from Steam API. Please try again later."
            )

        self._check_for_captcha(login_response)
        self._update_steam_guard(login_response)

        finalized_response = self._finalize_login()
        self._perform_redirects(finalized_response.json())
        self.set_sessionid_cookies()

        return self.session

    def _send_login_request(self) -> Response:
        rsa_params = self._fetch_rsa_params()
        encrypted_password = self._encrypt_password(rsa_params)
        rsa_timestamp = rsa_params["rsa_timestamp"]
        request_data = self._prepare_login_request_data(
            encrypted_password, rsa_timestamp
        )

        return self._api_call(
            "POST",
            "IAuthenticationService",
            "BeginAuthSessionViaCredentials",
            params=request_data,
        )

    def set_sessionid_cookies(self) -> None:
        community_domain = SteamUrl.COMMUNITY_URL[8:]
        store_domain = SteamUrl.STORE_URL[8:]
        community_cookie_dic = self.session.cookies.get_dict(domain=community_domain)
        store_cookie_dic = self.session.cookies.get_dict(domain=store_domain)

        for name in [
            "steamLoginSecure",
            "sessionid",
            "steamRefresh_steam",
            "steamCountry",
        ]:
            cookie = self.session.cookies.get_dict()[name]

            if name == "steamLoginSecure":
                store_cookie = create_cookie(name, store_cookie_dic[name], store_domain)
            else:
                store_cookie = create_cookie(name, cookie, store_domain)

            if name in ["sessionid", "steamLoginSecure"]:
                community_cookie = create_cookie(
                    name, community_cookie_dic[name], community_domain
                )
            else:
                community_cookie = create_cookie(name, cookie, community_domain)

            self.session.cookies.set(**community_cookie)
            self.session.cookies.set(**store_cookie)

    def _fetch_rsa_params(self, current_number_of_repetitions: int = 0) -> dict:
        self.session.post(SteamUrl.COMMUNITY_URL)
        request_data = {"account_name": self.username}
        response = self._api_call(
            "GET",
            "IAuthenticationService",
            "GetPasswordRSAPublicKey",
            params=request_data,
        )

        if response.status_code == HTTPStatus.OK and "response" in response.json():
            key_data = response.json()["response"]

            # Steam may return an empty 'response' value even if the status is 200
            if (
                "publickey_mod" in key_data
                and "publickey_exp" in key_data
                and "timestamp" in key_data
            ):
                rsa_mod = int(key_data["publickey_mod"], 16)
                rsa_exp = int(key_data["publickey_exp"], 16)
                return {
                    "rsa_key": PublicKey(rsa_mod, rsa_exp),
                    "rsa_timestamp": key_data["timestamp"],
                }

        maximal_number_of_repetitions = 5

        if current_number_of_repetitions < maximal_number_of_repetitions:
            return self._fetch_rsa_params(current_number_of_repetitions + 1)

        raise ApiException(
            f"Could not obtain rsa-key. Status code: {response.status_code}"
        )

    def _encrypt_password(self, rsa_params: dict) -> bytes:
        return b64encode(encrypt(self.password.encode("utf-8"), rsa_params["rsa_key"]))

    def _prepare_login_request_data(
        self, encrypted_password: bytes, rsa_timestamp: str
    ) -> dict:
        return {
            "persistence": "1",
            "encrypted_password": encrypted_password,
            "account_name": self.username,
            "encryption_timestamp": rsa_timestamp,
        }

    @staticmethod
    def _check_for_captcha(login_response: Response) -> None:
        if login_response.json().get("captcha_needed", False):
            raise CaptchaRequired("Captcha required")

    def _enter_steam_guard_if_necessary(self, login_response: Response) -> Response:
        if login_response.json()["requires_twofactor"]:
            self.one_time_code = generate_one_time_code(self.shared_secret)
            return self._send_login_request()

        return login_response

    @staticmethod
    def _assert_valid_credentials(login_response: Response) -> None:
        response = login_response.json()

        if response["success"]:
            return

        raise InvalidCredentials(response["message"])

    def _perform_redirects(self, response_dict: dict) -> None:
        parameters = response_dict.get("transfer_info")

        if parameters is None:
            raise SteampieException(
                "Cannot perform redirects after login, no parameters fetched"
            )

        for pass_data in parameters:
            pass_data["params"]["steamID"] = response_dict["steamID"]
            self.session.post(pass_data["url"], pass_data["params"])

    def _update_steam_guard(self, login_response: Response) -> None:
        response = login_response.json()["response"]
        client_id = response["client_id"]
        steamid = response["steamid"]
        request_id = response["request_id"]
        code_type = 3
        code = generate_one_time_code(self.shared_secret)

        update_data = {
            "client_id": client_id,
            "steamid": steamid,
            "code_type": code_type,
            "code": code,
        }
        response = self._api_call(
            "POST",
            "IAuthenticationService",
            "UpdateAuthSessionWithSteamGuardCode",
            params=update_data,
        )

        if response.status_code != HTTPStatus.OK:
            raise SteampieException("Cannot update Steam guard")

        self._pool_sessions_steam(client_id, request_id)

    def _pool_sessions_steam(self, client_id: str, request_id: str) -> None:
        pool_data = {"client_id": client_id, "request_id": request_id}
        response = self._api_call(
            "POST", "IAuthenticationService", "PollAuthSessionStatus", params=pool_data
        )
        self.refresh_token = response.json()["response"]["refresh_token"]

    def _finalize_login(self) -> Response:
        sessionid = self.session.cookies["sessionid"]
        redir = f"{SteamUrl.COMMUNITY_URL}/login/home/?goto="
        finalized_data = {
            "nonce": self.refresh_token,
            "sessionid": sessionid,
            "redir": redir,
        }

        return self.session.post(
            SteamUrl.LOGIN_URL + "/jwt/finalizelogin", data=finalized_data
        )
