import type { ButtonType, ColumnType, GridColumnReqType, GridColumnType, MapType, TableType, ViewType } from 'nocodb-sdk'
import { CommonAggregations, ViewTypes, getFirstNonPersonalView, isHiddenCol, isSystemColumn } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'

const [useProvideViewColumns, useViewColumns] = useInjectionState(
  (
    view: Ref<ViewType | undefined>,
    meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>,
    reloadData?: (params?: { shouldShowLoading?: boolean }) => void,
    isPublic = false,
  ) => {
    const rootFields = ref<ColumnType[]>([])

    const fields = ref<Field[]>()

    const fieldsMap = computed(() =>
      (fields.value || []).reduce<Record<string, any>>((acc, curr) => {
        if (curr.fk_column_id) {
          acc[curr.fk_column_id] = curr
        }
        return acc
      }, {}),
    )

    const filterQuery = ref('')

    const { $api, $e, $eventBus } = useNuxtApp()

    const { t } = useI18n()

    const { isUIAllowed } = useRoles()

    const { isSharedBase } = storeToRefs(useBase())

    const viewStore = useViewsStore()

    const { views } = storeToRefs(viewStore)

    const isDefaultView = computed(() => {
      return (
        getFirstNonPersonalView(views.value, {
          includeViewType: ViewTypes.GRID,
        })?.id === view.value?.id
      )
    })

    const isViewColumnsLoading = ref(true)

    const hidingViewColumnsMap = ref<Record<string, boolean>>({})

    const { addUndo, defineViewScope } = useUndoRedo()

    const isLocalMode = computed(() => isPublic || !isUIAllowed('viewFieldEdit') || isSharedBase.value)

    const hasViewFieldDataEditPermission = computed(() => isUIAllowed('viewFieldDataEdit'))

    const localChanges = ref<Record<string, Field>>({})

    const isColumnViewEssential = (column: ColumnType) => {
      // TODO: consider at some point ti delegate this via a cleaner design pattern to view specific check logic
      // which could be inside of a view specific helper class (and generalized via an interface)
      // (on the other hand, the logic complexity is still very low atm - might be overkill)
      return view.value?.type === ViewTypes.MAP && (view.value?.view as MapType)?.fk_geo_data_col_id === column.id
    }

    const metaColumnById = computed<Record<string, ColumnType>>(() => {
      if (!meta.value?.columns) return {}

      return (meta.value.columns as ColumnType[]).reduce(
        (acc, curr) => ({
          ...acc,
          [curr.id!]: curr,
        }),
        {},
      ) as Record<string, ColumnType>
    })

    const gridViewCols = ref<Record<string, GridColumnType>>({})

    const loadViewColumns = async () => {
      if (!meta.value || !view.value?.id) return

      let order = 1

      const data =
        ((isPublic
          ? meta.value?.columns
          : (
              await $api.internal.getOperation(meta.value!.fk_workspace_id!, meta.value!.base_id!, {
                operation: 'viewColumnList',
                viewId: view.value.id,
              })
            ).list) as any[]) ?? []

      const fieldById = data.reduce<Record<string, any>>((acc, curr) => {
        // If hide column api is in progress and we try to load columns before that then we need to assign local visibility state
        curr.show = hidingViewColumnsMap.value[curr.fk_column_id] && !!curr.show ? false : !!curr.show

        return {
          ...acc,
          [curr.fk_column_id]: curr,
        }
      }, {})

      fields.value = (meta.value?.columns || [])
        .filter((column: ColumnType) => {
          // filter created by and last modified by system columns
          if (isHiddenCol(column, meta.value)) return false
          return true
        })
        .map((column: ColumnType) => {
          const currentColumnField = fieldById[column.id!] || {}

          return {
            title: column.title,
            fk_column_id: column.id,
            ...currentColumnField,
            show: currentColumnField.show || isColumnViewEssential(currentColumnField),
            order: currentColumnField.order || order++,
            aggregation: currentColumnField?.aggregation ?? CommonAggregations.None,
            system: isSystemColumn(metaColumnById?.value?.[currentColumnField.fk_column_id!]),
            isViewEssentialField: isColumnViewEssential(column),
            initialShow:
              currentColumnField.show ||
              isColumnViewEssential(currentColumnField) ||
              (currentColumnField as GridColumnType)?.group_by,
          }
        })
        .sort((a: Field, b: Field) => a.order - b.order)

      if (isLocalMode.value && fields.value) {
        for (const key in localChanges.value) {
          const fieldIndex = fields.value.findIndex((f) => f.fk_column_id === key)
          if (fieldIndex !== undefined && fieldIndex > -1) {
            fields.value[fieldIndex] = localChanges.value[key]
            fields.value = fields.value.sort((a: Field, b: Field) => a.order - b.order)
          }
        }
      }

      const colsData: GridColumnType[] = (isPublic.value ? view.value?.columns : fields.value) ?? []

      gridViewCols.value = colsData.reduce<Record<string, GridColumnType>>(
        (o, col) => ({
          ...o,
          [col.fk_column_id as string]: col,
        }),
        {},
      )
    }

    const updateDefaultViewColumnMeta = async (
      columnId?: string,
      colMeta: { defaultViewColOrder?: number; defaultViewColVisibility?: boolean } = {},
      allFields = false,
    ) => {
      if (!meta.value?.columns) return

      meta.value.columns = (meta.value.columns || []).map((c: ColumnType) => {
        if (!allFields && c.id !== columnId) return c

        if (allFields && c.pv) return c

        c.meta = { ...parseProp(c.meta || {}), ...colMeta }
        return c
      })

      if (!allFields && columnId && meta.value?.columnsById?.[columnId]) {
        meta.value.columnsById[columnId].meta = {
          ...parseProp(meta.value.columnsById[columnId].meta),
          ...colMeta,
        }
      }

      if (allFields) {
        meta.value.columnsById = meta.value.columns.reduce((acc, c) => {
          acc[c.id!] = c

          return acc
        }, {} as Record<string, ColumnType>)
      }
    }

    const showAll = async (ignoreIds?: any) => {
      if (isLocalMode.value) {
        const fieldById = (fields.value || []).reduce<Record<string, any>>((acc, curr) => {
          if (curr.fk_column_id) {
            curr.show = !!curr.initialShow
            acc[curr.fk_column_id] = curr
          }
          return acc
        }, {})

        fields.value = (fields.value || [])?.map((field: Field) => {
          const updateField = {
            ...field,
            show: fieldById[field.fk_column_id!]?.show,
          }
          localChanges.value[field.fk_column_id!] = field
          return updateField
        })

        meta.value!.columns = meta.value!.columns?.map((column: ColumnType) => {
          if (fieldById[column.id!]) {
            return {
              ...column,
              ...fieldById[column.id!],
              id: fieldById[column.id!].fk_column_id,
            }
          }
          return column
        })

        reloadData?.()
        return
      }

      if (view?.value?.id) {
        if (ignoreIds) {
          await $api.internal.postOperation(view.value.fk_workspace_id!, view.value.base_id!, {
            operation: 'showAllColumns',
            viewId: view.value.id,
            ignoreIds,
          })
        } else {
          await $api.internal.postOperation(view.value.fk_workspace_id!, view.value.base_id!, {
            operation: 'showAllColumns',
            viewId: view.value.id,
          })
        }

        if (isDefaultView.value) {
          updateDefaultViewColumnMeta(undefined, { defaultViewColVisibility: true }, true)
        }
      }

      await loadViewColumns()
      reloadData?.()
      $e('a:fields:show-all')
    }

    const hideAll = async (ignoreIds?: any) => {
      if (isLocalMode.value) {
        const fieldById = (fields.value || []).reduce<Record<string, any>>((acc, curr) => {
          if (curr.fk_column_id) {
            curr.show = !!metaColumnById?.value?.[curr.fk_column_id!]?.pv || !!curr.isViewEssentialField
            acc[curr.fk_column_id] = curr
          }
          return acc
        }, {})

        fields.value = (fields.value || [])?.map((field: Field) => {
          const updateField = {
            ...field,
            show: fieldById[field.fk_column_id!]?.show,
          }
          localChanges.value[field.fk_column_id!] = field
          return updateField
        })

        meta.value!.columns = meta.value!.columns?.map((column: ColumnType) => {
          if (fieldById[column.id!]) {
            return {
              ...column,
              ...fieldById[column.id!],
              id: fieldById[column.id!].fk_column_id,
            }
          }
          return column
        })

        reloadData?.()
        return
      }
      if (view?.value?.id) {
        if (ignoreIds) {
          await $api.internal.postOperation(view.value.fk_workspace_id!, view.value.base_id!, {
            operation: 'hideAllColumns',
            viewId: view.value.id,
            ignoreIds,
          })
        } else {
          await $api.internal.postOperation(view.value.fk_workspace_id!, view.value.base_id!, {
            operation: 'hideAllColumns',
            viewId: view.value.id,
          })
        }

        if (isDefaultView.value) {
          updateDefaultViewColumnMeta(undefined, { defaultViewColVisibility: false }, true)
        }
      }

      await loadViewColumns()
      reloadData?.()
      $e('a:fields:show-all')
    }

    const saveOrUpdate = async (field: any, index: number, disableDataReload = false, updateDefaultViewColMeta = false) => {
      if (isLocalMode.value && fields.value) {
        fields.value[index] = field
        meta.value!.columns = meta.value!.columns?.map((column: ColumnType) => {
          if (column.id === field.fk_column_id) {
            return {
              ...column,
              ...field,
              id: field.fk_column_id,
            }
          }
          return column
        })

        localChanges.value[field.fk_column_id] = field
      }

      if (isUIAllowed('viewFieldEdit')) {
        if (field.id && view?.value?.id) {
          await $api.internal.postOperation(
            meta.value!.fk_workspace_id!,
            meta.value!.base_id!,
            {
              operation: 'viewColumnUpdate',
              viewId: view.value.id,
              columnId: field.id,
            },
            field,
          )

          if (updateDefaultViewColMeta) {
            updateDefaultViewColumnMeta(field.fk_column_id, {
              defaultViewColOrder: field.order,
              defaultViewColVisibility: field.show,
            })
          }
        } else if (view.value?.id) {
          const insertedField = (await $api.internal.postOperation(
            meta.value!.fk_workspace_id!,
            meta.value!.base_id!,
            {
              operation: 'viewColumnCreate',
              viewId: view.value.id,
            },
            field,
          )) as any

          /** update the field in fields if defined */
          if (fields.value) fields.value[index] = insertedField

          return insertedField
        }
      }

      if (!disableDataReload) {
        await loadViewColumns()
        reloadData?.({ shouldShowLoading: false })
      }
    }

    const showSystemFields = computed({
      get() {
        return (view.value?.show_system_fields as boolean) || false
      },
      set(v: boolean) {
        if (view?.value?.id) {
          if (!isLocalMode.value) {
            $api.dbView
              .update(view.value.id, {
                show_system_fields: v,
              })
              .finally(() => {
                loadViewColumns()
                reloadData?.()
              })
          }
          view.value.show_system_fields = v
        }
        $e('a:fields:system-fields')
      },
    })

    const fieldSearchBasisOptions: {
      searchBasisInfo: string
      filterCallback: (query: string, option: ColumnType) => boolean
    }[] = [
      {
        searchBasisInfo: t('msg.info.matchedByButtonLabel'),
        filterCallback: (query, option) => {
          if (!option) return false

          const column = option as ColumnType

          return isButton(column) && searchCompare([(column.colOptions as ButtonType)?.label], query)
        },
      },
      {
        searchBasisInfo: t('msg.info.matchedByFieldDescription'),
        filterCallback: (query, option) => {
          if (!option) return false

          const column = option as ColumnType

          if (!column.description) return false

          return searchCompare([column.description], query)
        },
      },
    ]

    const searchBasisIdMap = ref<Record<string, string>>({})

    /**
     * Apply search basis filter to the column
     * @param column - The column to apply the search basis filter to
     * @returns true if the column matches the search basis filter, false otherwise
     */
    const applySearchBasisFilter = (column?: ColumnType) => {
      if (!column) return false

      for (const basisOption of fieldSearchBasisOptions) {
        if (!basisOption.filterCallback(filterQuery.value, column)) continue

        searchBasisIdMap.value[column.id!] = basisOption.searchBasisInfo
        return true
      }

      return false
    }

    const filteredFieldList = computed(() => {
      searchBasisIdMap.value = {}

      return (fields.value || []).filter((field: Field) => {
        if (!field.initialShow && isLocalMode.value && !hasViewFieldDataEditPermission.value) {
          return false
        }
        const column = metaColumnById?.value?.[field.fk_column_id!]

        if (column?.pv) {
          // Step 1: Apply default filter
          if (!filterQuery.value || searchCompare([field.title], filterQuery.value)) return true

          // Step 2: Apply search basis options if default filter fails
          return applySearchBasisFilter(column)
        }

        // hide system columns if not enabled
        if (!showSystemFields.value && isSystemColumn(column)) {
          return false
        }

        // Step 1: Apply default filter
        if (!filterQuery.value || searchCompare([field.title], filterQuery.value)) return true

        // Step 2: Apply search basis options if default filter fails
        return applySearchBasisFilter(column)
      })
    })

    const numberOfHiddenFields = computed(() => {
      return (fields.value || [])
        ?.filter((field: Field) => {
          if (!field.initialShow && isLocalMode.value && !hasViewFieldDataEditPermission.value) {
            return false
          }

          if (metaColumnById?.value?.[field.fk_column_id!]?.pv) {
            return true
          }

          // hide system columns if not enabled
          if (!showSystemFields.value && isSystemColumn(metaColumnById?.value?.[field.fk_column_id!])) {
            return false
          }

          return true
        })
        .filter((field) => !field.show)?.length
    })

    const sortedAndFilteredFields = computed<ColumnType[]>(() => {
      return (fields?.value
        ?.filter((field: Field) => {
          // hide system columns if not enabled
          if (
            !showSystemFields.value &&
            metaColumnById.value &&
            metaColumnById?.value?.[field.fk_column_id!] &&
            isSystemColumn(metaColumnById.value?.[field.fk_column_id!]) &&
            !metaColumnById.value?.[field.fk_column_id!]?.pv
          ) {
            return false
          }
          return field.show && metaColumnById?.value?.[field.fk_column_id!]
        })
        ?.sort((a: Field, b: Field) => a.order - b.order)
        ?.map((field: Field) => metaColumnById?.value?.[field.fk_column_id!]) || []) as ColumnType[]
    })

    const toggleFieldVisibility = (checked: boolean, field: any) => {
      const fieldIndex = fields.value?.findIndex((f) => f.fk_column_id === field.fk_column_id)

      if (!fieldIndex && fieldIndex !== 0) return
      addUndo({
        undo: {
          fn: (v: boolean) => {
            field.show = !v
            saveOrUpdate(field, fieldIndex, false, isDefaultView.value)
          },
          args: [checked],
        },
        redo: {
          fn: (v: boolean) => {
            field.show = v
            saveOrUpdate(field, fieldIndex, false, isDefaultView.value)
          },
          args: [checked],
        },
        scope: defineViewScope({ view: view.value }),
      })
      saveOrUpdate(field, fieldIndex, !checked, isDefaultView.value)
    }

    const toggleFieldStyles = (field: any, style: 'underline' | 'bold' | 'italic', status: boolean) => {
      const fieldIndex = fields.value?.findIndex((f) => f.fk_column_id === field.fk_column_id)
      if (!fieldIndex && fieldIndex !== 0) return
      field[style] = status
      $e('a:fields:style', { style, status })
      saveOrUpdate(field, fieldIndex, true)
    }

    // reload view columns when active view changes
    // or when columns changes(delete/add)
    watch(
      [() => view?.value?.id, () => meta.value?.columns],
      async ([newViewId], [oldViewId]) => {
        // If we change view, we need to reset the hidingViewColumnsMap
        if (oldViewId && oldViewId !== newViewId) {
          hidingViewColumnsMap.value = {}
        }

        if (!ncIsEmptyArray(hidingViewColumnsMap.value) && Object.values(hidingViewColumnsMap.value).some((v) => v)) return

        // reload only if view belongs to current table
        if (newViewId && view.value?.fk_model_id === meta.value?.id) {
          isViewColumnsLoading.value = true
          try {
            await loadViewColumns()
          } catch (e) {
            console.error(e)
          }
          isViewColumnsLoading.value = false
        }
      },
      { immediate: true },
    )

    const resizingColOldWith = ref('180px')

    const updateGridViewColumn = async (id: string, props: Partial<GridColumnReqType>, undo = false) => {
      if (!undo) {
        const oldProps = Object.keys(props).reduce<Partial<GridColumnReqType>>((o: any, k) => {
          if (gridViewCols.value[id][k as keyof GridColumnType]) {
            if (k === 'width') o[k] = `${resizingColOldWith.value}px`
            else o[k] = gridViewCols.value[id][k as keyof GridColumnType]
          }
          return o
        }, {})
        addUndo({
          redo: {
            fn: (w: Partial<GridColumnReqType>) => updateGridViewColumn(id, w, true),
            args: [props],
          },
          undo: {
            fn: (w: Partial<GridColumnReqType>) => updateGridViewColumn(id, w, true),
            args: [oldProps],
          },
          scope: defineViewScope({ view: view.value }),
        })
      }
      try {
        // sync with server if allowed
        if (!isPublic.value && isUIAllowed('viewFieldEdit') && gridViewCols.value[id]?.id) {
          await $api.internal.postOperation(
            view.value!.fk_workspace_id!,
            view.value!.base_id!,
            {
              operation: 'gridColumnUpdate',
              gridViewColumnId: gridViewCols.value[id].id,
            },
            props,
          )
        }

        if (gridViewCols.value?.[id]) {
          Object.assign(gridViewCols.value[id], {
            ...gridViewCols.value[id],
            ...props,
          })
        } else {
          // fallback to reload
          await loadViewColumns()
        }
      } catch (e) {
        // this could happen if user doesn't have permission to update view columns
        // todo: find out root cause and handle with isUIAllowed
        console.error(e)
      }
    }

    watch(
      sortedAndFilteredFields,
      (v) => {
        if (rootFields) rootFields.value = v || []
      },
      { immediate: true },
    )

    const evtListener = (evt: string, payload: any) => {
      if (payload.fk_view_id !== view.value?.id) return

      if (evt === 'view_column_update') {
        const col = gridViewCols.value?.[payload.fk_column_id]
        if (col) {
          const reloadNeeded = payload?.group_by !== col?.group_by || (!col.show && payload?.show)

          Object.assign(col, payload)

          const field = fields.value?.find((f) => f.fk_column_id === payload.fk_column_id)
          if (field) {
            const currentColumnField = col || {}
            Object.assign(field, {
              show: currentColumnField.show || isColumnViewEssential(currentColumnField),
              order: currentColumnField.order || order++,
              aggregation: currentColumnField?.aggregation ?? CommonAggregations.None,
            })

            fields.value?.sort((a: Field, b: Field) => a.order - b.order)
          }

          if (reloadNeeded) {
            nextTick(() => reloadData?.({ shouldShowLoading: false }))
          }

          $eventBus.smartsheetStoreEventBus.emit(SmartsheetStoreEvents.TRIGGER_RE_RENDER)
        }
      } else if (evt === 'view_column_refresh') {
        loadViewColumns()
        nextTick(() => reloadData?.({ shouldShowLoading: false }))
      }
    }

    onMounted(() => {
      $eventBus.realtimeViewMetaEventBus.on(evtListener)
    })

    onBeforeUnmount(() => {
      $eventBus.realtimeViewMetaEventBus.off(evtListener)
    })

    provide(FieldsInj, rootFields)

    return {
      fields,
      fieldsMap,
      loadViewColumns,
      filteredFieldList,
      searchBasisIdMap,
      numberOfHiddenFields,
      filterQuery,
      showAll,
      hideAll,
      saveOrUpdate,
      sortedAndFilteredFields,
      showSystemFields,
      metaColumnById,
      toggleFieldVisibility,
      toggleFieldStyles,
      isViewColumnsLoading,
      updateGridViewColumn,
      gridViewCols,
      resizingColOldWith,
      isLocalMode,
      updateDefaultViewColumnMeta,
      hidingViewColumnsMap,
      hasViewFieldDataEditPermission,
    }
  },
  'useViewColumnsOrThrow',
)

export { useProvideViewColumns }

export function useViewColumnsOrThrow() {
  const viewColumns = useViewColumns()
  if (viewColumns == null) throw new Error('Please call `useProvideViewColumns` on the appropriate parent component')
  return viewColumns
}
