const EventEmitter = require('events');
const defaultConfig = require('./nrmseConfig.json');
const ConfigUtils = require('../helper/configUtils');

class ErrorMetrics {
  constructor(config = {}, logger) {
    this.emitter = new EventEmitter();
    this.configUtils = new ConfigUtils(defaultConfig);
    this.config = this.configUtils.initConfig(config);
    this.logger = logger;

    this.metricState = new Map();
    this.legacyMetricId = 'default';

    // Backward-compatible fields retained for existing callers/tests.
    this.cumNRMSD = 0;
    this.cumCount = 0;
  }

  registerMetric(metricId, profile = {}) {
    const key = String(metricId || this.legacyMetricId);
    const state = this._ensureMetricState(key);
    state.profile = { ...state.profile, ...profile };
    return state.profile;
  }

  resetMetric(metricId = this.legacyMetricId) {
    this.metricState.delete(String(metricId));
    if (metricId === this.legacyMetricId) {
      this.cumNRMSD = 0;
      this.cumCount = 0;
    }
  }

  getMetricState(metricId = this.legacyMetricId) {
    return this.metricState.get(String(metricId)) || null;
  }

  meanSquaredError(predicted, measured, options = {}) {
    const { p, m } = this._validateSeries(predicted, measured, options);
    let sumSqError = 0;
    for (let i = 0; i < p.length; i += 1) {
      const err = p[i] - m[i];
      sumSqError += err * err;
    }
    return sumSqError / p.length;
  }

  rootMeanSquaredError(predicted, measured, options = {}) {
    return Math.sqrt(this.meanSquaredError(predicted, measured, options));
  }

  normalizedRootMeanSquaredError(predicted, measured, processMin, processMax, options = {}) {
    const range = Number(processMax) - Number(processMin);
    if (!Number.isFinite(range) || range <= 0) {
      this._failOrLog(
        `Invalid process range: processMax (${processMax}) must be greater than processMin (${processMin}).`,
        options
      );
      return NaN;
    }
    const rmse = this.rootMeanSquaredError(predicted, measured, options);
    return rmse / range;
  }

  normalizeUsingRealtime(predicted, measured, options = {}) {
    const { p, m } = this._validateSeries(predicted, measured, options);
    const realtimeMin = Math.min(Math.min(...p), Math.min(...m));
    const realtimeMax = Math.max(Math.max(...p), Math.max(...m));
    const range = realtimeMax - realtimeMin;
    if (!Number.isFinite(range) || range <= 0) {
      throw new Error('Invalid process range: processMax must be greater than processMin.');
    }
    const rmse = this.rootMeanSquaredError(p, m, options);
    return rmse / range;
  }

  longTermNRMSD(input, metricId = this.legacyMetricId, options = {}) {
    const metricKey = String(metricId || this.legacyMetricId);
    const state = this._ensureMetricState(metricKey);
    const profile = this._resolveProfile(metricKey, options);
    const value = Number(input);
    if (!Number.isFinite(value)) {
      this._failOrLog(`longTermNRMSD input must be finite. Received: ${input}`, options);
      return 0;
    }

    // Keep backward compatibility if callers manipulate cumCount/cumNRMSD directly.
    if (metricKey === this.legacyMetricId && (state.sampleCount !== this.cumCount || state.longTermEwma !== this.cumNRMSD)) {
      state.sampleCount = Number(this.cumCount) || 0;
      state.longTermEwma = Number(this.cumNRMSD) || 0;
    }

    state.sampleCount += 1;
    const alpha = profile.ewmaAlpha;
    state.longTermEwma = state.sampleCount === 1 ? value : (alpha * value) + ((1 - alpha) * state.longTermEwma);

    if (metricKey === this.legacyMetricId) {
      this.cumCount = state.sampleCount;
      this.cumNRMSD = state.longTermEwma;
    }

    if (state.sampleCount < profile.minSamplesForLongTerm) {
      return 0;
    }
    return state.longTermEwma;
  }

  detectImmediateDrift(nrmse) {
    const thresholds = this.config.thresholds;
    if (nrmse > thresholds.NRMSE_HIGH) return { level: 3, feedback: 'High immediate drift detected' };
    if (nrmse > thresholds.NRMSE_MEDIUM) return { level: 2, feedback: 'Medium immediate drift detected' };
    if (nrmse > thresholds.NRMSE_LOW) return { level: 1, feedback: 'Low immediate drift detected' };
    return { level: 0, feedback: 'No drift detected' };
  }

  detectLongTermDrift(longTermNRMSD) {
    const thresholds = this.config.thresholds;
    const absValue = Math.abs(longTermNRMSD);
    if (absValue > thresholds.LONG_TERM_HIGH) return { level: 3, feedback: 'High long-term drift detected' };
    if (absValue > thresholds.LONG_TERM_MEDIUM) return { level: 2, feedback: 'Medium long-term drift detected' };
    if (absValue > thresholds.LONG_TERM_LOW) return { level: 1, feedback: 'Low long-term drift detected' };
    return { level: 0, feedback: 'No drift detected' };
  }

  detectDrift(nrmse, longTermNRMSD) {
    const ImmDrift = this.detectImmediateDrift(nrmse);
    const LongTermDrift = this.detectLongTermDrift(longTermNRMSD);
    return { ImmDrift, LongTermDrift };
  }

  assessDrift(predicted, measured, processMin, processMax, options = {}) {
    const metricKey = String(options.metricId || this.legacyMetricId);
    const profile = this._resolveProfile(metricKey, options);
    const strict = this._resolveStrict(options, profile);

    const aligned = this._alignSeriesByTimestamp(predicted, measured, options, profile);
    if (!aligned.valid) {
      if (strict) {
        throw new Error(aligned.reason);
      }
      return this._invalidAssessment(metricKey, aligned.reason);
    }

    const nrmse = this.normalizedRootMeanSquaredError(
      aligned.predicted,
      aligned.measured,
      processMin,
      processMax,
      { ...options, strictValidation: strict }
    );
    if (!Number.isFinite(nrmse)) {
      if (strict) {
        throw new Error('NRMSE calculation returned a non-finite value.');
      }
      return this._invalidAssessment(metricKey, 'non_finite_nrmse');
    }

    const longTermNRMSD = this.longTermNRMSD(nrmse, metricKey, { ...options, strictValidation: strict });
    const driftAssessment = this.detectDrift(nrmse, longTermNRMSD);
    const state = this._ensureMetricState(metricKey);
    state.lastResult = {
      nrmse,
      longTermNRMSD,
      immediateLevel: driftAssessment.ImmDrift.level,
      immediateFeedback: driftAssessment.ImmDrift.feedback,
      longTermLevel: driftAssessment.LongTermDrift.level,
      longTermFeedback: driftAssessment.LongTermDrift.feedback,
      valid: true,
      metricId: metricKey,
      sampleCount: state.sampleCount,
      longTermReady: state.sampleCount >= profile.minSamplesForLongTerm,
      flags: [],
    };
    return state.lastResult;
  }

  assessPoint(metricId, predictedValue, measuredValue, options = {}) {
    const metricKey = String(metricId || this.legacyMetricId);
    const profile = this._resolveProfile(metricKey, options);
    const state = this._ensureMetricState(metricKey);
    const strict = this._resolveStrict(options, profile);

    const p = Number(predictedValue);
    const m = Number(measuredValue);
    if (!Number.isFinite(p) || !Number.isFinite(m)) {
      const reason = `assessPoint requires finite numbers. predicted=${predictedValue}, measured=${measuredValue}`;
      if (strict) {
        throw new Error(reason);
      }
      return this._invalidAssessment(metricKey, reason);
    }

    const predictedTimestamp = Number(options.predictedTimestamp ?? options.timestamp ?? Date.now());
    const measuredTimestamp = Number(options.measuredTimestamp ?? options.timestamp ?? Date.now());
    const delta = Math.abs(predictedTimestamp - measuredTimestamp);
    if (delta > profile.alignmentToleranceMs) {
      const reason = `Sample timestamp delta (${delta} ms) exceeds tolerance (${profile.alignmentToleranceMs} ms)`;
      if (strict) {
        throw new Error(reason);
      }
      return this._invalidAssessment(metricKey, reason);
    }

    state.predicted.push(p);
    state.measured.push(m);
    state.predictedTimestamps.push(predictedTimestamp);
    state.measuredTimestamps.push(measuredTimestamp);

    while (state.predicted.length > profile.windowSize) state.predicted.shift();
    while (state.measured.length > profile.windowSize) state.measured.shift();
    while (state.predictedTimestamps.length > profile.windowSize) state.predictedTimestamps.shift();
    while (state.measuredTimestamps.length > profile.windowSize) state.measuredTimestamps.shift();

    if (state.predicted.length < 2 || state.measured.length < 2) {
      return this._invalidAssessment(metricKey, 'insufficient_samples');
    }

    let processMin = Number(options.processMin);
    let processMax = Number(options.processMax);
    if (!Number.isFinite(processMin) || !Number.isFinite(processMax) || processMax <= processMin) {
      processMin = Math.min(...state.predicted, ...state.measured);
      processMax = Math.max(...state.predicted, ...state.measured);
      if (!Number.isFinite(processMin) || !Number.isFinite(processMax) || processMax <= processMin) {
        processMin = 0;
        processMax = 1;
      }
    }

    return this.assessDrift(state.predicted, state.measured, processMin, processMax, {
      ...options,
      metricId: metricKey,
      strictValidation: strict,
      predictedTimestamps: state.predictedTimestamps,
      measuredTimestamps: state.measuredTimestamps,
    });
  }

  _ensureMetricState(metricId) {
    const key = String(metricId || this.legacyMetricId);
    if (!this.metricState.has(key)) {
      this.metricState.set(key, {
        predicted: [],
        measured: [],
        predictedTimestamps: [],
        measuredTimestamps: [],
        sampleCount: 0,
        longTermEwma: 0,
        profile: {},
        lastResult: null,
      });
    }
    return this.metricState.get(key);
  }

  _resolveProfile(metricId, options = {}) {
    const state = this._ensureMetricState(metricId);
    const base = this.config.processing || {};
    return {
      windowSize: Number(options.windowSize ?? state.profile.windowSize ?? base.windowSize ?? 50),
      minSamplesForLongTerm: Number(options.minSamplesForLongTerm ?? state.profile.minSamplesForLongTerm ?? base.minSamplesForLongTerm ?? 100),
      ewmaAlpha: Number(options.ewmaAlpha ?? state.profile.ewmaAlpha ?? base.ewmaAlpha ?? 0.1),
      alignmentToleranceMs: Number(options.alignmentToleranceMs ?? state.profile.alignmentToleranceMs ?? base.alignmentToleranceMs ?? 2000),
      strictValidation: Boolean(options.strictValidation ?? state.profile.strictValidation ?? base.strictValidation ?? true),
    };
  }

  _resolveStrict(options = {}, profile = null) {
    if (Object.prototype.hasOwnProperty.call(options, 'strictValidation')) {
      return Boolean(options.strictValidation);
    }
    if (profile && Object.prototype.hasOwnProperty.call(profile, 'strictValidation')) {
      return Boolean(profile.strictValidation);
    }
    return Boolean(this.config.processing?.strictValidation ?? true);
  }

  _validateSeries(predicted, measured, options = {}) {
    if (!Array.isArray(predicted) || !Array.isArray(measured)) {
      this._failOrLog('predicted and measured must be arrays.', options);
      return { p: [], m: [] };
    }
    if (!predicted.length || !measured.length) {
      this._failOrLog('predicted and measured arrays must not be empty.', options);
      return { p: [], m: [] };
    }
    if (predicted.length !== measured.length) {
      this._failOrLog('predicted and measured arrays must have the same length.', options);
      return { p: [], m: [] };
    }

    const p = predicted.map(Number);
    const m = measured.map(Number);
    const hasBad = p.some((v) => !Number.isFinite(v)) || m.some((v) => !Number.isFinite(v));
    if (hasBad) {
      this._failOrLog('predicted and measured arrays must contain finite numeric values.', options);
      return { p: [], m: [] };
    }
    return { p, m };
  }

  _alignSeriesByTimestamp(predicted, measured, options = {}, profile = null) {
    const strict = this._resolveStrict(options, profile);
    const tolerance = Number(options.alignmentToleranceMs ?? profile?.alignmentToleranceMs ?? 2000);
    const predictedTimestamps = Array.isArray(options.predictedTimestamps) ? options.predictedTimestamps.map(Number) : null;
    const measuredTimestamps = Array.isArray(options.measuredTimestamps) ? options.measuredTimestamps.map(Number) : null;

    if (!predictedTimestamps || !measuredTimestamps) {
      if (!Array.isArray(predicted) || !Array.isArray(measured)) {
        return { valid: false, reason: 'predicted and measured must be arrays.' };
      }
      if (predicted.length !== measured.length) {
        const reason = `Series length mismatch without timestamps: predicted=${predicted.length}, measured=${measured.length}`;
        if (strict) return { valid: false, reason };
        const n = Math.min(predicted.length, measured.length);
        if (n < 2) return { valid: false, reason };
        return {
          valid: true,
          predicted: predicted.slice(-n).map(Number),
          measured: measured.slice(-n).map(Number),
          flags: ['length_mismatch_realigned'],
        };
      }
      try {
        const { p, m } = this._validateSeries(predicted, measured, { ...options, strictValidation: true });
        return { valid: true, predicted: p, measured: m, flags: [] };
      } catch (error) {
        return { valid: false, reason: error.message };
      }
    }

    if (!Array.isArray(predicted) || !Array.isArray(measured)) {
      return { valid: false, reason: 'predicted and measured must be arrays.' };
    }
    if (predicted.length !== predictedTimestamps.length || measured.length !== measuredTimestamps.length) {
      return { valid: false, reason: 'timestamp arrays must match value-array lengths.' };
    }

    const predictedSamples = predicted
      .map((v, i) => ({ value: Number(v), ts: predictedTimestamps[i] }))
      .filter((s) => Number.isFinite(s.value) && Number.isFinite(s.ts))
      .sort((a, b) => a.ts - b.ts);
    const measuredSamples = measured
      .map((v, i) => ({ value: Number(v), ts: measuredTimestamps[i] }))
      .filter((s) => Number.isFinite(s.value) && Number.isFinite(s.ts))
      .sort((a, b) => a.ts - b.ts);

    const alignedPredicted = [];
    const alignedMeasured = [];
    let i = 0;
    let j = 0;
    while (i < predictedSamples.length && j < measuredSamples.length) {
      const p = predictedSamples[i];
      const m = measuredSamples[j];
      const delta = p.ts - m.ts;
      if (Math.abs(delta) <= tolerance) {
        alignedPredicted.push(p.value);
        alignedMeasured.push(m.value);
        i += 1;
        j += 1;
      } else if (delta < 0) {
        i += 1;
      } else {
        j += 1;
      }
    }

    if (alignedPredicted.length < 2 || alignedMeasured.length < 2) {
      return { valid: false, reason: 'insufficient aligned samples after timestamp matching.' };
    }

    return { valid: true, predicted: alignedPredicted, measured: alignedMeasured, flags: [] };
  }

  _invalidAssessment(metricId, reason) {
    return {
      nrmse: NaN,
      longTermNRMSD: 0,
      immediateLevel: 0,
      immediateFeedback: 'Drift assessment unavailable',
      longTermLevel: 0,
      longTermFeedback: 'Drift assessment unavailable',
      valid: false,
      metricId: String(metricId || this.legacyMetricId),
      sampleCount: this._ensureMetricState(metricId).sampleCount,
      longTermReady: false,
      flags: [reason],
    };
  }

  _failOrLog(message, options = {}) {
    const strict = this._resolveStrict(options);
    if (strict) {
      throw new Error(message);
    }
    this.logger?.warn?.(message);
  }
}

module.exports = ErrorMetrics;
