/**
 * engine/features.js — Feature flag registry. Recipe in CLAUDE.md → "Feature Flags".
 */

// Entry shape: id → { description, default: bool, addedIn?: version, expires?: ISO-date, requiredCcRuntime?: string }
//
// Optional fields:
//   `requiredCcRuntime` (e.g. 'copilot') signals that the flag only takes
//   effect when `resolveCcCli(engine)` matches. The dashboard uses this to
//   render the toggle as disabled with a tooltip when the configured CC
//   runtime is incompatible. Engine code still has to gate explicitly via
//   `isFeatureOn` + a runtime check — `requiredCcRuntime` is a UX hint only.
//
// REGISTRY GROWTH POLICY — DO NOT DELETE THE FRAMEWORK.
//
// The surrounding framework (isFeatureOn, listFeatures, hasFeature, env-var
// override resolver, expiration timestamps) is load-bearing: dashboard.js
// boot wires it into /api/features (list), /api/features/toggle, and the
// `window.MINIONS_FEATURES` client bootstrap. Deleting the registry — even
// if it became empty — would break dashboard startup and the experimental-flags UI.
//
// New flags belong here. Register them in this object instead of removing
// entries. Keep at least one entry so the framework remains demonstrably wired.
//
// Decision logged in the 2026-05-13 daily architecture & bug review meeting
// (knowledge/architecture/2026-05-13-ripley-meeting-conclusion-daily-architecture-bug-review-2.md,
// PR-C, Option B — keep framework, document the registry).
const FEATURES = {
  'slim-ux': {
    description: 'Experimental redesigned dashboard surface (placeholder gate; Slim UX)',
    default: false,
    addedIn: '0.1.1757',
    expires: '2026-11-01',
  },
  // slim-ux-promo — gates the Slim UX *entry point*, not Slim UX itself. When
  // off (default) the "Try new Slim UX" button is hidden on the classic
  // dashboard header AND the first-visit welcome popup is suppressed inside
  // Slim UX. Slim UX stays reachable via the `slim-ux` flag / Settings; this
  // only controls whether it's advertised to users.
  'slim-ux-promo': {
    description: 'Show the "Try new Slim UX" entry-point button on the classic dashboard and the first-visit welcome popup inside Slim UX. Off by default — Slim UX itself is still reachable via the slim-ux flag.',
    default: false,
    addedIn: '0.1.2057',
    expires: '2026-11-01',
  },
  // ccUseWorkerPool — sub-tasks B/C/D of W-mp2w003600196c51 (CC perf).
  // Routes Command Center / doc-chat through engine/cc-worker-pool.js
  // (persistent `copilot --acp` per CC tab) instead of spawning a fresh CLI
  // per turn. Saves ~14 s of cold-start cost.
  //
  // **Copilot-only — structural, not just a UX preference.** The pool's
  // transport is Agent Client Protocol (JSON-RPC over stdin/stdout, framed
  // initialize → session/new → session/prompt → session/update). Claude Code
  // has no equivalent `--acp` mode; its only session-reuse path is
  // per-invocation `--resume <id>` against `~/.claude/projects/<dir>/<id>.jsonl`.
  // Honoring this flag on a Claude CC runtime would silently switch the
  // operator's CC turns to copilot — see `shared.resolveCcUseWorkerPool`
  // which now refuses the override when CC runtime is not copilot
  // (W-mphlriic00095f69).
  //
  // Resolution: `shared.resolveCcUseWorkerPool(engine)` (engine/shared.js)
  // is the canonical predicate every dashboard.js call site uses. Explicit
  // `engine.ccUseWorkerPool` true/false in config wins ONLY when CC runtime
  // is copilot; otherwise the helper forces `false`. PR #2492 flipped the
  // copilot resolver default ON; W-mpqf3ybq000455f2 flipped this registry
  // default ON to match the resolved behavior (so the Settings/`/api/features`
  // surface reports the same default the runtime actually uses). The pool
  // has been stable since PR #2492's cold-spawn measurements; opt-out via
  // `config.features.ccUseWorkerPool: false` or `engine.ccUseWorkerPool: false`
  // still works.
  //
  // `requiredCcRuntime: 'copilot'` here is a UX hint — the Settings panel
  // greys out the toggle when the resolved CC runtime mismatches so users
  // see the constraint instead of toggling a no-op.
  'ccUseWorkerPool': {
    description: 'Route Command Center / doc-chat through a persistent `copilot --acp` worker per tab instead of spawning a fresh CLI per turn (~14s cold-start savings). Copilot-only — has no effect when the CC runtime is Claude.',
    default: true,
    addedIn: '0.1.1916',
    requiredCcRuntime: 'copilot',
  },
  // dashboard-refresh-diagnostics — W-mphejzx100081972. Enables the in-browser
  // /api/status poll telemetry ring buffer in dashboard/js/refresh.js: per-cycle
  // ETag/status/bytes/render-duration capture, visibility-transition tracking,
  // a small "diag" footer chip that opens a table modal, and a "send to engine"
  // button that POSTs the buffer to /api/diagnostics/refresh for offline triage.
  // Default ON (W-mphlr4lv0008c24f) — the diagnostic capture is cheap (in-memory
  // ring buffer, no network, no DOM writes) and is gated by _isRefreshDiagOn()
  // at the top of refresh() so the disabled path remains byte-identical to the
  // pre-flag steady state. Having it always-on means the next staleness
  // complaint can be diagnosed immediately from window._refreshDiagnostics
  // without needing a settings flip first.
  // Disable via config.features['dashboard-refresh-diagnostics']: false or
  // env MINIONS_FEATURE_DASHBOARD_REFRESH_DIAGNOSTICS=0.
  'dashboard-refresh-diagnostics': {
    description: 'Capture the last 50 dashboard /api/status poll cycles (timing, ETag, status code, render duration, per-renderer change flags) in a browser-side ring buffer accessible via window._refreshDiagnostics, a footer "diag" chip, and POST /api/diagnostics/refresh.',
    default: true,
    addedIn: '0.1.2034',
  },
};

const ENV_TRUTHY = new Set(['1', 'true', 'on', 'yes']);
const ENV_FALSY = new Set(['0', 'false', 'off', 'no', '']);

function envKey(id) {
  return 'MINIONS_FEATURE_' + String(id).toUpperCase().replace(/-/g, '_');
}

function readEnvOverride(id) {
  const raw = process.env[envKey(id)];
  if (raw === undefined) return undefined;
  const v = String(raw).trim().toLowerCase();
  if (ENV_TRUTHY.has(v)) return true;
  if (ENV_FALSY.has(v)) return false;
  return undefined;
}

function isFeatureOn(id, config, registry = FEATURES) {
  if (!Object.prototype.hasOwnProperty.call(registry, id)) {
    throw new Error(`Unknown feature flag: "${id}". Register it in engine/features.js.`);
  }
  const env = readEnvOverride(id);
  if (env !== undefined) return env;
  const fromConfig = config && config.features && config.features[id];
  if (typeof fromConfig === 'boolean') return fromConfig;
  return registry[id].default === true;
}

function listFeatures(config, registry = FEATURES) {
  const now = Date.now();
  return Object.entries(registry).map(([id, meta]) => {
    const expiresAt = meta.expires ? Date.parse(meta.expires) : NaN;
    return {
      id,
      description: meta.description || '',
      default: meta.default === true,
      enabled: isFeatureOn(id, config, registry),
      addedIn: meta.addedIn || null,
      expires: meta.expires || null,
      expired: expiresAt < now,
      requiredCcRuntime: meta.requiredCcRuntime || null,
    };
  });
}

function hasFeature(id, registry = FEATURES) {
  return Object.prototype.hasOwnProperty.call(registry, id);
}

module.exports = { FEATURES, isFeatureOn, listFeatures, hasFeature };
