"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 () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
exports.deletePath = deletePath;
exports.putPath = putPath;
exports.registerActionHandler = registerActionHandler;
exports.deRegisterActionHandler = deRegisterActionHandler;
const lodash_1 = require("lodash");
const debug_1 = require("./debug");
const requestResponse_1 = require("./requestResponse");
const skConfig = __importStar(require("./config/config"));
const signalk_schema_1 = require("@signalk/signalk-schema");
const unitpreferences_1 = require("./unitpreferences");
const debug = (0, debug_1.createDebug)('signalk-server:put');
const pathPrefix = '/signalk';
const versionPrefix = '/v1';
const apiPathPrefix = pathPrefix + versionPrefix + '/api/';
const actionHandlers = {};
let putMetaHandler;
let deleteMetaHandler;
let putNotificationHandler;
function start(app) {
    app.registerActionHandler = registerActionHandler;
    app.deRegisterActionHandler = deRegisterActionHandler;
    app.delete(apiPathPrefix + '*', function (req, res) {
        let path = String(req.path).replace(apiPathPrefix, '');
        path = path.replace(/\/$/, '').replace(/\//g, '.');
        const parts = path.length > 0 ? path.split('.') : [];
        if (parts.length < 3) {
            res.status(400).send('invalid path');
            return;
        }
        const context = `${parts[0]}.${parts[1]}`;
        const skpath = parts.slice(2).join('.');
        deletePath(app, context, skpath, req)
            .then((reply) => {
            res.status(reply.statusCode);
            res.json(reply);
        })
            .catch((err) => {
            console.error(err);
            res.status(500).send(err.message);
        });
    });
    app.put(apiPathPrefix + '*', function (req, res) {
        let path = String(req.path).replace(apiPathPrefix, '');
        const value = req.body;
        if (value.value === undefined) {
            res.status(400).send('input is missing a value');
            return;
        }
        path = path.replace(/\/$/, '').replace(/\//g, '.');
        const parts = path.length > 0 ? path.split('.') : [];
        if (parts.length < 3) {
            res.status(400).send('invalid path');
            return;
        }
        const context = `${parts[0]}.${parts[1]}`;
        const skpath = parts.slice(2).join('.');
        putPath(app, context, skpath, value, req)
            .then((reply) => {
            res.status(reply.statusCode);
            res.json(reply);
        })
            .catch((err) => {
            console.error(err);
            res.status(500).send(err.message);
        });
    });
    putMetaHandler = (context, path, value, cb) => {
        const parts = path.split('.');
        let metaPath = path;
        let metaValue = value;
        if (parts[parts.length - 1] !== 'meta') {
            const name = parts[parts.length - 1];
            metaPath = parts.slice(0, parts.length - 2).join('.');
            metaValue = {
                ...app.config.baseDeltaEditor.getMeta(context, metaPath),
                [name]: value
            };
        }
        else {
            metaPath = parts.slice(0, parts.length - 1).join('.');
        }
        // Validate displayUnits.category if present
        const displayUnits = metaValue.displayUnits;
        if (displayUnits?.category) {
            const schemaMeta = (0, signalk_schema_1.getMetadata)('vessels.self.' + metaPath);
            // Allow override: use PUT's units if provided, otherwise use schema's units
            const pathSiUnit = metaValue.units ||
                schemaMeta?.units;
            const validationError = (0, unitpreferences_1.validateCategoryAssignment)(pathSiUnit, displayUnits.category);
            if (validationError) {
                return {
                    state: 'COMPLETED',
                    statusCode: 400,
                    message: validationError
                };
            }
        }
        // set empty zones array explicitly to null
        for (const prop in metaValue) {
            if (Array.isArray(metaValue[prop]) &&
                metaValue[prop].length === 0) {
                metaValue[prop] = null;
            }
        }
        const previousMeta = app.config.baseDeltaEditor.getMeta(context, metaPath);
        app.config.baseDeltaEditor.setMeta(context, metaPath, metaValue);
        // Remove fields that were deleted from the in-memory metadata registry
        // so they don't get re-injected via the spread below
        const full_meta = (0, signalk_schema_1.getMetadata)('vessels.self.' + metaPath);
        if (previousMeta && full_meta) {
            for (const key of Object.keys(previousMeta)) {
                if (!(key in metaValue)) {
                    delete full_meta[key];
                }
            }
        }
        app.handleMessage('defaults', {
            context: 'vessels.self',
            updates: [
                {
                    meta: [
                        {
                            path: metaPath,
                            value: { ...full_meta, ...metaValue }
                        }
                    ]
                }
            ]
        });
        if (app.config.hasOldDefaults) {
            let data;
            try {
                data = skConfig.readDefaultsFile(app);
            }
            catch (e) {
                if (e.code === 'ENOENT') {
                    data = {};
                }
                else {
                    console.error(e);
                    cb({ state: 'FAILURE', message: 'Unable to read defaults file' });
                    return;
                }
            }
            const pathWithContext = context + '.' + path;
            (0, lodash_1.set)(data, pathWithContext, value);
            skConfig.writeDefaultsFile(app, data, (err) => {
                if (err) {
                    cb({ state: 'FAILURE', message: 'Unable to save to defaults file' });
                }
                else {
                    cb({ state: 'SUCCESS' });
                }
            });
        }
        else {
            skConfig
                .writeBaseDeltasFile(app)
                .then(() => {
                cb({ state: 'SUCCESS' });
            })
                .catch(() => {
                cb({ state: 'FAILURE', message: 'Unable to save to defaults file' });
            });
        }
        return { state: 'PENDING' };
    };
    deleteMetaHandler = (context, path, cb) => {
        const parts = path.split('.');
        let metaPath = path;
        let full_meta;
        if (parts[parts.length - 1] !== 'meta') {
            const name = parts[parts.length - 1];
            metaPath = parts.slice(0, parts.length - 2).join('.');
            const metaValue = {
                ...app.config.baseDeltaEditor.getMeta(context, metaPath)
            };
            if (typeof metaValue[name] === 'undefined') {
                return { state: 'COMPLETED', statusCode: 404 };
            }
            delete metaValue[name];
            full_meta = (0, signalk_schema_1.getMetadata)('vessels.self.' + metaPath);
            delete full_meta[name];
            app.config.baseDeltaEditor.setMeta(context, metaPath, metaValue);
            if (Object.keys(metaValue).length === 0) {
                app.config.baseDeltaEditor.removeMeta(context, metaPath);
            }
        }
        else {
            metaPath = parts.slice(0, parts.length - 1).join('.');
            full_meta = (0, signalk_schema_1.getMetadata)('vessels.self.' + metaPath);
            const metaValue = app.config.baseDeltaEditor.getMeta(context, metaPath);
            if (!metaValue) {
                return { state: 'COMPLETED', statusCode: 404 };
            }
            Object.keys(metaValue).forEach((key) => {
                delete full_meta[key];
            });
            app.config.baseDeltaEditor.removeMeta(context, metaPath);
        }
        app.handleMessage('defaults', {
            context: 'vessels.self',
            updates: [
                {
                    meta: [
                        {
                            path: metaPath,
                            value: full_meta
                        }
                    ]
                }
            ]
        });
        skConfig
            .writeBaseDeltasFile(app)
            .then(() => {
            cb({ state: 'COMPLETED', statusCode: 200 });
        })
            .catch(() => {
            cb({
                state: 'COMPLETED',
                statusCode: 502,
                message: 'Unable to save to defaults file'
            });
        });
        return { state: 'PENDING' };
    };
    putNotificationHandler = (context, path, value) => {
        return putNotification(app, context, path, value);
    };
}
function deletePath(app, contextParam, path, req, requestId, updateCb) {
    const context = contextParam || 'vessels.self';
    debug('received delete %s %s', context, path);
    return new Promise((resolve, reject) => {
        (0, requestResponse_1.createRequest)(app, 'delete', {
            context: context,
            requestId: requestId ?? undefined,
            delete: { path: path }
        }, req && req.skPrincipal ? req.skPrincipal.identifier : undefined, undefined, updateCb)
            .then((request) => {
            if (req &&
                app.securityStrategy.shouldAllowPut(req, context, null, path) ===
                    false) {
                (0, requestResponse_1.updateRequest)(request.requestId, 'COMPLETED', { statusCode: 403 })
                    .then(resolve)
                    .catch(reject);
                return;
            }
            const parts = path.split('.');
            let handler;
            if ((parts.length > 1 && parts[parts.length - 1] === 'meta') ||
                (parts.length > 1 && parts[parts.length - 2] === 'meta')) {
                handler = deleteMetaHandler;
            }
            if (handler) {
                const actionResult = handler(context, path, (reply) => {
                    debug('got result: %j', reply);
                    (0, requestResponse_1.updateRequest)(request.requestId, reply.state, reply)
                        .then(() => undefined)
                        .catch((err) => {
                        console.error(err);
                    });
                });
                Promise.resolve(actionResult)
                    .then((result) => {
                    debug('got result: %j', result);
                    (0, requestResponse_1.updateRequest)(request.requestId, result.state, result)
                        .then((reply) => {
                        if (reply.state === 'PENDING') {
                            ;
                            reply.action = {
                                href: reply.href
                            };
                        }
                        resolve(reply);
                    })
                        .catch(reject);
                })
                    .catch((err) => {
                    (0, requestResponse_1.updateRequest)(request.requestId, 'COMPLETED', {
                        statusCode: 500,
                        message: err.message
                    })
                        .then(resolve)
                        .catch(reject);
                });
            }
            else {
                (0, requestResponse_1.updateRequest)(request.requestId, 'COMPLETED', {
                    statusCode: 405,
                    message: `DELETE not supported for ${path}`
                })
                    .then(resolve)
                    .catch(reject);
            }
        })
            .catch(reject);
    });
}
function putPath(app, contextParam, path, body, req, requestId, updateCb) {
    const context = contextParam || 'vessels.self';
    debug('received put %s %s %j', context, path, body);
    return new Promise((resolve, reject) => {
        (0, requestResponse_1.createRequest)(app, 'put', {
            context: context,
            requestId: requestId ?? undefined,
            put: { path: path, value: body.value }
        }, req && req.skPrincipal ? req.skPrincipal.identifier : undefined, undefined, updateCb)
            .then((request) => {
            if (req &&
                app.securityStrategy.shouldAllowPut(req, context, null, path) ===
                    false) {
                (0, requestResponse_1.updateRequest)(request.requestId, 'COMPLETED', { statusCode: 403 })
                    .then(resolve)
                    .catch(reject);
                return;
            }
            let handler;
            const parts = path.split('.');
            if ((parts.length > 1 && parts[parts.length - 1] === 'meta') ||
                (parts.length > 1 && parts[parts.length - 2] === 'meta')) {
                handler = putMetaHandler;
            }
            else {
                const handlers = actionHandlers[context]
                    ? actionHandlers[context][path]
                    : null;
                if (handlers && Object.keys(handlers).length > 0) {
                    if (body.source) {
                        handler = handlers[body.source];
                    }
                    else if (Object.keys(handlers).length === 1) {
                        handler = Object.values(handlers)[0];
                    }
                    else {
                        (0, requestResponse_1.updateRequest)(request.requestId, 'COMPLETED', {
                            statusCode: 400,
                            message: 'there are multiple sources for the given path, but no source was specified in the request'
                        })
                            .then(resolve)
                            .catch(reject);
                        return;
                    }
                }
                if (!handler && parts[0] === 'notifications') {
                    handler = putNotificationHandler;
                }
            }
            if (handler) {
                function fixReply(reply) {
                    if (reply.state === 'FAILURE') {
                        reply.state = 'COMPLETED';
                        reply.statusCode = 502;
                    }
                    else if (reply.state === 'SUCCESS') {
                        reply.state = 'COMPLETED';
                        reply.statusCode = 200;
                    }
                }
                const actionResult = handler(context, path, body.value, (reply) => {
                    debug('got result: %j', reply);
                    fixReply(reply);
                    (0, requestResponse_1.updateRequest)(request.requestId, reply.state, reply)
                        .then(() => undefined)
                        .catch((err) => {
                        console.error(err);
                    });
                });
                Promise.resolve(actionResult)
                    .then((result) => {
                    debug('got result: %j', result);
                    fixReply(result);
                    (0, requestResponse_1.updateRequest)(request.requestId, result.state, result)
                        .then((reply) => {
                        if (reply.state === 'PENDING') {
                            ;
                            reply.action = {
                                href: reply.href
                            };
                        }
                        resolve(reply);
                    })
                        .catch(reject);
                })
                    .catch((err) => {
                    (0, requestResponse_1.updateRequest)(request.requestId, 'COMPLETED', {
                        statusCode: 500,
                        message: err.message
                    })
                        .then(resolve)
                        .catch(reject);
                });
            }
            else if (app.interfaces.ws &&
                app.interfaces.ws.canHandlePut(path, body.source)) {
                app.interfaces.ws
                    .handlePut(request.requestId, context, path, body.source, body.value)
                    .then(resolve)
                    .catch(reject);
            }
            else {
                (0, requestResponse_1.updateRequest)(request.requestId, 'COMPLETED', {
                    statusCode: 405,
                    message: `PUT not supported for ${path}`
                })
                    .then(resolve)
                    .catch(reject);
            }
        })
            .catch(reject);
    });
}
function registerActionHandler(context, path, source, callback) {
    debug(`registered action handler for ${context} ${path} ${source}`);
    if (actionHandlers[context] === undefined) {
        actionHandlers[context] = {};
    }
    if (actionHandlers[context][path] === undefined) {
        actionHandlers[context][path] = {};
    }
    actionHandlers[context][path][source] = callback;
    return () => {
        deRegisterActionHandler(context, path, source, callback);
    };
}
function deRegisterActionHandler(context, path, source, callback) {
    if (actionHandlers[context] &&
        actionHandlers[context][path][source] === callback) {
        delete actionHandlers[context][path][source];
        debug(`de-registered action handler for ${context} ${path} ${source}`);
    }
}
function putNotification(app, context, path, value) {
    const parts = path.split('.');
    const notifPath = parts.slice(0, parts.length - 1).join('.');
    const key = parts[parts.length - 1];
    const existing = (0, lodash_1.get)(app.signalk.self, notifPath);
    if (existing === undefined || !existing.value) {
        return { state: 'COMPLETED', statusCode: 404 };
    }
    if (key !== 'method' && key !== 'state') {
        return { state: 'COMPLETED', statusCode: 405 };
    }
    existing.value[key] = value;
    existing.timestamp = new Date().toISOString();
    const delta = {
        updates: [
            {
                $source: existing.$source,
                values: [
                    {
                        path: notifPath,
                        value: existing.value
                    }
                ]
            }
        ]
    };
    app.handleMessage('server', delta);
    return { state: 'COMPLETED', statusCode: 200 };
}
//# sourceMappingURL=put.js.map