<!-- Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/ -->

<script setup lang="ts">
import { isEqual, cloneDeep, merge, isEmpty } from 'lodash-es'
import type { ConcreteComponent, Ref } from 'vue'
import {
  computed,
  ref,
  nextTick,
  shallowRef,
  reactive,
  toRef,
  watch,
  markRaw,
  useSlots,
} from 'vue'
import { FormKit, FormKitSchema } from '@formkit/vue'
import type {
  FormKitPlugin,
  FormKitSchemaNode,
  FormKitSchemaCondition,
  FormKitNode,
  FormKitClasses,
  FormKitSchemaDOMNode,
  FormKitSchemaComponent,
  FormKitMessageProps,
} from '@formkit/core'
import { createMessage, getNode, reset } from '@formkit/core'
import type { Except, SetRequired } from 'type-fest'
import { refDebounced, watchOnce } from '@vueuse/shared'
import getUuid from '@shared/utils/getUuid'
import log from '@shared/utils/log'
import { camelize } from '@shared/utils/formatter'
import UserError from '@shared/errors/UserError'
import type {
  EnumObjectManagerObjects,
  EnumFormUpdaterId,
  FormUpdaterRelationField,
  FormUpdaterQuery,
  FormUpdaterQueryVariables,
  ObjectAttributeValue,
} from '@shared/graphql/types'
import { QueryHandler } from '@shared/server/apollo/handler'
import { useObjectAttributeLoadFormFields } from '@shared/entities/object-attributes/composables/useObjectAttributeLoadFormFields'
import { useObjectAttributeFormFields } from '@shared/entities/object-attributes/composables/useObjectAttributeFormFields'
import testFlags from '@shared/utils/testFlags'
import { edgesToArray } from '@shared/utils/helpers'
import type { FormUpdaterTrigger } from '@shared/types/form'
import type { EntityObject } from '@shared/types/entity'
import { getFirstFocusableElement } from '@shared/utils/getFocusableElements'
import { parseGraphqlId } from '@shared/graphql/utils'
import { useFormUpdaterQuery } from './graphql/queries/formUpdater.api'
import { FormHandlerExecution, FormValidationVisibility } from './types'
import type {
  ChangedField,
  FormData,
  FormFieldAdditionalProps,
  FormFieldValue,
  FormHandler,
  FormHandlerFunction,
  FormSchemaField,
  FormSchemaLayout,
  FormSchemaNode,
  FormValues,
  ReactiveFormSchemData,
} from './types'
import FormLayout from './FormLayout.vue'
import FormGroup from './FormGroup.vue'

export interface Props {
  id?: string
  schema?: FormSchemaNode[]
  formUpdaterId?: EnumFormUpdaterId
  handlers?: FormHandler[]
  changeFields?: Record<string, Partial<FormSchemaField>>
  // Maybe in the future this is no longer needed, when FormKit supports group
  // without value grouping below group name (https://github.com/formkit/formkit/issues/461).
  flattenFormGroups?: string[]
  schemaData?: Except<ReactiveFormSchemData, 'fields'>
  formKitPlugins?: FormKitPlugin[]
  formKitSectionsSchema?: Record<
    string,
    Partial<FormKitSchemaNode> | FormKitSchemaCondition
  >
  class?: FormKitClasses | string | Record<string, boolean>

  // Can be used to define initial values on frontend side and fetched schema from the server.
  initialValues?: Partial<FormValues>
  initialEntityObject?: EntityObject
  queryParams?: Record<string, unknown>
  validationVisibility?: FormValidationVisibility
  disabled?: boolean
  autofocus?: boolean

  // Some special properties for working with object attribute fields inside of a form schema.
  useObjectAttributes?: boolean
  objectAttributeSkippedFields?: string[]

  // Implement the submit in this way, because we need to react on async usage of the submit function.
  // Don't forget that to submit a form with "Enter" key, you need to add a button with type="submit" inside of the form.
  // Or to have a button outside of form with "form" attribite with the same value as the form id.
  // After this method is called, form resets its values and state. If you need to call something afterwards,
  // like make route navigation, you can return a function from the submit handler, which will be called after the form reset.
  onSubmit?: (
    values: FormData,
  ) => Promise<void | (() => void)> | void | (() => void)
}

// Zammad currently expects formIds to be BigInts. Maybe convert to UUIDs later.
// const formId = `form-${getUuid()}`

// This is the formId generation logic from the legacy desktop app.
let formId = new Date().getTime() + Math.floor(Math.random() * 99999).toString()
formId = formId.substr(formId.length - 9, 9)

const props = withDefaults(defineProps<Props>(), {
  schema: () => {
    return []
  },
  changeFields: () => {
    return reactive({})
  },
  validationVisibility: FormValidationVisibility.Submit,
  useObjectAttributes: false,
})

const slots = useSlots()

const hasSchema = computed(
  () => Boolean(slots.default) || Boolean(props.schema),
)
const formSchemaInitialized = ref(false)

if (!hasSchema.value) {
  log.error(
    'No schema defined. Please use the schema prop or the default slot for the schema.',
  )
}

// Rename prop 'class' for usage in the template, because of reserved word
const localClass = toRef(props, 'class')

const emit = defineEmits<{
  (
    e: 'changed',
    fieldName: string,
    newValue: FormFieldValue,
    oldValue: FormFieldValue,
  ): void
  (e: 'node', node: FormKitNode): void
  (e: 'settled'): void
}>()

const showInitialLoadingAnimation = ref(false)
const debouncedShowInitialLoadingAnimation = refDebounced(
  showInitialLoadingAnimation,
  300,
)

const formKitInitialNodesSettled = ref(false)
const formNode: Ref<FormKitNode | undefined> = ref()
const formElement = ref<HTMLElement>()

const changeFields = toRef(props, 'changeFields')

const updaterChangedFields = new Set<string>()

const autofocusFirstInput = () => {
  nextTick(() => {
    const firstInput = getFirstFocusableElement(formElement.value)

    firstInput?.focus()
    firstInput?.scrollIntoView({ block: 'nearest' })
  })
}

const setFormNode = (node: FormKitNode) => {
  formNode.value = node

  // Save the initial entity object in the form node context, so that fields can use it.
  if (node.context && props.initialEntityObject) {
    node.context.initialEntityObject = props.initialEntityObject
  }

  node.settled.then(() => {
    showInitialLoadingAnimation.value = false
    formKitInitialNodesSettled.value = true

    // Reset directly after the initial request.
    updaterChangedFields.clear()

    const formName = node.context?.id || node.name
    testFlags.set(`${formName}.settled`)
    emit('settled')

    if (props.autofocus) autofocusFirstInput()
  })

  node.on('autofocus', autofocusFirstInput)

  emit('node', node)
}

const formNodeContext = computed(() => formNode.value?.context)

// Build the flat value when its requested for specific form groups.
const getFlatValues = (values: FormValues, formGroups: string[]) => {
  const flatValues = {
    ...values,
  }

  formGroups.forEach((formGroup) => {
    Object.assign(flatValues, flatValues[formGroup])
    delete flatValues[formGroup]
  })

  return flatValues
}

// Use the node context value, instead of the v-model, because of performance reason.
const values = computed<FormValues>(() => {
  if (!formNodeContext.value) {
    return {}
  }

  if (!props.flattenFormGroups) return formNodeContext.value.value

  return getFlatValues(formNodeContext.value.value, props.flattenFormGroups)
})

const relationFields: FormUpdaterRelationField[] = []
const relationFieldBelongsToObjectField: Record<string, string> = {}

const formUpdaterProcessing = computed(
  () => formNode.value?.context?.state.formUpdaterProcessing || false,
)

let delayedSubmit = false
const onSubmitRaw = () => {
  if (formUpdaterProcessing.value) {
    delayedSubmit = true
  }
}

const onSubmit = (values: FormData) => {
  // Needs to be checked, because the 'onSubmit' function is not required.
  if (!props.onSubmit) return undefined

  const flatValues = props.flattenFormGroups
    ? getFlatValues(values, props.flattenFormGroups)
    : values
  const emitValues = {
    ...flatValues,
    formId,
  }

  const submitResult = props.onSubmit(emitValues)

  if (submitResult instanceof Promise) {
    return submitResult
      .then((afterReset) => {
        // it's possible to destroy Form before this is called
        if (!formNode.value) return
        reset(formNode.value, values)
        if (typeof afterReset === 'function') afterReset()
      })
      .catch((errors: UserError) => {
        if (errors instanceof UserError) {
          formNode.value?.setErrors(
            // TODO: we need to check/style the general error output when we want to show it related to the form
            errors.generalErrors as string[],
            errors.getFieldErrorList(),
          )
        }
      })
  }

  if (formNode.value) {
    reset(formNode.value, values)
  }

  if (typeof submitResult === 'function') {
    submitResult()
  }

  return submitResult
}

let formUpdaterQueryHandler: QueryHandler<
  FormUpdaterQuery,
  FormUpdaterQueryVariables
>

const delayedSubmitPlugin = (node: FormKitNode) => {
  node.on('message-removed', async ({ payload }) => {
    if (payload.key === 'formUpdaterProcessing' && delayedSubmit) {
      // We need to wait on the "next tick", so that the validation for updated fields is ready.
      setTimeout(() => {
        delayedSubmit = false
        node.submit()
      }, 0)
    }
  })

  return false
}

const localFormKitPlugins = computed(() => {
  return [delayedSubmitPlugin, ...(props.formKitPlugins || [])]
})

const formConfig = computed(() => {
  return {
    validationVisibility: props.validationVisibility,
  }
})

// Define the additional component library for the used components which are not form fields.
// Because of a typescript error, we need to cased the type: https://github.com/formkit/formkit/issues/274
const additionalComponentLibrary = {
  FormLayout: markRaw(FormLayout) as unknown as ConcreteComponent,
  FormGroup: markRaw(FormGroup) as unknown as ConcreteComponent,
}

// Define the static schema, which will be filled with the real fields from the `schemaData`.
const staticSchema = ref<FormKitSchemaNode[]>([])

const fixedAndSkippedFields: string[] = []

const schemaData = reactive<ReactiveFormSchemData>({
  fields: {},
  values,
  ...props.schemaData,
})

const internalFieldCamelizeName: Record<string, string> = {}

const getInternalId = (item?: { id?: string; internalId?: number }) => {
  if (!item) return undefined
  if (item.internalId) return item.internalId
  if (!item.id) return undefined
  return parseGraphqlId(item.id).id
}

let initialEntityObjectAttributeMap: Record<string, FormFieldValue> = {}
const setInitialEntityObjectAttributeMap = (
  initialEntityObject = props.initialEntityObject,
) => {
  if (isEmpty(initialEntityObject)) return

  const { objectAttributeValues } = initialEntityObject

  if (!objectAttributeValues) return

  // Reduce object attribute values to flat structure
  initialEntityObjectAttributeMap =
    objectAttributeValues.reduce((acc: Record<string, FormFieldValue>, cur) => {
      const { attribute } = cur

      if (!attribute || !attribute.name) return acc

      acc[attribute.name] = cur.value
      return acc
    }, {}) || {}
}

// Initialize the initial entity object attribute map during the setup in a static way.
// It will maybe be updated later, when the resetForm is used with a different entity object.
setInitialEntityObjectAttributeMap()

const getInitialEntityObjectValue = (
  fieldName: string,
  initialEntityObject = props.initialEntityObject,
): FormFieldValue => {
  if (isEmpty(initialEntityObject)) return undefined

  let value: FormFieldValue
  if (relationFieldBelongsToObjectField[fieldName]) {
    const belongsToObject =
      initialEntityObject[relationFieldBelongsToObjectField[fieldName]]

    if (!belongsToObject) return undefined

    if ('edges' in belongsToObject) {
      value = edgesToArray(
        belongsToObject as { edges?: { node: { internalId: number } }[] },
      ).map((item) => getInternalId(item))
    } else {
      value = getInternalId(belongsToObject)
    }
  }

  if (!value) {
    const targetFieldName = internalFieldCamelizeName[fieldName] || fieldName

    value =
      targetFieldName in initialEntityObjectAttributeMap
        ? initialEntityObjectAttributeMap[targetFieldName]
        : initialEntityObject[targetFieldName]
  }

  return value
}

const getResetFormValues = (
  rootNode: FormKitNode,
  values: FormValues,
  object?: EntityObject,
  groupNode?: FormKitNode,
  resetDirty = true,
) => {
  const resetValues: FormValues = {}
  const dirtyNodes: FormKitNode[] = []

  const setResetFormValue = (
    name: string,
    value: FormFieldValue,
    parentName?: string,
  ) => {
    if (parentName) {
      resetValues[parentName] ||= {}
      ;(resetValues[parentName] as Record<string, FormFieldValue>)[name] = value
      return
    }

    resetValues[name] = value
  }

  Object.entries(schemaData.fields).forEach(([field, { props }]) => {
    const formElement = getNode(props.id || props.name)

    let parentName = ''
    if (formElement?.parent && formElement?.parent.name !== rootNode.name) {
      parentName = formElement.parent.name
    }

    // Do not use the parentName, when we are in group node reset context.
    const groupName = groupNode?.name
    if (groupName) {
      if (parentName !== groupName) return
      parentName = ''
    }

    if (!resetDirty && formElement?.context?.state.dirty) {
      dirtyNodes.push(formElement)
      setResetFormValue(field, formElement._value as FormFieldValue, parentName)
      return
    }

    if (field in values) {
      setResetFormValue(field, values[field], parentName)
      return
    }
    if (parentName && parentName in values) {
      const value = (values[parentName] as Record<string, FormFieldValue>)[
        field
      ]

      setResetFormValue(field, value, parentName)
      return
    }

    const objectValue = getInitialEntityObjectValue(field, object)
    if (objectValue !== undefined) {
      setResetFormValue(field, objectValue, parentName)
    }
  })

  return {
    dirtyNodes,
    resetValues,
  }
}

const resetForm = (
  values: FormValues = {},
  object: EntityObject | undefined = undefined,
  { resetDirty = true }: { resetDirty?: boolean } = {},
  groupNode: FormKitNode | undefined = undefined,
) => {
  if (!formNode.value) return

  const rootNode = formNode.value

  if (object) setInitialEntityObjectAttributeMap(object)

  const { dirtyNodes, resetValues } = getResetFormValues(
    rootNode,
    values,
    object,
    groupNode,
    resetDirty,
  )

  reset(
    groupNode || rootNode,
    Object.keys(resetValues).length ? resetValues : undefined,
  )

  // keep dirty nodes as dirty
  // TODO: check if we need to skip the formUpdater???
  dirtyNodes.forEach((node) => {
    node.input(node._value, false)
  })
}

defineExpose({
  formNode,
  formId,
  resetForm,
})

const localInitialValues: FormValues = { ...props.initialValues }

const initializeFieldRelation = (
  fieldName: string,
  relation: FormSchemaField['relation'],
  belongsToObjectField?: string,
) => {
  if (relation) {
    relationFields.push({
      name: fieldName,
      relation: relation.type,
      filterIds: relation.filterIds,
    })
  }

  if (belongsToObjectField) {
    relationFieldBelongsToObjectField[fieldName] = belongsToObjectField
  }
}

const setInternalField = (fieldName: string, internal: boolean) => {
  if (!internal) return

  internalFieldCamelizeName[fieldName] = camelize(fieldName)
}

const updateSchemaLink = (
  specificProps: FormFieldAdditionalProps,
  fieldName: string,
) => {
  // native fields don't have link attribute, and we don't have a way to get rendered link from graphql
  const values = (props.initialEntityObject?.objectAttributeValues ||
    []) as ObjectAttributeValue[]
  const attribute = values.find(({ attribute }) => attribute.name === fieldName)
  if (attribute?.renderedLink) {
    specificProps.link = attribute.renderedLink
  }
}

const updateSchemaDataField = (
  field: FormSchemaField | SetRequired<Partial<FormSchemaField>, 'name'>,
) => {
  const {
    show,
    updateFields,
    relation,
    props: specificProps = {},
    ...fieldProps
  } = field
  const showField = show ?? schemaData.fields[field.name]?.show ?? true

  // Not needed in this context.
  delete fieldProps.if

  // Special handling for the disabled prop, so that the form can handle also
  // the disable state from outside.
  if ('disabled' in fieldProps && !fieldProps.disabled) {
    fieldProps.disabled = undefined
  }

  updateSchemaLink(fieldProps, field.name)

  if (schemaData.fields[field.name]) {
    schemaData.fields[field.name] = {
      show: showField,
      updateFields: updateFields || false,
      props: Object.assign(
        schemaData.fields[field.name].props,
        fieldProps,
        specificProps,
      ),
    }
  } else {
    initializeFieldRelation(
      field.name,
      relation,
      specificProps?.belongsToObjectField,
    )

    setInternalField(field.name, Boolean(fieldProps.internal))

    const combinedFieldProps = Object.assign(fieldProps, specificProps)

    // Select the correct initial value (at this time localInitialValues has not already the information
    // from the initial entity object, so we need to check it manually).
    combinedFieldProps.value =
      field.name in localInitialValues
        ? localInitialValues[field.name]
        : getInitialEntityObjectValue(field.name) ?? combinedFieldProps.value

    // Save current initial value for later usage, when not already exists.
    if (!(field.name in localInitialValues))
      localInitialValues[field.name] = combinedFieldProps.value

    schemaData.fields[field.name] = {
      show: showField,
      updateFields: updateFields || false,
      props: combinedFieldProps,
    }
  }
}

const updateChangedFields = (
  changedFields: Record<string, Partial<FormSchemaField>>,
) => {
  Object.keys(changedFields).forEach(async (fieldName) => {
    if (!schemaData.fields[fieldName]) return

    const { value, ...changedFieldProps } = changedFields[fieldName]

    const field: SetRequired<Partial<FormSchemaField>, 'name'> = {
      ...changedFieldProps,
      name: fieldName,
    }

    if (
      value !== undefined &&
      (!formKitInitialNodesSettled.value ||
        (!schemaData.fields[fieldName].show && changedFieldProps.show))
    ) {
      field.value = value
    }

    // When a field will be visible with the update call, we need to wait before on a settled form, before we
    // continue (so that we have all values present inside the form).
    // This situtation can happen, when the form is used very fast.
    if (
      formKitInitialNodesSettled.value &&
      !schemaData.fields[fieldName].show &&
      changedFieldProps.show &&
      !formNode.value?.isSettled
    ) {
      await formNode.value?.settled
    }

    updaterChangedFields.add(fieldName)
    updateSchemaDataField(field)

    if (!formKitInitialNodesSettled.value) return

    if (
      !('value' in field) &&
      value !== undefined &&
      value !== values.value[fieldName]
    ) {
      updaterChangedFields.add(fieldName)
      getNode(fieldName)?.input(value, false)
    }
  })

  nextTick(() => {
    updaterChangedFields.clear()
    formNode.value?.store.remove('formUpdaterProcessing')
  })
}

const formHandlerExecution: Record<
  FormHandlerExecution,
  FormHandlerFunction[]
> = {
  [FormHandlerExecution.Initial]: [],
  [FormHandlerExecution.FieldChange]: [],
}
if (props.handlers) {
  props.handlers.forEach((handler) => {
    Object.values(FormHandlerExecution).forEach((execution) => {
      if (handler.execution.includes(execution)) {
        formHandlerExecution[execution].push(handler.callback)
      }
    })
  })
}

const executeFormHandler = (
  execution: FormHandlerExecution,
  currentValues: FormValues,
  changedField?: ChangedField,
) => {
  if (formHandlerExecution[execution].length === 0) return

  formHandlerExecution[execution].forEach((handler) => {
    handler(
      execution,
      formNode.value,
      currentValues,
      changeFields,
      updateSchemaDataField,
      schemaData,
      changedField,
      props.initialEntityObject,
    )
  })
}

const formUpdaterVariables = shallowRef<FormUpdaterQueryVariables>()
let nextFormUpdaterVariables: Maybe<FormUpdaterQueryVariables>
const executeFormUpdaterRefetch = () => {
  if (!nextFormUpdaterVariables) return

  formUpdaterVariables.value = nextFormUpdaterVariables

  // Reset the next variables so that it's not triggered a second time.
  nextFormUpdaterVariables = null
}

const handlesFormUpdater = (
  trigger: FormUpdaterTrigger,
  fieldName: string,
  newValue: FormFieldValue,
  oldValue: FormFieldValue,
) => {
  if (!props.formUpdaterId || !formUpdaterQueryHandler) return

  // We mark this as raw, because we want no deep reactivity on the form updater query variables.
  nextFormUpdaterVariables = markRaw({
    id: props.initialEntityObject?.id,
    formUpdaterId: props.formUpdaterId,
    data: {
      ...values.value,
      [fieldName]: newValue,
    },
    meta: {
      // We need a unique requestId, so that the query will always be executed on changes, also when the variables
      // are the same until the last request, because it could be that core workflow is setting a value back.
      requestId: getUuid(),
      formId,
      changedField: {
        name: fieldName,
        newValue,
        oldValue,
      },
    },
    relationFields,
  })

  formNode.value?.store.set(
    createMessage({
      blocking: true,
      key: 'formUpdaterProcessing',
      value: true,
      visible: false,
    }),
  )

  if (trigger !== 'blur') executeFormUpdaterRefetch()
}

const previousValues = new WeakMap<FormKitNode, FormFieldValue>()
const changedInputValueHandling = (inputNode: FormKitNode) => {
  inputNode.on('commit', ({ payload: newValue, origin: node }) => {
    const oldValue = previousValues.get(node)
    if (isEqual(newValue, oldValue)) return

    if (!formKitInitialNodesSettled.value) {
      previousValues.set(node, cloneDeep(newValue))
      return
    }
    if (
      inputNode.props.triggerFormUpdater &&
      !updaterChangedFields.has(node.name)
    ) {
      handlesFormUpdater(
        inputNode.props.formUpdaterTrigger,
        node.name,
        newValue,
        oldValue,
      )
    }
    emit('changed', node.name, newValue, oldValue)
    executeFormHandler(FormHandlerExecution.FieldChange, values.value, {
      name: node.name,
      newValue,
      oldValue,
    })
    previousValues.set(node, cloneDeep(newValue))
    updaterChangedFields.delete(node.name)
  })

  inputNode.on('blur', async () => {
    if (inputNode.props.formUpdaterTrigger !== 'blur') return

    if (!formNode.value?.isSettled) await formNode.value?.settled

    if (nextFormUpdaterVariables) executeFormUpdaterRefetch()
  })

  inputNode.hook.message((payload: FormKitMessageProps, next) => {
    if (payload.key === 'submitted' && formUpdaterProcessing.value) {
      payload.value = false
    }
    return next(payload)
  })

  return false
}

const buildStaticSchema = () => {
  const { getFormFieldSchema, getFormFieldsFromScreen } =
    useObjectAttributeFormFields(fixedAndSkippedFields)

  const buildFormKitField = (
    field: FormSchemaField,
  ): FormKitSchemaComponent => {
    const fieldId = field.id || field.name

    return {
      $cmp: 'FormKit',
      if: field.if ? field.if : `$fields.${field.name}.show`,
      bind: `$fields.${field.name}.props`,
      props: {
        type: field.type,
        key: fieldId,
        name: field.name,
        id: fieldId,
        formId,
        plugins: [changedInputValueHandling],
        triggerFormUpdater: field.triggerFormUpdater ?? !!props.formUpdaterId,
      },
    }
  }

  const getLayoutType = (
    layoutNode: FormSchemaLayout,
  ): FormKitSchemaDOMNode | FormKitSchemaComponent => {
    let layoutField: FormKitSchemaDOMNode | FormKitSchemaComponent

    if ('component' in layoutNode) {
      layoutField = {
        $cmp: layoutNode.component,
        props: layoutNode.props,
      }
    } else {
      layoutField = {
        $el: layoutNode.element,
        attrs: layoutNode.attrs,
      }
    }

    if (layoutNode.if) {
      layoutField.if = layoutNode.if
    }

    return layoutField
  }

  type ResolveFormSchemaNode = Exclude<FormSchemaNode, string>
  type ResolveFormKitSchemaNode = Exclude<FormKitSchemaNode, string>

  const resolveSchemaNode = (
    node: ResolveFormSchemaNode,
  ): Maybe<ResolveFormKitSchemaNode | ResolveFormKitSchemaNode[]> => {
    if ('isLayout' in node && node.isLayout) {
      return getLayoutType(node)
    }

    if ('isGroupOrList' in node && node.isGroupOrList) {
      return {
        $cmp: 'FormKit',
        ...(node.if && { if: node.if }),
        props: {
          type: node.type,
          name: node.name,
          id: node.name,
          key: node.name,
          plugins: node.plugins,
        },
      }
    }

    if ('object' in node && getFormFieldSchema && getFormFieldsFromScreen) {
      if ('name' in node && node.name && !node.type) {
        const { screen, object, ...fieldNode } = node

        const resolvedField = getFormFieldSchema(fieldNode.name, object, screen)

        if (!resolvedField) return null

        node = {
          ...resolvedField,
          ...fieldNode,
        } as FormSchemaField
      } else if ('screen' in node && !('name' in node)) {
        const resolvedFields = getFormFieldsFromScreen(node.screen, node.object)
        const formKitFields: ResolveFormKitSchemaNode[] = []
        resolvedFields.forEach((screenField) => {
          updateSchemaDataField(screenField)
          formKitFields.push(buildFormKitField(screenField))
        })

        return formKitFields
      }
    }

    updateSchemaDataField(node as FormSchemaField)
    return buildFormKitField(node as FormSchemaField)
  }

  const resolveSchema = (schema: FormSchemaNode[] = props.schema) => {
    return schema.reduce((resolvedSchema: FormKitSchemaNode[], node) => {
      if (typeof node === 'string') {
        resolvedSchema.push(node)
        return resolvedSchema
      }

      const resolvedNode = resolveSchemaNode(node)

      if (!resolvedNode) return resolvedSchema

      if ('children' in node) {
        const childrens = Array.isArray(node.children)
          ? [...resolveSchema(node.children)]
          : node.children

        resolvedSchema.push({
          ...(resolvedNode as Exclude<FormKitSchemaNode, string>),
          children: childrens,
        })
        return resolvedSchema
      }

      if (Array.isArray(resolvedNode)) {
        resolvedSchema.push(...resolvedNode)
      } else {
        resolvedSchema.push(resolvedNode)
      }

      return resolvedSchema
    }, [])
  }

  staticSchema.value = resolveSchema()
}

watchOnce(formKitInitialNodesSettled, () => {
  watch(
    changeFields,
    (newValue) => {
      updateChangedFields(newValue)
    },
    {
      deep: true,
    },
  )
})

watch(
  () => props.schemaData,
  () => Object.assign(schemaData, props.schemaData),
  {
    deep: true,
  },
)

const setFormSchemaInitialized = () => {
  if (!formSchemaInitialized.value) {
    formSchemaInitialized.value = true
  }
}

const initializeFormSchema = () => {
  buildStaticSchema()

  if (props.formUpdaterId) {
    formUpdaterVariables.value = markRaw({
      id: props.initialEntityObject?.id,
      formUpdaterId: props.formUpdaterId,
      data: localInitialValues,
      meta: {
        initial: true,
        formId,
      },
      relationFields,
    })

    formUpdaterQueryHandler = new QueryHandler(
      useFormUpdaterQuery(
        formUpdaterVariables as Ref<FormUpdaterQueryVariables>,
        {
          fetchPolicy: 'no-cache',
        },
      ),
    )

    formUpdaterQueryHandler.onResult((queryResult) => {
      // Execute the form handler function so that they can manipulate the form updater result.
      if (!formSchemaInitialized.value) {
        executeFormHandler(FormHandlerExecution.Initial, localInitialValues)
      }

      if (queryResult?.data.formUpdater) {
        updateChangedFields(
          changeFields.value
            ? merge(queryResult.data.formUpdater, changeFields.value)
            : queryResult.data.formUpdater,
        )
      }

      setFormSchemaInitialized()
    })
  } else {
    executeFormHandler(FormHandlerExecution.Initial, localInitialValues)
    if (changeFields.value) updateChangedFields(changeFields.value)

    setFormSchemaInitialized()
  }
}

// TODO: maybe we should react on schema changes and rebuild the static schema with a new form-id and re-rendering of
// the complete form (= use the formId as the key for the whole form to trigger the re-rendering of the component...)
if (props.schema) {
  showInitialLoadingAnimation.value = true

  if (props.useObjectAttributes) {
    // TODO: rebuild schema, when object attributes
    // was changed from outside(not such important,
    // because we have currently the reload solution like in the desktop view).
    if (props.objectAttributeSkippedFields) {
      fixedAndSkippedFields.push(...props.objectAttributeSkippedFields)
    }

    const objectAttributeObjects: EnumObjectManagerObjects[] = []

    const addObjectAttributeToObjects = (object: EnumObjectManagerObjects) => {
      if (objectAttributeObjects.includes(object)) return

      objectAttributeObjects.push(object)
    }

    const detectObjectAttributeObjects = (
      schema: FormSchemaNode[] = props.schema,
    ) => {
      schema.forEach((item) => {
        if (typeof item === 'string') return

        if ('object' in item) {
          if ('name' in item && item.name && !item.type) {
            fixedAndSkippedFields.push(item.name)
          }

          addObjectAttributeToObjects(item.object)
        }

        if ('children' in item && Array.isArray(item.children)) {
          detectObjectAttributeObjects(item.children)
        }
      })
    }

    detectObjectAttributeObjects()

    // We need only to fetch object attributes, when there are used in the given schema.
    if (objectAttributeObjects.length > 0) {
      const { objectAttributesLoading } = useObjectAttributeLoadFormFields(
        objectAttributeObjects,
      )

      const unwatchTriggerFormInitialize = watch(
        objectAttributesLoading,
        (loading) => {
          if (!loading) {
            nextTick(() => unwatchTriggerFormInitialize())
            initializeFormSchema()
          }
        },
        { immediate: true },
      )
    } else {
      initializeFormSchema()
    }
  } else {
    initializeFormSchema()
  }
}
</script>

<script lang="ts">
export default {
  inheritAttrs: false,
}
</script>

<template>
  <div
    v-if="debouncedShowInitialLoadingAnimation"
    class="flex items-center justify-center"
  >
    <CommonIcon name="mobile-loading" animation="spin" />
  </div>
  <FormKit
    v-if="
      hasSchema &&
      ((formSchemaInitialized && Object.keys(schemaData.fields).length > 0) ||
        $slots.default)
    "
    v-bind="$attrs"
    :id="id"
    type="form"
    novalidate
    :config="formConfig"
    :form-class="localClass"
    :actions="false"
    :incomplete-message="false"
    :plugins="localFormKitPlugins"
    :sections-schema="formKitSectionsSchema"
    :disabled="disabled"
    @node="setFormNode"
    @submit="onSubmit"
    @submit-raw="onSubmitRaw"
  >
    <slot name="before-fields" />
    <slot
      name="default"
      :schema="staticSchema"
      :data="schemaData"
      :library="additionalComponentLibrary"
    >
      <div
        v-show="
          formKitInitialNodesSettled && !debouncedShowInitialLoadingAnimation
        "
        ref="formElement"
      >
        <FormKitSchema
          :schema="staticSchema"
          :data="schemaData"
          :library="additionalComponentLibrary"
        />
      </div>
    </slot>
    <slot name="after-fields" />
  </FormKit>
</template>