/**
 * Disk Settings Bootstrap Loader (DC-048)
 *
 * Reads /app/data/disk-settings.json (resolved via platform-paths.dataDir)
 * at boot time and rehydrates process.env values for engine settings that
 * were previously captured only via in-memory process.env writes on the
 * POST /api/v1/disk-settings route.
 *
 * Why this exists:
 *   health-checker.js, audit-logger.js, and backups.js all read
 *   `process.env.HEALTH_*` / `process.env.AUDIT_MAX_ENTRIES` /
 *   `process.env.BACKUP_MAX_STORAGE_BYTES` at MODULE LOAD. The previous
 *   POST handler only wrote those values to process.env at runtime, so
 *   any value persisted to disk-settings.json was silently discarded on
 *   every container restart. Users who saved "Health Retention = 7 days"
 *   would see 30 days come back at the next boot.
 *
 * Behavior:
 *   - Only sets a key if process.env[key] is already UNDEFINED. Explicit
 *     container / compose env still wins on cold boot (so operators can
 *     override via the env without editing disk-settings.json).
 *   - Logs a single INFO line at boot summarizing what was rehydrated.
 *   - Never throws. A missing or malformed disk-settings.json is logged
 *     and ignored — the engine falls back to its compiled-in defaults.
 *
 * Order of operations in src/app.js:
 *   require('./config/disk-settings-loader')();   // ← MUST be before any
 *   const healthChecker = require('./monitoring/health-checker');         //  engine module
 *   const auditLogger   = require('./security/audit-logger');            //  that reads env
 *
 * Mapping table (mirrors the POST handler in routes/disk-settings.js):
 *   disk-settings.json field  →  process.env key
 *   healthCheckInterval       →  HEALTH_CHECK_INTERVAL      (ms)
 *   healthMaxEntries          →  HEALTH_MAX_ENTRIES         (entries)
 *   healthRetentionDays       →  HEALTH_HISTORY_RETENTION   (days)
 *   statsMaxEntries           →  CONTAINER_STATS_MAX_ENTRIES(entries; reserved, no engine consumer yet)
 *   auditMaxEntries           →  AUDIT_MAX_ENTRIES          (entries)
 *   backupMaxStorageBytes     →  BACKUP_MAX_STORAGE_BYTES   (bytes)
 *
 * Returns an object describing what was applied — useful for tests + boot logs.
 */

'use strict';

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

const ENV_MAP = Object.freeze({
  healthCheckInterval:   'HEALTH_CHECK_INTERVAL',
  healthMaxEntries:      'HEALTH_MAX_ENTRIES',
  healthRetentionDays:   'HEALTH_HISTORY_RETENTION',
  statsMaxEntries:       'CONTAINER_STATS_MAX_ENTRIES',
  auditMaxEntries:       'AUDIT_MAX_ENTRIES',
  backupMaxStorageBytes: 'BACKUP_MAX_STORAGE_BYTES',
});

// Numeric fields MUST be coerced to integers; a stray string in disk-settings.json
// would otherwise land in process.env as a string and the next
// parseInt(process.env.X || 'N') in the engine would silently fall back to N
// when the value is unparseable. Defensive coercion here keeps the engine
// consistent with the values the user just saved.
const NUMERIC_FIELDS = Object.freeze([
  'healthCheckInterval',
  'healthMaxEntries',
  'healthRetentionDays',
  'statsMaxEntries',
  'auditMaxEntries',
  'backupMaxStorageBytes',
]);

function loadPersistedSettings(dataDir) {
  if (!dataDir) return null;
  const settingsFile = path.join(dataDir, 'disk-settings.json');
  if (!fs.existsSync(settingsFile)) return null;
  try {
    const raw = fs.readFileSync(settingsFile, 'utf8');
    const parsed = JSON.parse(raw);
    if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
      return parsed;
    }
    return null;
  } catch (err) {
    // Log + swallow. The engine's compiled-in defaults are the safe fallback.
    // Do NOT re-throw — a malformed settings file must not stop the API from booting.
    process.stderr.write(
      `[disk-settings-loader] WARN: failed to parse ${settingsFile}: ${err.message}; using engine defaults\n`,
    );
    return null;
  }
}

/**
 * Resolve dataDir WITHOUT importing platform-paths at the top level — the loader
 * is required very early in app.js, before platform-paths has been fully loaded
 * by sibling modules. A local require is safe (it's idempotent and side-effect
 * free — platform-paths is pure constants).
 */
function resolveDataDir() {
  try {
    // eslint-disable-next-line global-require
    const platformPaths = require('../../platform-paths');
    return platformPaths.dataDir;
  } catch {
    return process.env.DATA_DIR || '/etc/dashcaddy';
  }
}

function applyToEnv(persisted, { logger } = {}) {
  const applied = [];
  const skipped = [];
  if (!persisted) return { applied, skipped };

  for (const [field, envKey] of Object.entries(ENV_MAP)) {
    if (!Object.prototype.hasOwnProperty.call(persisted, field)) continue;
    let value = persisted[field];
    if (value === null || value === undefined || value === '') continue;

    if (NUMERIC_FIELDS.includes(field)) {
      const n = Number(value);
      if (!Number.isFinite(n)) {
        skipped.push({ field, envKey, reason: 'non-numeric' });
        continue;
      }
      value = String(Math.trunc(n));
    } else {
      value = String(value);
    }

    if (process.env[envKey] !== undefined && process.env[envKey] !== '') {
      // Explicit env wins over persisted file. This is the only way operators
      // can override a saved value without first deleting the file.
      skipped.push({ field, envKey, reason: 'env-already-set' });
      continue;
    }

    process.env[envKey] = value;
    applied.push({ field, envKey, value });
  }

  return { applied, skipped };
}

let hasRun = false;

/**
 * Run the loader once. Idempotent — second invocation is a no-op so test
 * suites that `jest.resetModules()` between cases don't re-apply values
 * from a stale persisted file across tests.
 */
function applyDiskSettings(options = {}) {
  if (hasRun) return { applied: [], skipped: [], alreadyRun: true };
  hasRun = true;

  const dataDir = options.dataDir || resolveDataDir();
  const persisted = loadPersistedSettings(dataDir);
  const { applied, skipped } = applyToEnv(persisted, options);

  const summary = {
    applied,
    skipped,
    source: persisted ? path.join(dataDir, 'disk-settings.json') : null,
    alreadyRun: false,
  };

  if (applied.length > 0) {
    const msg = `[disk-settings-loader] rehydrated ${applied.length} setting(s) from ${summary.source}: `
      + applied.map((a) => `${a.field}=${a.value}`).join(', ');
    // Always emit to stderr at boot — operators need to see rehydration
    // regardless of whether the app logger is wired yet (the loader runs
    // at module-load time, before app.js createApp() builds the logger).
    if (options.logger) options.logger.info(msg);
    else process.stderr.write(msg + '\n');
  } else if (skipped.length === 0 && !persisted) {
    // No persisted file: silent. (No boot noise when nothing to do.)
  } else if (skipped.length > 0) {
    const msg = `[disk-settings-loader] skipped ${skipped.length} setting(s) (env-already-set or non-numeric): `
      + skipped.map((s) => `${s.envKey}(${s.reason})`).join(', ');
    if (options.logger) options.logger.info(msg);
    else process.stderr.write(msg + '\n');
  }

  return summary;
}

// Exposed for tests that need to reset the once-guard between cases.
function _resetForTesting() {
  hasRun = false;
}

module.exports = applyDiskSettings;
module.exports.applyDiskSettings = applyDiskSettings;
module.exports._resetForTesting = _resetForTesting;
module.exports.ENV_MAP = ENV_MAP;
