// Server-side configuration: API key resolution + live reload of the
// StartOS config file. Module owns its own state — `serverApiKey` is
// exported as a `let` binding so importers see the current value
// (ESM live bindings).
//
// Resolution priority:
//   1. process.env.GEMINI_API_KEY     (pins; never re-read after boot)
//   2. /data/config/startos-config.json's gemini_api_key
//   3. /data/.env's GEMINI_API_KEY
//
// Whenever the StartOS config file changes (via the "Set Gemini API
// Key" action), the new value is picked up within CONFIG_POLL_MS — no
// service restart required.

import fs from "fs/promises";
import path from "path";

// ── Module state ────────────────────────────────────────────────────────────
// Initialized by initConfig(). serverApiKey is exported as a live binding;
// importers see the current value, not a snapshot.
let envApiKey = "";
let envPath = null;
let configDir = null;
let startosConfigPath = null;

export let serverApiKey = "";

// Core-decoupling shared "operator key" — read live from the StartOS
// config sidecar the same way serverApiKey is, so the operator can set it
// via the "Set Relay Operator Key" action without a service restart.
// `RECAP_RELAY_OPERATOR_KEY` env pins the value (local dev). Consumed by
// relay-default.js's getRelayOperatorKey(); see that for the semantics.
let envRelayOperatorKey = "";
export let relayOperatorKey = "";

// ── Init ────────────────────────────────────────────────────────────────────
// Call once at boot. Sets up paths, reads the initial value, kicks off the
// poll loop. Idempotent if you really want to call it twice (the interval
// would just stack — don't).
export async function initConfig({ dataDir }) {
  envPath = path.join(dataDir, ".env");
  configDir = path.join(dataDir, "config");
  startosConfigPath = path.join(configDir, "startos-config.json");
  envApiKey = process.env.GEMINI_API_KEY || "";
  serverApiKey = envApiKey;
  envRelayOperatorKey = (process.env.RECAP_RELAY_OPERATOR_KEY || "").trim();
  relayOperatorKey = envRelayOperatorKey;

  await fs.mkdir(configDir, { recursive: true }).catch(() => {});
  await refreshServerApiKey("startup");
  await refreshRelayOperatorKey("startup");

  const pollMs = parseInt(process.env.RECAP_CONFIG_POLL_MS || "3000", 10);
  setInterval(() => {
    refreshServerApiKey("config poll").catch(() => {});
    refreshRelayOperatorKey("config poll").catch(() => {});
  }, pollMs);
}

// ── Internals ───────────────────────────────────────────────────────────────
async function readApiKeyFromConfig() {
  try {
    const content = await fs.readFile(startosConfigPath, "utf-8");
    const config = JSON.parse(content);
    return config.gemini_api_key || "";
  } catch {
    return "";
  }
}

async function readApiKeyFromEnvFile() {
  try {
    const envContent = await fs.readFile(envPath, "utf-8");
    const match = envContent.match(/^GEMINI_API_KEY=(.+)$/m);
    if (match) return match[1].trim().replace(/^["']|["']$/g, "");
  } catch {}
  return "";
}

async function refreshServerApiKey(reason) {
  if (envApiKey) return; // env var pins the value
  const fromConfig = await readApiKeyFromConfig();
  const next = fromConfig || (await readApiKeyFromEnvFile()) || "";
  if (next !== serverApiKey) {
    serverApiKey = next;
    console.log(`[config] server API key ${next ? "loaded" : "cleared"} (${reason})`);
  }
}

async function readRelayOperatorKeyFromConfig() {
  try {
    const content = await fs.readFile(startosConfigPath, "utf-8");
    const config = JSON.parse(content);
    return (config.recap_relay_operator_key || "").trim();
  } catch {
    return "";
  }
}

async function refreshRelayOperatorKey(reason) {
  if (envRelayOperatorKey) return; // env var pins the value
  const next = await readRelayOperatorKeyFromConfig();
  if (next !== relayOperatorKey) {
    relayOperatorKey = next;
    console.log(
      `[config] relay operator key ${next ? "loaded" : "cleared"} (${reason})`,
    );
  }
}

// ── Public helpers ──────────────────────────────────────────────────────────
// Resolves the per-request key — either the client's own (BYO) or the
// server's stored key (when the client signals USE_SERVER_KEY or sends
// nothing).
export function resolveApiKey(clientKey) {
  if (!clientKey || clientKey === "USE_SERVER_KEY") return serverApiKey;
  return clientKey;
}

// Where the .env file lives (some other modules read it for legacy
// settings like YT_COOKIES_FROM).
export function getEnvPath() {
  return envPath;
}

// Snapshot of the full StartOS config blob — keys for every provider
// (gemini, anthropic, openai, openai-compatible, ollama) plus the
// admin-auth fields. Each request reads it once and passes it into
// resolveProviderOpts() per provider. Returns {} if the file doesn't
// exist or is unreadable.
export async function getConfigSnapshot() {
  try {
    const content = await fs.readFile(startosConfigPath, "utf-8");
    return JSON.parse(content) || {};
  } catch {
    return {};
  }
}

// Patch the StartOS config file in place. Reads current, merges in the
// given fields, writes atomically (tmp + rename). Used by the picker
// UI's Delete button to clear server-side credentials for a provider.
// The next config poll picks up the changes within CONFIG_POLL_MS;
// resolveProviderOpts already reads getConfigSnapshot per-request, so
// effectively the change is immediate.
//
// `patch` is a plain object of { config_field: value } pairs.
// Pass empty strings to clear a field rather than deleting the key —
// the StartOS schema declares every field with a default of '', so
// empty string is the canonical "unset" representation.
export async function mergeConfig(patch) {
  if (!patch || typeof patch !== "object") return;
  let current = {};
  try {
    const content = await fs.readFile(startosConfigPath, "utf-8");
    current = JSON.parse(content) || {};
  } catch {}
  const merged = { ...current, ...patch };
  const tmp = startosConfigPath + ".tmp";
  await fs.mkdir(path.dirname(startosConfigPath), { recursive: true });
  await fs.writeFile(tmp, JSON.stringify(merged, null, 2), { mode: 0o600 });
  await fs.rename(tmp, startosConfigPath);
  // Re-run the gemini-key refresher so serverApiKey reflects the
  // patch immediately (otherwise it'd lag until the poll tick).
  if (Object.prototype.hasOwnProperty.call(patch, "gemini_api_key")) {
    await refreshServerApiKey("merge config");
  }
  if (Object.prototype.hasOwnProperty.call(patch, "recap_relay_operator_key")) {
    await refreshRelayOperatorKey("merge config");
  }
}
