const convert = require('../convert/index.js');

// Map MeasurementContainer measurement-type names to convert-module
// "measure" families. Mirrors MeasurementContainer.measureMap so a policy
// declared with the type names domains use ('flow', 'pressure', ...) can be
// validated against the same convert-module families MeasurementContainer
// uses internally.
const TYPE_TO_MEASURE = Object.freeze({
  pressure: 'pressure',
  atmpressure: 'pressure',
  flow: 'volumeFlowRate',
  power: 'power',
  hydraulicpower: 'power',
  reactivepower: 'reactivePower',
  apparentpower: 'apparentPower',
  temperature: 'temperature',
  volume: 'volume',
  length: 'length',
  mass: 'mass',
  energy: 'energy',
  reactiveenergy: 'reactiveEnergy',
});

const DEFAULT_REQUIRED_TYPES = Object.freeze(['flow', 'pressure', 'power', 'temperature']);

class UnitPolicy {
  constructor({ canonical, output, curve, requireUnitForTypes, logger } = {}) {
    this._canonical = freezeShallow(canonical);
    this._output = freezeShallow(output);
    this._curve = curve ? freezeShallow(curve) : null;
    this._requireUnitForTypes = Object.freeze(
      Array.isArray(requireUnitForTypes) ? [...requireUnitForTypes] : [...DEFAULT_REQUIRED_TYPES]
    );
    this._logger = logger || null;
    // Warn-once memo: same (label, candidate) pair only logs the first time.
    this._warned = new Set();

    // Dual-shape accessors: each of canonical/output/curve is BOTH a method
    // (legacy `policy.canonical('flow')`) AND a frozen property bag
    // (`policy.canonical.flow`). The function carries the frozen map's own
    // properties via Object.defineProperty so consumers can pick either form.
    this.canonical = makeAccessor(this._canonical);
    this.output = makeAccessor(this._output);
    this.curve = makeAccessor(this._curve || {});
  }

  static declare(spec = {}) {
    if (!spec.canonical || typeof spec.canonical !== 'object') {
      throw new Error('UnitPolicy.declare: canonical units map is required');
    }
    if (!spec.output || typeof spec.output !== 'object') {
      throw new Error('UnitPolicy.declare: output units map is required');
    }
    return new UnitPolicy(spec);
  }

  setLogger(logger) {
    this._logger = logger || null;
    return this;
  }

  /**
   * Validate a user-supplied unit string against `expectedMeasure`. On any
   * mismatch return `fallback` and warn once for this (label, candidate)
   * pair. On success return the trimmed candidate.
   */
  resolve(candidate, expectedMeasure, fallback, label = 'unit') {
    const fallbackUnit = String(fallback || '').trim();
    const raw = typeof candidate === 'string' ? candidate.trim() : '';
    if (!raw) return fallbackUnit;

    try {
      const desc = convert().describe(raw);
      const measure = resolveMeasure(expectedMeasure);
      if (measure && desc.measure !== measure) {
        throw new Error(`expected ${measure} but got ${desc.measure}`);
      }
      return raw;
    } catch (error) {
      this._warnOnce(label, raw, `Invalid ${label} unit '${raw}' (${error.message}). Falling back to '${fallbackUnit}'.`);
      return fallbackUnit;
    }
  }

  /**
   * Strict numeric conversion. Throws if value is not finite.
   * No-ops (still returning a Number) when from/to are missing or equal.
   */
  convert(value, fromUnit, toUnit, contextLabel = 'unit conversion') {
    const numeric = Number(value);
    if (!Number.isFinite(numeric)) {
      throw new Error(`${contextLabel}: value '${value}' is not finite`);
    }
    if (!fromUnit || !toUnit || fromUnit === toUnit) return numeric;
    return convert(numeric).from(fromUnit).to(toUnit);
  }

  /**
   * Returns the option bag for `new MeasurementContainer(options, logger)`.
   * Exact shape required by MeasurementContainer; see
   * src/measurements/MeasurementContainer.js constructor.
   */
  containerOptions() {
    const defaultUnits = { ...this._output };
    const preferredUnits = { ...this._output };
    const canonicalUnits = { ...this._canonical };
    return {
      defaultUnits,
      preferredUnits,
      canonicalUnits,
      storeCanonical: true,
      strictUnitValidation: true,
      throwOnInvalidUnit: true,
      requireUnitForTypes: [...this._requireUnitForTypes],
    };
  }

  _warnOnce(label, candidate, message) {
    const key = `${label}::${candidate}`;
    if (this._warned.has(key)) return;
    this._warned.add(key);
    if (this._logger && typeof this._logger.warn === 'function') {
      this._logger.warn(message);
    } else {
      // Last-resort fallback so misconfigurations don't go silent in
      // domains that haven't wired a logger yet.
      console.warn(message);
    }
  }
}

function freezeShallow(obj) {
  return Object.freeze({ ...(obj || {}) });
}

// Build a function that doubles as a frozen property bag. `accessor(type)`
// returns the unit for that type (legacy method shape). `accessor.flow` etc.
// return the unit directly (new property shape). Own-properties are
// non-writable, non-configurable; attempts to assign / delete / redefine
// throw in strict mode — proving the bag is genuinely frozen.
function makeAccessor(map) {
  const fn = (type) => map[type] || null;
  for (const key of Object.keys(map)) {
    Object.defineProperty(fn, key, {
      value: map[key],
      writable: false,
      enumerable: true,
      configurable: false,
    });
  }
  return Object.freeze(fn);
}

// Accepts either the convert-module measure family ('volumeFlowRate') or one
// of our type names ('flow') and returns the convert-module measure.
function resolveMeasure(expected) {
  if (!expected) return null;
  const lower = String(expected).trim().toLowerCase();
  if (TYPE_TO_MEASURE[lower]) return TYPE_TO_MEASURE[lower];
  return expected;
}

module.exports = UnitPolicy;
