"use strict";
const express = require('express');
const fs = require('fs');
const path = require('path');
const { atomicWriteFileSync } = require('../atomicWrite');
const busboy = require('busboy');
const { compile } = require('mathjs');
const { createDebug } = require('../debug');
const debug = createDebug('signalk-server:unitpreferences-api');
/**
 * Validate a formula string by attempting to compile it with mathjs
 * @param {string} formula - The formula to validate
 * @returns {string|null} - Error message if invalid, null if valid
 */
function validateFormula(formula) {
    try {
        compile(formula);
        return null;
    }
    catch (e) {
        return e.message;
    }
}
const VALID_PRESET_NAME = /^[a-zA-Z0-9_-]+$/;
/**
 * Validate all formulas in a unit definitions object
 * @param {object} definitions - The unit definitions to validate
 * @returns {string|null} - Error message if any formula is invalid, null if all valid
 */
function validateDefinitions(definitions) {
    for (const [siUnit, def] of Object.entries(definitions)) {
        if (def.conversions) {
            for (const [targetUnit, conversion] of Object.entries(def.conversions)) {
                if (conversion.formula) {
                    const error = validateFormula(conversion.formula);
                    if (error) {
                        return `Invalid formula for ${siUnit} -> ${targetUnit}: ${error}`;
                    }
                }
                if (conversion.inverseFormula) {
                    const error = validateFormula(conversion.inverseFormula);
                    if (error) {
                        return `Invalid inverseFormula for ${siUnit} -> ${targetUnit}: ${error}`;
                    }
                }
            }
        }
    }
    return null;
}
/**
 * Validate a preset JSON structure thoroughly
 * @param {object} preset - The preset object to validate
 * @param {object} definitions - Unit definitions for validation
 * @returns {string|null} - Error message if invalid, null if valid
 */
function validatePreset(preset, definitions) {
    const errors = [];
    if (!preset || typeof preset !== 'object' || Array.isArray(preset)) {
        return 'Preset must be a JSON object';
    }
    if (!preset.version || typeof preset.version !== 'string') {
        return 'Missing or invalid "version"';
    }
    if (!preset.name || typeof preset.name !== 'string') {
        return 'Missing or invalid "name"';
    }
    if (!preset.categories ||
        typeof preset.categories !== 'object' ||
        Array.isArray(preset.categories)) {
        return 'Missing or invalid "categories"';
    }
    if (Object.keys(preset.categories).length === 0) {
        return 'categories cannot be empty';
    }
    for (const [name, cat] of Object.entries(preset.categories)) {
        if (!cat || typeof cat !== 'object') {
            errors.push(`"${name}": must be an object`);
            continue;
        }
        if (!cat.baseUnit) {
            errors.push(`"${name}": missing baseUnit`);
            continue;
        }
        if (!cat.targetUnit) {
            errors.push(`"${name}": missing targetUnit`);
            continue;
        }
        // Skip datetime validation for now - datetime formats are handled differently
        const datetimeBaseUnits = [
            'RFC 3339 (UTC)',
            'ISO-8601 (UTC)',
            'Epoch Seconds'
        ];
        if (datetimeBaseUnits.includes(cat.baseUnit)) {
            continue;
        }
        const unitDef = definitions[cat.baseUnit];
        if (!unitDef) {
            errors.push(`"${name}": unknown baseUnit "${cat.baseUnit}"`);
            continue;
        }
        // targetUnit same as baseUnit means no conversion, which is valid
        if (cat.targetUnit !== cat.baseUnit) {
            if (!unitDef.conversions || !unitDef.conversions[cat.targetUnit]) {
                const available = Object.keys(unitDef.conversions || {}).join(', ');
                errors.push(`"${name}": "${cat.targetUnit}" not in ${cat.baseUnit} conversions. Available: ${available}`);
            }
        }
    }
    if (errors.length > 0) {
        return errors.join('\n');
    }
    return null;
}
const { getConfig, getCategories, getCustomCategories, getActivePreset, getMergedDefinitions, reloadPreset, reloadCustomDefinitions, reloadCustomCategories, invalidatePresetCache, getDefaultCategory, getBaseUnitToCategories, getCategoryForBaseUnit, loadUserPreferences, saveUserPreferences } = require('../unitpreferences');
const PACKAGE_UNITPREFS_DIR = path.join(__dirname, '../../unitpreferences');
module.exports = function (app) {
    const configUnitprefsDir = path.join(app.config.configPath, 'unitpreferences');
    function checkPresetExists(presetName) {
        const builtInPath = path.join(PACKAGE_UNITPREFS_DIR, 'presets', `${presetName}.json`);
        if (fs.existsSync(builtInPath)) {
            return { type: 'builtin' };
        }
        const customPath = path.join(configUnitprefsDir, 'presets/custom', `${presetName}.json`);
        if (fs.existsSync(customPath)) {
            return { type: 'custom' };
        }
        return null;
    }
    const router = express.Router();
    // GET /signalk/v1/unitpreferences/config
    router.get('/config', (req, res) => {
        try {
            const config = getConfig();
            res.json(config);
        }
        catch (err) {
            debug('Error getting config:', err);
            res.status(500).json({ error: 'Failed to get config' });
        }
    });
    // PUT /signalk/v1/unitpreferences/config
    router.put('/config', (req, res) => {
        try {
            const configPath = path.join(configUnitprefsDir, 'config.json');
            atomicWriteFileSync(configPath, JSON.stringify(req.body, null, 2));
            reloadPreset();
            app.emit('unitpreferencesChanged', { type: 'global' });
            res.json({ success: true });
        }
        catch (err) {
            debug('Error saving config:', err);
            res.status(500).json({ error: 'Failed to save config' });
        }
    });
    // GET /signalk/v1/unitpreferences/categories
    router.get('/categories', (req, res) => {
        try {
            const categories = getCategories();
            res.json(categories);
        }
        catch (err) {
            debug('Error getting categories:', err);
            res.status(500).json({ error: 'Failed to get categories' });
        }
    });
    // GET /signalk/v1/unitpreferences/definitions
    router.get('/definitions', (req, res) => {
        try {
            const definitions = getMergedDefinitions();
            res.json(definitions);
        }
        catch (err) {
            debug('Error getting definitions:', err);
            res.status(500).json({ error: 'Failed to get definitions' });
        }
    });
    // GET /signalk/v1/unitpreferences/custom-definitions
    router.get('/custom-definitions', (req, res) => {
        try {
            const customPath = path.join(configUnitprefsDir, 'custom-units-definitions.json');
            if (fs.existsSync(customPath)) {
                const data = JSON.parse(fs.readFileSync(customPath, 'utf-8'));
                res.json(data);
            }
            else {
                res.json({});
            }
        }
        catch (err) {
            debug('Error getting custom definitions:', err);
            res.status(500).json({ error: 'Failed to get custom definitions' });
        }
    });
    // PUT /signalk/v1/unitpreferences/custom-definitions
    router.put('/custom-definitions', (req, res) => {
        try {
            // Validate all formulas before saving
            const validationError = validateDefinitions(req.body);
            if (validationError) {
                res.status(400).json({ error: validationError });
                return;
            }
            const customPath = path.join(configUnitprefsDir, 'custom-units-definitions.json');
            atomicWriteFileSync(customPath, JSON.stringify(req.body, null, 2));
            reloadCustomDefinitions();
            app.emit('unitpreferencesChanged', { type: 'global' });
            res.json({ success: true });
        }
        catch (err) {
            debug('Error saving custom definitions:', err);
            res.status(500).json({ error: 'Failed to save custom definitions' });
        }
    });
    // GET /signalk/v1/unitpreferences/custom-categories
    router.get('/custom-categories', (req, res) => {
        try {
            const customCats = getCustomCategories();
            res.json(customCats);
        }
        catch (err) {
            debug('Error getting custom categories:', err);
            res.status(500).json({ error: 'Failed to get custom categories' });
        }
    });
    // PUT /signalk/v1/unitpreferences/custom-categories
    router.put('/custom-categories', (req, res) => {
        try {
            const customPath = path.join(configUnitprefsDir, 'custom-categories.json');
            atomicWriteFileSync(customPath, JSON.stringify(req.body, null, 2));
            reloadCustomCategories();
            app.emit('unitpreferencesChanged', { type: 'global' });
            res.json({ success: true });
        }
        catch (err) {
            debug('Error saving custom categories:', err);
            res.status(500).json({ error: 'Failed to save custom categories' });
        }
    });
    // GET /signalk/v1/unitpreferences/presets
    router.get('/presets', (req, res) => {
        try {
            const presetsDir = path.join(PACKAGE_UNITPREFS_DIR, 'presets');
            const customDir = path.join(configUnitprefsDir, 'presets', 'custom');
            const builtIn = [];
            const custom = [];
            // List built-in presets from package dir
            const builtInFiles = fs.readdirSync(presetsDir);
            for (const file of builtInFiles) {
                if (file.endsWith('.json')) {
                    const presetPath = path.join(presetsDir, file);
                    const preset = JSON.parse(fs.readFileSync(presetPath, 'utf-8'));
                    builtIn.push({
                        name: file.replace('.json', ''),
                        displayName: preset.name,
                        description: preset.description
                    });
                }
            }
            // List custom presets from config dir
            if (fs.existsSync(customDir)) {
                const customFiles = fs.readdirSync(customDir);
                for (const file of customFiles) {
                    if (file.endsWith('.json')) {
                        const presetPath = path.join(customDir, file);
                        const preset = JSON.parse(fs.readFileSync(presetPath, 'utf-8'));
                        custom.push({
                            name: file.replace('.json', ''),
                            displayName: preset.name,
                            description: preset.description
                        });
                    }
                }
            }
            res.json({ builtIn, custom });
        }
        catch (err) {
            debug('Error listing presets:', err);
            res.status(500).json({ error: 'Failed to list presets' });
        }
    });
    // GET /signalk/v1/unitpreferences/presets/:name
    router.get('/presets/:name', (req, res) => {
        try {
            const presetName = req.params.name;
            if (!VALID_PRESET_NAME.test(presetName)) {
                res.status(400).json({ error: 'Invalid preset name' });
                return;
            }
            // Check custom presets in config dir first
            const customPath = path.join(configUnitprefsDir, 'presets/custom', `${presetName}.json`);
            if (fs.existsSync(customPath)) {
                const preset = JSON.parse(fs.readFileSync(customPath, 'utf-8'));
                res.json(preset);
                return;
            }
            // Fall back to built-in in package dir
            const builtInPath = path.join(PACKAGE_UNITPREFS_DIR, 'presets', `${presetName}.json`);
            if (fs.existsSync(builtInPath)) {
                const preset = JSON.parse(fs.readFileSync(builtInPath, 'utf-8'));
                res.json(preset);
                return;
            }
            res.status(404).json({ error: 'Preset not found' });
        }
        catch (err) {
            debug('Error getting preset:', err);
            res.status(500).json({ error: 'Failed to get preset' });
        }
    });
    // PUT /signalk/v1/unitpreferences/presets/custom/:name
    router.put('/presets/custom/:name', (req, res) => {
        try {
            const presetName = req.params.name;
            if (!VALID_PRESET_NAME.test(presetName)) {
                res.status(400).json({ error: 'Invalid preset name' });
                return;
            }
            // Prevent overwriting built-in presets
            const builtInNames = ['metric', 'imperial-us', 'imperial-uk'];
            if (builtInNames.includes(presetName)) {
                res.status(400).json({ error: 'Cannot overwrite built-in preset' });
                return;
            }
            // Validate preset structure
            const definitions = getMergedDefinitions();
            const validationError = validatePreset(req.body, definitions);
            if (validationError) {
                res.status(400).json({ error: validationError });
                return;
            }
            const customDir = path.join(configUnitprefsDir, 'presets/custom');
            if (!fs.existsSync(customDir)) {
                fs.mkdirSync(customDir, { recursive: true });
            }
            const presetPath = path.join(customDir, `${presetName}.json`);
            atomicWriteFileSync(presetPath, JSON.stringify(req.body, null, 2));
            invalidatePresetCache(presetName);
            res.json({ success: true });
        }
        catch (err) {
            debug('Error saving custom preset:', err);
            res.status(500).json({ error: 'Failed to save custom preset' });
        }
    });
    // DELETE /signalk/v1/unitpreferences/presets/custom/:name
    router.delete('/presets/custom/:name', (req, res) => {
        try {
            const presetName = req.params.name;
            if (!VALID_PRESET_NAME.test(presetName)) {
                res.status(400).json({ error: 'Invalid preset name' });
                return;
            }
            const presetPath = path.join(configUnitprefsDir, 'presets/custom', `${presetName}.json`);
            if (!fs.existsSync(presetPath)) {
                res.status(404).json({ error: 'Preset not found' });
                return;
            }
            fs.unlinkSync(presetPath);
            invalidatePresetCache(presetName);
            res.json({ success: true });
        }
        catch (err) {
            debug('Error deleting preset:', err);
            res.status(500).json({ error: 'Failed to delete preset' });
        }
    });
    // GET /signalk/v1/unitpreferences/active
    router.get('/active', (req, res) => {
        try {
            const preset = getActivePreset();
            const definitions = getMergedDefinitions();
            const result = {
                ...preset,
                categories: { ...preset.categories }
            };
            for (const [category, catDef] of Object.entries(result.categories)) {
                const unitDef = definitions[catDef.baseUnit];
                const conversion = unitDef?.conversions?.[catDef.targetUnit];
                if (conversion) {
                    result.categories[category] = {
                        ...catDef,
                        formula: conversion.formula,
                        inverseFormula: conversion.inverseFormula,
                        symbol: conversion.symbol
                    };
                }
            }
            res.json(result);
        }
        catch (err) {
            debug('Error getting active preset:', err);
            res.status(500).json({ error: 'Failed to get active preset' });
        }
    });
    // GET /signalk/v1/unitpreferences/default-categories
    router.get('/default-categories', (req, res) => {
        try {
            const defaultCatPath = path.join(PACKAGE_UNITPREFS_DIR, 'default-categories.json');
            if (fs.existsSync(defaultCatPath)) {
                const data = JSON.parse(fs.readFileSync(defaultCatPath, 'utf-8'));
                res.json(data);
            }
            else {
                res.json({ categories: {} });
            }
        }
        catch (err) {
            debug('Error getting default categories:', err);
            res.status(500).json({ error: 'Failed to get default categories' });
        }
    });
    // GET /signalk/v1/unitpreferences/default-category/:path
    router.get('/default-category/*', (req, res) => {
        try {
            const signalkPath = req.params[0];
            const category = getDefaultCategory(signalkPath);
            res.json({ path: signalkPath, category: category || null });
        }
        catch (err) {
            debug('Error getting default category:', err);
            res.status(500).json({ error: 'Failed to get default category' });
        }
    });
    // POST /signalk/v1/unitpreferences/presets/custom/upload
    const MAX_PRESET_SIZE = 100 * 1024; // 100KB
    router.post('/presets/custom/upload', (req, res) => {
        let responseSent = false;
        const sendResponse = (status, body) => {
            if (responseSent)
                return;
            responseSent = true;
            res.status(status).json(body);
        };
        try {
            const bb = busboy({
                headers: req.headers,
                limits: { fileSize: MAX_PRESET_SIZE }
            });
            let fileData = '';
            let fileName = '';
            let fileTruncated = false;
            bb.on('file', (fieldname, file, info) => {
                fileName = info.filename || '';
                if (!fileName.endsWith('.json')) {
                    sendResponse(400, { error: 'File must have .json extension' });
                    file.resume();
                    return;
                }
                file.on('data', (data) => {
                    fileData += data.toString();
                });
                file.on('limit', () => {
                    fileTruncated = true;
                });
            });
            bb.on('finish', () => {
                if (responseSent)
                    return;
                if (fileTruncated) {
                    sendResponse(400, {
                        error: `File too large. Maximum size is ${MAX_PRESET_SIZE / 1024}KB`
                    });
                    return;
                }
                if (!fileData) {
                    sendResponse(400, { error: 'No file uploaded' });
                    return;
                }
                // Parse JSON
                let preset;
                try {
                    preset = JSON.parse(fileData);
                }
                catch (e) {
                    sendResponse(400, { error: `Invalid JSON: ${e.message}` });
                    return;
                }
                // Validate preset structure
                const definitions = getMergedDefinitions();
                const validationError = validatePreset(preset, definitions);
                if (validationError) {
                    sendResponse(400, { error: validationError });
                    return;
                }
                // Derive preset name from filename or preset.name
                let presetName = fileName.replace('.json', '');
                if (!/^[a-zA-Z0-9_-]+$/.test(presetName)) {
                    // Fall back to sanitized preset.name
                    presetName = preset.name.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
                }
                // Check for existing preset with same name
                const existing = checkPresetExists(presetName);
                if (existing) {
                    if (existing.type === 'builtin') {
                        sendResponse(400, {
                            error: `Cannot overwrite built-in preset "${presetName}"`
                        });
                        return;
                    }
                    else {
                        sendResponse(409, {
                            error: `duplicate`,
                            existingName: presetName
                        });
                        return;
                    }
                }
                // Ensure custom directory exists
                const customDir = path.join(configUnitprefsDir, 'presets/custom');
                if (!fs.existsSync(customDir)) {
                    fs.mkdirSync(customDir, { recursive: true });
                }
                // Save the preset
                const presetPath = path.join(customDir, `${presetName}.json`);
                atomicWriteFileSync(presetPath, JSON.stringify(preset, null, 2));
                invalidatePresetCache(presetName);
                debug(`Custom preset uploaded: ${presetName}`);
                sendResponse(200, {
                    success: true,
                    name: presetName,
                    displayName: preset.name,
                    isCustom: true
                });
            });
            bb.on('error', (err) => {
                debug('Error uploading preset:', err);
                sendResponse(500, { error: 'Upload failed' });
            });
            req.pipe(bb);
        }
        catch (err) {
            debug('Error in preset upload:', err);
            sendResponse(500, { error: 'Failed to upload preset' });
        }
    });
    // GET /signalk/v1/unitpreferences/primary-categories
    router.get('/primary-categories', (req, res) => {
        try {
            const baseUnitMap = getBaseUnitToCategories();
            const username = req.skPrincipal?.identifier;
            const ambiguousUnits = {};
            for (const [baseUnit, cats] of Object.entries(baseUnitMap)) {
                if (cats.length > 1) {
                    ambiguousUnits[baseUnit] = cats;
                }
            }
            const effectivePrimary = {};
            for (const baseUnit of Object.keys(ambiguousUnits)) {
                effectivePrimary[baseUnit] = getCategoryForBaseUnit(baseUnit, username);
            }
            res.json({ ambiguousUnits, effectivePrimary });
        }
        catch (err) {
            debug('Error getting primary categories:', err);
            res.status(500).json({ error: 'Failed to get primary categories' });
        }
    });
    // PUT /signalk/v1/unitpreferences/primary-categories
    router.put('/primary-categories', (req, res) => {
        try {
            const username = req.skPrincipal?.identifier;
            if (!username) {
                res.status(401).json({ error: 'Authentication required' });
                return;
            }
            if (!req.body ||
                typeof req.body !== 'object' ||
                Array.isArray(req.body)) {
                res.status(400).json({ error: 'Request body must be a JSON object' });
                return;
            }
            const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
            const baseUnitMap = getBaseUnitToCategories();
            const validated = {};
            for (const [baseUnit, category] of Object.entries(req.body)) {
                if (UNSAFE_KEYS.has(baseUnit)) {
                    res.status(400).json({ error: `Invalid key: ${baseUnit}` });
                    return;
                }
                if (typeof category !== 'string') {
                    res
                        .status(400)
                        .json({ error: `Value for "${baseUnit}" must be a string` });
                    return;
                }
                if (!baseUnitMap[baseUnit]) {
                    res.status(400).json({ error: `Unknown base unit: ${baseUnit}` });
                    return;
                }
                if (!baseUnitMap[baseUnit].includes(category)) {
                    res.status(400).json({
                        error: `Category "${category}" does not use base unit "${baseUnit}"`
                    });
                    return;
                }
                validated[baseUnit] = category;
            }
            const userPrefs = loadUserPreferences(username) || {};
            userPrefs.primaryCategories = {
                ...userPrefs.primaryCategories,
                ...validated
            };
            saveUserPreferences(username, userPrefs);
            app.emit('unitpreferencesChanged', { username });
            res.json({ success: true });
        }
        catch (err) {
            debug('Error saving primary categories:', err);
            res.status(500).json({ error: 'Failed to save primary categories' });
        }
    });
    return {
        start: function () {
            if (!app.securityStrategy.isDummy()) {
                app.securityStrategy.addAdminWriteMiddleware('/signalk/v1/unitpreferences/config');
                app.securityStrategy.addAdminWriteMiddleware('/signalk/v1/unitpreferences/custom-definitions');
                app.securityStrategy.addAdminWriteMiddleware('/signalk/v1/unitpreferences/custom-categories');
                app.securityStrategy.addAdminWriteMiddleware('/signalk/v1/unitpreferences/presets/custom/upload');
                app.securityStrategy.addWriteMiddleware('/signalk/v1/unitpreferences/primary-categories');
                // addAdminMiddleware protects all methods (including DELETE)
                app.securityStrategy.addAdminMiddleware('/signalk/v1/unitpreferences/presets/custom/:name');
            }
            app.use('/signalk/v1/unitpreferences', router);
            debug('Unit preferences API mounted at /signalk/v1/unitpreferences');
        }
    };
};
//# sourceMappingURL=unitpreferences-api.js.map