'use strict';

const path = require('path');
const { Registry } = require('../plugins/registry-base');
const { loadSiblingManifestsFromDir } = require('../plugins/manifest-loader');
const { addDiagnostics, addDiagnostic } = require('../plugins/diagnostics');
const { evaluateDirectMessageAccess, evaluateGroupAccess } = require('./policy');

/**
 * Channel registry — replaces hardcoded require list in agent.js:10-12.
 *
 * Each channel ships:
 *   - <name>-channel.js       (the implementation, extends ChannelBase)
 *   - <name>-channel.plugin.json (manifest with id, kind=channel,
 *                                 capabilities, configKey, requires)
 *
 * Adding a new channel = drop two files in wall-e/channels/. Zero edits
 * to agent.js, zero edits to anything else. Compare to current cost: edit
 * agent.js requires + new instantiation block + lifecycle wiring.
 *
 * The registry stores `{ id, manifest, ChannelClass, source }` per entry.
 * Consumers (agent.js) call `instantiate(id, opts)` to get a live channel
 * instance for a given config block.
 */

const _registry = new Registry('channel');
let _bootstrapped = false;

/**
 * Register a channel. Used by the bootstrap to seed defaults, and by
 * 3rd-party plugin paths (Item H) to add channels at runtime.
 *
 * @param {object} entry
 * @param {string} entry.id - Unique channel id (e.g. "slack_dm")
 * @param {object} entry.manifest - Plugin manifest (validated upstream)
 * @param {Function} entry.ChannelClass - constructor extending ChannelBase
 * @param {string} [entry.source] - "bundled" / "user" / "npm"
 * @param {object} [opts]
 * @param {boolean} [opts.replace=false]
 * @returns {() => boolean} deregister closure
 */
function register(entry, opts = {}) {
  return _registry.register(entry, opts);
}

/** Get the registered entry by id. */
function get(id) {
  return _registry.get(id);
}

/** Has channel? */
function has(id) {
  return _registry.has(id);
}

/** All registered channels in insertion order. */
function list() {
  return _registry.list();
}

/** All registered ids. */
function ids() {
  return _registry.ids();
}

/** Number of registered channels. */
function size() {
  return _registry.size();
}

/**
 * Instantiate a channel for live use. Returns null if the channel id is
 * unknown or the constructor throws.
 *
 * @param {string} id
 * @param {object} ctorOpts - Passed to ChannelClass constructor
 * @returns {object|null}
 */
function instantiate(id, ctorOpts) {
  const entry = _registry.get(id);
  if (!entry || typeof entry.ChannelClass !== 'function') return null;
  try {
    const opts = { ...(ctorOpts || {}) };
    if (!opts.channelPolicy) {
      Object.defineProperty(opts, 'channelPolicy', {
        enumerable: false,
        configurable: true,
        value: {
          evaluateDirectMessageAccess,
          evaluateGroupAccess,
        },
      });
    }
    return new entry.ChannelClass(opts);
  } catch (err) {
    addDiagnostic({
      level: 'error',
      pluginId: id,
      kind: 'channel',
      message: `Channel constructor failed: ${err.message}`,
      component: 'channel-registry',
    });
    return null;
  }
}

/**
 * Default seeding — bundled channels from wall-e/channels/, plus 3rd-party
 * channels discovered via `node_modules/` scan (Item H of plugin-sdk
 * deep-dive). Called once (idempotent).
 *
 * 3rd-party plugins are scanned only if `opts.scanNpm !== false`. Disable
 * for tests that want to assert pure-bundled output.
 */
function bootstrap(opts = {}) {
  const channelsDir = opts.dir || path.join(__dirname);
  const { manifests: bundled, diagnostics: bundledDiag } =
    loadSiblingManifestsFromDir(channelsDir, 'bundled');
  if (bundledDiag?.length) {
    addDiagnostics(bundledDiag, { component: 'channel-registry' });
  }

  // 3rd-party scan (Item H). Tests can opt-out via opts.scanNpm: false.
  let thirdParty = [];
  if (opts.scanNpm !== false) {
    try {
      const { scanWorkspace } = require('../plugins/npm-scanner');
      const ws = opts.workspaceDir || path.resolve(__dirname, '..', '..');
      const { manifests, diagnostics } = scanWorkspace(ws);
      if (diagnostics?.length) {
        addDiagnostics(diagnostics, { component: 'channel-registry' });
      }
      thirdParty = manifests.filter((m) => m.kind === 'channel');
    } catch (err) {
      addDiagnostic({
        level: 'warn',
        message: `npm-scanner failed: ${err.message}`,
        component: 'channel-registry',
      });
    }
  }

  for (const manifest of [...bundled, ...thirdParty]) {
    if (manifest.kind !== 'channel') continue;
    let ChannelClass;
    try {
      ChannelClass = require(manifest.entryFile);
    } catch (err) {
      addDiagnostic({
        level: 'error',
        pluginId: manifest.id,
        kind: 'channel',
        message: `Failed to load channel module ${manifest.entryFile}: ${err.message}`,
        component: 'channel-registry',
      });
      continue;
    }

    if (typeof ChannelClass !== 'function') {
      addDiagnostic({
        level: 'error',
        pluginId: manifest.id,
        kind: 'channel',
        message: `Channel module ${manifest.entryFile} does not export a class`,
        component: 'channel-registry',
      });
      continue;
    }

    try {
      _registry.register(
        {
          id: manifest.id,
          manifest,
          ChannelClass,
          source: manifest.source || 'bundled',
        },
        { replace: true }
      );
    } catch (err) {
      addDiagnostic({
        level: 'error',
        pluginId: manifest.id,
        kind: 'channel',
        message: `Channel registration failed: ${err.message}`,
        component: 'channel-registry',
      });
    }
  }
  _bootstrapped = true;
}

/** Lazy bootstrap — most consumers should use this. */
function ensureBootstrapped() {
  if (!_bootstrapped) bootstrap();
}

/**
 * Returns true if the channel is enabled in the live config.
 * Manifest declares `configKey` (e.g. "slack_dm"); we look up
 * `config.channels[configKey]` and check `.enabled` truthiness.
 */
function isEnabledByConfig(manifest, config) {
  if (!manifest || !config) return false;
  const key = manifest.configKey || manifest.id;
  const block = config.channels && config.channels[key];
  if (!block) return false;
  return Boolean(block.enabled);
}

/** Returns the manifest's declared `requires.config` paths satisfied? */
function configRequirementsSatisfied(manifest, config) {
  if (!manifest?.requires?.config?.length) return true;
  for (const dot of manifest.requires.config) {
    const parts = dot.split('.');
    let cur = config;
    for (const p of parts) {
      if (cur == null) return false;
      cur = cur[p];
    }
    if (cur == null || cur === '') return false;
  }
  return true;
}

/** Test helper — reset for isolated tests. */
function _resetForTest() {
  _registry.clear();
  _bootstrapped = false;
}

module.exports = {
  register,
  get,
  has,
  list,
  ids,
  size,
  instantiate,
  bootstrap,
  ensureBootstrapped,
  isEnabledByConfig,
  configRequirementsSatisfied,
  evaluateDirectMessageAccess,
  evaluateGroupAccess,
  _resetForTest,
};
