"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
function _export(target, all) {
    for(var name in all)Object.defineProperty(target, name, {
        enumerable: true,
        get: Object.getOwnPropertyDescriptor(all, name).get
    });
}
_export(exports, {
    get provision () {
        return provision;
    },
    get provisionCommand () {
        return provisionCommand;
    }
});
const _nodepath = require("node:path");
const _commander = require("commander");
const _lodash = require("lodash");
const _sequelize = require("sequelize");
const _xlsx = require("xlsx");
const _nodefs = /*#__PURE__*/ _interop_require_default(require("node:fs"));
const _json5 = /*#__PURE__*/ _interop_require_default(require("json5"));
const _constants = require("@tamanu/constants");
const _logging = require("@tamanu/shared/services/logging");
const _settings = require("@tamanu/settings");
const _crypto = require("@tamanu/shared/utils/crypto");
const _database = require("../database");
const _integrations = require("../integrations");
const _loadSettingFile = require("../utils/loadSettingFile");
const _referenceDataImporter = require("../admin/referenceDataImporter");
const _importerEndpoint = require("../admin/importer/importerEndpoint");
const _utils = require("../auth/utils");
const _programImporter = require("../admin/programImporter/programImporter");
function _interop_require_default(obj) {
    return obj && obj.__esModule ? obj : {
        default: obj
    };
}
/**
 * Converts the json files in the defaultProvisioningData directory into an XLSX workbook
 * @returns {WorkBook}
 */ const parseDefaultProvisioningJsonSheets = (dir)=>{
    const entries = _nodefs.default.readdirSync(dir).filter((f)=>f.endsWith('.json5')).sort().map((f)=>({
            sheetName: (0, _nodepath.parse)(f).name,
            filePath: (0, _nodepath.join)(dir, f)
        }));
    const wb = _xlsx.utils.book_new();
    entries.forEach(({ sheetName, filePath })=>{
        const payload = _json5.default.parse(_nodefs.default.readFileSync(filePath, 'utf8'));
        const name = payload.sheetName || sheetName;
        if (!Array.isArray(payload.columns) || payload.columns.length === 0) {
            throw new Error(`Invalid sheet JSON (missing non-empty "columns") in ${filePath}`);
        }
        if (!Array.isArray(payload.data)) {
            throw new Error(`Invalid sheet JSON (missing "data" array) in ${filePath}`);
        }
        const data = payload.data;
        const ws = _xlsx.utils.json_to_sheet(data, {
            header: payload.columns
        });
        _xlsx.utils.book_append_sheet(wb, ws, name);
    });
    return wb;
};
const sleep = (ms)=>new Promise((resolveSleep)=>{
        setTimeout(resolveSleep, ms);
    });
const initialiseDatabaseWithRetry = async ()=>{
    const maxAttempts = 30;
    const delayMs = 5000;
    for(let attempt = 1; attempt <= maxAttempts; attempt += 1){
        try {
            return await (0, _database.initDatabase)({
                testMode: false
            });
        } catch (error) {
            if (attempt === maxAttempts) {
                throw error;
            }
            _logging.log.warn('Database not ready for provisioning, retrying', {
                attempt,
                maxAttempts,
                delayMs,
                error: error.message
            });
            await sleep(delayMs);
        }
    }
    throw new Error('Unreachable: exhausted database initialisation attempts');
};
/**
 * Validates that a reference data file contains all sheets importable through the reference data importer
 * @param {string} file - File path
 */ function validateFullReferenceDataImport(workbook) {
    // These are two very unique cases. 'user' has special logic and 'administeredVaccine' is a special case used for existing deployments.
    const EXCLUDED_FROM_FULL_IMPORT_CHECK = [
        'user',
        'administeredVaccine'
    ];
    const sheetNameDictionary = (0, _lodash.keyBy)(Object.keys(workbook.Sheets), _importerEndpoint.normaliseSheetName);
    // Check all required data types are present and have data
    const missingDataTypes = [];
    for (const dataType of _constants.GENERAL_IMPORTABLE_DATA_TYPES){
        if (EXCLUDED_FROM_FULL_IMPORT_CHECK.includes(dataType)) continue;
        const sheetName = sheetNameDictionary[dataType];
        if (!sheetName || _xlsx.utils.sheet_to_json(workbook.Sheets[sheetName]).length === 0) {
            missingDataTypes.push(dataType);
        }
    }
    if (missingDataTypes.length > 0) {
        throw new Error(`Reference data file has no rows for the following data types:\n${missingDataTypes.join('\n')}`);
    }
    _logging.log.info('Reference data file validation passed - all required sheets contain data');
}
async function encryptSecretSettings(settings, scope) {
    const schema = (0, _settings.getScopedSchema)(scope);
    const secretPaths = (0, _settings.extractSecretPaths)(schema);
    if (secretPaths.length === 0) return;
    let keyBuffer;
    for (const path of secretPaths){
        const value = (0, _lodash.get)(settings, path);
        if (value === undefined) continue;
        if (value === null || value === '') {
            (0, _lodash.unset)(settings, path);
            continue;
        }
        if ((0, _crypto.isEncryptedSecret)(value)) continue;
        if (!keyBuffer) keyBuffer = await (0, _crypto.getSettingsPskKeyBuffer)();
        (0, _lodash.set)(settings, path, await (0, _crypto.encryptSecret)(keyBuffer, String(value)));
    }
}
async function provision(provisioningFile, { skipIfNotNeeded }) {
    const store = await initialiseDatabaseWithRetry();
    const userCount = await store.models.User.count({
        where: {
            id: {
                [_sequelize.Op.ne]: _constants.SYSTEM_USER_UUID
            }
        }
    });
    if (userCount > 0) {
        if (skipIfNotNeeded) {
            _logging.log.info(`Found ${userCount} users already in the database, but expecting to, not provisioning`);
            return;
        }
        throw new Error(`Found ${userCount} users already in the database, aborting provision`);
    }
    (0, _integrations.checkIntegrationsConfig)();
    const { users = {}, facilities = {}, programs = [], referenceData = [], settings = {} } = await (0, _loadSettingFile.loadSettingFile)(provisioningFile);
    /// //////////////
    /// REFERENCE DATA
    const errors = [];
    const stats = [];
    const importerOptions = {
        errors,
        models: store.models,
        stats,
        includedDataTypes: [
            ..._constants.GENERAL_IMPORTABLE_DATA_TYPES,
            ..._constants.PERMISSION_IMPORTABLE_DATA_TYPES
        ],
        checkPermission: ()=>true
    };
    for (const { file: referenceDataFile = null, url: referenceDataUrl = null, defaultSpreadsheet: isUsingDefaultSpreadsheet = false, ...rest } of referenceData ?? []){
        if (isUsingDefaultSpreadsheet) {
            const defaultProvisioningDataDirectory = (0, _nodepath.resolve)(__dirname, 'defaultProvisioningData');
            _logging.log.info('Using reference data json files from this branch', {
                directory: defaultProvisioningDataDirectory
            });
            const defaultReferenceDataWorkbook = parseDefaultProvisioningJsonSheets(defaultProvisioningDataDirectory);
            // We only validate the default import to ensure it stays complete. It is fine to allow partial imports through the other options.
            validateFullReferenceDataImport(defaultReferenceDataWorkbook);
            await (0, _referenceDataImporter.referenceDataImporter)({
                workbook: defaultReferenceDataWorkbook,
                ...importerOptions
            });
        }
        if (!referenceDataFile && !referenceDataUrl && !isUsingDefaultSpreadsheet) {
            throw new Error(`Unknown reference data import with keys ${Object.keys(rest).join(', ')}`);
        }
        if (referenceDataFile) {
            const realpath = (0, _nodepath.resolve)(provisioningFile, referenceDataFile);
            _logging.log.info('Importing reference data file', {
                file: realpath
            });
            await (0, _referenceDataImporter.referenceDataImporter)({
                file: realpath,
                ...importerOptions
            });
        } else if (referenceDataUrl) {
            _logging.log.info('Downloading reference data file', {
                url: referenceDataUrl
            });
            const file = await fetch(referenceDataUrl);
            const data = Buffer.from(await (await file.blob()).arrayBuffer());
            _logging.log.info('Importing reference data', {
                size: data.byteLength
            });
            await (0, _referenceDataImporter.referenceDataImporter)({
                data,
                file: referenceDataUrl,
                ...importerOptions
            });
        }
    }
    if (errors.length) {
        for (const error of errors){
            _logging.log.error(error);
        }
        throw new Error(`Encountered ${errors.length} errors during provisioning`);
    }
    _logging.log.info('Imported reference data successfully', stats);
    /// //////////
    /// FACILITIES
    for (const [id, value] of Object.entries(facilities)){
        const fields = {
            ...value
        };
        delete fields.user;
        delete fields.password;
        delete fields.settings;
        const facility = await store.models.Facility.findByPk(id);
        if (facility) {
            _logging.log.info('Updating facility', {
                id
            });
            await facility.update(fields);
        } else {
            var _fields, _fields1;
            _logging.log.info('Creating facility', {
                id
            });
            (_fields = fields).name || (_fields.name = id);
            (_fields1 = fields).code || (_fields1.code = id);
            await store.models.Facility.create({
                id,
                ...fields
            });
        }
    }
    /// ////////
    /// SETTINGS
    const combineSettings = async (settingData, scope, facilityId)=>{
        const existing = await store.models.Setting.get('', facilityId, scope);
        const combined = (0, _lodash.defaultsDeep)(settingData, existing);
        await encryptSecretSettings(combined, scope);
        return store.models.Setting.set('', combined, scope, facilityId);
    };
    if (settings.global) {
        await combineSettings(settings.global, _constants.SETTINGS_SCOPES.GLOBAL);
        _logging.log.info('Set global settings');
    }
    if (settings.facilities) {
        await Promise.all(Object.entries(settings.facilities).map(([facilityId, facilitySettings])=>combineSettings(facilitySettings, _constants.SETTINGS_SCOPES.FACILITY, facilityId)));
        _logging.log.info('Set facility settings');
    }
    if (settings.central) {
        await combineSettings(settings.central, _constants.SETTINGS_SCOPES.CENTRAL);
        _logging.log.info('Set central settings');
    }
    /// /////
    /// USERS
    const allUsers = [
        ...Object.entries(users),
        ...Object.entries(facilities).map(([id, { user, password }])=>user && password && [
                user,
                {
                    displayName: `System: ${id} sync`,
                    password
                }
            ]).filter(Boolean)
    ];
    for (const [email, { role = 'admin', password, ...fields }] of allUsers){
        let realPassword = password;
        if (!realPassword) {
            realPassword = (0, _utils.getRandomBase64String)(16);
            // eslint-disable-next-line no-console
            console.log(`NEW PASSWORD for ${email}: ${realPassword}`);
        }
        const user = await store.models.User.findOne({
            where: {
                email
            }
        });
        if (user) {
            _logging.log.info('Updating user', {
                email
            });
            user.set({
                role,
                ...fields
            });
            user.setPassword(realPassword);
            await user.save();
        } else {
            _logging.log.info('Creating user', {
                email
            });
            await store.models.User.create({
                email,
                role,
                password: realPassword,
                ...fields
            });
        }
    }
    /// ////////
    /// PROGRAMS
    const programOptions = {
        errors,
        models: store.models,
        stats,
        checkPermission: ()=>true
    };
    for (const { file: programFile = null, url: programUrl = null, ...rest } of programs){
        if (!programFile && !programUrl) {
            throw new Error(`Unknown program import with keys ${Object.keys(rest).join(', ')}`);
        }
        if (programFile) {
            const realpath = (0, _nodepath.resolve)(provisioningFile, programFile);
            _logging.log.info('Importing program file', {
                file: realpath
            });
            await (0, _programImporter.programImporter)({
                file: realpath,
                ...programOptions
            });
        } else if (programUrl) {
            _logging.log.info('Downloading program file', {
                url: programUrl
            });
            const file = await fetch(programUrl);
            const data = Buffer.from(await (await file.blob()).arrayBuffer());
            _logging.log.info('Importing program', {
                size: data.byteLength
            });
            await (0, _programImporter.programImporter)({
                data,
                file: programUrl,
                ...programOptions
            });
        }
    }
    if (errors.length) {
        for (const error of errors){
            _logging.log.error(error);
        }
        throw new Error(`Encountered ${errors.length} errors during provisioning`);
    }
    _logging.log.info('Imported programs successfully', stats);
    _logging.log.info('Done.');
}
const provisionCommand = new _commander.Command('provision').description('Set up initial data. See https://beyond-essential.slab.com/posts/tamanu-provisioning-file-h1urgi86 for details or /docs/provisioning/example.json5 for a sample file.').argument('<file>', 'Path to the provisioning file').option('--skip-if-not-needed', 'If there are already users in the database, exit(0) instead of aborting').action(provision);

//# sourceMappingURL=provision.js.map