import { type TSchema, TypeBoxError, z } from "alepha";
import type { BaseInputField } from "./FormModel.ts";
import { prettyName } from "./prettyName.ts";

/**
 * Semantic icon hint derived from schema metadata. UI layers map this
 * to their own icon set — this module is headless and ships no JSX.
 */
export type IconHint =
  | "email"
  | "password"
  | "phone"
  | "url"
  | "number"
  | "calendar"
  | "clock"
  | "list"
  | "text"
  | "user"
  | "file"
  | "switch";

export interface FieldConstraints {
  minLength?: number;
  maxLength?: number;
  minimum?: number;
  maximum?: number;
  pattern?: string;
}

export interface FieldMeta {
  id?: string;
  label: string;
  description?: string;
  error?: string;
  required: boolean;
  type?: string;
  format?: string;
  isEnum: boolean;
  isArray: boolean;
  isObject: boolean;
  isArrayOfObjects: boolean;
  enum?: readonly unknown[];
  iconHint?: IconHint;
  constraints: FieldConstraints;
  testId?: string;
  schema: TSchema;
  /**
   * Raw `$control` value from the schema, untyped here. The UI layer
   * (`alepha/react/ui`) provides the strict {@link SchemaControl} type and
   * a `resolveSchemaControl` helper to evaluate the function form.
   */
  control?: unknown;
}

export interface ParseFieldOptions {
  label?: string;
  description?: string;
  error?: Error;
}

/**
 * Derives a {@link FieldMeta} from an `InputField` (from `useForm`) plus
 * optional overrides. Pure — no React, no JSX, no UI library coupling.
 *
 * UI components consume this metadata to render labels, descriptions,
 * error messages, icons, and validation constraints.
 */
export const parseField = (
  input: BaseInputField,
  options: ParseFieldOptions = {},
): FieldMeta => {
  // Peel optional/nullable/default wrappers: structure, `.meta()`, format and
  // constraints all live on the inner schema, not the wrapper. `required` is
  // tracked separately via `input.required`.
  const schema = z.schema.unwrap(input.schema) as TSchema & {
    minLength?: number;
    maxLength?: number;
    minValue?: number;
    maxValue?: number;
  };

  // Title / description / `$control` ride on zod's `.meta()` registry now
  // (typebox kept them as plain schema properties). Read them from there.
  const meta = (
    typeof (schema as any).meta === "function" ? (schema as any).meta() : null
  ) as Record<string, unknown> | null;

  const label =
    options.label ??
    (typeof meta?.title === "string" ? meta.title : undefined) ??
    prettyName(input.path);

  const description =
    options.description ??
    (typeof meta?.description === "string" ? meta.description : undefined);

  const error =
    options.error instanceof TypeBoxError
      ? (options.error as TypeBoxError).value?.message
      : undefined;

  // Structural classification via zod guards (the schema is a real zod schema,
  // not a JSON-Schema object — `schema.type`/`schema.enum` no longer apply).
  const isEnum = z.schema.isEnum(schema);
  const isArray = z.schema.isArray(schema);
  const isObject = z.schema.isObject(schema);
  const isInteger = z.schema.isInteger(schema);
  const isNumber = !isInteger && z.schema.isNumber(schema);
  const isBoolean = z.schema.isBoolean(schema);
  const type = isObject
    ? "object"
    : isArray
      ? "array"
      : isEnum
        ? "string"
        : isInteger
          ? "integer"
          : isNumber
            ? "number"
            : isBoolean
              ? "boolean"
              : z.schema.isString(schema)
                ? "string"
                : undefined;

  const format =
    typeof z.schema.format(schema) === "string"
      ? z.schema.format(schema)
      : undefined;
  const element = (schema as any).element ?? (schema as any).items;
  const isArrayOfObjects = isArray && z.schema.isObject(element);

  const name = input.props.name;
  const iconHint = inferIconHint({ type, format, name, isEnum, isArray });

  const constraints: FieldConstraints = {};
  if (typeof schema.minLength === "number")
    constraints.minLength = schema.minLength;
  if (typeof schema.maxLength === "number")
    constraints.maxLength = schema.maxLength;
  if (typeof schema.minValue === "number")
    constraints.minimum = schema.minValue;
  if (typeof schema.maxValue === "number")
    constraints.maximum = schema.maxValue;
  const pattern = readPattern(schema);
  if (pattern) constraints.pattern = pattern;

  return {
    id: input.props.id,
    label,
    description,
    error,
    required: input.required,
    type,
    format,
    isEnum,
    isArray,
    isObject,
    isArrayOfObjects,
    enum: isEnum ? z.schema.enumValues(schema) : undefined,
    iconHint,
    constraints,
    testId: (input.props as Record<string, unknown>)["data-testid"] as
      | string
      | undefined,
    schema: input.schema,
    control: meta?.$control,
  };
};

/** Best-effort read of a `ZodString` regex pattern from its check list. */
const readPattern = (schema: unknown): string | undefined => {
  const checks = (schema as any)?._zod?.def?.checks as
    | Array<{ _zod?: { def?: { pattern?: RegExp; format?: string } } }>
    | undefined;
  if (!Array.isArray(checks)) return undefined;
  for (const check of checks) {
    const pattern = check?._zod?.def?.pattern;
    if (pattern instanceof RegExp) return pattern.source;
  }
  return undefined;
};

const inferIconHint = (params: {
  type?: string;
  format?: string;
  name?: string;
  isEnum: boolean;
  isArray: boolean;
}): IconHint | undefined => {
  const { type, format, name, isEnum, isArray } = params;

  if (format === "email") return "email";
  if (format === "url" || format === "uri") return "url";
  if (format === "tel" || format === "phone") return "phone";
  if (format === "date" || format === "date-time") return "calendar";
  if (format === "time") return "clock";

  if (name?.toLowerCase().includes("password")) return "password";
  if (name?.toLowerCase().includes("email")) return "email";
  if (name?.toLowerCase().includes("phone")) return "phone";

  if (type === "boolean") return "switch";
  if (type === "number" || type === "integer") return "number";
  if (isEnum || isArray) return "list";
  if (type === "string") return "text";

  return undefined;
};
