import type { ComputeAppConfig, ComputeConfig } from "./types.ts";

type SerializableComputeConfig = {
  region?: ComputeConfig["region"];
  app?: ComputeAppConfig;
  apps?: Record<string, ComputeAppConfig>;
};

/**
 * Renders a `prisma.compute.ts` source file for a config — the scaffolding
 * counterpart of `loadComputeConfig`. Output round-trips through
 * `normalizeComputeConfig` and is stable for committing to a repository.
 */
export function serializeComputeConfig(
  config: ComputeConfig,
  options?: {
    /** Import specifier for the typed helper. Defaults to this SDK. */
    importFrom?: string;
  },
): string {
  const importFrom = options?.importFrom ?? "@prisma/compute-sdk/config";
  const body = renderValue(serializableComputeConfig(config), 0);

  return [
    `import { defineComputeConfig } from ${JSON.stringify(importFrom)};`,
    "",
    `export default defineComputeConfig(${body});`,
    "",
  ].join("\n");
}

/**
 * Renders a `prisma.compute.json` source file for a config — the same
 * serialization as `serializeComputeConfig` in the dependency-free static
 * format. Output round-trips through `normalizeComputeConfig` and is stable
 * for committing to a repository.
 */
export function serializeComputeConfigJson(
  config: ComputeConfig,
  options?: {
    /**
     * `$schema` URL emitted for editor validation. Omitted by default:
     * generated configs must not point editors at a URL that does not
     * resolve, so pass `COMPUTE_CONFIG_JSON_SCHEMA_URL` explicitly once the
     * schema is actually published there.
     */
    schemaUrl?: string | null;
  },
): string {
  const schemaUrl = options?.schemaUrl ?? null;
  const serializable = {
    ...(schemaUrl === null ? {} : { $schema: schemaUrl }),
    ...serializableComputeConfig(config),
  };

  return `${JSON.stringify(serializable, null, 2)}\n`;
}

function serializableComputeConfig(
  config: ComputeConfig,
): SerializableComputeConfig {
  const serializable: SerializableComputeConfig = {};
  if (config.region !== undefined) {
    serializable.region = config.region;
  }
  if ("app" in config && config.app !== undefined) {
    serializable.app = serializableComputeAppConfig(config.app);
  } else {
    serializable.apps = Object.fromEntries(
      Object.entries(
        (config as { apps: Record<string, ComputeAppConfig> }).apps,
      ).map(([key, app]) => [key, serializableComputeAppConfig(app)]),
    );
  }
  return serializable;
}

function serializableComputeAppConfig(app: ComputeAppConfig): ComputeAppConfig {
  return {
    name: app.name,
    region: app.region,
    root: app.root,
    framework: app.framework,
    entry: app.entry,
    httpPort: app.httpPort,
    env: app.env,
    build: app.build,
  };
}

const IDENTIFIER_KEY = /^[A-Za-z_$][A-Za-z0-9_$]*$/;

function renderValue(value: unknown, depth: number): string {
  if (value === null) {
    return "null";
  }

  switch (typeof value) {
    case "string":
      return JSON.stringify(value);
    case "number":
    case "boolean":
      return String(value);
    case "object":
      break;
    default:
      throw new Error(
        `Cannot serialize a ${typeof value} value in a compute config.`,
      );
  }

  if (Array.isArray(value)) {
    return `[${value.map((entry) => renderValue(entry, depth)).join(", ")}]`;
  }

  const entries = Object.entries(value as Record<string, unknown>).filter(
    ([, entry]) => entry !== undefined,
  );
  if (entries.length === 0) {
    return "{}";
  }

  const indent = "  ".repeat(depth + 1);
  const closingIndent = "  ".repeat(depth);
  const rendered = entries.map(([key, entry]) => {
    // "__proto__" needs a computed key: in an object literal both the bare
    // and the quoted form set the prototype instead of defining a property.
    const renderedKey =
      key === "__proto__"
        ? '["__proto__"]'
        : IDENTIFIER_KEY.test(key)
          ? key
          : JSON.stringify(key);
    return `${indent}${renderedKey}: ${renderValue(entry, depth + 1)},`;
  });

  return `{\n${rendered.join("\n")}\n${closingIndent}}`;
}
