"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.DEFAULT_PRESET = void 0;
exports.setApplicationDataPath = setApplicationDataPath;
exports.getConfigUnitprefsDir = getConfigUnitprefsDir;
exports.loadAll = loadAll;
exports.getCategories = getCategories;
exports.getCustomCategories = getCustomCategories;
exports.getStandardDefinitions = getStandardDefinitions;
exports.getCustomDefinitions = getCustomDefinitions;
exports.getActivePreset = getActivePreset;
exports.getConfig = getConfig;
exports.reloadPreset = reloadPreset;
exports.invalidatePresetCache = invalidatePresetCache;
exports.reloadCustomDefinitions = reloadCustomDefinitions;
exports.reloadCustomCategories = reloadCustomCategories;
exports.getMergedDefinitions = getMergedDefinitions;
exports.getDefaultCategory = getDefaultCategory;
exports.getBaseUnitToCategories = getBaseUnitToCategories;
exports.getCategoryForBaseUnit = getCategoryForBaseUnit;
exports.loadUserPreferences = loadUserPreferences;
exports.saveUserPreferences = saveUserPreferences;
exports.getActivePresetForUser = getActivePresetForUser;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const atomicWrite_1 = require("../atomicWrite");
const debug_1 = require("../debug");
const debug = (0, debug_1.createDebug)('signalk-server:unitpreferences:loader');
const PACKAGE_UNITPREFS_DIR = path.join(__dirname, '../../unitpreferences');
exports.DEFAULT_PRESET = 'nautical-metric';
const VALID_USERNAME = /^[a-zA-Z0-9_.\-@]+$/;
const USER_PREFS_FILE = '1.0.0.json';
let categories;
let customCategories = {};
let standardDefinitions;
let customDefinitions;
let activePreset;
let config;
let defaultCategories = {};
let defaultCategoryWildcards = [];
let baseUnitToCategoriesCache = null;
let mergedDefinitionsCache = null;
let categoriesCache = null;
const userPreferencesCache = new Map();
const presetByNameCache = new Map();
let applicationDataPath = '';
let configUnitprefsDir = '';
let defaultPrimaryCategories = {};
function validateUsername(username) {
    if (!VALID_USERNAME.test(username) || username === '.' || username === '..') {
        throw new Error(`Invalid username: ${username}`);
    }
}
function getUserPrefsPath(username) {
    validateUsername(username);
    const result = path.join(applicationDataPath, 'users', username, 'unitpreferences', USER_PREFS_FILE);
    const resolved = path.resolve(result);
    const usersRoot = path.resolve(applicationDataPath, 'users') + path.sep;
    if (!resolved.startsWith(usersRoot)) {
        throw new Error(`Invalid username path: ${username}`);
    }
    return result;
}
function setApplicationDataPath(configPath) {
    applicationDataPath = path.join(configPath, 'applicationData');
    configUnitprefsDir = path.join(configPath, 'unitpreferences');
    ensureConfigDir();
}
function getConfigUnitprefsDir() {
    return configUnitprefsDir;
}
function ensureConfigDir() {
    if (!configUnitprefsDir)
        return;
    // Create directory structure
    fs.mkdirSync(path.join(configUnitprefsDir, 'presets', 'custom'), {
        recursive: true
    });
    // Seed mutable files from package dir if not present in config dir
    const mutableFiles = [
        'config.json',
        'custom-units-definitions.json',
        'custom-categories.json'
    ];
    for (const file of mutableFiles) {
        const destPath = path.join(configUnitprefsDir, file);
        if (!fs.existsSync(destPath)) {
            const srcPath = path.join(PACKAGE_UNITPREFS_DIR, file);
            if (fs.existsSync(srcPath)) {
                fs.copyFileSync(srcPath, destPath);
            }
        }
    }
    // Migrate custom presets from package dir if any exist
    const pkgCustomDir = path.join(PACKAGE_UNITPREFS_DIR, 'presets', 'custom');
    if (fs.existsSync(pkgCustomDir)) {
        const configCustomDir = path.join(configUnitprefsDir, 'presets', 'custom');
        for (const file of fs.readdirSync(pkgCustomDir)) {
            if (file.endsWith('.json')) {
                const destPath = path.join(configCustomDir, file);
                if (!fs.existsSync(destPath)) {
                    fs.copyFileSync(path.join(pkgCustomDir, file), destPath);
                }
            }
        }
    }
}
function loadAll() {
    // Load categories (read-only, from package)
    categories = JSON.parse(fs.readFileSync(path.join(PACKAGE_UNITPREFS_DIR, 'categories.json'), 'utf-8'));
    // Load standard definitions (read-only, from package)
    standardDefinitions = JSON.parse(fs.readFileSync(path.join(PACKAGE_UNITPREFS_DIR, 'standard-units-definitions.json'), 'utf-8'));
    // Load custom definitions from config dir
    const customPath = configUnitprefsDir
        ? path.join(configUnitprefsDir, 'custom-units-definitions.json')
        : path.join(PACKAGE_UNITPREFS_DIR, 'custom-units-definitions.json');
    if (fs.existsSync(customPath)) {
        customDefinitions = JSON.parse(fs.readFileSync(customPath, 'utf-8'));
    }
    else {
        customDefinitions = {};
    }
    // Load custom categories from config dir
    const customCatPath = configUnitprefsDir
        ? path.join(configUnitprefsDir, 'custom-categories.json')
        : path.join(PACKAGE_UNITPREFS_DIR, 'custom-categories.json');
    if (fs.existsSync(customCatPath)) {
        customCategories = JSON.parse(fs.readFileSync(customCatPath, 'utf-8'));
    }
    else {
        customCategories = {};
    }
    // Load config from config dir
    const cfgPath = configUnitprefsDir
        ? path.join(configUnitprefsDir, 'config.json')
        : path.join(PACKAGE_UNITPREFS_DIR, 'config.json');
    if (fs.existsSync(cfgPath)) {
        config = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
    }
    else {
        config = { activePreset: exports.DEFAULT_PRESET };
    }
    // Load default categories (read-only, from package)
    const defaultCatPath = path.join(PACKAGE_UNITPREFS_DIR, 'default-categories.json');
    if (fs.existsSync(defaultCatPath)) {
        const defaultCatData = JSON.parse(fs.readFileSync(defaultCatPath, 'utf-8'));
        // Build flat lookup: path -> category
        defaultCategories = {};
        for (const [categoryName, catDef] of Object.entries(defaultCatData.categories || {})) {
            const def = catDef;
            for (const p of def.paths || []) {
                defaultCategories[p] = categoryName;
            }
        }
    }
    rebuildDefaultCategoryWildcards();
    // Load default primary categories (read-only, from package)
    const primaryCatPath = path.join(PACKAGE_UNITPREFS_DIR, 'primary-categories.json');
    if (fs.existsSync(primaryCatPath)) {
        defaultPrimaryCategories = JSON.parse(fs.readFileSync(primaryCatPath, 'utf-8'));
    }
    // Invalidate caches
    baseUnitToCategoriesCache = null;
    mergedDefinitionsCache = null;
    categoriesCache = null;
    userPreferencesCache.clear();
    presetByNameCache.clear();
    // Load active preset
    loadActivePreset();
}
// Wildcard '*' matches a single signalk path segment (no dots).
// Example: 'electrical.batteries.*.voltage' matches 'electrical.batteries.house.voltage'
// but not 'electrical.batteries.house.voltage.value'.
function rebuildDefaultCategoryWildcards() {
    defaultCategoryWildcards = [];
    for (const [pattern, category] of Object.entries(defaultCategories)) {
        if (pattern.includes('*')) {
            const regex = new RegExp('^' +
                pattern
                    .replace(/[\\^$+?()[\]{}|]/g, '\\$&')
                    .replace(/\./g, '\\.')
                    .replace(/\*/g, '[^.]+') +
                '$');
            defaultCategoryWildcards.push({ regex, category });
        }
    }
}
function loadActivePreset() {
    const presetName = config.activePreset;
    // Check custom presets in config dir first
    if (configUnitprefsDir) {
        const customPresetPath = path.join(configUnitprefsDir, 'presets/custom', `${presetName}.json`);
        if (fs.existsSync(customPresetPath)) {
            activePreset = JSON.parse(fs.readFileSync(customPresetPath, 'utf-8'));
            return;
        }
    }
    // Fall back to built-in presets in package dir
    const builtInPath = path.join(PACKAGE_UNITPREFS_DIR, 'presets', `${presetName}.json`);
    if (fs.existsSync(builtInPath)) {
        activePreset = JSON.parse(fs.readFileSync(builtInPath, 'utf-8'));
        return;
    }
    // Default to nautical-metric
    activePreset = JSON.parse(fs.readFileSync(path.join(PACKAGE_UNITPREFS_DIR, `presets/${exports.DEFAULT_PRESET}.json`), 'utf-8'));
}
// The returned object is shared and must be treated as read-only by callers.
// Invalidated by loadAll(), reloadCustomCategories().
function getCategories() {
    if (categoriesCache) {
        return categoriesCache;
    }
    const merged = { ...categories };
    merged.categoryToBaseUnit = {
        ...categories.categoryToBaseUnit,
        ...customCategories
    };
    categoriesCache = merged;
    return categoriesCache;
}
function getCustomCategories() {
    return customCategories;
}
function getStandardDefinitions() {
    return standardDefinitions;
}
function getCustomDefinitions() {
    return customDefinitions;
}
function getActivePreset() {
    return activePreset;
}
function getConfig() {
    return config;
}
function reloadPreset() {
    presetByNameCache.clear();
    // Re-read config from config dir
    const cfgPath = configUnitprefsDir
        ? path.join(configUnitprefsDir, 'config.json')
        : path.join(PACKAGE_UNITPREFS_DIR, 'config.json');
    if (fs.existsSync(cfgPath)) {
        config = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
    }
    loadActivePreset();
}
// Drop a single preset (or all) from the cache. Call after writing or
// deleting a custom preset file directly so subsequent loads see the new
// content.
function invalidatePresetCache(presetName) {
    if (presetName === undefined) {
        presetByNameCache.clear();
    }
    else {
        presetByNameCache.delete(presetName);
    }
}
function reloadCustomDefinitions() {
    mergedDefinitionsCache = null;
    const customPath = configUnitprefsDir
        ? path.join(configUnitprefsDir, 'custom-units-definitions.json')
        : path.join(PACKAGE_UNITPREFS_DIR, 'custom-units-definitions.json');
    if (fs.existsSync(customPath)) {
        customDefinitions = JSON.parse(fs.readFileSync(customPath, 'utf-8'));
    }
    else {
        customDefinitions = {};
    }
}
function reloadCustomCategories() {
    baseUnitToCategoriesCache = null;
    categoriesCache = null;
    const customCatPath = configUnitprefsDir
        ? path.join(configUnitprefsDir, 'custom-categories.json')
        : path.join(PACKAGE_UNITPREFS_DIR, 'custom-categories.json');
    if (fs.existsSync(customCatPath)) {
        customCategories = JSON.parse(fs.readFileSync(customCatPath, 'utf-8'));
    }
    else {
        customCategories = {};
    }
}
// The returned object is shared and must be treated as read-only by callers.
// Invalidated by loadAll(), reloadCustomDefinitions().
function getMergedDefinitions() {
    if (mergedDefinitionsCache) {
        return mergedDefinitionsCache;
    }
    // Custom definitions override standard
    const merged = structuredClone(standardDefinitions);
    for (const [siUnit, def] of Object.entries(customDefinitions)) {
        if (!merged[siUnit]) {
            merged[siUnit] = def;
        }
        else {
            merged[siUnit].conversions = {
                ...merged[siUnit].conversions,
                ...def.conversions
            };
        }
    }
    mergedDefinitionsCache = merged;
    return mergedDefinitionsCache;
}
function getDefaultCategory(signalkPath, pathSiUnit, username) {
    if (defaultCategories[signalkPath]) {
        return defaultCategories[signalkPath];
    }
    for (const { regex, category } of defaultCategoryWildcards) {
        if (regex.test(signalkPath)) {
            return category;
        }
    }
    if (pathSiUnit) {
        return getCategoryForBaseUnit(pathSiUnit, username);
    }
    return null;
}
function getBaseUnitToCategories() {
    if (!baseUnitToCategoriesCache) {
        // Object.create(null): baseUnit values come from admin-controlled
        // custom-categories.json and become keys here; a null-prototype map
        // is immune to __proto__/constructor pollution.
        const cache = Object.create(null);
        const cats = getCategories();
        for (const [category, baseUnit] of Object.entries(cats.categoryToBaseUnit)) {
            if (!cache[baseUnit]) {
                cache[baseUnit] = [];
            }
            cache[baseUnit].push(category);
        }
        baseUnitToCategoriesCache = cache;
    }
    return baseUnitToCategoriesCache;
}
function getCategoryForBaseUnit(baseUnit, username) {
    const map = getBaseUnitToCategories();
    const matchingCategories = map[baseUnit];
    if (!matchingCategories || matchingCategories.length === 0)
        return null;
    if (matchingCategories.length === 1)
        return matchingCategories[0];
    if (username) {
        const userPrefs = loadUserPreferences(username);
        if (userPrefs?.primaryCategories?.[baseUnit]) {
            const userPrimary = userPrefs.primaryCategories[baseUnit];
            if (matchingCategories.includes(userPrimary))
                return userPrimary;
        }
    }
    const defaultPrimary = defaultPrimaryCategories[baseUnit];
    if (defaultPrimary && matchingCategories.includes(defaultPrimary))
        return defaultPrimary;
    // Deterministic fallback when neither user nor system default resolves.
    return [...matchingCategories].sort()[0];
}
function loadUserPreferences(username) {
    if (!applicationDataPath)
        return null;
    const cached = userPreferencesCache.get(username);
    if (cached !== undefined) {
        return cached === null ? null : structuredClone(cached);
    }
    try {
        const userPrefPath = getUserPrefsPath(username);
        if (fs.existsSync(userPrefPath)) {
            const prefs = JSON.parse(fs.readFileSync(userPrefPath, 'utf-8'));
            userPreferencesCache.set(username, prefs);
            return structuredClone(prefs);
        }
    }
    catch (err) {
        const code = err.code;
        if (code !== 'ENOENT') {
            debug('Error reading user preferences for %s: %O', username, err);
        }
    }
    userPreferencesCache.set(username, null);
    return null;
}
function saveUserPreferences(username, prefs) {
    if (!applicationDataPath)
        throw new Error('applicationDataPath not set');
    const filePath = getUserPrefsPath(username);
    const dir = path.dirname(filePath);
    fs.mkdirSync(dir, { recursive: true });
    (0, atomicWrite_1.atomicWriteFileSync)(filePath, JSON.stringify(prefs, null, 2));
    userPreferencesCache.set(username, structuredClone(prefs));
}
// Cached results are shared and must be treated as read-only by callers.
// Invalidated by loadAll(), reloadPreset(), invalidatePresetCache().
function loadPresetByName(presetName) {
    // Reject malformed names before touching the cache so attacker-controlled
    // values from user prefs cannot grow the Map with junk keys.
    if (!/^[a-zA-Z0-9_-]+$/.test(presetName)) {
        return null;
    }
    const cached = presetByNameCache.get(presetName);
    if (cached !== undefined) {
        return cached;
    }
    const preset = readPresetFromDisk(presetName);
    presetByNameCache.set(presetName, preset);
    return preset;
}
function readPresetFromDisk(presetName) {
    // Check custom presets in config dir first
    if (configUnitprefsDir) {
        const customPresetPath = path.join(configUnitprefsDir, 'presets/custom', `${presetName}.json`);
        if (fs.existsSync(customPresetPath)) {
            return JSON.parse(fs.readFileSync(customPresetPath, 'utf-8'));
        }
    }
    // Fall back to built-in presets in package dir
    const builtInPath = path.join(PACKAGE_UNITPREFS_DIR, 'presets', `${presetName}.json`);
    if (fs.existsSync(builtInPath)) {
        return JSON.parse(fs.readFileSync(builtInPath, 'utf-8'));
    }
    return null;
}
function getActivePresetForUser(username) {
    // 1. Check applicationData for user's preset preference
    if (username) {
        const userPref = loadUserPreferences(username);
        if (userPref?.activePreset) {
            const preset = loadPresetByName(userPref.activePreset);
            if (preset)
                return preset;
        }
    }
    // 2. User-specific preset from config (legacy)
    if (username && config.userPresets?.[username]) {
        const preset = loadPresetByName(config.userPresets[username]);
        if (preset)
            return preset;
    }
    // 3. Admin preset
    if (config.activePreset) {
        const preset = loadPresetByName(config.activePreset);
        if (preset)
            return preset;
    }
    // 4. Default
    return loadPresetByName(exports.DEFAULT_PRESET) || activePreset;
}
//# sourceMappingURL=loader.js.map