import crypto from 'crypto';
import type { WebSocket as NodeSocket } from 'ws';
import { encodeBuffer, decodeBuffer } from './utils';
import { WebRequest, ReqFn, SubFn } from './fetch';

export interface IWebRequest extends Omit<WebRequest, 'InitWebSocket'> {}

/**
 * getFutuApi 内部实例化 WebSocket 连接，并基于 WebSocket 封装了的查询对象 WebRequest, 你可以使用 `webRequest` 查询各种接口，
 * 也可以使用 `webSocket` 来关闭连接(`webSocket.close()`)和实现事件监听(如 onclose/onopen/onmessage等)
 *
 * Example:
 *
 * ```ts
 * import { getFutuApi } from 'futu-sdk';
 * // import { Trd_Common } from 'futu-proto';
 *
 * const { webRequest, webSocket } = getFutuApi('ws://127.0.0.1:33333', '9d261112869397f0');
 * try {
 *   const { accList } = (await webRequest.GetAccList({ userID: 0, needGeneralSecAccount: true })) || {};
 *   console.log({ accList });
 * } finally {
 *   webSocket.close();
 * }
 * ```
 * @param wsUrl 连接本地 WebSocket 的地址，如 `ws://127.0.0.1:33333`
 * @param key WebSocket 密钥，每次启动 OpenD_GUI 不设置都会随机生成，CLI 暂不清楚如何获取这个密钥
 * @returns `{ webRequest: WebRequest, webSocket: WebSocket }`
 */
export const getFutuApi = (wsUrl: string, key: string) => {
  const reqCallback = new Map<number, (result: any) => void>();
  const subCallback = {} as Record<number, Array<(result: any) => void>>;
  const webSocket = typeof window === 'undefined' ? (new (require('ws'))(wsUrl) as NodeSocket) : new WebSocket(wsUrl);

  webSocket.binaryType = 'arraybuffer';

  const reqFn: ReqFn = (cmd, protobuf, callback) => {
    const { session, message } = encodeBuffer(cmd, protobuf);
    if (webSocket.readyState !== WebSocket.OPEN) {
      throw new Error('WebSocket closed');
    }
    webSocket.send(message);
    reqCallback.set(session, callback);
    return () => {
      reqCallback.delete(session);
    };
  };

  const subFn: SubFn = (cmd, callback) => {
    if (!subCallback[cmd]) {
      subCallback[cmd] = [];
    }
    subCallback[cmd].push(callback);
    return () => {
      subCallback[cmd] = subCallback[cmd].filter(cb => cb !== callback);
    };
  };

  const reqFnPromise = new Promise<ReqFn>((resolve, reject) => {
    webSocket.addEventListener('open', () => {
      new WebRequest(Promise.resolve(reqFn), subFn)
        .InitWebSocket({
          websocketKey: key ? crypto.createHash('md5').update(key).digest('hex') : '',
          programmingLanguage: 'JavaScript',
        })
        .then(() => resolve(reqFn))
        .catch(reject);
    });

    webSocket.addEventListener('message', event => {
      const { session, cmd, message } = decodeBuffer(event.data as ArrayBuffer);
      if (subCallback[cmd]) {
        for (const callback of subCallback[cmd]) {
          callback(message);
        }
      } else {
        reqCallback.get(session)?.(message);
      }
    });

    webSocket.addEventListener('close', event => {
      reject(event);
    });
  });

  return {
    webSocket,
    webRequest: new WebRequest(reqFnPromise, subFn) as IWebRequest,
  };
};
