/**
 * Grounding-model registry.
 *
 * Single lookup map from `modelId` → adapter class. Adding a new
 * provider is a one-file addition (a new `XxxAdapter.js`) plus one
 * line here. No callers of the registry need to change.
 *
 * The admin UI fetches `listGroundingModels()` to drive the picker;
 * the DesktopTool calls `createGroundingModel(id, cfg)` per request
 * (adapters are cheap to construct, so we don't memoise — keeps the
 * cfg-per-tenant case clean).
 */

import KimiAdapter from './KimiAdapter.js';
import MockAdapter from './MockAdapter.js';
import { ModelId, ErrorCode } from './constants.js';
import { makeError } from './GroundingModel.js';

/**
 * The registry. Order matters only for UI display: first entry is
 * the default selection in the admin form.
 *
 * @type {Object<string, typeof import('./GroundingModel.js').GroundingModel>}
 */
const REGISTRY = Object.freeze({
  [ModelId.KIMI_K2_6]: KimiAdapter,
  [ModelId.MOCK]:      MockAdapter,
  // Future adapters drop in here:
  // [ModelId.CLAUDE_SONNET_4_6]:   ClaudeAdapter,
  // [ModelId.OPENAI_COMPUTER_USE]: OpenAIComputerUseAdapter,
});

/**
 * List adapter metadata for the admin UI. Includes the static fields
 * (displayName, modelId) and the dynamic knob schema (instance method).
 *
 * Filtering: by default, internal/mock adapters are excluded. Pass
 * `{ includeInternal: true }` to surface them (tests, dev tools).
 *
 * @param {Object} [opts]
 * @param {boolean} [opts.includeInternal=false]
 * @returns {{
 *   id: string,
 *   displayName: string,
 *   knobs: import('./types.js').KnobDef[],
 *   capabilities: import('./types.js').Capabilities,
 * }[]}
 */
export function listGroundingModels(opts = {}) {
  const includeInternal = opts.includeInternal === true;
  return Object.entries(REGISTRY)
    .filter(([id]) => includeInternal || !INTERNAL_IDS.has(id))
    .map(([id, Klass]) => {
      // We need an instance to read knobs/capabilities — they're
      // instance methods (allow per-instance config to influence what
      // the model can do; e.g. a future adapter might disable a knob
      // when running against a specific provider variant).
      const probe = createUnboundProbe(Klass);
      return {
        id,
        displayName: Klass.displayName,
        knobs:        probe.supportedKnobs(),
        capabilities: probe.capabilities(),
      };
    });
}

/**
 * Construct a configured adapter instance for a given model ID.
 *
 * @param {string} id              - one of ModelId.*
 * @param {object} [cfg]           - adapter-specific config (apiKey/endpoint/etc.)
 * @returns {import('./GroundingModel.js').GroundingModel}
 */
export function createGroundingModel(id, cfg) {
  const Klass = REGISTRY[id];
  if (!Klass) {
    throw makeError(ErrorCode.UNKNOWN_MODEL,
      `unknown grounding model "${id}". Known: ${Object.keys(REGISTRY).join(', ')}`);
  }
  return new Klass(cfg);
}

/** @returns {boolean} */
export function isRegistered(id) {
  return Object.prototype.hasOwnProperty.call(REGISTRY, id);
}

/** @returns {string[]} */
export function listModelIds() {
  return Object.keys(REGISTRY);
}

// ── internal ────────────────────────────────────────────────────────

const INTERNAL_IDS = new Set([ModelId.MOCK]);

/**
 * Construct a NO-CONFIG probe instance just to read its static-shape
 * info (knobs, capabilities). Most adapter constructors throw on
 * missing config — but knob schemas / capability descriptors are
 * pure-function lookups that don't actually need the provider creds.
 *
 * We bypass the constructor here by calling the methods against a
 * minimal stand-in. If an adapter has constructor-time side effects
 * that prevent this pattern, override `static probeShape()` on the
 * class.
 */
function createUnboundProbe(Klass) {
  if (typeof Klass.probeShape === 'function') {
    return Klass.probeShape();
  }
  // Most adapters: try construction with permissive stubs, fall back
  // to method extraction.
  try {
    return new Klass({ endpoint: 'about:blank', apiKey: 'probe', fetchImpl: () => {} });
  } catch {
    // Adapter constructor too strict — call methods directly off prototype.
    const stub = Object.create(Klass.prototype);
    return {
      supportedKnobs: () => stub.supportedKnobs?.() ?? [],
      capabilities:   () => stub.capabilities?.() ?? {},
    };
  }
}
