import { io } from "socket.io-client"

import { Channel } from "./channel";
import { BagmanConfig, ClientSocket, SubscribeOptions } from "./types";
import { SecurityContext } from "./security-context";

export type GlobalEvent = "disconnect" | "disconnecting" | "connect" | "connect_error";

const GLOBAL_EVENT: GlobalEvent[] = ["connect", "connect_error", "disconnect", "disconnecting"];

export class Bagman {
    /**
     * Configuration for this Bagman Client
     * @private
     */
    private config: BagmanConfig;
    /**
     * The socket connection.
     * @private
     */
    private socket: ClientSocket;

    /**
     * The Holder for Authentication
     * @private
     */
    private securityCtx: SecurityContext;

    /**
     * Creates a new `Bagman` instance.
     * @param {BagmanArgs} - The URL for the websocket server.
     */
    constructor(config: BagmanConfig) {
        this.config = config;
        this.securityCtx = new SecurityContext(config);

        this.socket = io(this.config.url || "http://localhost:8080", {
            transports: ["websocket"],
            autoConnect: false,
        });

        // connect to bagman server if authorized
        if (this.securityCtx.isAuthorized()) {
            this.socket.auth = {
                'apiKey': this.securityCtx.token()
            };
            this.socket.connect();
        }
    }

    async authorize() {
        await this.securityCtx.authorize();

        // connect to bagman server with token
        if (this.securityCtx.isAuthorized()) {
            // add api key headers to client
            // after authorisation
            this.socket.auth = {
                'apiKey': this.securityCtx.token()
            };
            this.socket.connect();
        }
    }

    /**
     * Subscribes to a channel.
     * @async
     * @param {string} channel - The name of the channel to subscribe to.
     * @param {object} [options] - Optional parameters for the subscription.
     * @param {boolean} [options.withPresence=false] - Whether to subscribe to the channel with presence functionality.
     * @throws {Error} Throws an error if the client is not authorized.
     * @throws {Error} Throws an error if the subscription fails.
     * @returns {Promise<Channel>} A Promise that resolves with a Channel instance representing the subscribed channel.
     */
    async subscribe(channel: string, { withPresence }: SubscribeOptions = { withPresence: false }): Promise<Channel> {
        if (!this.securityCtx.isAuthorized()) throw new Error("Unauthorized. Please authorize client and proceed again.");

        const ack = await this.socket.emitWithAck('client:subscribe', { channel, withPresence });
        if (ack.status === "error") {
            throw new Error(ack.message);
        }
        
        return new Channel(this.socket, channel);
    }

    /**
     * Listens to a specific global event.
     * @template T The type of the arguments to be passed to the callback.
     * @param {GlobalEvent} event - The global event to listen to.
     * @param {(...args: T) => Promise<void> | void} cb - The callback to be called when the event is emitted.
     * @throws {Error} When an invalid event is provided.
     */
    listen<T extends [...any[]]>(event: GlobalEvent, cb: (...args: T) => Promise<void> | void) {
        if (!GLOBAL_EVENT.includes(event)) throw new Error(`Invalid Global event: ${event}`);
        this.socket.on(event, cb);
    }

    close() {
        // defensive on not connected socket
        if (this.socket.connected) {
            this.socket.disconnect();
        }
    }
}