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

export interface AsyncValidationContext<T> {
  value: T;
  addIssue(field: keyof T | string, message: string): void;
  signal?: AbortSignal;
}
export type AsyncRefinement<T> = (context: AsyncValidationContext<T>) => void | Promise<void>;

export class AsyncObjectSchema<T extends object = Record<string, unknown>> {
  readonly #refinements: AsyncRefinement<T>[] = [];
  constructor(readonly base: ObjectSchema<T>) {}
  refine(refinement: AsyncRefinement<T>): this {
    this.#refinements.push(refinement);
    return this;
  }
  describe(): SchemaDescriptor {
    return this.base.describe();
  }
  async parse(input: unknown, signal?: AbortSignal): Promise<ParseResult<T>> {
    const result = this.base.parse(input) as ParseResult<T>;
    if (!result.ok) return result;
    const errors: Record<string, string> = {};
    const context: AsyncValidationContext<T> = {
      value: result.value,
      signal,
      addIssue(field, message) {
        if (!(String(field) in errors)) errors[String(field)] = message;
      },
    };
    for (const refinement of this.#refinements) {
      if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
      await refinement(context);
    }
    return { ok: Object.keys(errors).length === 0, value: result.value, errors };
  }
}

export function asyncSchema<T extends object>(schema: ObjectSchema<T>): AsyncObjectSchema<T> {
  return new AsyncObjectSchema<T>(schema);
}

export async function parseBodyAsync<T extends object>(
  schema: AsyncObjectSchema<T>,
  request: Request,
  signal?: AbortSignal,
): Promise<{ ok: true; value: T } | { ok: false; response: Response }> {
  let body: Record<string, unknown> = {};
  const contentType = request.headers.get("content-type") ?? "";
  try {
    if (contentType.includes("json")) body = (await request.json()) as Record<string, unknown>;
    else {
      const data = await request.formData();
      for (const [key, value] of data) body[key] = value;
    }
  } catch {
    /* handled by schema */
  }
  const result = await schema.parse(body, signal);
  return result.ok
    ? { ok: true, value: result.value }
    : { ok: false, response: invalid(result.errors) };
}

export interface OpenApiSchema {
  type: "object";
  properties: Record<string, Record<string, unknown>>;
  required?: string[];
}
export function schemaToOpenApi(schema: ObjectSchema | AsyncObjectSchema): OpenApiSchema {
  const descriptor = schema.describe();
  const required: string[] = [];
  const properties: Record<string, Record<string, unknown>> = {};
  for (const [name, field] of Object.entries(descriptor.fields)) {
    const property: Record<string, unknown> = {
      type: field.type === "boolean" ? "boolean" : field.type,
    };
    if (field.label) property.title = field.label;
    for (const rule of field.rules) {
      if (rule.kind === "min") property[field.type === "string" ? "minLength" : "minimum"] = rule.n;
      else if (rule.kind === "max")
        property[field.type === "string" ? "maxLength" : "maximum"] = rule.n;
      else if (rule.kind === "length") property.minLength = property.maxLength = rule.n;
      else if (["email", "url", "uuid", "date"].includes(rule.kind))
        property.format = rule.kind === "url" ? "uri" : rule.kind;
      else if (rule.kind === "oneOf") property.enum = rule.values;
      else if (rule.kind === "pattern") property.pattern = rule.source;
      else if (rule.kind === "integer") property.type = "integer";
    }
    properties[name] = property;
    if (!field.optional) required.push(name);
  }
  return { type: "object", properties, ...(required.length ? { required } : {}) };
}

export function mergeValidationResults<T>(...results: ParseResult<T>[]): ParseResult<T> {
  return {
    ok: results.every((result) => result.ok),
    value: Object.assign({}, ...results.map((result) => result.value)) as T,
    errors: Object.assign({}, ...results.map((result) => result.errors)),
  };
}
