/**
 * ChildRouter — declarative parent-side child registration & event routing.
 *
 * Replaces the per-node `registerChild` switch + manual
 * `child.measurements.emitter.on(...)` wiring repeated in pumpingStation,
 * rotatingMachine and machineGroupControl.
 *
 * See CONTRACTS.md §5. Built on top of `childRegistrationUtils`, which
 * already canonicalises softwareType (e.g. rotatingmachine → machine).
 *
 * Wildcard / partial-filter subscriptions enumerate every concrete
 * `<type>.<variant>.<position>` event name the filter matches and attach a
 * plain `emitter.on(...)` per combination. No emit patching — multi-parent
 * stacks compose cleanly because each parent owns its own listeners.
 */
const { POSITION_VALUES } = require('../constants/positions');

const SOFTWARE_TYPE_ALIASES = {
  rotatingmachine: 'machine',
  machinegroupcontrol: 'machinegroup',
};

// Canonical measurement-type set used to enumerate position-only and
// match-everything filters. Sourced from MeasurementContainer.measureMap
// plus the EVOLV-specific synthetic types the nodes routinely emit
// (level / volumePercent / efficiency / Ncog / netFlowRate). Keep in sync
// with MeasurementContainer if new types land there.
const KNOWN_TYPES = Object.freeze([
  'flow',
  'pressure',
  'atmPressure',
  'power',
  'hydraulicPower',
  'reactivePower',
  'apparentPower',
  'temperature',
  'level',
  'volume',
  'volumePercent',
  'length',
  'mass',
  'energy',
  'reactiveEnergy',
  'efficiency',
  'Ncog',
  'netFlowRate',
]);

function canonicalType(rawType) {
  const t = String(rawType || '').toLowerCase();
  return SOFTWARE_TYPE_ALIASES[t] || t;
}

function lowerPosition(p) {
  return String(p).toLowerCase();
}

class ChildRouter {
  constructor(domain) {
    this.domain = domain;
    this.logger = domain?.logger || null;

    this._registerSubs = new Map();      // softwareType -> Array<fn>
    this._measurementSubs = new Map();   // softwareType -> Array<{filter, fn}>
    this._predictionSubs = new Map();    // softwareType -> Array<{filter, fn}>

    // Every plain emitter listener we attach, so tearDown can remove them.
    this._listeners = [];
  }

  // ── declaration API ────────────────────────────────────────────────

  onRegister(softwareType, fn) {
    if (typeof fn !== 'function') {
      throw new TypeError('ChildRouter.onRegister: fn must be a function');
    }
    const key = canonicalType(softwareType);
    if (!this._registerSubs.has(key)) this._registerSubs.set(key, []);
    this._registerSubs.get(key).push(fn);
    return this;
  }

  onMeasurement(softwareType, filter, fn) {
    return this._addEventSub(this._measurementSubs, softwareType, filter, fn, 'onMeasurement');
  }

  onPrediction(softwareType, filter, fn) {
    return this._addEventSub(this._predictionSubs, softwareType, filter, fn, 'onPrediction');
  }

  _addEventSub(table, softwareType, filter, fn, label) {
    if (typeof filter === 'function' && fn === undefined) {
      fn = filter;
      filter = {};
    }
    if (typeof fn !== 'function') {
      throw new TypeError(`ChildRouter.${label}: fn must be a function`);
    }
    const key = canonicalType(softwareType);
    if (!table.has(key)) table.set(key, []);
    table.get(key).push({ filter: filter || {}, fn });
    return this;
  }

  // ── dispatch ──────────────────────────────────────────────────────

  dispatchRegister(child, softwareType) {
    const key = canonicalType(softwareType);

    const regHandlers = this._registerSubs.get(key) || [];
    for (const fn of regHandlers) {
      try { fn.call(this.domain, child, key); }
      catch (err) { this._logHandlerError('onRegister', key, err); }
    }

    const emitter = child?.measurements?.emitter;
    if (!emitter || typeof emitter.on !== 'function') return;

    this._attachVariantListeners(child, key, emitter, 'measured', this._measurementSubs);
    this._attachVariantListeners(child, key, emitter, 'predicted', this._predictionSubs);
  }

  _attachVariantListeners(child, key, emitter, variant, table) {
    const subs = table.get(key) || [];
    for (const { filter, fn } of subs) {
      const types = filter.type ? [filter.type] : KNOWN_TYPES;
      const positions = filter.position ? [lowerPosition(filter.position)] : POSITION_VALUES.map(lowerPosition);
      const handlerLabel = variant === 'measured' ? 'onMeasurement' : 'onPrediction';

      for (const type of types) {
        for (const pos of positions) {
          const eventName = `${type}.${variant}.${pos}`;
          const listener = (data) => this._invoke(fn, data, child, handlerLabel);
          emitter.on(eventName, listener);
          this._listeners.push({ emitter, eventName, listener });
        }
      }
    }
  }

  _invoke(fn, eventData, child, handlerLabel) {
    try { fn.call(this.domain, eventData, child); }
    catch (err) { this._logHandlerError(handlerLabel, '', err); }
  }

  _logHandlerError(kind, key, err) {
    if (this.logger?.warn) {
      this.logger.warn(`ChildRouter ${kind}${key ? `[${key}]` : ''} handler threw: ${err?.message || err}`);
    }
  }

  // ── teardown ──────────────────────────────────────────────────────

  tearDown() {
    for (const { emitter, eventName, listener } of this._listeners) {
      if (typeof emitter.off === 'function') emitter.off(eventName, listener);
      else if (typeof emitter.removeListener === 'function') emitter.removeListener(eventName, listener);
    }
    this._listeners = [];
  }
}

module.exports = ChildRouter;
module.exports.KNOWN_TYPES = KNOWN_TYPES;
