const { getFormatter } = require('./formatters');

//this class will handle the output events for the node red node
class OutputUtils {
  // `options.alwaysEmit` is an optional list of field keys that bypass delta
  // compression: they are re-emitted on every tick even when unchanged. Use it
  // sparingly for slowly-varying values that must still trace as a continuous
  // line downstream (e.g. a pump's realized control position `ctrl`, which sits
  // constant in steady state and otherwise produces ~1 point per long stretch —
  // invisible in a Grafana timeseries with createEmpty:false). Defaults to none,
  // so existing nodes keep pure delta-compression behaviour.
  constructor(options = {}) {
    this.output = {};
    this.alwaysEmit = new Set(options.alwaysEmit || []);
  }

  checkForChanges(output, format) {
    if (!output || typeof output !== 'object') {
      return {};
    }
    this.output[format] = this.output[format] || {};
    const changedFields = {};
    for (const key in output) {
      if (!Object.prototype.hasOwnProperty.call(output, key)) continue;
      const forced = this.alwaysEmit.has(key) && output[key] !== undefined;
      if (forced || output[key] !== this.output[format][key]) {
        let value = output[key];
        // For fields: if the value is an object (and not a Date), stringify it.
        if (value !== null && typeof value === 'object' && !(value instanceof Date)) {
          changedFields[key] = JSON.stringify(value);
        } else {
          changedFields[key] = value;
        }
      }
    }

    // Update the saved output state.
    this.output[format] = { ...this.output[format], ...changedFields };

    return changedFields;
  }

  formatMsg(output, config, format) {
      let msg = {};

      // Compare output with last output and only include changed values
      const changedFields = this.checkForChanges(output,format);

      if (Object.keys(changedFields).length > 0) {
        // Fall back to `<softwareType>_<id>` when `general.name` is unset —
        // the original convention before name became a registered config field.
        const measurement = config.general.name
          || `${config.functionality?.softwareType}_${config.general.id}`;
        const flatTags = this.flattenTags(this.extractRelevantConfig(config));
        const formatterName = this.resolveFormatterName(config, format);
        const formatter = getFormatter(formatterName);
        const payload = formatter.format(measurement, {
          fields: changedFields,
          tags: flatTags,
          config,
          channel: format,
        });
        msg = this.wrapMessage(measurement, payload);
        return msg;
      }
      return null;
  }

  resolveFormatterName(config, channel) {
    const outputConfig = config.output || {};
    if (channel === 'process') {
      return outputConfig.process || 'process';
    }
    if (channel === 'influxdb') {
      return outputConfig.dbase || 'influxdb';
    }
    return outputConfig[channel] || channel;
  }

  wrapMessage(measurement, payload) {
    return {
      topic: measurement,
      payload,
    };
  }

  flattenTags(obj) {
    const result = {};
    for (const key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        const value = obj[key];
        // Skip tags that carry no information. When a config field is unset,
        // extractRelevantConfig hands us `undefined`; stringifying that wrote
        // literal `category="undefined"` / `geoLocation="undefined"` tags that
        // clutter every Grafana legend and needlessly inflate tag cardinality.
        // Drop null / undefined / empty-string before they reach InfluxDB.
        if (value === null || value === undefined || value === '') continue;
        if (typeof value === 'object' && !(value instanceof Date)) {
          // Recursively flatten the nested object.
          const flatChild = this.flattenTags(value);
          for (const childKey in flatChild) {
            if (Object.prototype.hasOwnProperty.call(flatChild, childKey)) {
              result[`${key}_${childKey}`] = String(flatChild[childKey]);
            }
          }
        } else {
          // InfluxDB tags must be strings.
          result[key] = String(value);
        }
      }
    }
    return result;
  }

  extractRelevantConfig(config) {

    return {
      // general properties
      id: config.general?.id,
      // functionality properties
      softwareType: config.functionality?.softwareType,
      role: config.functionality?.role,
      positionVsParent: config.functionality?.positionVsParent,
      // asset properties (exclude machineCurve)
      uuid: config.asset?.uuid,
      tagcode: config.asset?.tagCode || config.asset?.tagcode,
      geoLocation: config.asset?.geoLocation,
      category: config.asset?.category,
      type: config.asset?.type,
      model: config.asset?.model,
      unit: config.general?.unit,
    };
  }
}

module.exports = OutputUtils;
