// ---------------------------------------------------------------------------
// ConfigFileSink — best-effort write-back of source changes to executor.jsonc.
//
// Plugins (openapi, graphql, mcp) call `sink.upsertSource` after their DB
// writes so the committable file stays in sync with runtime state. Errors
// are logged and swallowed — a failed file write must never fail a DB
// mutation, and the next successful mutation (or a boot-time sync) will
// eventually reconcile.
//
// The FileSystem layer is injected so library code here doesn't pick a
// platform binding. The host app provides NodeFileSystem (or BunFileSystem).
// ---------------------------------------------------------------------------

import { Effect } from "effect";
import type { Layer } from "effect";
import type { FileSystem } from "effect";

import { SECRET_REF_PREFIX, type ConfigHeaderValue, type SourceConfig } from "./schema";
import { addSourceToConfig, removeSourceFromConfig } from "./write";

// Translate a plugin-side header value (`{ secretId, prefix? }` for secret
// refs) into the config file's `secret-public-ref:<id>` string form.
type PluginHeaderValue = string | { secretId: string; prefix?: string };

export const headerToConfigValue = (value: PluginHeaderValue): ConfigHeaderValue => {
  if (typeof value === "string") return value;
  const ref = `${SECRET_REF_PREFIX}${value.secretId}`;
  return value.prefix ? { value: ref, prefix: value.prefix } : ref;
};

export const headersToConfigValues = (
  headers: Record<string, PluginHeaderValue> | undefined,
): Record<string, ConfigHeaderValue> | undefined => {
  if (!headers) return undefined;
  const out: Record<string, ConfigHeaderValue> = {};
  for (const [k, v] of Object.entries(headers)) out[k] = headerToConfigValue(v);
  return out;
};

export interface ConfigFileSink {
  readonly upsertSource: (source: SourceConfig) => Effect.Effect<void>;
  readonly removeSource: (namespace: string) => Effect.Effect<void>;
}

export interface ConfigFileSinkOptions {
  readonly path: string;
  readonly fsLayer: Layer.Layer<FileSystem.FileSystem>;
  /** Called when a file operation fails. Defaults to console.warn. */
  readonly onError?: (op: "upsert" | "remove", err: unknown) => void;
}

const defaultOnError = (op: "upsert" | "remove", err: unknown): void => {
  console.warn(`[config-sink] ${op} failed`, err);
};

export const makeFileConfigSink = (options: ConfigFileSinkOptions): ConfigFileSink => {
  const { path, fsLayer, onError = defaultOnError } = options;

  return {
    upsertSource: (source) =>
      addSourceToConfig(path, source).pipe(
        Effect.provide(fsLayer),
        Effect.catch((err: unknown) => Effect.sync(() => onError("upsert", err))),
      ),

    removeSource: (namespace) =>
      removeSourceFromConfig(path, namespace).pipe(
        Effect.provide(fsLayer),
        Effect.catch((err: unknown) => Effect.sync(() => onError("remove", err))),
      ),
  };
};
