/** Data type of the additional field. */
export type AdditionalFieldType = "string" | "number" | "boolean" | "date"

/** Runtime value held by an `AdditionalField` (matches `AdditionalFieldType`). */
export type AdditionalFieldValue = string | number | boolean | Date

/** Value stored for an additional field while it is owned by a form. */
export type AdditionalFieldFormValue = AdditionalFieldValue | null

/** Default delay for custom additional-field validation after a value changes. */
export const DEFAULT_ADDITIONAL_FIELD_VALIDATION_DEBOUNCE_MS = 300

/** Runtime additional-field values keyed by their configured model names. */
export type AdditionalFieldFormValues = Record<string, AdditionalFieldFormValue>

/** UI rendering choice. Default is inferred from `AdditionalField.type`. */
export type AdditionalFieldInputType =
  | "input"
  | "textarea"
  | "number"
  | "slider"
  | "switch"
  | "checkbox"
  | "select"
  | "combobox"
  | "date"
  | "datetime"
  | "hidden"

/**
 * Augmentation target for widening `AdditionalField` slot types
 * (`label`, `renderProps`, `renderResult`) in UI packages.
 *
 * @example
 * declare module "@better-auth-ui/core" {
 *   interface AdditionalFieldRegister { label: ReactNode }
 * }
 */
// biome-ignore lint/suspicious/noEmptyInterface: augmentation target
export interface AdditionalFieldRegister {}

/** Resolved label type. Defaults to `string`. */
export type AdditionalFieldLabel = AdditionalFieldRegister extends {
  label: infer L
}
  ? L
  : string

/** Resolved argument type for `AdditionalField.render`. */
export type AdditionalFieldRenderProps = AdditionalFieldRegister extends {
  renderProps: infer P
}
  ? P
  : {
      name: string
      field: AdditionalField
      value: AdditionalFieldFormValue
      onBlur: () => void
      onChange: (value: AdditionalFieldFormValue) => void
      isInvalid?: boolean
      errors?: unknown[]
      isPending?: boolean
    }

/** Resolved return type for `AdditionalField.render`. */
export type AdditionalFieldRenderResult = AdditionalFieldRegister extends {
  renderResult: infer R
}
  ? R
  : unknown

/** Option for a `select` input. */
export interface AdditionalFieldOption {
  label: AdditionalFieldLabel
  value: string
}

/** Configuration for a single additional user field. */
export interface AdditionalField {
  /** Field name. Used as the user object key and form input `name`. */
  name: string
  /** Data type of the field. */
  type: AdditionalFieldType
  /** Visible label rendered next to the input. */
  label: AdditionalFieldLabel
  /** Override the default UI rendering. @default inferred from `type` */
  inputType?: AdditionalFieldInputType
  /** Placeholder text. */
  placeholder?: string
  /** Content rendered as a prefix addon inside the input group. */
  prefix?: AdditionalFieldLabel
  /** Content rendered as a suffix addon inside the input group. */
  suffix?: AdditionalFieldLabel
  /**
   * `Intl.NumberFormat` options for number fields. Use `maximumFractionDigits`
   * (and optionally `minimumFractionDigits`) to allow decimals, or `style: "currency"`
   * / `style: "percent"` for richer formatting.
   */
  formatOptions?: Intl.NumberFormatOptions
  /** Minimum value. Applies to `number` and `slider` input types. */
  min?: number
  /** Maximum value. Applies to `number` and `slider` input types. */
  max?: number
  /** Step value. Applies to `number` and `slider` input types. */
  step?: number
  /** @default false */
  required?: boolean
  /**
   * Default value used to seed the input on the sign-up form. On the user
   * profile, the value is always re-seeded from the persisted session.
   */
  defaultValue?: AdditionalFieldValue | null
  /**
   * Render the field but exclude it from submission payloads.
   * @default false
   */
  readOnly?: boolean
  /**
   * Show a copy-to-clipboard button as a suffix. Input variant only.
   * @default false
   */
  copyable?: boolean
  /** Options for the select input type. */
  options?: AdditionalFieldOption[]
  /**
   * Custom client-side validation. Throw an `Error` (the `message` is shown
   * to the user) when invalid; return / resolve normally when valid.
   *
   * Receives the current typed form value.
   */
  validate?: (
    value: AdditionalFieldValue | null | undefined
  ) => void | Promise<void>
  /**
   * Delay custom validation after a value changes. Set to `0` to validate
   * immediately.
   * @default 300
   */
  validateDebounceMs?: number
  /**
   * Render on the sign-up form. Pass `"above"` to render between the `email`
   * and `password` fields; otherwise the field renders below the password
   * block. `true` is an alias for `"below"`.
   * @default false
   */
  signUp?: boolean | "above" | "below"
  /** Render on the user profile. @default true */
  profile?: boolean
  /**
   * Custom renderer. Replaces the host UI package's built-in input. Use the
   * form bindings supplied by the host package to read and update its value.
   */
  render?: (props: AdditionalFieldRenderProps) => AdditionalFieldRenderResult
}

/** Ordered list of `AdditionalField` configurations. */
export type AdditionalFields = AdditionalField[]

/** Resolve the initial form value for a configured additional field. */
export function getAdditionalFieldDefaultValue(
  field: AdditionalField
): AdditionalFieldFormValue {
  if (field.defaultValue != null) return field.defaultValue
  return field.type === "boolean" ? false : null
}

/** Build collision-safe form defaults for a runtime list of fields. */
export function getAdditionalFieldDefaultValues(
  fields: readonly AdditionalField[]
): AdditionalFieldFormValues {
  return Object.fromEntries(
    fields.map((field) => [field.name, getAdditionalFieldDefaultValue(field)])
  )
}

/** Keep only writable configured values when building an API payload. */
export function getAdditionalFieldSubmitValues(
  fields: readonly AdditionalField[],
  values: AdditionalFieldFormValues
): AdditionalFieldFormValues {
  const submittedValues: AdditionalFieldFormValues = {}

  for (const field of fields) {
    if (!field.readOnly && field.name in values) {
      submittedValues[field.name] = values[field.name] ?? null
    }
  }

  return submittedValues
}

/** Validate required semantics without relying on native constraint state. */
export function validateAdditionalFieldRequired(
  field: AdditionalField,
  value: AdditionalFieldFormValue,
  requiredMessage: string
): string | undefined {
  if (!field.required) return undefined

  const isMissing =
    value == null ||
    value === "" ||
    (field.type === "boolean" && value !== true) ||
    (typeof value === "number" && !Number.isFinite(value)) ||
    (value instanceof Date && Number.isNaN(value.getTime()))

  return isMissing ? requiredMessage : undefined
}

/** Run a configured custom validator and normalize thrown values as errors. */
export async function validateAdditionalFieldValue(
  field: AdditionalField,
  value: AdditionalFieldFormValue
): Promise<string | undefined> {
  try {
    await field.validate?.(value)
    return undefined
  } catch (error) {
    return error instanceof Error ? error.message : String(error)
  }
}

/**
 * Convert a raw form value into the JS value Better Auth expects.
 * Returns `null` for blank input (explicit clear), `undefined` when omitted
 * or unparseable. Booleans always return `true`/`false`.
 */
export function parseAdditionalFieldValue(
  field: AdditionalField,
  raw: string | null | undefined
): AdditionalFieldValue | null | undefined {
  if (field.type === "boolean") {
    // FormData: checked checkbox/switch sends "on"; unchecked sends nothing.
    return raw === "on" || raw === "true"
  }

  if (raw == null) return undefined
  if (raw === "") return null

  if (field.type === "number") {
    const parsed = Number(raw)
    return Number.isNaN(parsed) ? undefined : parsed
  }

  if (field.type === "date") {
    const parsed = new Date(raw)
    return Number.isNaN(parsed.getTime()) ? undefined : parsed
  }

  return raw
}

/** Parse and validate a model's configured fields from submitted form data. */
export async function parseAdditionalFieldValues(
  fields: AdditionalFields,
  formData: FormData
): Promise<Record<string, AdditionalFieldValue | null>> {
  const values: Record<string, AdditionalFieldValue | null> = {}

  for (const field of fields) {
    if (field.readOnly) continue

    const value = parseAdditionalFieldValue(
      field,
      formData.get(field.name) as string | null
    )
    await field.validate?.(value)
    if (value !== undefined) values[field.name] = value
  }

  return values
}

/** Seed field defaults from a Better Auth model returned by a query. */
export function fieldsWithModelValues(
  fields: AdditionalFields,
  model: Record<string, unknown>
): AdditionalFields {
  return fields.map((field) => {
    const value = model[field.name]

    if (
      value !== null &&
      typeof value !== "string" &&
      typeof value !== "number" &&
      typeof value !== "boolean" &&
      !(value instanceof Date)
    ) {
      return field
    }

    return { ...field, defaultValue: value }
  })
}

/** Format a persisted additional-field value for compact read-only display. */
export function formatAdditionalFieldValue(
  value: unknown,
  languageTag?: string
): string | undefined {
  if (value === null || value === undefined || value === "") return undefined
  if (value instanceof Date) return value.toLocaleString(languageTag)
  if (typeof value === "string") {
    const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
    if (dateOnly) {
      const [, year, month, day] = dateOnly
      return new Date(
        Number(year),
        Number(month) - 1,
        Number(day)
      ).toLocaleString(languageTag)
    }

    const parsed = new Date(value)
    if (/^\d{4}-\d{2}-\d{2}T/.test(value) && !Number.isNaN(parsed.getTime())) {
      return parsed.toLocaleString(languageTag)
    }
    return value
  }
  if (typeof value === "number" || typeof value === "boolean") {
    return String(value)
  }
  return undefined
}

/** Resolve the effective `inputType`, defaulting based on `field.type`. */
export function resolveInputType(
  field: AdditionalField
): AdditionalFieldInputType {
  if (field.inputType) return field.inputType

  switch (field.type) {
    case "number":
      return "number"
    case "boolean":
      return "switch"
    case "date":
      return "date"
    default:
      return "input"
  }
}
