"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateIWDPAppClient = exports.subscribeByIWDP = exports.cleanAllDebugTargets = exports.cleanDebugTarget = exports.subscribeCommand = void 0;
const lodash_1 = require("lodash");
const enum_1 = require("@/@types/enum");
const db_1 = require("@/db");
const client_1 = require("@/client");
const middlewares_1 = require("@/middlewares");
const pub_sub_channel_1 = require("@/utils/pub-sub-channel");
const log_1 = require("@/utils/log");
const debug_target_1 = require("@/utils/debug-target");
const aegis_1 = require("@/utils/aegis");
const log = new log_1.Logger('pub-sub-manager', enum_1.WinstonColor.BrightGreen);
// store the data of a DebugTarget, key: clientId
const channelMap = new Map();
/**
 * subscribe to the upward command, trigger occasion:
 *  1. tunnel appConnect event
 *  2. app ws connection: maybe repeat subscribe because the iOS close event is later connect event
 *  3. get IWDP pages: should filter this situation, because frontend will request every 2s
 */
const subscribeCommand = async (debugTarget, ws) => {
    const { clientId, title, platform } = debugTarget;
    if (!channelMap.has(clientId))
        addChannelItem(debugTarget);
    else {
        if (isIWDPPage(clientId))
            return;
    }
    aegis_1.aegis.reportEvent({
        name: enum_1.ReportEvent.RemoteDebug,
        ext1: title,
        ext2: enum_1.DevicePlatform[platform],
    });
    const { appClientList, downwardChannelSet, cmdIdChannelIdMap, upwardSubscriber } = channelMap.get(clientId);
    createAppClientList(debugTarget, ws);
    upwardSubscriber.pUnsubscribe();
    upwardSubscriber.disconnect();
    const newUpwardSubscriber = createUpwardSubscriber(clientId);
    channelMap.get(clientId).upwardSubscriber = newUpwardSubscriber;
    /**
     * subscribe upward message.
     * because there maybe multiple devtools client, such as multiple chrome extensions,
     * we need use batch subscribe (pSubscribe)
     */
    newUpwardSubscriber.pSubscribe((message, upwardChannelId) => {
        if (!upwardChannelId)
            return log.warn('pSubscribe without channelId');
        let msgObj;
        try {
            msgObj = JSON.parse(message);
        }
        catch (e) {
            log.error('%s channel message are invalid JSON, %s', upwardChannelId, message);
        }
        const downwardChannelId = pub_sub_channel_1.upwardChannelToDownwardChannel(upwardChannelId);
        cmdIdChannelIdMap.set(msgObj.id, downwardChannelId);
        downwardChannelSet.add(downwardChannelId);
        const { appClientList: latestAppClientList } = channelMap.get(clientId);
        latestAppClientList.forEach((appClient) => {
            appClient.sendToApp(msgObj).catch((e) => {
                if (e !== 1 /* DomainFiltered */) {
                    return log.error('%s app client send error: %j', appClient.constructor.name, e);
                }
            });
        });
    });
    // publish downward message to devtools frontend
    appClientList.forEach((appClient) => {
        appClient.removeAllListeners("message" /* Message */);
        appClient.on("message" /* Message */, (msg) => {
            const handler = getAppClientMessageHandler(debugTarget);
            if (handler)
                handler(msg);
        });
    });
};
exports.subscribeCommand = subscribeCommand;
/**
 * clean cache of one DebugTarget
 * should invoke when tunnel appDisconnect event, or WSAppClient ws close event.
 */
const cleanDebugTarget = async (clientId, closeDevtools, cleanCache = false) => {
    if (cleanCache) {
        await debug_target_1.removeDebugTarget(clientId);
        log.info('removeDebugTarget %s', clientId);
        return;
    }
    const debugTarget = await debug_target_1.decreaseRefAndSave(clientId);
    if (debugTarget)
        return;
    const channelInfo = channelMap.get(clientId);
    if (!channelInfo)
        return;
    const { publisherMap, upwardSubscriber, internalPublisher } = channelInfo;
    if (closeDevtools) {
        internalPublisher.publish(enum_1.InternalChannelEvent.AppWSClose);
    }
    Array.from(publisherMap.values()).forEach((publisher) => publisher.disconnect());
    // need some delay for the finish of `InternalChannelEvent.AppWSClose` event
    process.nextTick(() => {
        upwardSubscriber.pUnsubscribe();
        upwardSubscriber.disconnect();
        internalPublisher.disconnect();
        channelMap.delete(clientId);
    });
};
exports.cleanDebugTarget = cleanDebugTarget;
/**
 * clean all cache of DebugTarget
 */
const cleanAllDebugTargets = async () => Promise.all(Array.from(channelMap.values()).map(({ debugTarget }) => exports.cleanDebugTarget(debugTarget.clientId, true, true)));
exports.cleanAllDebugTargets = cleanAllDebugTargets;
let oldIWDPDebugTargets = [];
/**
 * subscribe upward message from IWDP, and clean the outdated IWDP page
 */
const subscribeByIWDP = (debugTargets) => {
    const outdatedDebugTargets = lodash_1.differenceBy(oldIWDPDebugTargets, debugTargets, 'clientId');
    if (outdatedDebugTargets.length)
        log.info('outdatedDebugTargets %j', outdatedDebugTargets);
    outdatedDebugTargets.forEach(({ clientId }) => {
        exports.cleanDebugTarget(clientId, true);
    });
    debugTargets.forEach((debugTarget) => {
        const oldDebugTarget = oldIWDPDebugTargets.find((item) => item.clientId === debugTarget.clientId);
        if (oldDebugTarget)
            debugTarget.ts = oldDebugTarget.ts;
        exports.subscribeCommand(debugTarget);
    });
    oldIWDPDebugTargets = debugTargets;
};
exports.subscribeByIWDP = subscribeByIWDP;
const addChannelItem = (debugTarget) => {
    const { clientId } = debugTarget;
    const { Publisher } = db_1.getDBOperator();
    const internalChannelId = pub_sub_channel_1.createInternalChannel(clientId, '');
    const upwardSubscriber = createUpwardSubscriber(clientId);
    const internalPublisher = new Publisher(internalChannelId);
    channelMap.set(clientId, {
        downwardChannelSet: new Set(),
        cmdIdChannelIdMap: new Map(),
        publisherMap: new Map(),
        upwardSubscriber,
        internalPublisher,
        debugTarget,
        appClientList: [],
    });
};
const createUpwardSubscriber = (clientId) => {
    const { Subscriber } = db_1.getDBOperator();
    const upwardChannelId = pub_sub_channel_1.createUpwardChannel(clientId, '*');
    log.verbose('subscribe to redis channel %s', upwardChannelId);
    return new Subscriber(upwardChannelId);
};
/**
 * create matched debug tunnels by DebugTarget
 */
const createAppClientList = (debugTarget, ws) => {
    const { clientId } = debugTarget;
    const { appClientList } = channelMap.get(clientId);
    const options = client_1.appClientManager.getAppClientOptions(debugTarget.platform);
    return options
        .map(({ Ctor: AppClientCtor, ...option }) => {
        var _a;
        try {
            const outdatedAppClientIndex = appClientList.findIndex((appClient) => appClient.constructor.name === AppClientCtor.name);
            if (outdatedAppClientIndex !== -1) {
                const outdatedAppClient = appClientList.splice(outdatedAppClientIndex, 1)[0];
                outdatedAppClient.destroy();
                log.info('%s is outdated, re-constructor now', outdatedAppClient.constructor.name);
            }
            const urlParsedContext = middlewares_1.debugTargetToUrlParsedContext(debugTarget);
            const newOption = {
                ...option,
                urlParsedContext,
                iWDPWsUrl: debugTarget.iWDPWsUrl,
                ws,
            };
            if (AppClientCtor.name === "WSAppClient" /* WS */ && !ws) {
                log.warn('WSAppClient constructor option need ws');
                return;
            }
            if (AppClientCtor.name === "IWDPAppClient" /* IWDP */ && !debugTarget.iWDPWsUrl) {
                log.warn('IWDPAppClient constructor option need iWDPWsUrl, if you are debug iOS without USB, please ignore this warning.');
                return;
            }
            log.verbose(`create app client ${AppClientCtor.name}`);
            const appClient = new AppClientCtor(clientId, newOption);
            appClientList.push(appClient);
            return appClient;
        }
        catch (e) {
            log.error('create app client error: %s', (_a = e) === null || _a === void 0 ? void 0 : _a.stack);
            return null;
        }
    })
        .filter((v) => v);
};
/**
 * when re-plug iOS device, IWDPAppClient ws url will change, so need update new IWDPAppClient
 */
const updateIWDPAppClient = (debugTarget) => {
    const { clientId } = debugTarget;
    if (debugTarget.platform !== enum_1.DevicePlatform.IOS || !channelMap.has(clientId))
        return;
    const { appClientList } = channelMap.get(clientId);
    const options = client_1.appClientManager.getAppClientOptions(debugTarget.platform);
    const iWDPOption = options.find(({ Ctor: AppClientCtor }) => AppClientCtor.name === "IWDPAppClient" /* IWDP */);
    if (!iWDPOption)
        return;
    const { Ctor: AppClientCtor, ...option } = iWDPOption;
    const hasIWDPAppClient = debugTarget.appClientTypeList.indexOf("IWDPAppClient" /* IWDP */) !== -1;
    const outdatedIWDPAppClientIndex = appClientList.findIndex((item) => item.constructor.name === "IWDPAppClient" /* IWDP */);
    if (hasIWDPAppClient) {
        if (outdatedIWDPAppClientIndex === -1) {
            log.info('append IWDPAppClient');
            const {} = iWDPOption;
            const iWDPAppClient = new AppClientCtor(clientId, {
                ...option,
                urlParsedContext: middlewares_1.debugTargetToUrlParsedContext(debugTarget),
                iWDPWsUrl: debugTarget.iWDPWsUrl,
            });
            appClientList.push(iWDPAppClient);
            iWDPAppClient.on("message" /* Message */, (msg) => getAppClientMessageHandler(debugTarget)(msg));
        }
        else {
            const iWDPAppClient = appClientList[outdatedIWDPAppClientIndex];
            if (iWDPAppClient.url === debugTarget.iWDPWsUrl)
                return;
            const outdatedAppClient = appClientList.splice(outdatedIWDPAppClientIndex, 1)[0];
            outdatedAppClient.destroy();
            const urlParsedContext = middlewares_1.debugTargetToUrlParsedContext(debugTarget);
            const newOption = {
                ...option,
                urlParsedContext,
                iWDPWsUrl: debugTarget.iWDPWsUrl,
            };
            const appClient = new AppClientCtor(clientId, newOption);
            appClientList.push(appClient);
            appClient.removeAllListeners("message" /* Message */);
            appClient.on("message" /* Message */, (msg) => getAppClientMessageHandler(debugTarget)(msg));
            log.info(`create app client ${AppClientCtor.name}, update iWDPWsUrl to %s`, debugTarget.iWDPWsUrl);
        }
    }
    else if (outdatedIWDPAppClientIndex !== -1) {
        const outdated = appClientList.splice(outdatedIWDPAppClientIndex, 1)[0];
        outdated.destroy();
        log.warn('IWDPAppClient is outdated and destroyed!');
    }
};
exports.updateIWDPAppClient = updateIWDPAppClient;
const getAppClientMessageHandler = (debugTarget) => async (msg) => {
    const channelInfo = channelMap.get(debugTarget.clientId);
    if (!channelInfo) {
        return log.error('channelInfo does not exist!');
    }
    const { downwardChannelSet, cmdIdChannelIdMap, publisherMap } = channelInfo;
    msg.ts = Date.now();
    const msgStr = JSON.stringify(msg);
    const { Publisher } = db_1.getDBOperator();
    if ('id' in msg) {
        // publish CommandRes to `downwardChannelId`
        const commandRes = msg;
        const downwardChannelId = cmdIdChannelIdMap.get(commandRes.id);
        if (!downwardChannelId)
            return;
        if (!publisherMap.has(downwardChannelId)) {
            const publisher = new Publisher(downwardChannelId);
            publisherMap.set(downwardChannelId, publisher);
        }
        const publisher = publisherMap.get(downwardChannelId);
        publisher.publish(msgStr);
    }
    else {
        if (downwardChannelSet.size === 0) {
            downwardChannelSet.add(pub_sub_channel_1.createDownwardChannel(debugTarget.clientId));
        }
        // broadcast to all channel, because could'n determine the receiver of EventRes
        downwardChannelSet.forEach((channelId) => {
            if (!publisherMap.has(channelId)) {
                const publisher = new Publisher(channelId);
                publisherMap.set(channelId, publisher);
            }
            const publisher = publisherMap.get(channelId);
            publisher.publish(msgStr);
        });
    }
};
const isIWDPPage = (clientId) => {
    const { appClientList } = channelMap.get(clientId);
    return (appClientList === null || appClientList === void 0 ? void 0 : appClientList.length) === 1 && appClientList[0].constructor.name === "IWDPAppClient" /* IWDP */;
};
