"use strict";
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
    if (kind === "m") throw new TypeError("Private method is not writable");
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
    return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _Client_user, _Client_application;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Client = void 0;
const v10_1 = require("discord-api-types/v10");
const async_event_emitter_1 = require("@vladfrangu/async_event_emitter");
const IPC_1 = require("./transport/IPC");
const WebSocket_1 = require("./transport/WebSocket");
const ClientUser_1 = require("./structures/ClientUser");
const RPCError_1 = require("./utils/RPCError");
const rest_1 = require("@discordjs/rest");
const node_crypto_1 = __importDefault(require("node:crypto"));
const Transport_1 = require("./structures/Transport");
class Client extends async_event_emitter_1.AsyncEventEmitter {
    get user() {
        return __classPrivateFieldGet(this, _Client_user, "f");
    }
    get application() {
        return __classPrivateFieldGet(this, _Client_application, "f");
    }
    get isConnected() {
        return this.transport.isConnected;
    }
    constructor(options) {
        super();
        /**
         * application id
         */
        Object.defineProperty(this, "clientId", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /**
         * application secret
         */
        Object.defineProperty(this, "clientSecret", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /**
         * pipe id
         */
        Object.defineProperty(this, "pipeId", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "refreshToken", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /**
         * transport instance
         */
        Object.defineProperty(this, "transport", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /**
         * current user
         */
        _Client_user.set(this, void 0);
        /**
         * current application
         */
        _Client_application.set(this, void 0);
        Object.defineProperty(this, "rest", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "refreshTimeout", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "connectionPromise", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "nonceMap", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: new Map()
        });
        this.clientId = options.clientId;
        this.clientSecret = options.clientSecret;
        this.pipeId = options.pipeId;
        this.rest = new rest_1.REST({ version: "10" }).setToken("this-is-a-dummy");
        this.transport =
            !options.transport?.type || options.transport.type === "ipc"
                ? new IPC_1.IPCTransport({
                    client: this,
                    pathList: options.transport?.pathList
                })
                : new (options.transport.type === "websocket" ? WebSocket_1.WebSocketTransport : options.transport.type)({
                    client: this
                });
        this.transport.on("message", (message) => {
            if (message.cmd === "DISPATCH" && message.evt === "READY") {
                if (message.data.user)
                    __classPrivateFieldSet(this, _Client_user, new ClientUser_1.ClientUser(this, message.data.user), "f");
                if (message.data.config && message.data.config.cdn_host)
                    this.rest.options.cdn = message.data.config.cdn_host;
                this.emit("connected");
            }
            else {
                if (message.nonce && this.nonceMap.has(message.nonce)) {
                    const nonceObj = this.nonceMap.get(message.nonce);
                    if (message.evt === "ERROR") {
                        nonceObj.error.code = message.data.code;
                        nonceObj.error.message = message.data.message;
                        nonceObj?.reject(nonceObj.error);
                    }
                    else
                        nonceObj?.resolve(message);
                    this.nonceMap.delete(message.nonce);
                }
                this.emit(message.evt, message.data);
            }
        });
    }
    /**
     * @hidden
     */
    async request(cmd, args, evt) {
        const error = new RPCError_1.RPCError(Transport_1.RPC_ERROR_CODE.UNKNOWN_ERROR);
        RPCError_1.RPCError.captureStackTrace(error, this.request);
        return new Promise((resolve, reject) => {
            const nonce = node_crypto_1.default.randomUUID();
            this.transport.send({ cmd, args, evt, nonce });
            this.nonceMap.set(nonce, { resolve, reject, error });
        });
    }
    // #endregion
    // #region Authorization handlers
    async authenticate(accessToken) {
        const { application, user } = (await this.request("AUTHENTICATE", { access_token: accessToken })).data;
        __classPrivateFieldSet(this, _Client_application, application, "f");
        __classPrivateFieldSet(this, _Client_user, new ClientUser_1.ClientUser(this, user), "f");
        this.emit("ready");
    }
    async refreshAccessToken() {
        this.emit("debug", "CLIENT | Refreshing access token!");
        const exchangeResponse = await this.rest.post(v10_1.Routes.oauth2TokenExchange(), {
            body: new URLSearchParams({
                client_id: this.clientId,
                client_secret: this.clientSecret ?? "",
                grant_type: "refresh_token",
                refresh_token: this.refreshToken ?? ""
            }),
            headers: {
                "content-type": "application/x-www-form-urlencoded"
            },
            passThroughBody: true
        });
        this.hanleAccessTokenResponse(exchangeResponse);
        this.emit("debug", "CLIENT | Access token refreshed!");
        return exchangeResponse.access_token;
    }
    hanleAccessTokenResponse(data) {
        if (!("access_token" in data) ||
            !("refresh_token" in data) ||
            !("expires_in" in data) ||
            !("token_type" in data))
            throw new TypeError(`Invalid access token response!\nData: ${JSON.stringify(data, null, 2)}`);
        this.rest.setToken(data.access_token);
        this.rest.options.authPrefix = data.token_type;
        this.refreshToken = data.refresh_token;
        this.refreshTimeout = setTimeout(() => void this.refreshAccessToken(), data.expires_in);
    }
    async authorize(options) {
        if (!this.clientSecret)
            throw new ReferenceError("Client secret is required for authorization!");
        let rpcToken;
        if (options.useRPCToken) {
            rpcToken = // Sadly discord-api-types doesn't have the oauth2/token/rpc endpoint
                (await this.rest.post("/oauth2/token/rpc", {
                    body: new URLSearchParams({
                        client_id: this.clientId,
                        client_secret: this.clientSecret
                    }),
                    headers: {
                        "content-type": "application/x-www-form-urlencoded"
                    }
                })).rpc_token;
        }
        const { code } = (await this.request("AUTHORIZE", {
            scopes: options.scopes,
            client_id: this.clientId,
            rpc_token: options.useRPCToken ? rpcToken : undefined,
            prompt: options.prompt ?? "consent"
        })).data;
        const exchangeResponse = await this.rest.post(v10_1.Routes.oauth2TokenExchange(), {
            body: new URLSearchParams({
                client_id: this.clientId,
                client_secret: this.clientSecret,
                grant_type: "authorization_code",
                code
            }),
            headers: {
                "content-type": "application/x-www-form-urlencoded"
            },
            passThroughBody: true
        });
        this.hanleAccessTokenResponse(exchangeResponse);
        return exchangeResponse.access_token;
    }
    // #endregion
    /**
     * Used to subscribe to events. `evt` of the payload should be set to the event being subscribed to. `args` of the payload should be set to the args needed for the event.
     * @param event event name now subscribed to
     * @param args args for the event
     * @returns an object to unsubscribe from the event
     */
    async subscribe(event, args) {
        await this.request("SUBSCRIBE", args, event);
        return {
            /**
             * Unsubscribes from the event
             */
            unsubscribe: () => this.request("UNSUBSCRIBE", args, event)
        };
    }
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////
    /**
     * connect to the local rpc server
     */
    async connect() {
        if (this.connectionPromise)
            return this.connectionPromise;
        const error = new RPCError_1.RPCError(Transport_1.RPC_ERROR_CODE.UNKNOWN_ERROR);
        RPCError_1.RPCError.captureStackTrace(error, this.connect);
        this.connectionPromise = new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
                this.connectionPromise = undefined;
                error.code = Transport_1.CUSTOM_RPC_ERROR_CODE.CONNECTION_TIMEOUT;
                error.message = "Connection timed out";
                reject(error);
            }, 10e3);
            if (typeof timeout === "object" && "unref" in timeout)
                timeout.unref();
            this.once("connected", () => {
                this.connectionPromise = undefined;
                this.transport.once("close", (reason) => {
                    this.nonceMap.forEach((promise) => {
                        promise.error.code =
                            typeof reason === "object" ? reason.code : Transport_1.CUSTOM_RPC_ERROR_CODE.CONNECTION_ENDED;
                        promise.error.message =
                            typeof reason === "object" ? reason.message : (reason ?? "Connection ended");
                        promise.reject(promise.error);
                    });
                    this.emit("disconnected");
                });
                clearTimeout(timeout);
                resolve();
            });
            this.transport.connect().catch(reject);
        });
        return this.connectionPromise;
    }
    /**
     * will try to authorize if a scope is specified, else it's the same as `connect()`
     * @param options options for the authorization
     */
    async login(options) {
        await this.connect();
        if (!options || !options.scopes) {
            this.emit("ready");
            return;
        }
        if (options.accessToken) {
            await this.authenticate(options.accessToken);
            return;
        }
        let accessToken = "";
        if (options.refreshToken) {
            this.refreshToken = options.refreshToken;
            accessToken = await this.refreshAccessToken();
        }
        else {
            if (!this.clientSecret)
                throw new ReferenceError("Client secret is required for authorization!");
            accessToken = await this.authorize(options);
        }
        await this.authenticate(accessToken);
    }
    /**
     * disconnects from the local rpc server
     */
    async destroy() {
        if (this.refreshTimeout) {
            clearTimeout(this.refreshTimeout);
            this.refreshTimeout = undefined;
            this.refreshToken = undefined;
        }
        await this.transport.close();
    }
    getCdn() {
        return this.rest.cdn;
    }
}
exports.Client = Client;
_Client_user = new WeakMap(), _Client_application = new WeakMap();
