import * as Convict from "convict";
import { isSchemaObject, primitiveConstructors } from "./utils";

export const defaultOrder = [
  "name",
  "default",
  "arg",
  "env",
  "format",
  "nullable",
  "sensitive",
  "doc",
];

export function renderField(
  fieldName: keyof Convict.SchemaObj<unknown> | "name",
  name: string,
  obj: Convict.SchemaObj<unknown>,
): string {
  switch (fieldName) {
    case "arg":
      return `${obj.arg ?? "-"}`;
    case "doc":
      return `${obj.doc ?? "-"}`;
    case "default":
      return `${JSON.stringify(obj.default)}`;
    case "format":
      return renderFormat(obj);
    case "nullable":
      return renderNullable(obj);
    case "sensitive":
      return renderSensitive(obj);
    case "env":
      return `${obj.env ?? "-"}`;
    case "name":
      return name;
    default:
      return `unknown`;
  }
}

export function renderDoc<T = any>(
  schema: Convict.Schema<T>,
  order: string[] = defaultOrder,
): string {
  const header = renderHeader(order);
  return renderSchema(schema, "", order, header);
}

export function renderSchemaObj(
  name: string,
  obj: Convict.SchemaObj<unknown>,
  order: string[] = defaultOrder,
): string {
  return `|${order.map((fieldName) => renderField(fieldName, name, obj)).join("|")}|\n`;
}

export function renderSchema<T>(
  schema: Convict.Schema<T>,
  namePrefix: string = "",
  order: string[] = defaultOrder,
  render = "",
): string {
  let res = render;

  for (let [key, value] of Object.entries(schema)) {
    if (isSchemaObject(value as any)) {
      res += renderSchemaObj(
        `${namePrefix ? namePrefix + "." + key : key}`,
        value as any,
        order,
      );
    } else {
      res += renderSchema(value as any, key, order);
    }
  }
  return res;
}

function renderHeader(order: string[] = defaultOrder): string {
  return `
# Configuration Parameters

|${order.join("|")}|
|${order.map((s) => s.replaceAll(/./g, "-")).join("|")}|\n`;
}

export function renderNullable(obj: Convict.SchemaObj<unknown>): string {
  return `${obj.nullable ?? false}`;
}

export function renderSensitive(obj: Convict.SchemaObj<unknown>): string {
  return `${obj.sensitive ?? false}`;
}

export function renderFormat(obj: Convict.SchemaObj<unknown>): string {
  if (Array.isArray(obj.format)) {
    return obj.format.join(" &#124; ");
  }

  if (primitiveConstructors.includes(obj.format as any)) {
    return `${(obj.format as Function).name}`;
  }

  switch (typeof obj.format) {
    case "function":
      return `${(obj as any).format.name ?? "unknown format function"}`;
    case "string":
      return `${obj.format}`;
    case undefined:
      return "any";
    default:
      return "unknown";
  }
}
