"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TransportRemote = exports.TRANSPORT_ERROR_PARAMETER_MESSAGE = exports.TRANSPORT_ERROR_TRANSPORT_CALL_INTERRUPTED = exports.TRANSPORT_ERROR_TRANSPORT_CALL_TIMEOUT = exports.TRANSPORT_ERROR_TRANSPORT_ERROR = void 0;
const utils_1 = require("@darlean/utils");
const uuid = __importStar(require("uuid"));
const instances_1 = require("./instances");
exports.TRANSPORT_ERROR_TRANSPORT_ERROR = 'TRANSPORT_ERROR';
exports.TRANSPORT_ERROR_TRANSPORT_CALL_TIMEOUT = 'TRANSPORT_CALL_TIMEOUT';
exports.TRANSPORT_ERROR_TRANSPORT_CALL_INTERRUPTED = 'TRANSPORT_CALL_INTERRUPTED';
exports.TRANSPORT_ERROR_PARAMETER_MESSAGE = 'Message';
class TransportRemote {
    /**
     * Creates a new TransportRemote.
     * @param appId The id of the current application with which the remote makes itself known to the transport
     * @param transport The transport mechanism used to send/receive messages
     * @param container The multi-type instance container that this remote can dispatch incoming action requests to
     */
    constructor(appId, transport, container) {
        this.appId = appId;
        this.transport = transport;
        this.pendingCalls = new Map();
        this.instanceContainer = container;
    }
    async init() {
        this.session = await this.transport.connect(this.appId, (envelope, contents, failure) => this.handleMessage(envelope, contents, failure));
    }
    async finalize() {
        await this.session?.finalize();
        this.session = undefined;
        const pending = this.pendingCalls;
        this.pendingCalls = new Map();
        for (const p of pending.values()) {
            console.log('PENDING CALL', p);
        }
        // The following code cancels aLL pending calls which effectively helps to
        // prevent the application from hanging at exit, but doing so is a sign that
        // somewhere else things are not cleaned up properly.
        // So let's keep this code commented out for now.
        /*const pending = this.pendingCalls;
        this.pendingCalls = new Map();
        for (const p of pending.values()) {
            clearTimeout(p.timeout);
            p.reject(TRANSPORT_ERROR_TRANSPORT_CALL_INTERRUPTED);
        }*/
    }
    async invoke(options) {
        return (0, utils_1.deeper)('io.darlean.remotetransport.invoke', options.destination).perform(() => this.invokeImpl(options));
    }
    async invokeImpl(options) {
        const callId = uuid.v4();
        const env = {
            receiverId: options.destination,
            child: {
                remoteCallKind: 'call',
                remoteCallId: callId
            },
            returnEnvelopes: [this.makeReturnEnvelope(callId)]
        };
        return new Promise((resolve, reject) => {
            const call = {
                resolve,
                reject,
                timeout: setTimeout(() => {
                    this.pendingCalls.delete(callId);
                    resolve({
                        errorCode: exports.TRANSPORT_ERROR_TRANSPORT_CALL_TIMEOUT
                    });
                }, 60 * 1000)
            };
            this.pendingCalls.set(callId, call);
            if (options.aborter) {
                options.aborter.handle(() => {
                    this.pendingCalls.delete(callId);
                    clearTimeout(call.timeout);
                    resolve({
                        errorCode: exports.TRANSPORT_ERROR_TRANSPORT_CALL_INTERRUPTED
                    });
                });
            }
            if (this.session) {
                this.session.send(env, this.toTransportRequest(options.content));
            }
            else {
                clearTimeout(call.timeout);
                resolve({
                    errorCode: exports.TRANSPORT_ERROR_TRANSPORT_ERROR,
                    errorParameters: {
                        [exports.TRANSPORT_ERROR_PARAMETER_MESSAGE]: 'Transport not ready'
                    }
                });
            }
        });
    }
    makeReturnEnvelope(callId) {
        return {
            receiverId: this.appId,
            child: {
                remoteCallId: callId,
                remoteCallKind: 'return'
            }
        };
    }
    toTransportRequest(content) {
        return content;
    }
    fromTransportRequest(content) {
        return content;
    }
    toTransportResponse(content) {
        return {
            result: content.result,
            error: content.error ? this.errorToTransportError(content.error) : undefined
        };
    }
    errorToTransportError(error) {
        // Explicitly copy over all fields. The default BSON implementation skips some fields.
        return {
            code: error.code,
            kind: error.kind,
            message: error.message,
            nested: error.nested?.map((x) => this.errorToTransportError(x)) ?? undefined,
            parameters: error.parameters,
            stack: error.stack,
            template: error.template
        };
    }
    fromTransportResponse(content) {
        return content;
    }
    handleMessage(envelope, contents, failure) {
        const env = envelope.child;
        if (env) {
            if (env.remoteCallKind === 'return') {
                // Handling of a return message that contains the response (or error result) of a previous outgoing call
                const call = this.pendingCalls.get(env.remoteCallId);
                if (call) {
                    clearTimeout(call.timeout);
                    this.pendingCalls.delete(env.remoteCallId);
                    if (failure) {
                        call.resolve({
                            errorCode: exports.TRANSPORT_ERROR_TRANSPORT_ERROR,
                            errorParameters: {
                                [exports.TRANSPORT_ERROR_PARAMETER_MESSAGE]: failure.message
                            }
                        });
                    }
                    else {
                        const result = this.fromTransportResponse(contents);
                        call.resolve({
                            content: result
                        });
                    }
                }
            }
            else {
                // Handle a new message that is to be sent to a local actor
                setImmediate(async () => {
                    const request = this.fromTransportRequest(contents);
                    await (0, utils_1.deeper)('io.darlean.remotetransport.incoming-action', `${request.actorType}::${request.actorId}::${request.actionName}`).perform(async () => {
                        try {
                            // eslint-disable-next-line @typescript-eslint/no-explicit-any
                            const proxy = this.instanceContainer.obtain(request.actorType, request.actorId);
                            try {
                                const result = await proxy[request.actionName](...request.arguments);
                                const response = {
                                    result
                                };
                                for (const returnEnvelope of envelope.returnEnvelopes ?? []) {
                                    this.session?.send(returnEnvelope, this.toTransportResponse(response));
                                }
                            }
                            catch (e) {
                                const err = (0, instances_1.toActionError)(e);
                                const msg = `in ${request.actorType}::${request.actionName} (application: ${this.appId})`;
                                if (err.stack) {
                                    const lines = err.stack.split('\n');
                                    const lines2 = [lines[0], '    ' + msg, ...lines.slice(1)];
                                    err.stack = lines2.join('\n');
                                }
                                else {
                                    err.stack = msg;
                                }
                                // The proxy already catches application errors and properly encapsulates those
                                // within an ApplicationError. Also, when framework errors occur, they are
                                // delivered as FrameworkError. So, we just have to make sure here that anything
                                // unexpected that passed through is nicely converted.
                                const response = {
                                    error: err
                                };
                                for (const returnEnvelope of envelope.returnEnvelopes ?? []) {
                                    this.session?.send(returnEnvelope, this.toTransportResponse(response));
                                }
                            }
                        }
                        catch (e) {
                            const response = {
                                error: (0, instances_1.toFrameworkError)(e)
                            };
                            for (const returnEnvelope of envelope.returnEnvelopes ?? []) {
                                this.session?.send(returnEnvelope, this.toTransportResponse(response));
                            }
                        }
                    });
                });
            }
        }
    }
}
exports.TransportRemote = TransportRemote;
