/**
 * v1.15.0-mcp-multi: MCP config file schema + loader.
 *
 * Reads a JSON config file (default `/data/mcp.json`) that declares MCP
 * servers — their transport type, connection parameters, and enabled state.
 * Schema shape matches opencode's `mcpServers` key for copy-paste compat.
 *
 * Secrets stay out of the config file via `{env:VAR}` substitution
 * (opencode-compatible). Any string value can reference an environment
 * variable, e.g. a header `"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"`
 * resolves from `process.env` at load. This keeps real keys in `.env`
 * (`env_file` in docker-compose) rather than the gitignored config.
 */
import { readFileSync } from 'node:fs';
import { z } from 'zod';
import type { FastifyBaseLogger } from 'fastify';

const McpPermissionSchema = z.enum(['allow', 'ask', 'deny']).default('allow');

const McpServerConfigSchema = z.discriminatedUnion('type', [
  z.object({
    type: z.literal('streamableHttp'),
    url: z.string().url(),
    headers: z.record(z.string()).optional(),
    enabled: z.boolean().default(true),
    permission: McpPermissionSchema,
  }),
  z.object({
    type: z.literal('stdio'),
    command: z.string().min(1),
    args: z.array(z.string()).default([]),
    env: z.record(z.string()).optional(),
    enabled: z.boolean().default(true),
    permission: McpPermissionSchema,
  }),
]);

const McpConfigSchema = z.object({
  mcpServers: z.record(z.string(), McpServerConfigSchema).default({}),
});

export type McpServerConfig = z.infer<typeof McpServerConfigSchema>;

export interface McpServerEntry {
  name: string;
  config: McpServerConfig;
}

const ENV_VAR_PATTERN = /\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g;

/**
 * Recursively replace `{env:VAR}` references in string values with the
 * matching environment variable (opencode-compatible). Runs before Zod
 * validation so a resolved value (e.g. a `{env:...}` URL) still validates.
 * An unset var resolves to '' and logs a warning so a missing secret is
 * visible in the boot log rather than silently sending a literal placeholder.
 * Pass an optional `unsetVars` set to collect the names that resolved to '';
 * the loader surfaces them on a validation failure (an empty value in a strict
 * url/command field invalidates the whole config — see loadMcpConfig).
 */
export function substituteEnvVars(
  value: unknown,
  log: FastifyBaseLogger,
  unsetVars?: Set<string>,
): unknown {
  if (typeof value === 'string') {
    return value.replace(ENV_VAR_PATTERN, (_match, name: string) => {
      const resolved = process.env[name];
      if (resolved === undefined) {
        unsetVars?.add(name);
        log.warn(`mcp: env var ${name} referenced in config is unset; substituting empty string`);
        return '';
      }
      return resolved;
    });
  }
  if (Array.isArray(value)) {
    return value.map((v) => substituteEnvVars(v, log, unsetVars));
  }
  if (value && typeof value === 'object') {
    const out: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
      out[k] = substituteEnvVars(v, log, unsetVars);
    }
    return out;
  }
  return value;
}

/**
 * Read and validate the MCP config file. Returns enabled servers only.
 * File missing → log info, return []. Parse/validation error → log warn, return [].
 */
export function loadMcpConfig(configPath: string, log: FastifyBaseLogger): McpServerEntry[] {
  let raw: string;
  try {
    raw = readFileSync(configPath, 'utf8');
  } catch {
    log.info(`mcp: config not found at ${configPath}, skipping`);
    return [];
  }

  let json: unknown;
  try {
    json = JSON.parse(raw);
  } catch (err) {
    log.warn({ err }, `mcp: failed to parse ${configPath} as JSON`);
    return [];
  }

  const unsetVars = new Set<string>();
  const result = McpConfigSchema.safeParse(substituteEnvVars(json, log, unsetVars));
  if (!result.success) {
    // Connect the two otherwise-disconnected warnings: an unset {env:VAR} that
    // resolved to '' can invalidate a strict field (url/command) and drop the
    // whole config, so name the unset vars alongside the validation errors.
    const hint = unsetVars.size
      ? ` — ${unsetVars.size} referenced env var(s) unset & substituted with '' (${[...unsetVars].join(', ')}); an unset {env:VAR} in a url/command field invalidates the whole config`
      : '';
    log.warn(
      { errors: result.error.flatten().fieldErrors, unsetEnvVars: [...unsetVars] },
      `mcp: invalid config at ${configPath}${hint}`,
    );
    return [];
  }

  const entries: McpServerEntry[] = [];
  for (const [name, config] of Object.entries(result.data.mcpServers)) {
    if (config.enabled) {
      entries.push({ name, config });
    }
  }

  return entries;
}
