const fs = require('fs');
const path = require('path');

/**
 * Current config version. All config JSONs should declare this version.
 * Bump this when the config schema changes.
 */
const CURRENT_CONFIG_VERSION = '1.0.0';

class ConfigManager {
    constructor(relPath = '.') {
        this.configDir = path.resolve(__dirname, relPath);

        /**
         * Migration functions keyed by "fromVersion->toVersion".
         * Each function receives a config object and returns the migrated config.
         *
         * Example:
         *   this.migrations['1.0.0->1.1.0'] = (config) => {
         *       config.newSection = { enabled: false };
         *       return config;
         *   };
         */
        this.migrations = {};
    }

    /**
     * Load a configuration file by name.
     * Automatically checks the config version and migrates if needed.
     * @param {string} configName - Name of the config file (without .json extension)
     * @returns {Object} Parsed configuration object (migrated to current version if necessary)
     */
    getConfig(configName) {
        try {
            const configPath = path.resolve(this.configDir, `${configName}.json`);
            const configData = fs.readFileSync(configPath, 'utf8');
            let config = JSON.parse(configData);

            // Auto-migrate if version is behind current
            const configVersion = config.version || '0.0.0';
            if (configVersion !== CURRENT_CONFIG_VERSION) {
                config = this.migrateConfig(config, configVersion, CURRENT_CONFIG_VERSION);
            }

            return config;
        } catch (error) {
            if (error.message && error.message.startsWith('Failed to load config')) {
                throw error;
            }
            throw new Error(`Failed to load config '${configName}': ${error.message}`);
        }
    }

    /**
     * Get list of available configuration files
     * @returns {Array<string>} Array of config names (without .json extension)
     */
    getAvailableConfigs() {
        try {
            const resolvedDir = path.resolve(this.configDir);
            const files = fs.readdirSync(resolvedDir);
            return files
                .filter(file => file.endsWith('.json'))
                .map(file => path.basename(file, '.json'));
        } catch (error) {
            throw new Error(`Failed to read config directory: ${error.message}`);
        }
    }

    /**
     * Check if a specific config exists
     * @param {string} configName - Name of the config file
     * @returns {boolean} True if config exists
     */
    hasConfig(configName) {
        const configPath = path.resolve(this.configDir, `${configName}.json`);
        return fs.existsSync(configPath);
    }

    /**
     * Build a runtime config by merging base schema + node schema + UI overrides.
     * Eliminates the need for each nodeClass to manually construct general/asset/functionality sections.
     *
     * @param {string} nodeName - Node type name (e.g., 'valve', 'measurement')
     * @param {object} uiConfig - Raw config from Node-RED UI
     * @param {string} nodeId - Node-RED node ID (from node.id)
     * @param {object} [domainConfig={}] - Domain-specific config sections (e.g., { scaling: {...}, smoothing: {...} })
     * @returns {object} Merged runtime config
     *
     * @example
     * const cfgMgr = new ConfigManager();
     * const config = cfgMgr.buildConfig('measurement', uiConfig, node.id, {
     *   scaling: { enabled: uiConfig.scaling, inputMin: uiConfig.i_min, ... },
     *   smoothing: { smoothWindow: uiConfig.count, ... }
     * });
     */
    buildConfig(nodeName, uiConfig, nodeId, domainConfig = {}) {
        // Build base sections from UI config (common to ALL nodes)
        const config = {
            general: {
                name: uiConfig.name || nodeName,
                id: nodeId,
                unit: uiConfig.unit || 'unitless',
                logging: {
                    enabled: uiConfig.enableLog !== undefined ? uiConfig.enableLog : true,
                    logLevel: uiConfig.logLevel || 'info'
                }
            },
            functionality: {
                softwareType: nodeName.toLowerCase(),
                positionVsParent: uiConfig.positionVsParent || 'atEquipment',
                distance: uiConfig.hasDistance ? uiConfig.distance : null
            },
            output: {
                process: uiConfig.processOutputFormat || 'process',
                dbase: uiConfig.dbaseOutputFormat || 'influxdb'
            }
        };

        // Asset section is emitted per-key: only fields the editor actually
        // set propagate to the domain config. Schemas that omit a key (e.g.
        // rotatingMachine deliberately drops asset.supplier/category/type
        // because those come from the asset registry at runtime) no longer
        // get those keys injected and then stripped by ValidationUtils with
        // a warning. Empty strings from HTML defaults stay falsy → omitted →
        // schema default applies.
        const asset = {};
        const uuid = uiConfig.uuid || uiConfig.assetUuid;
        const tagCode = uiConfig.tagCode || uiConfig.assetTagCode;
        if (uuid) asset.uuid = uuid;
        if (tagCode) asset.tagCode = tagCode;
        if (uiConfig.supplier) asset.supplier = uiConfig.supplier;
        if (uiConfig.category) asset.category = uiConfig.category;
        if (uiConfig.assetType) asset.type = uiConfig.assetType;
        if (uiConfig.model) asset.model = uiConfig.model;
        if (uiConfig.unit) asset.unit = uiConfig.unit;
        if (Object.keys(asset).length > 0) config.asset = asset;

        // Merge domain-specific sections. Must be a DEEP merge: domainConfig
        // commonly returns subsets of `general` / `asset` (e.g. {general:
        // {unit}}, {asset: {curveUnits}}) and a shallow assign would wipe out
        // sibling keys this method just populated — notably `general.id`
        // (nodeId) and `asset.model`, causing child-registration id collisions
        // and curve-lookup failures downstream.
        ConfigManager._deepMerge(config, domainConfig);

        return config;
    }

    static _isPlainObject(v) {
        return Object.prototype.toString.call(v) === '[object Object]';
    }

    /**
     * In-place recursive merge. Arrays and primitives in `src` replace `dst`;
     * plain objects are merged key-by-key so siblings on `dst` survive.
     */
    static _deepMerge(dst, src) {
        if (!ConfigManager._isPlainObject(src)) return dst;
        for (const key of Object.keys(src)) {
            const v = src[key];
            if (Array.isArray(v)) {
                dst[key] = [...v];
            } else if (ConfigManager._isPlainObject(v)) {
                if (!ConfigManager._isPlainObject(dst[key])) dst[key] = {};
                ConfigManager._deepMerge(dst[key], v);
            } else {
                dst[key] = v;
            }
        }
        return dst;
    }

    /**
     * Migrate a config object from one version to another by applying
     * registered migration functions in sequence.
     * @param {object} config - The config object to migrate
     * @param {string} fromVersion - Current version of the config
     * @param {string} toVersion - Target version
     * @returns {object} Migrated config with updated version field
     */
    migrateConfig(config, fromVersion, toVersion) {
        const migrationKey = `${fromVersion}->${toVersion}`;
        const migrationFn = this.migrations[migrationKey];

        if (migrationFn) {
            config = migrationFn(config);
        }

        // Stamp the current version so it won't re-migrate
        config.version = toVersion;
        return config;
    }

    /**
     * Get the base config schema (shared across all nodes).
     * @returns {object} Base config schema
     */
    getBaseConfig() {
        return this.getConfig('baseConfig');
    }

    createEndpoint(nodeName) {
        try {
            // Load the config for this node
            const config = this.getConfig(nodeName);

            // Convert config to JSON
            const configJSON = JSON.stringify(config, null, 2);

            // Assemble the complete script
            return `
            // Create the namespace structure
            window.EVOLV = window.EVOLV || {};
            window.EVOLV.nodes = window.EVOLV.nodes || {};
            window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};

            // Inject the pre-loaded config data directly into the namespace
            window.EVOLV.nodes.${nodeName}.config = ${configJSON};

            console.log('${nodeName} config loaded and endpoint created');
            `;
        } catch (error) {
            throw new Error(`Failed to create endpoint for '${nodeName}': ${error.message}`);
        }
    }
}

module.exports = ConfigManager;
