"use strict";
/* eslint-disable @typescript-eslint/ban-types */
/**
 * Contains types and functions to define (implement) and call actor instances locally (within
 * the same process).
 *
 * Actors are just plain classes with public async action methods, decorated with {@link action|@action}. They can
 * optionally implement {@link IActivatable} by adding an async {@link IActivatable.activate} method
 * and/or {@link IDeactivatable} by adding an async {@link IDeactivatable.deactivate} method.
 *
 * Although it is possible to instantiate and/or invoke such an actor instance directly by calling
 * its methods, the locking and automatic activation/deactivation that normally takes place is
 * then bypassed. Therefore, it is not recommended to directly create and invoke actor instances
 * (except, for example, for unit tests that do not require this additional behaviour).
 *
 * The prefered way of invoking actor instances is by means of an {@link InstanceWrapper}, either
 * directly by creating a new {@link InstanceWrapper} around a certain actor instance, or by creating
 * an {@link InstanceContainer} with an {@link InstanceCreator} that creates new instances
 * on the fly (and also deactivates instances when the configured container capacity is exceeded).
 *
 * Both {@link InstanceWrapper} and {@link InstanceContainer} respect the configured locking and
 * activation/deactivation mechanisms (by means of the {@link action|@action}, {@link actor|@action}
 * and {@link service|@service} decorators).
 *
 * @packageDocumentation
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.toActionError = exports.toFrameworkError = exports.toApplicationError = exports.VolatileTimer = exports.InstanceWrapper = exports.MultiTypeInstanceContainer = exports.InstanceContainer = exports.FRAMEWORK_ERROR_APPLICATION_ERROR = exports.APPLICATION_ERROR_UNEXPECTED_ERROR = exports.APPLICATION_ERROR_FRAMEWORK_ERROR = void 0;
const utils_1 = require("@darlean/utils");
const various_1 = require("./various");
const shared_1 = require("./shared");
const events_1 = require("events");
const base_1 = require("@darlean/base");
const ACTIVATOR = 'ACTIVATOR';
const DEACTIVATOR = 'DEACTIVATOR';
const ACTIVATE_METHOD = 'activate';
const DEACTIVATE_METHOD = 'deactivate';
exports.APPLICATION_ERROR_FRAMEWORK_ERROR = 'FRAMEWORK_ERROR';
exports.APPLICATION_ERROR_UNEXPECTED_ERROR = 'UNEXPECTED_ERROR';
exports.FRAMEWORK_ERROR_APPLICATION_ERROR = 'APPLICATION_ERROR';
/**
 * Container for instances of a certain type T. The container acts as a cache
 * for already created instances up to a certain capacity. Instances
 * are removed by means of a LRU policy.
 */
class InstanceContainer {
    constructor(actorType, creator, capacity, actorLock) {
        this.finalizing = false;
        this.actorType = (0, shared_1.normalizeActorType)(actorType);
        this.creator = creator;
        this.capacity = capacity;
        this.instances = new Map();
        this.cleaning = new Map();
        this.callCounter = 0;
        this.actorLock = actorLock;
    }
    async delete(id) {
        const idt = (0, various_1.idToText)(id);
        return await this.deleteImpl(idt);
    }
    obtain(id) {
        return this.wrapper(id).getProxy();
    }
    /**
     *
     * @param id
     * @returns
     * @throws {@link FrameworkError} when something goes wrong.
     */
    wrapper(id) {
        const idt = (0, various_1.idToText)(id);
        const current = this.instances.get(idt);
        if (current) {
            this.instances.delete(idt);
            this.instances.set(idt, current);
            return current;
        }
        // TODO: Is this correct here? Should we not return a proxy, and should the proxy
        // not throw an error when metods are invoked while finalizing??
        if (this.finalizing) {
            throw new base_1.FrameworkError(base_1.FRAMEWORK_ERROR_FINALIZING, 'Not allowed to create new instance of [ActorType] because the container is finalizing', { ActorType: this.actorType });
        }
        const instanceinfo = this.creator(id);
        const actorLock = this.actorLock;
        const instanceWrapperActorLock = actorLock
            ? (onBroken) => actorLock.acquire([this.actorType, ...id], onBroken)
            : undefined;
        const wrapper = new InstanceWrapper(this.actorType, instanceinfo.instance, instanceWrapperActorLock);
        wrapper?.on('deactivated', () => {
            this.instances.delete(idt);
            this.cleaning.delete(idt);
        });
        if (instanceinfo.afterCreate) {
            instanceinfo.afterCreate(wrapper);
        }
        this.instances.set(idt, wrapper);
        this.cleanup();
        return wrapper;
    }
    async finalize() {
        // console.log('Finalizing container', this.actorType);
        this.finalizing = true;
        // Make a copy of this.instances to avoid the deletion operations to have impact on the
        // iterator like missing items. Because finalizing = true, we should not add items anymore
        // to this.instances.
        const keys = Array.from(this.instances.keys());
        for (const instance of keys) {
            await this.deleteImpl(instance);
        }
    }
    async deleteImpl(id) {
        const instance = this.instances.get(id);
        if (instance) {
            // console.log('Deleting instance', this.actorType, id);
            try {
                await instance.deactivate();
                // console.log('Deleted instance', this.actorType, id);
            }
            catch (e) {
                console.log('Error deleting instance', this.actorType, id, e);
            }
        }
    }
    cleanup() {
        if (this.instances.size - this.cleaning.size > this.capacity) {
            for (const id of this.instances.keys()) {
                if (this.instances.size - this.cleaning.size <= this.capacity) {
                    break;
                }
                if (!this.cleaning.has(id)) {
                    this.cleaning.set(id, true);
                    setImmediate(async () => {
                        try {
                            await this.deleteImpl(id);
                        }
                        catch (e) {
                            console.log('Error during finalizing', e);
                        }
                    });
                }
            }
        }
    }
}
exports.InstanceContainer = InstanceContainer;
/**
 * Implementation of {@link IMultiTypeInstanceContainer}.
 */
class MultiTypeInstanceContainer {
    constructor() {
        this.finalizing = false;
        this.containers = new Map();
    }
    register(type, container) {
        if (this.finalizing) {
            throw new Error('Registering a new container is not allowed while finalizing');
        }
        this.containers.set((0, shared_1.normalizeActorType)(type), container);
    }
    /**
     *
     * @param type
     * @param id
     * @returns
     * @throws {@link FrameworkError} with code {@link FRAMEWORK_ERROR_UNKNOWN_ACTOR_TYPE}
     * when the actor type is unknown
     */
    obtain(type, id) {
        const container = this.containers.get(type);
        if (container) {
            return container.obtain(id);
        }
        else {
            throw new base_1.FrameworkError(base_1.FRAMEWORK_ERROR_UNKNOWN_ACTOR_TYPE, 'Actor type [ActorType] is unknown', {
                ActorType: type
            });
        }
    }
    async finalize() {
        if (this.finalizing) {
            return;
        }
        this.finalizing = true;
        const containers = Array.from(this.containers.values()).reverse();
        for (const container of containers) {
            await container.finalize();
        }
    }
}
exports.MultiTypeInstanceContainer = MultiTypeInstanceContainer;
/**
 * Wrapper around an instance of type T. The wrapper understands class and method
 * decorations, and applies the proper locking and activation/deactivation of the
 * instance.
 */
class InstanceWrapper extends events_1.EventEmitter {
    /**
     * Creates a new wrapper around the provided instance of type T.
     * @param instance The instance around which the wrapper should be created.
     * @throws {@link FrameworkError} with code {@link FRAMEWORK_ERROR_UNKNOWN_ACTION}
     * when methods on this object are invokes that do not exist in the underlying instance.
     */
    constructor(actorType, instance, actorLock) {
        super();
        this.actorType = actorType;
        this.instance = instance;
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        const self = this;
        this.state = 'created';
        this.actorLock = actorLock;
        this.lifecycleMutex = new utils_1.Mutex();
        this.methods = this.obtainMethods();
        const p = Proxy.revocable(instance, {
            get: (target, prop) => {
                const name = (0, shared_1.normalizeActionName)(prop.toString());
                const func = this.methods.get(name);
                if (func) {
                    return function (...args) {
                        return self.handleCall(func, name, args);
                    };
                }
                else {
                    throw new base_1.FrameworkError(base_1.FRAMEWORK_ERROR_UNKNOWN_ACTION, 'Action [ActionName] does not exist on [ActorType]', { ActorType: this.actorType, ActionName: name });
                }
            }
        });
        this.proxy = p.proxy;
        this.revoke = p.revoke;
        this.lock = new utils_1.SharedExclusiveLock('exclusive');
        this.callCounter = 0;
    }
    /**
     * Performs deactivation of the underlying instance (which includes invoking the {@link IDeactivatable.deactivate})
     * method when it exists and waiting for it to complete) and then invalidates the internal proxy (that could have previously been
     * obtained via {@link getProxy}), so that all future requests to the proxy raise an exception. Once deactivated, deactivation cannot be undone.
     */
    async deactivate(skipMutex = false) {
        await (0, utils_1.deeper)('io.darlean.instances.deactivate').perform(async () => {
            if (!skipMutex) {
                await (0, utils_1.deeper)('io.darlean.instances.acquire-lifecycle-mutex').perform(() => this.lifecycleMutex.acquire());
            }
            try {
                if (this.state === 'created') {
                    return;
                }
                if (this.state !== 'inactive') {
                    this.state = 'deactivating';
                    try {
                        const func = this.methods.get(DEACTIVATOR);
                        await this.handleCall(func, DEACTIVATOR, [], { locking: 'exclusive' }, undefined, true);
                    }
                    finally {
                        this.revoke();
                        this.state = 'inactive';
                        await this.releaseActorLock();
                        this.emit('deactivated');
                    }
                }
            }
            finally {
                if (!skipMutex) {
                    this.lifecycleMutex.release();
                }
            }
        });
    }
    /**
     *
     * @returns Returns a reference to the proxy that can be used to invoke methods on the underlying
     * instance until {@link deactivate} is invoked. After that, the proxy does not invoke the underlying
     * instance anymore but only throws exceptions.
     */
    getProxy() {
        return this.proxy;
    }
    getInstance() {
        return this.instance;
    }
    async invoke(method, args) {
        if (typeof method === 'string') {
            const name = (0, shared_1.normalizeActionName)(method);
            method = this.methods.get(name);
            if (!method) {
                throw new base_1.FrameworkError(base_1.FRAMEWORK_ERROR_UNKNOWN_ACTION, 'Action [ActionName] does not exist on [ActorType]', {
                    ActorType: this.actorType,
                    ActionName: name
                });
            }
        }
        return await this.handleCall(method, typeof method === 'string' ? method : method?.name ?? '', args);
    }
    async handleCall(method, actionName, args, defaultConfig, conditional, shortCircuit = false) {
        return await (0, utils_1.deeper)('io.darlean.instances.handle-call', `${this.actorType}::${method?.name}`).perform(async () => {
            const config = method?._darlean_options;
            if (!config && !defaultConfig && method !== undefined) {
                // Only accept calls to methods that are explicitly marked to be an action. This is
                // a security measure that stops unintended access to methods that are not intended
                // to be invoked as action.
                throw new Error(`Method [${method?.name}] is not an action (is it properly decorated with @action?)`);
            }
            const locking = config?.locking ?? defaultConfig?.locking ?? 'exclusive';
            const callId = this.callCounter.toString();
            this.callCounter++;
            if (!shortCircuit) {
                await (0, utils_1.deeper)('io.darlean.instances.ensure-active').perform(() => this.ensureActive());
            }
            await (0, utils_1.deeper)('io.darlean.instances.acquire-local-lock').perform(() => this.acquireLocalLock(locking, callId));
            try {
                if (method) {
                    if (!conditional || conditional()) {
                        // Invoke the actual method on the underlying instance
                        const scope = actionName === ACTIVATOR
                            ? 'actor.invoke-activator'
                            : actionName === DEACTIVATOR
                                ? 'actor.invoke-deactivator'
                                : 'actor.invoke-action';
                        return await (0, utils_1.deeper)(scope, `${this.actorType}::${actionName}`).perform(() => method.apply(this.instance, args));
                    }
                }
            }
            catch (e) {
                throw toApplicationError(e);
            }
            finally {
                this.releaseLocalLock(locking, callId);
            }
        });
    }
    async ensureActive() {
        await (0, utils_1.deeper)('io.darlean.instances.acquire-lifecycle-mutex').perform(() => this.lifecycleMutex.acquire());
        try {
            if (this.state === 'created') {
                this.state = 'activating';
                try {
                    await (0, utils_1.deeper)('io.darlean.instances.ensure-actor-lock').perform(() => this.ensureActorLock());
                    const func = this.methods.get(ACTIVATOR);
                    await this.handleCall(func, ACTIVATOR, [], { locking: 'exclusive' }, undefined, true);
                    this.state = 'active';
                }
                catch (e) {
                    await this.deactivate(true);
                    throw e;
                }
            }
            if (this.state === 'inactive') {
                // TODO Move this code to perform_call because it first has to acquire action lock, and in between,
                // state may have been changed
                throw new base_1.FrameworkError(base_1.FRAMEWORK_ERROR_INCORRECT_STATE, 'It is not allowed to execute an action when the state is [State]', {
                    State: this.state
                });
            }
        }
        finally {
            this.lifecycleMutex.release();
        }
    }
    async ensureActorLock() {
        // Assume we are already in the lifecycleLock
        const actorLock = this.actorLock;
        if (!actorLock) {
            return;
        }
        if (this.acquiredActorLock) {
            return;
        }
        if (this.acquiredActorLock) {
            return;
        }
        if (!this.actorLock) {
            throw new Error('No actor lock available, instance likely to be deactivated');
        }
        // console.log('Acquiring actor lock', this.actorType);
        this.acquiredActorLock = await actorLock(() => {
            this.acquiredActorLock = undefined;
            this.actorLock = undefined;
            setImmediate(async () => {
                try {
                    await this.deactivate();
                }
                catch (e) {
                    (0, utils_1.currentScope)().info('Error during deactivating actor of type [ActorType] because the actor lock was broken: [Error]', () => ({
                        Error: e,
                        ActorType: this.actorType
                    }));
                }
            });
        });
    }
    async releaseActorLock() {
        // Assume we are already in the lifecycle mutex
        const acquiredActorLock = this.acquiredActorLock;
        if (!acquiredActorLock) {
            return;
        }
        this.acquiredActorLock = undefined;
        this.actorLock = undefined;
        try {
            await acquiredActorLock.release();
        }
        catch (e) {
            (0, utils_1.currentScope)().info('Error during release of actor lock for an instance of type [ActorType]: [Error]', () => ({
                Error: e,
                ActorType: this.actorType
            }));
        }
    }
    async acquireLocalLock(locking, callId) {
        if (locking === 'shared') {
            await this.lock.beginShared(callId);
        }
        else if (locking === 'exclusive') {
            await this.lock.beginExclusive(callId);
        }
    }
    releaseLocalLock(locking, callId) {
        if (locking === 'shared') {
            this.lock.endShared(callId);
        }
        else if (locking === 'exclusive') {
            this.lock.endExclusive(callId);
        }
    }
    obtainMethods() {
        const prototype = Object.getPrototypeOf(this.instance);
        if (!prototype._darlean_methods) {
            const m = new Map();
            const methodNames = getMethods(prototype);
            for (const methodName of methodNames) {
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                const func = prototype[methodName];
                const config = func._darlean_options;
                if (config) {
                    if (config.kind === 'action') {
                        const name = config.name || methodName;
                        const normalized = (0, shared_1.normalizeActionName)(name);
                        m.set(normalized, func);
                    }
                    else if (config.kind === 'activator') {
                        m.set(ACTIVATOR, func);
                    }
                    else if (config.kind === 'deactivator') {
                        m.set(DEACTIVATOR, func);
                    }
                }
            }
            if (!m.has(ACTIVATOR) && methodNames.includes(ACTIVATE_METHOD)) {
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                m.set(ACTIVATOR, prototype[ACTIVATE_METHOD]);
            }
            if (!m.has(DEACTIVATOR) && methodNames.includes(DEACTIVATE_METHOD)) {
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                m.set(DEACTIVATOR, prototype[DEACTIVATE_METHOD]);
            }
            prototype._darlean_methods = m;
            return m;
        }
        return prototype._darlean_methods;
    }
}
exports.InstanceWrapper = InstanceWrapper;
class VolatileTimer {
    constructor(time, wrapper) {
        this.time = time;
        this.wrapper = wrapper;
    }
    setWrapper(wrapper) {
        this.wrapper = wrapper;
    }
    once(handler, delay, args) {
        return this.repeat(handler, 0, delay, 0, args);
    }
    repeat(handler, interval, delay, repeatCount, args) {
        const timer = this.time.repeat(async () => {
            try {
                //console.log(`Invoking timer for [${handler.name}] with args [${args}]`, !!this.wrapper);
                await this.wrapper?.invoke(handler, args ?? []);
            }
            catch (e) {
                console.log(`Error in executing volatile timer for [${handler.name}]: ${e}`);
            }
        }, handler.name, interval, delay, repeatCount);
        this.wrapper?.on('deactivated', () => {
            timer.cancel();
        });
        return {
            cancel: () => timer.cancel(),
            pause: (duration) => timer.pause(duration),
            resume: (delay) => timer.resume(delay)
        };
    }
}
exports.VolatileTimer = VolatileTimer;
function toApplicationError(e) {
    if (e instanceof base_1.ApplicationError) {
        return e;
    }
    if (e instanceof base_1.FrameworkError) {
        return new base_1.ApplicationError(exports.APPLICATION_ERROR_FRAMEWORK_ERROR, e.code, undefined, e.stack, [e], e.message);
    }
    if (typeof e === 'object') {
        const err = e;
        return new base_1.ApplicationError(err.name, undefined, undefined, err.stack, undefined, err.message);
    }
    else if (typeof e === 'string') {
        if (e.includes(' ')) {
            return new base_1.ApplicationError(exports.APPLICATION_ERROR_UNEXPECTED_ERROR, e);
        }
        else {
            return new base_1.ApplicationError(e, e);
        }
    }
    else {
        return new base_1.ApplicationError(exports.APPLICATION_ERROR_UNEXPECTED_ERROR, 'Unexpected error');
    }
}
exports.toApplicationError = toApplicationError;
function toFrameworkError(e) {
    if (e instanceof base_1.FrameworkError) {
        return e;
    }
    if (e instanceof base_1.ApplicationError) {
        return new base_1.FrameworkError(exports.FRAMEWORK_ERROR_APPLICATION_ERROR, e.code, undefined, e.stack, [e], e.message);
    }
    if (typeof e === 'object') {
        const err = e;
        return new base_1.FrameworkError(err.name, undefined, undefined, err.stack, undefined, err.message);
    }
    else if (typeof e === 'string') {
        if (e.includes(' ')) {
            return new base_1.FrameworkError(exports.APPLICATION_ERROR_UNEXPECTED_ERROR, e);
        }
        else {
            return new base_1.FrameworkError(e, e);
        }
    }
    else {
        return new base_1.FrameworkError(exports.APPLICATION_ERROR_UNEXPECTED_ERROR, 'Unexpected error');
    }
}
exports.toFrameworkError = toFrameworkError;
function toActionError(e) {
    if (e instanceof base_1.FrameworkError) {
        return e;
    }
    if (e instanceof base_1.ApplicationError) {
        return e;
    }
    if (typeof e === 'object') {
        const err = e;
        return new base_1.FrameworkError(err.name, undefined, undefined, err.stack, undefined, err.message);
    }
    else if (typeof e === 'string') {
        if (e.includes(' ')) {
            return new base_1.FrameworkError(exports.APPLICATION_ERROR_UNEXPECTED_ERROR, e);
        }
        else {
            return new base_1.FrameworkError(e, e);
        }
    }
    else {
        return new base_1.FrameworkError(exports.APPLICATION_ERROR_UNEXPECTED_ERROR, 'Unexpected error');
    }
}
exports.toActionError = toActionError;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getMethods(obj) {
    const methods = [];
    do {
        for (const prop of Object.getOwnPropertyNames(obj)) {
            if (obj[prop] instanceof Function)
                methods.push(prop);
        }
        obj = Object.getPrototypeOf(obj);
    } while (obj !== null);
    return methods;
}
