// Provider registry. Each provider wraps a single LLM/SDK behind a
// uniform interface (see ./gemini.js for the reference shape). The rest
// of the server talks to providers through getProvider() and never
// imports SDKs directly.
//
// Adding a new provider:
//   1. Create ./<name>.js exporting createXxxProvider({ apiKey, ... }).
//   2. Add it to PROVIDER_NAMES + the switch in getProvider().
//   3. Add the matching opts shape to PROVIDER_KEY_FIELDS so
//      resolveProviderOpts() can pull the right key/baseURL out of the
//      StartOS config.
//   4. Wire its config field into startos/file-models/config.json.ts
//      and add a "Set <Provider> Key" StartOS action.
//
// Capabilities (see provider.capabilities) signal what each one can do.
// Some providers analyze but can't transcribe (Claude, OpenAI-compat,
// Ollama); the orchestration layer in server/index.js can mix providers
// across the transcription + analysis pipelines.

import { createGeminiProvider } from "./gemini.js";
import { createAnthropicProvider } from "./anthropic.js";
import { createOpenAIProvider } from "./openai.js";
import { createOpenAICompatibleProvider } from "./openai-compatible.js";
import { createOllamaProvider } from "./ollama.js";
import { createWhisperProvider } from "./whisper.js";
import { createRelayProvider } from "./relay.js";
import { getInstallId } from "../install-id.js";
import { getRawLicenseKey } from "../license.js";
import { getRelayBaseURL, getRelayOperatorKey } from "../relay-default.js";

export const PROVIDER_NAMES = [
  "gemini",
  "anthropic",
  "openai",
  "openai-compatible",
  "ollama",
  "whisper",
  "relay",
];

// Map provider name → which fields to read from the StartOS config blob
// when resolving its construction opts. Used by resolveProviderOpts().
export const PROVIDER_KEY_FIELDS = {
  gemini: { apiKey: "gemini_api_key" },
  anthropic: { apiKey: "anthropic_api_key" },
  openai: { apiKey: "openai_api_key" },
  "openai-compatible": {
    apiKey: "openai_compatible_api_key",
    baseURL: "openai_compatible_base_url",
  },
  ollama: { baseURL: "ollama_base_url" },
  whisper: {
    apiKey: "whisper_api_key",
    baseURL: "whisper_base_url",
  },
  // Relay is operator-only — base URL is HARDCODED in
  // server/relay-default.js, NOT read from StartOS config. The empty
  // object is intentional: resolveProviderOpts uses `name in
  // PROVIDER_KEY_FIELDS` to recognise the provider, then the
  // relay-specific block at the bottom of resolveProviderOpts
  // injects baseURL + installId + licenseKey server-side. Without
  // this entry the lookup throws "Unknown provider: relay" before
  // reaching the injection block.
  relay: {},
};

export function getProvider(name, opts = {}) {
  switch (name) {
    case "gemini":
      return createGeminiProvider(opts);
    case "anthropic":
      return createAnthropicProvider(opts);
    case "openai":
      return createOpenAIProvider(opts);
    case "openai-compatible":
      return createOpenAICompatibleProvider(opts);
    case "ollama":
      return createOllamaProvider(opts);
    case "whisper":
      return createWhisperProvider(opts);
    case "relay":
      return createRelayProvider(opts);
    default:
      throw new Error(
        `Unknown provider: ${name}. Available: ${PROVIDER_NAMES.join(", ")}`
      );
  }
}

// Pull the construction opts for a provider out of the StartOS config
// blob, optionally overridden per-provider by client-side opts the web
// UI passed in the request body.
//
// `config` is the parsed startos-config.json snapshot.
// `clientOpts` is { apiKey?, baseURL? } for THIS provider only —
//   typically a value out of req.body.providerOpts[name].
// `req` is the Express request — only used for the relay provider in
//   multi-tenant mode, where the relay's install_id + license depend
//   on WHICH user is making the call. Pass it through whenever a
//   request is in scope. Single-mode + non-relay providers ignore it.
//
// Resolution priority for each field: client opt → config opt.
// Returns { apiKey?, baseURL? } as appropriate for the provider.
export function resolveProviderOpts(name, { config = {}, clientOpts = {}, req = null } = {}) {
  const fields = PROVIDER_KEY_FIELDS[name];
  if (!fields) {
    throw new Error(`Unknown provider: ${name}`);
  }
  const opts = {};
  if (fields.apiKey) {
    const fromConfig = config[fields.apiKey] || "";
    const fromClient = (clientOpts.apiKey || "").trim();
    opts.apiKey = fromClient || fromConfig;
  }
  if (fields.baseURL) {
    const fromConfig = config[fields.baseURL] || "";
    const fromClient = (clientOpts.baseURL || "").trim();
    opts.baseURL = fromClient || fromConfig;
    // Last-resort fallback for Ollama: the canonical StartOS internal
    // hostname. Reachable when the optional Ollama dependency is
    // installed alongside Recap on the same StartOS server, even if
    // the user hasn't run the "Set Ollama Server URL" action.
    if (!opts.baseURL && name === "ollama") {
      opts.baseURL = "http://ollama.startos:11434";
    }
  }
  // User-defined model list: providers with dynamic catalogs (ollama,
  // openai-compatible, whisper) accept a comma- or newline-separated
  // list of model names in clientOpts.models. Parse and pass through
  // as `defaultModels` so listTranscriptionModels / listAnalysisModels
  // return the right thing AND so the orchestration layer's fallback
  // chain knows what to walk through if the user's chosen model fails.
  if (typeof clientOpts.models === "string" && clientOpts.models.trim()) {
    const seen = new Set();
    const models = clientOpts.models
      .split(/[,\n]/)
      .map((s) => s.trim())
      .filter((s) => {
        if (!s || seen.has(s)) return false;
        seen.add(s);
        return true;
      });
    if (models.length > 0) {
      opts.defaultModels = models;
    }
  }
  // Relay-specific injections: baseURL (hardcoded constant or env
  // override) + install-id + license key. None of these come from
  // clientOpts — relay identity + endpoint must not be spoofable from
  // a request body.
  //
  // Identity rules:
  //   - single mode: always the operator install + operator license
  //   - multi mode + signed-in user WITH their own keysat_license:
  //       user's synthetic_install_id + user's license. The relay's
  //       license-keyed credit ledger (Path 3) routes consumption to
  //       the right user-pool.
  //   - multi mode + free / trial / Core user OR signed-in user with
  //     no license: operator's install + license. Their relay calls
  //     are paid out of the operator's credit pool (tenant_credits
  //     gates them locally to control fan-out).
  if (name === "relay") {
    opts.baseURL = getRelayBaseURL();
    const ident = pickRelayIdentity(req);
    if (ident.cloud) {
      // Core-decoupling cloud identity: authenticate the server with the
      // operator key + name the user; no per-user Keysat license.
      opts.cloud = true;
      opts.userId = ident.userId;
      opts.operatorKey = ident.operatorKey;
    } else {
      opts.installId = ident.installId;
      if (ident.licenseKey) opts.licenseKey = ident.licenseKey;
    }
  }
  return opts;
}

// pickRelayIdentity(req) — single source of truth for "which (install_id,
// license) do we present to the relay for THIS request". Centralized so
// the rule doesn't drift across the four resolveProviderOpts callsites.
function pickRelayIdentity(req) {
  // Single mode (or no request in scope, e.g. boot-time relay capability
  // probe): operator identity, period.
  if (!req || req.recapMode !== "multi") {
    return { installId: getInstallId(), licenseKey: getRawLicenseKey() || null };
  }
  // Multi mode + PAID cloud user (core-decoupling): cloud identity —
  // authenticate the server with the operator key and name the user by
  // their Recaps account id. NO Keysat license; the relay owns the
  // tier, keyed by user-id. `req.user.tier` is the Recaps-side cache of
  // that relay tier (kept in sync by the operator grant flow). Falls
  // back to the operator pool when the operator key isn't configured.
  const tier = req.user?.tier;
  if (
    req.user &&
    req.user.id &&
    !req.user.is_admin &&
    (tier === "pro" || tier === "max")
  ) {
    const operatorKey = getRelayOperatorKey();
    if (operatorKey) {
      return { cloud: true, userId: req.user.id, operatorKey };
    }
  }
  // Multi mode + everyone else (admin, anon trial, signed-in free user,
  // family-share tenant on a self-hosted multi-tenant operator's box):
  // pay out of the operator's pool.
  return { installId: getInstallId(), licenseKey: getRawLicenseKey() || null };
}
