"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ResourcesApi = exports.skUuid = exports.CHART_TILE_REGEX = exports.RESOURCES_API_PATH = void 0;
/* eslint-disable @typescript-eslint/no-explicit-any */
const debug_1 = require("../../debug");
const debug = (0, debug_1.createDebug)('signalk-server:api:resources');
const server_api_1 = require("@signalk/server-api");
const uuid_1 = require("uuid");
const __1 = require("../");
const validate_1 = require("./validate");
const config_1 = require("../../config/config");
exports.RESOURCES_API_PATH = `/signalk/v2/api/resources`;
exports.CHART_TILE_REGEX = /\/charts\/[^?]+\/\d+\/\d+\/\d+$/;
const skUuid = () => `${(0, uuid_1.v4)()}`;
exports.skUuid = skUuid;
class ResourcesApi {
    resProvider = {};
    app;
    settings;
    constructor(app) {
        this.app = app;
        this.initResourceRoutes(app);
        this.parseSettings();
    }
    async start() {
        return new Promise(async (resolve) => {
            resolve();
        });
    }
    async parseSettings() {
        const defaultSettings = {
            defaultProviders: {
                routes: 'resources-provider',
                waypoints: 'resources-provider',
                regions: 'resources-provider',
                notes: 'resources-provider',
                charts: 'resources-provider'
            }
        };
        if (!('resourcesApi' in this.app.config.settings)) {
            debug('***** Applying Default Settings ********');
            this.app.config.settings['resourcesApi'] = defaultSettings;
        }
        else {
            const s = this.app.config.settings['resourcesApi'];
            Object.entries(defaultSettings.defaultProviders).forEach((k) => {
                if (!(k[0] in s.defaultProviders)) {
                    s.defaultProviders[k[0]] = k[1];
                }
            });
        }
        this.settings = this.app.config.settings['resourcesApi'];
        debug('** Parsed Settings ***', this.app.config.settings);
    }
    saveSettings() {
        if (this.settings) {
            (0, config_1.writeSettingsFile)(this.app, this.app.config.settings, () => debug('***SETTINGS SAVED***'));
        }
    }
    register(pluginId, provider) {
        debug(`** Registering ${provider.type} provider => ${pluginId} `);
        if (!provider) {
            throw new Error(`Error registering provider ${pluginId}!`);
        }
        if (!provider.type) {
            throw new Error(`Invalid ResourceProvider.type value!`);
        }
        if (this.isResourceProvider(provider)) {
            if (!this.resProvider[provider.type]) {
                this.resProvider[provider.type] = new Map();
            }
            this.resProvider[provider.type].set(pluginId, provider.methods);
            if (this.settings?.defaultProviders) {
                if (!(provider.type in this.settings.defaultProviders)) {
                    this.settings.defaultProviders[provider.type] = pluginId;
                    debug(`Added default provider for ${provider.type}`);
                    this.saveSettings();
                }
            }
        }
        else {
            throw new Error(`Error missing ResourceProvider.methods!`);
        }
        debug(`Type = ${provider.type}`, this.resProvider[provider.type]);
    }
    unRegister(pluginId) {
        if (!pluginId) {
            return;
        }
        debug(`** Un-registering ${pluginId} plugin as a resource provider....`);
        for (const resourceType in this.resProvider) {
            if (this.resProvider[resourceType].has(pluginId)) {
                debug(`** Un-registering ${pluginId} as ${resourceType} provider....`);
                this.resProvider[resourceType].delete(pluginId);
                // update default provider
                if (this.settings.defaultProviders[resourceType] &&
                    this.settings.defaultProviders[resourceType] === pluginId) {
                    const p = this.checkForProvider(resourceType);
                    if (p) {
                        this.settings.defaultProviders[resourceType] = p;
                        debug(`Assigned ${pluginId} as default provider for ${resourceType}.`);
                    }
                    else {
                        delete this.settings.defaultProviders[resourceType];
                        debug(`Removed ${pluginId} as default provider for ${resourceType}.`);
                    }
                }
            }
        }
        this.saveSettings();
        debug(this.resProvider);
    }
    isResourceProvider(provider) {
        return !provider.methods.listResources ||
            !provider.methods.getResource ||
            !provider.methods.setResource ||
            !provider.methods.deleteResource ||
            typeof provider.methods.listResources !== 'function' ||
            typeof provider.methods.getResource !== 'function' ||
            typeof provider.methods.setResource !== 'function' ||
            typeof provider.methods.deleteResource !== 'function'
            ? false
            : true;
    }
    async getResource(resType, resId, providerId) {
        debug(`** getResource(${resType}, ${resId})`);
        const provider = this.checkForProvider(resType, providerId);
        if (!provider) {
            return Promise.reject(new Error(`No provider for ${resType}`));
        }
        return this.getFromAll(resType, resId);
    }
    async listResources(resType, params, providerId) {
        debug.enabled &&
            debug(`** listResources(${resType}, ${JSON.stringify(params)})`);
        const provider = this.checkForProvider(resType, providerId);
        debug(`** provider = ${provider}`);
        if (!provider) {
            return Promise.reject(new Error(`No provider for ${resType}`));
        }
        return this.listFromAll(resType, params);
    }
    async setResource(resType, resId, data, providerId) {
        debug.enabled &&
            debug(`** setResource(${resType}, ${resId}, ${JSON.stringify(data)})`);
        if ((0, server_api_1.isSignalKResourceType)(resType)) {
            let isValidId;
            if (resType === 'charts') {
                isValidId = validate_1.validate.chartId(resId);
            }
            else {
                isValidId = validate_1.validate.uuid(resId);
            }
            if (!isValidId) {
                return Promise.reject(new Error(`Invalid resource id provided (${resId})`));
            }
            validate_1.validate.resource(resType, resId, 'PUT', data);
        }
        else {
            if (!resId) {
                return Promise.reject(new Error(`No resource id provided!`));
            }
        }
        const provider = await this.getProviderForWrite(resType, resId, providerId);
        if (provider) {
            this.resProvider[resType]
                ?.get(provider)
                ?.setResource(resId, data)
                .then((r) => {
                this.app.handleMessage(provider, this.buildDeltaMsg(resType, resId, data), server_api_1.SKVersion.v2);
                return r;
            })
                .catch((e) => {
                debug(e);
                return Promise.reject(new Error(`Error writing ${resType} ${resId}`));
            });
        }
        else {
            return Promise.reject(new Error(`No provider for ${resType}`));
        }
    }
    async deleteResource(resType, resId, providerId) {
        debug(`** deleteResource(${resType}, ${resId})`);
        let provider = undefined;
        if (providerId) {
            provider = this.checkForProvider(resType, providerId);
        }
        else {
            provider = await this.getProviderForResourceId(resType, resId);
        }
        if (provider) {
            this.resProvider[resType]
                ?.get(provider)
                ?.deleteResource(resId)
                .then((r) => {
                this.app.handleMessage(provider, this.buildDeltaMsg(resType, resId, null), server_api_1.SKVersion.v2);
                return r;
            })
                .catch((e) => {
                debug(e);
                return Promise.reject(new Error(`Error deleting ${resType} ${resId}`));
            });
        }
        else {
            return Promise.reject(new Error(`No provider for ${resType}`));
        }
    }
    /** Returns true if there is a registered provider for the resource type */
    hasRegisteredProvider(resType) {
        const result = this.resProvider[resType] && this.resProvider[resType].size !== 0
            ? true
            : false;
        debug(`hasRegisteredProvider(${resType}).result = ${result}`);
        return result;
    }
    /** Returns the provider id to use to write a resource entry */
    async getProviderForWrite(resType, resId, providerId) {
        debug('***** getProviderForWrite()', resType, resId, providerId);
        let pv4resid;
        if (resId) {
            pv4resid = await this.getProviderForResourceId(resType, resId);
        }
        if (resId && pv4resid) {
            if (providerId && pv4resid !== providerId) {
                debug(`Detected provider for resource does not match supplied provider!`);
            }
            debug('***** Using provider ->', pv4resid);
            return pv4resid;
        }
        if (providerId) {
            debug(`***** Checking if provider ${providerId} is valid for ${resType}.`);
            const pv4restype = this.checkForProvider(resType, providerId);
            if (pv4restype) {
                debug('***** Using provider ->', pv4restype);
                return pv4restype;
            }
            else {
                debug(`***** ProviderId supplied is INVALID for ${resType}!`);
                return undefined;
            }
        }
        // use default provider for resType
        debug(`***** No providerId supplied...getting the default provider for ${resType}.`);
        if (this.settings.defaultProviders[resType]) {
            const pv = this.checkForProvider(resType, this.settings.defaultProviders[resType]);
            debug('***** Using default provider ->', pv);
            return pv;
        }
        else {
            return undefined;
        }
    }
    /** Validates providerId for a given resourceType */
    checkForProvider(resType, providerId) {
        debug(`** checkForProvider(${resType}, ${providerId})`);
        let result = undefined;
        if (!this.resProvider[resType]) {
            debug(`${resType} not found!`);
            return result;
        }
        if (providerId) {
            result = this.resProvider[resType].has(providerId)
                ? providerId
                : undefined;
        }
        else {
            result = this.resProvider[resType].keys().next().value;
        }
        debug(`** checkForProvider().result = ${result}`);
        return result;
    }
    /** Retrieve matching resources from ALL providers */
    async listFromAll(resType, params) {
        debug.enabled && debug(`listFromAll(${resType}, ${JSON.stringify(params)})`);
        const result = {};
        if (!this.resProvider[resType]) {
            return result;
        }
        const req = [];
        this.resProvider[resType].forEach((v) => {
            try {
                req.push(v.listResources(params));
            }
            catch (err) {
                debug(err);
            }
        });
        const resp = await Promise.allSettled(req);
        resp.forEach((r) => {
            if (r.status === 'fulfilled') {
                Object.assign(result, r.value);
            }
        });
        return result;
    }
    /** Query ALL providers for supplied resource id */
    async getFromAll(resType, resId, property) {
        debug(`getFromAll(${resType}, ${resId})`);
        const result = {};
        if (!this.resProvider[resType]) {
            return result;
        }
        const req = [];
        this.resProvider[resType].forEach((id) => {
            try {
                req.push(id.getResource(resId, property));
            }
            catch (err) {
                debug(err);
            }
        });
        const resp = await Promise.allSettled(req);
        resp.forEach((r) => {
            if (r.status === 'fulfilled') {
                Object.assign(result, r.value);
            }
        });
        if (Object.keys(result).length === 0) {
            throw new Error(`Resource not found! (${resId})`);
        }
        return result;
    }
    /** Return providerId for supplied resource id */
    async getProviderForResourceId(resType, resId, fallbackToDefault) {
        debug(`getProviderForResourceId(${resType}, ${resId}, ${fallbackToDefault})`);
        let result = undefined;
        if (!this.resProvider[resType]) {
            return result;
        }
        const req = [];
        const idList = [];
        this.resProvider[resType].forEach((v, k) => {
            try {
                req.push(v.getResource(resId));
                idList.push(k);
            }
            catch (err) {
                debug(err);
            }
        });
        const resp = await Promise.allSettled(req);
        let idx = 0;
        resp.forEach((r) => {
            if (r.status === 'fulfilled') {
                result = !result ? idList[idx] : result;
            }
            idx++;
        });
        if (!result && fallbackToDefault) {
            result = this.resProvider[resType].keys().next().value;
        }
        debug(`getProviderForResourceId().result = ${result}`);
        return result;
    }
    /** Return array of provider ids for supplied resource type */
    getProvidersForResourceType(resType) {
        const result = this.resProvider[resType]
            ? Array.from(this.resProvider[resType].keys())
            : [];
        debug(`getProvidersForResourceType().result = ${result}`);
        return result;
    }
    initResourceRoutes(server) {
        const updateAllowed = (req) => {
            return server.securityStrategy.shouldAllowPut(req, 'vessels.self', null, 'resources');
        };
        // list all serviced paths under resources
        server.get(`${exports.RESOURCES_API_PATH}`, (req, res) => {
            res.json(this.getResourcePaths());
        });
        // Providers: Return list of providers
        server.get(`${exports.RESOURCES_API_PATH}/:resourceType/_providers`, async (req, res) => {
            debug(`** ${req.method} ${req.path}`);
            res.json(this.getProvidersForResourceType(req.params.resourceType));
        });
        // Providers: Return the default provider for the supplied resource type
        server.get(`${exports.RESOURCES_API_PATH}/:resourceType/_providers/_default`, async (req, res) => {
            debug(`** ${req.method} ${req.path}`);
            if (!this.settings.defaultProviders[req.params.resourceType]) {
                res.status(404).json({
                    state: 'FAILED',
                    statusCode: 404,
                    message: `Resource type not found! (${req.params.resourceType})`
                });
            }
            else {
                res.json(this.settings.defaultProviders[req.params.resourceType]);
            }
        });
        // Providers: Set the default write provider for a resource type
        server.post(`${exports.RESOURCES_API_PATH}/:resourceType/_providers/_default/:providerId`, async (req, res) => {
            debug(`** ${req.method} ${req.path}`);
            if (!updateAllowed(req)) {
                res.status(403).json(__1.Responses.unauthorised);
                return;
            }
            if (!this.hasRegisteredProvider(req.params.resourceType)) {
                res.status(400).json({
                    state: 'FAILED',
                    statusCode: 400,
                    message: `Invalid resource type (${req.params.resourceType}) supplied!`
                });
                return;
            }
            if (!this.checkForProvider(req.params.resourceType, req.params.providerId)) {
                res.status(400).json({
                    state: 'FAILED',
                    statusCode: 400,
                    message: `Resource provider not found for ${req.params.resourceType}!`
                });
                return;
            }
            this.settings.defaultProviders[req.params.resourceType] =
                req.params.providerId;
            this.saveSettings();
            res.status(201).json({
                state: 'COMPLETED',
                statusCode: 201,
                message: `${req.params.providerId}`
            });
        });
        // facilitate retrieval of a specific resource
        server.get(`${exports.RESOURCES_API_PATH}/:resourceType/:resourceId`, async (req, res, next) => {
            debug(`** GET ${exports.RESOURCES_API_PATH}/:resourceType/:resourceId`);
            if (!this.hasRegisteredProvider(req.params.resourceType)) {
                next();
                return;
            }
            try {
                if (req.query.provider) {
                    const provider = this.checkForProvider(req.params.resourceType, req.query.provider ? req.query.provider : undefined);
                    if (!provider) {
                        debug('** No provider found... calling next()...');
                        next();
                        return;
                    }
                    const retVal = await this.resProvider[req.params.resourceType]
                        ?.get(provider)
                        ?.getResource(req.params.resourceId);
                    res.json(retVal);
                }
                else {
                    const retVal = await this.getFromAll(req.params.resourceType, req.params.resourceId);
                    res.json(retVal);
                }
            }
            catch (_err) {
                res.status(404).json({
                    state: 'FAILED',
                    statusCode: 404,
                    message: `Resource not found! (${req.params.resourceId})`
                });
            }
        });
        // facilitate retrieval of a specific resource property
        server.get(`${exports.RESOURCES_API_PATH}/:resourceType/:resourceId/*`, async (req, res, next) => {
            debug(`** GET ${exports.RESOURCES_API_PATH}/:resourceType/:resourceId/*`);
            if (req.path.match(exports.CHART_TILE_REGEX)) {
                debug('*** CHART TILE request -> next()');
                next();
                return;
            }
            if (!this.hasRegisteredProvider(req.params.resourceType)) {
                next();
                return;
            }
            try {
                const property = req.params['0']
                    ? req.params['0'].split('/').join('.')
                    : undefined;
                if (req.query.provider) {
                    const provider = this.checkForProvider(req.params.resourceType, req.query.provider ? req.query.provider : undefined);
                    if (!provider) {
                        debug('** No provider found... calling next()...');
                        next();
                        return;
                    }
                    const retVal = await this.resProvider[req.params.resourceType]
                        ?.get(provider)
                        ?.getResource(req.params.resourceId, property);
                    res.json(retVal);
                }
                else {
                    const retVal = await this.getFromAll(req.params.resourceType, req.params.resourceId, property);
                    res.json(retVal);
                }
            }
            catch (_err) {
                res.status(404).json({
                    state: 'FAILED',
                    statusCode: 404,
                    message: `Resource not found! (${req.params.resourceId})`
                });
            }
        });
        // facilitate retrieval of a collection of resource entries
        server.get(`${exports.RESOURCES_API_PATH}/:resourceType`, async (req, res, next) => {
            debug(`** GET ${exports.RESOURCES_API_PATH}/:resourceType`);
            if (!this.hasRegisteredProvider(req.params.resourceType)) {
                next();
                return;
            }
            const parsedQuery = Object.entries(req.query).reduce((acc, [name, value]) => {
                try {
                    acc[name] = JSON.parse(value);
                    return acc;
                }
                catch (_error) {
                    acc[name] = value;
                    return acc;
                }
            }, {});
            if ((0, server_api_1.isSignalKResourceType)(req.params.resourceType)) {
                try {
                    validate_1.validate.query(req.params.resourceType, undefined, req.method, parsedQuery);
                }
                catch (e) {
                    res.status(400).json({
                        state: 'FAILED',
                        statusCode: 400,
                        message: e.message
                    });
                    return;
                }
            }
            try {
                if (req.query.provider) {
                    const provider = this.checkForProvider(req.params.resourceType, req.query.provider ? req.query.provider : undefined);
                    if (!provider) {
                        debug('** No provider found... calling next()...');
                        next();
                        return;
                    }
                    const retVal = await this.resProvider[req.params.resourceType]
                        ?.get(provider)
                        ?.listResources(parsedQuery);
                    res.json(retVal);
                }
                else {
                    const retVal = await this.listFromAll(req.params.resourceType, parsedQuery);
                    res.json(retVal);
                }
            }
            catch (err) {
                console.error(err);
                res.status(404).json({
                    state: 'FAILED',
                    statusCode: 404,
                    message: `Error retrieving resources!`
                });
            }
        });
        // facilitate creation of new resource entry of supplied type
        server.post(`${exports.RESOURCES_API_PATH}/:resourceType`, async (req, res, next) => {
            debug(`** POST ${req.path}`);
            if (!this.hasRegisteredProvider(req.params.resourceType)) {
                next();
                return;
            }
            const provider = await this.getProviderForWrite(req.params.resourceType, '', req.query.provider ? req.query.provider : undefined);
            if (!provider) {
                debug('** No provider found... calling next()...');
                next();
                return;
            }
            if (!updateAllowed(req)) {
                res.status(403).json(__1.Responses.unauthorised);
                return;
            }
            if ((0, server_api_1.isSignalKResourceType)(req.params.resourceType)) {
                try {
                    validate_1.validate.resource(req.params.resourceType, undefined, req.method, req.body);
                }
                catch (e) {
                    res.status(400).json({
                        state: 'FAILED',
                        statusCode: 400,
                        message: e.message
                    });
                    return;
                }
            }
            let id;
            if (req.params.resourceType === 'charts') {
                id = req.body.identifier ?? (0, exports.skUuid)();
            }
            else {
                id = (0, exports.skUuid)();
            }
            try {
                await this.resProvider[req.params.resourceType]
                    ?.get(provider)
                    ?.setResource(id, req.body);
                server.handleMessage(provider, this.buildDeltaMsg(req.params.resourceType, id, req.body), server_api_1.SKVersion.v2);
                res.status(201).json({
                    state: 'COMPLETED',
                    statusCode: 201,
                    id
                });
            }
            catch (_err) {
                res.status(400).json({
                    state: 'FAILED',
                    statusCode: 400,
                    message: `Error saving ${req.params.resourceType} resource (${id})!`
                });
            }
        });
        // facilitate creation / update of resource entry at supplied id
        server.put(`${exports.RESOURCES_API_PATH}/:resourceType/:resourceId`, async (req, res, next) => {
            debug(`** PUT ${exports.RESOURCES_API_PATH}/:resourceType/:resourceId`);
            if (!this.hasRegisteredProvider(req.params.resourceType)) {
                next();
                return;
            }
            if (!updateAllowed(req)) {
                res.status(403).json(__1.Responses.unauthorised);
                return;
            }
            if ((0, server_api_1.isSignalKResourceType)(req.params.resourceType)) {
                let isValidId;
                if (req.params.resourceType === 'charts') {
                    isValidId = validate_1.validate.chartId(req.params.resourceId);
                }
                else {
                    isValidId = validate_1.validate.uuid(req.params.resourceId);
                }
                if (!isValidId) {
                    res.status(400).json({
                        state: 'FAILED',
                        statusCode: 400,
                        message: `Invalid resource id provided (${req.params.resourceId})`
                    });
                    return;
                }
                debug(req.body);
                try {
                    validate_1.validate.resource(req.params.resourceType, req.params.resourceId, req.method, req.body);
                }
                catch (e) {
                    res.status(400).json({
                        state: 'FAILED',
                        statusCode: 400,
                        message: e.message
                    });
                    return;
                }
            }
            try {
                const provider = await this.getProviderForWrite(req.params.resourceType, req.params.resourceId, req.query.provider ? req.query.provider : undefined);
                if (!provider) {
                    debug('** No provider found... calling next()...');
                    next();
                    return;
                }
                await this.resProvider[req.params.resourceType]
                    ?.get(provider)
                    ?.setResource(req.params.resourceId, req.body);
                server.handleMessage(provider, this.buildDeltaMsg(req.params.resourceType, req.params.resourceId, req.body), server_api_1.SKVersion.v2);
                res.status(200).json({
                    state: 'COMPLETED',
                    statusCode: 200,
                    message: req.params.resourceId
                });
            }
            catch (_err) {
                res.status(404).json({
                    state: 'FAILED',
                    statusCode: 404,
                    message: `Error saving ${req.params.resourceType} resource (${req.params.resourceId})!`
                });
            }
        });
        // facilitate deletion of specific of resource entry at supplied id
        server.delete(`${exports.RESOURCES_API_PATH}/:resourceType/:resourceId`, async (req, res, next) => {
            debug(`** DELETE ${exports.RESOURCES_API_PATH}/:resourceType/:resourceId`);
            if (!this.hasRegisteredProvider(req.params.resourceType)) {
                next();
                return;
            }
            if (!updateAllowed(req)) {
                res.status(403).json(__1.Responses.unauthorised);
                return;
            }
            try {
                let provider = undefined;
                if (req.query.provider) {
                    provider = this.checkForProvider(req.params.resourceType, req.query.provider ? req.query.provider : undefined);
                }
                else {
                    provider = await this.getProviderForResourceId(req.params.resourceType, req.params.resourceId);
                }
                if (!provider) {
                    debug('** No provider found... calling next()...');
                    next();
                    return;
                }
                await this.resProvider[req.params.resourceType]
                    ?.get(provider)
                    ?.deleteResource(req.params.resourceId);
                server.handleMessage(provider, this.buildDeltaMsg(req.params.resourceType, req.params.resourceId, null), server_api_1.SKVersion.v2);
                res.status(200).json({
                    state: 'COMPLETED',
                    statusCode: 200,
                    message: req.params.resourceId
                });
            }
            catch (_err) {
                res.status(400).json({
                    state: 'FAILED',
                    statusCode: 400,
                    message: `Error deleting resource (${req.params.resourceId})!`
                });
            }
        });
    }
    getResourcePaths() {
        const resPaths = {};
        for (const i in this.resProvider) {
            if (this.resProvider.hasOwnProperty(i)) {
                resPaths[i] = {
                    description: `Path containing ${i.slice(-1) === 's' ? i.slice(0, i.length - 1) : i} resources`
                };
            }
        }
        return resPaths;
    }
    buildDeltaMsg(resType, resid, resValue) {
        return {
            updates: [
                {
                    values: [
                        {
                            path: `resources.${resType}.${resid}`,
                            value: resValue
                        }
                    ]
                }
            ]
        };
    }
}
exports.ResourcesApi = ResourcesApi;
//# sourceMappingURL=index.js.map