import {
  checkField,
  type ObjectSchema,
  type ParseResult,
  type RuleDescriptor,
  type SchemaDescriptor,
} from "./index.ts";

export interface JsonSchemaDocument {
  $schema: "https://json-schema.org/draft/2020-12/schema";
  $id?: string;
  title?: string;
  type: "object";
  properties: Record<string, Record<string, unknown>>;
  required?: string[];
  additionalProperties: false;
}

function descriptorOf(schema: ObjectSchema | SchemaDescriptor): SchemaDescriptor {
  return "describe" in schema ? schema.describe() : schema;
}

function fieldJsonSchema(field: SchemaDescriptor["fields"][string]): Record<string, unknown> {
  const output: Record<string, unknown> = field.type === "unknown" ? {} : { type: field.type };
  if (field.label) output.title = field.label;
  if (field.trim) output["x-wrn-trim"] = true;
  if (field.typeMessage) output["x-wrn-type-message"] = field.typeMessage;
  for (const rule of field.rules) {
    switch (rule.kind) {
      case "min":
        output[field.type === "string" ? "minLength" : "minimum"] = rule.n;
        break;
      case "max":
        output[field.type === "string" ? "maxLength" : "maximum"] = rule.n;
        break;
      case "length":
        output.minLength = rule.n;
        output.maxLength = rule.n;
        break;
      case "email":
      case "uuid":
      case "date":
        output.format = rule.kind;
        break;
      case "url":
        output.format = "uri";
        break;
      case "oneOf":
        output.enum = rule.values;
        break;
      case "pattern":
        output.pattern = rule.source;
        if (rule.flags) output["x-wrn-pattern-flags"] = rule.flags;
        break;
      case "integer":
        output.type = "integer";
        break;
    }
    if (rule.message) {
      const messages = (output["x-wrn-messages"] ??= []) as string[];
      messages.push(rule.message);
    }
  }
  return output;
}

export function toJsonSchema(
  schema: ObjectSchema | SchemaDescriptor,
  options: { id?: string; title?: string } = {},
): JsonSchemaDocument {
  const descriptor = descriptorOf(schema);
  const properties: Record<string, Record<string, unknown>> = {};
  const required: string[] = [];
  for (const [name, field] of Object.entries(descriptor.fields)) {
    properties[name] = fieldJsonSchema(field);
    if (!field.optional) required.push(name);
  }
  return {
    $schema: "https://json-schema.org/draft/2020-12/schema",
    ...(options.id ? { $id: options.id } : {}),
    ...(options.title ? { title: options.title } : {}),
    type: "object",
    properties,
    ...(required.length ? { required } : {}),
    additionalProperties: false,
  };
}

export function openApiRequestBody(
  schema: ObjectSchema | SchemaDescriptor,
  options: { description?: string; required?: boolean; contentTypes?: string[] } = {},
) {
  const document = toJsonSchema(schema);
  const { $schema: _, ...openApiSchema } = document;
  const content = Object.fromEntries(
    (options.contentTypes ?? ["application/json"]).map((contentType) => [
      contentType,
      { schema: openApiSchema },
    ]),
  );
  return {
    ...(options.description ? { description: options.description } : {}),
    required: options.required ?? true,
    content,
  };
}

export type ValidationMessageKey = "required" | "number" | `rule.${RuleDescriptor["kind"]}`;
export type ValidationMessageTranslator = (
  key: ValidationMessageKey,
  params: Record<string, unknown>,
) => string;

export function localizeDescriptor(
  schema: ObjectSchema | SchemaDescriptor,
  translate: ValidationMessageTranslator,
): SchemaDescriptor {
  const descriptor = structuredClone(descriptorOf(schema));
  for (const [field, value] of Object.entries(descriptor.fields)) {
    value.requiredMessage ??= translate("required", { field, label: value.label ?? field });
    if (value.type === "number") {
      value.typeMessage ??= translate("number", { field, label: value.label ?? field });
    }
    value.rules = value.rules.map((rule) => ({
      ...rule,
      message:
        rule.message ??
        translate(`rule.${rule.kind}`, {
          field,
          label: value.label ?? field,
          ...(rule as unknown as Record<string, unknown>),
        }),
    }));
  }
  return descriptor;
}

export function parseDescriptor<T = Record<string, unknown>>(
  descriptor: SchemaDescriptor,
  source: Record<string, unknown>,
): ParseResult<T> {
  const value: Record<string, unknown> = {};
  const errors: Record<string, string> = {};
  for (const [name, field] of Object.entries(descriptor.fields)) {
    const result = checkField(field, source[name]);
    value[name] = result.value;
    if (result.error) errors[name] = result.error;
  }
  return { ok: Object.keys(errors).length === 0, value: value as T, errors };
}
