import {
  Alert,
  Badge,
  Box,
  Button,
  Card,
  Checkbox,
  Group,
  HoverCard,
  LoadingOverlay,
  Popover,
  Select,
  Stack,
  Switch,
  Table,
  TextInput,
  Tooltip,
  Typography
} from '@tidbcloud/uikit'
import { FormErrorMessage } from '@tidbcloud/uikit/biz'
import {
  IconAlertCircle,
  IconAlertTriangle,
  IconEdit05,
  IconFilterFunnel01,
  IconInfoCircle,
  IconParagraphSpacing
} from '@tidbcloud/uikit/icons'
import { observer } from 'mobx-react'
import { ReactNode, useEffect, useMemo, useState } from 'react'
import { useQuery } from 'react-query'

import { DefaultCSVFormat } from 'dbaas/features/Import'
import {
  importServicePreviewLocal,
  useImportServiceGetTableColumns,
  useImportServiceListSupportedDataTypes
} from 'dbaas/services/api-clients/legacy/serverless'
import {
  ImportFileTypeEnum as ServerlessImportFileTypeEnum,
  ImportsGetTableColumnsResp as ServerlessImportsGetTableColumnsResp,
  ImportsPreviewLocalResp as ServerlessImportsPreviewLocalResp
} from 'dbaas/services/api-clients/legacy/serverless/interface'
import { ErrorCode, getErrorMessage } from 'dbaas/services/errorCodes'
import useStores from 'dbaas/stores/useStores'
import { toCountMap, toMap } from 'dbaas/utils/toMap'
import { eventTracking } from 'dbaas/utils/tracking'

import { ColumnNameRegExp } from '../constant'
import CSVConfiguration from '../CSVConfiguration'

const validateColumnHeader = (header: string, map: Map<string, number>) => {
  if (!header || !header.trim()) {
    return `Column name can't be empty`
  } else if (map.get(header)! > 1) {
    return `Duplicated column`
  } else if (header.length > 64) {
    return `The column name should be less than 65 characters`
  } else if (header.includes(' ')) {
    return `The column name "${header}" contains spaces. It is recommended to change it to "${header.replaceAll(
      ' ',
      '_'
    )}"`
  } else if (!ColumnNameRegExp.test(header)) {
    return `The column name must consist of characters within the Unicode Basic Multilingual Plane (BMP), excluding the null character (\\u0000) and whitespace characters.`
  }
}

const getColumnDetailState = (columns: ColumnDetail[]): Record<string, string> => {
  const res = {}
  const map = toCountMap(columns, 'header')

  columns.forEach((i) => {
    const message = validateColumnHeader(i.header, map)
    if (message) {
      res[i.header] = message
    }
  })

  return res
}

const getColumnsDetail = (
  data: ServerlessImportsPreviewLocalResp | ServerlessImportsGetTableColumnsResp,
  options: {
    useFirstRowAsHeader?: boolean
    targetPkColumns?: boolean[]
    targetAutoGenColumns?: boolean[]
  }
): ColumnDetail[] => {
  if (!data) return []
  const { useFirstRowAsHeader = true, targetPkColumns = [], targetAutoGenColumns = [] } = options
  const types = data.table?.row_data_types!
  const values = data.table?.row_values!

  return (
    data.table?.row_headers?.reduce((acc, next, index) => {
      const i: ColumnDetail = {
        header: next || (useFirstRowAsHeader ? '' : `column_${index + 1}`),
        originalHeader: next,
        type: types[index] ?? '',
        values: values.map((j) => j.column_values?.[index]! ?? '') ?? [],
        checked: true,
        key: `${Date.now()}-${index}`,
        header_editable: true,
        type_editable: true,
        is_primary: targetPkColumns[index] ?? false,
        is_autogen: targetAutoGenColumns[index] ?? false
      }
      acc.push(i)
      return acc
    }, [] as ColumnDetail[]) ?? []
  )
}

export interface ColumnDetail {
  header: string
  originalHeader: string
  type: string
  values: string[]
  checked: boolean
  key: string
  header_editable: boolean
  type_editable: boolean
  is_primary: boolean
  is_autogen: boolean
}

export interface LocalImportConfig {
  file_name?: string
  target_table?: {
    schema?: string
    table?: string
  }
  columns?: {
    column_name?: string
    column_type?: string
  }[]
  original_file_name?: string
  pk_columns?: string[]
  total_size?: number
}

interface PreviewProps {
  isTargetingNewTable: boolean
  csvFormat: typeof DefaultCSVFormat
  handleCsvConfigChange: (val: Partial<typeof DefaultCSVFormat>) => void
  localImportConfig: LocalImportConfig
  error?: ReactNode
  title?: string
  onDataChange?: (data: LocalImportConfig, canStartImport: boolean) => void
}

export const ServerlessImportFromLocalFilePreview = observer(
  ({
    isTargetingNewTable,
    csvFormat,
    handleCsvConfigChange,
    localImportConfig,
    error,
    title,
    onDataChange
  }: PreviewProps) => {
    const {
      store: { tenantData, activeProjectId, currentCluster }
    } = useStores()
    const [columns, setColumns] = useState<ColumnDetail[]>([])
    const [message, setMessage] = useState<ReactNode>('')
    const [pkColumns, setPkColumns] = useState<number[]>([]) // primary keys
    const columnState = useMemo(() => {
      return getColumnDetailState(columns)
    }, [columns])
    const hasEmptyHeaderInFile = isTargetingNewTable && columns.some((i) => !i.header)
    const {
      isFetching: isLoadingLocalFileColumnDetail,
      isError: isPreviewLocalError,
      error: previewLocalError
    } = useQuery(
      ['/imports:previewLocal', csvFormat],
      () => {
        return importServicePreviewLocal(tenantData.id, activeProjectId, currentCluster?.id!, {
          upload_id: localImportConfig.file_name!,
          file_type: ServerlessImportFileTypeEnum.CSV,
          csv_format: csvFormat
        })
      },
      {
        refetchOnWindowFocus: false,
        enabled: Boolean(currentCluster?.id) && Boolean(localImportConfig.file_name),
        onSuccess(data) {
          const next = getColumnsDetail(data, {
            useFirstRowAsHeader: csvFormat.header!
          })
          setColumns(next)
          refetch()
        }
      }
    )

    const handleTableColumnCheck = (data: ServerlessImportsGetTableColumnsResp) => {
      const targetPkColumns = data.table?.column_is_pk ?? []
      const targetAutoGenColumns = data.table?.column_auto_gen ?? []

      const columnsFromTarget = getColumnsDetail(data, {
        useFirstRowAsHeader: csvFormat.header,
        targetPkColumns,
        targetAutoGenColumns
      })

      if (columnsFromTarget.length === 0) {
        setMessage('')
        return
      }

      // columns from local file might have lower case name
      const localColumnMap = toMap(columns, 'header')

      const checkIsInLocalFile = (): [boolean, JSX.Element] => {
        // so when comparing local and target, it needs to check lower case name
        const isInLocal = (columnName: string) => {
          if (csvFormat.header) {
            return localColumnMap.has(columnName) || localColumnMap.has(columnName.toLowerCase())
          } else {
            // 按顺序排，只有多出来的列的才不算在文件里
            return columnsFromTarget.findIndex((i) => i.originalHeader === columnName) <= columns.length - 1
          }
        }

        // some column is in target (but not in the local file) IS primary key and IS NOT auto_increment or auto_random
        const columnsNotInSource = columnsFromTarget.filter(
          (c) => !isInLocal(c.header) && c.is_primary && !c.is_autogen
        )
        const failed = columnsNotInSource.length > 0
        const message = (
          <Box>
            <div>
              The following columns have primary or unique key constraint but do not exist in the source file:{' '}
              <b>{columnsNotInSource.map((i) => i.originalHeader).join(', ')}</b>.
            </div>
            <div>To avoid data import issues, you have the following options:</div>
            <Box component="ul" sx={{ paddingLeft: 16, margin: 8 }}>
              <li>Provide a source file that includes these PK/UK columns.</li>
              {csvFormat.header && (
                <li>Modify the target table's PK/UK columns to match the existing columns in the source file.</li>
              )}
              <li>Set the attributes of the PK/UK columns to auto_increment or auto_random.</li>
            </Box>
          </Box>
        )

        return [failed, message]
      }

      const checkIsInTargetTable = (): [boolean, JSX.Element] => {
        const isInTarget = (columnName: string) => {
          return columnsFromTarget.findIndex((i) => i.originalHeader.toLowerCase() === columnName.toLowerCase()) > -1
        }
        const columnsNotInTarget = columns.filter((c) => !isInTarget(c.header))
        const failed = csvFormat.header ? columnsNotInTarget.length > 0 : columns.length > columnsFromTarget.length
        const message = (
          <Box>
            {csvFormat.header ? (
              <div>
                The following columns from source file cannot be found in the target table:{' '}
                <b>{columnsNotInTarget.map((i) => i.originalHeader).join(', ')}.</b>
              </div>
            ) : (
              <div>The number of columns in the source file exceeds the number of columns in the target table.</div>
            )}
            <div>You can use any of the following methods to resolve this issue:</div>
            <Box component="ul" sx={{ paddingLeft: 16, margin: 8 }}>
              <li>Provide a source file with the same structure as the target table.</li>
              <li>Modify the structure of the target table to match the column count and names of the source file.</li>
            </Box>
          </Box>
        )
        return [failed, message]
      }

      ;(function () {
        let [failed1, message1] = checkIsInLocalFile()
        if (failed1) {
          setMessage(message1)
          return
        }
        let [failed2, message2] = checkIsInTargetTable()
        if (failed2) {
          setMessage(message2)
          return
        }
        setMessage('')
      })()

      setColumns((prev) =>
        columnsFromTarget.map((i, index) => ({
          ...i,
          values:
            localColumnMap.get(i.header)?.values ??
            localColumnMap.get(i.header.toLowerCase())?.values ??
            prev[index]?.values ??
            [],
          header_editable: false,
          type_editable: false
        }))
      )
    }

    const {
      data: targetColumnData,
      refetch,
      isFetching: isLoadingTargetTableColumDetail
    } = useImportServiceGetTableColumns(
      tenantData.id,
      activeProjectId,
      currentCluster?.id!,
      {
        database: localImportConfig.target_table?.schema!,
        table: localImportConfig.target_table?.table!
      },
      {
        query: {
          refetchOnWindowFocus: false,
          retry: false,
          enabled: false, // use refetch
          onSuccess(data) {
            // If the selected table exists, we need to fetch its column information
            handleTableColumnCheck(data)
          },
          onError(error: any) {
            const msg = getErrorMessage(error, true)
            if (error.errorCode !== ErrorCode.RECORD_NOT_FOUND && msg) {
              setMessage(msg)
            }
          }
        }
      }
    )

    const { data: typesData } = useImportServiceListSupportedDataTypes({
      query: {
        refetchOnWindowFocus: false
      }
    })

    const dataTypeOptions = useMemo(() => {
      return Array.from(new Set([...(typesData?.data_types ?? []), ...columns.map((i) => i.type)]))
    }, [typesData, columns])
    const isLoadingTable = isLoadingLocalFileColumnDetail || isLoadingTargetTableColumDetail
    const isDisable = isLoadingTable || isPreviewLocalError || Boolean(message) || Object.keys(columnState).length > 0

    useEffect(() => {
      onDataChange?.(
        {
          ...localImportConfig,
          pk_columns: pkColumns.map((i) => columns[i].header),
          columns: targetColumnData?.table?.row_headers?.length
            ? undefined
            : columns
                .filter((i) => i.checked)
                .map((i) => ({
                  column_name: i.header,
                  column_type: i.type
                }))
        },
        !isDisable
      )
    }, [onDataChange, pkColumns, targetColumnData, columns, localImportConfig, isDisable])

    return (
      <Stack data-testid="import-data-preview">
        {title && <Typography variant="title-lg">{title}</Typography>}
        <Group justify="space-between">
          <Group gap={24}>
            <Switch
              labelPosition="left"
              label={
                <Typography variant="label-lg">
                  {isTargetingNewTable ? 'Use first row as column name' : 'Ignore the first row'}
                </Typography>
              }
              checked={csvFormat.header}
              onChange={(event) => {
                const checked = event.currentTarget.checked
                handleCsvConfigChange({ header: checked })
                eventTracking(`toggle use first row as column name`)
              }}
            />

            <Group gap={8}>
              <Typography variant="label-lg">Primary Key:</Typography>
              <Typography variant="body-lg">
                {pkColumns.length === 0 ? 'Unselected' : pkColumns.map((i) => columns[i].header).join(', ')}
              </Typography>
            </Group>
          </Group>

          <Group justify="flex-end">
            <Popover withArrow position="bottom">
              <Popover.Target>
                <Button
                  variant="default"
                  size="xs"
                  leftSection={<IconFilterFunnel01 size={14} />}
                  className="wd-hidden"
                  data-mp-event="mp-columnfilter-1691461015135"
                >
                  Column filter
                </Button>
              </Popover.Target>

              <Popover.Dropdown>
                <Checkbox.Group
                  value={columns.filter((i) => i.checked).map((i) => i.header)}
                  onChange={(values) =>
                    setColumns((prev) => prev.map((i) => ({ ...i, checked: values.includes(i.header) })))
                  }
                >
                  <Stack gap="xs">
                    {columns.map((i) => (
                      <Checkbox value={i.header} key={i.key} label={i.header} />
                    ))}
                  </Stack>
                </Checkbox.Group>
              </Popover.Dropdown>
            </Popover>

            <Popover withArrow shadow="md" position="bottom-end" arrowPosition="center">
              <Popover.Target>
                <Button
                  variant="default"
                  size="xs"
                  leftSection={<IconEdit05 size={14} />}
                  data-mp-event="mp-editcsvconfiguration-1691461015137"
                >
                  Edit CSV configuration
                </Button>
              </Popover.Target>

              <Popover.Dropdown>
                <CSVConfiguration onChange={handleCsvConfigChange} defaultValue={csvFormat} hideHeaderOption />
              </Popover.Dropdown>
            </Popover>
          </Group>
        </Group>

        {message && (
          <Alert icon={<IconAlertCircle size={16} />} color="red" className="wd-mb-4">
            <div>{message}</div>
          </Alert>
        )}

        {!message && hasEmptyHeaderInFile && (
          <Alert icon={<IconAlertCircle size={16} />} title="Warning" color="yellow" className="wd-mb-4">
            <div>
              Please upload a valid file with correct column names. You can also uncheck "Use first row as column
              names", enter the correct column names, and then click "Start Import".
            </div>
          </Alert>
        )}

        {!previewLocalError && (
          <Table withTableBorder={false} withColumnBorders={false} withRowBorders={false}>
            <LoadingOverlay visible={isLoadingTable} overlayProps={{ blur: 5 }} />
            <Table.Thead bg="carbon.3">
              <Table.Tr>
                <Table.Th px={8} py={4}>
                  <Group miw={130} gap={8} wrap="nowrap">
                    <Typography variant="title-md">Column Name</Typography>
                    <HoverCard withArrow shadow="md" position="top" width={500}>
                      <HoverCard.Target>
                        <Box component={IconInfoCircle} size={16} c="carbon.7" />
                      </HoverCard.Target>
                      <HoverCard.Dropdown>
                        <Typography variant="body-md">
                          {`The column name must consist of characters within the Unicode Basic Multilingual Plane (BMP),
                          excluding the null character (\\u0000) and whitespace characters. Additionally, the column name
                          must not exceed 64 characters in length.`}
                        </Typography>
                      </HoverCard.Dropdown>
                    </HoverCard>
                  </Group>
                </Table.Th>
                <Table.Th px={8} py={4}>
                  <Box miw={120}>
                    <Typography variant="title-md">Data Type</Typography>
                  </Box>
                </Table.Th>
                <Table.Th px={8} py={4}>
                  <Group miw={70} gap={8} wrap="nowrap">
                    <Typography variant="title-md">Primary</Typography>
                    <HoverCard withArrow shadow="md" position="top" width={500}>
                      <HoverCard.Target>
                        <Box component={IconInfoCircle} size={16} c="carbon.7" />
                      </HoverCard.Target>
                      <HoverCard.Dropdown>
                        <Typography variant="body-md">
                          You can select multiple fields to form a joint primary key. The joint primary key will be
                          formed in the order of the selected field names.
                        </Typography>
                      </HoverCard.Dropdown>
                    </HoverCard>
                  </Group>
                </Table.Th>
                <Table.Th px={8} py={4}>
                  <Typography variant="title-md">Column Data</Typography>
                </Table.Th>
              </Table.Tr>
            </Table.Thead>

            <Table.Tbody>
              {columns
                .filter((i) => i.checked)
                .map((i, index) => {
                  return (
                    <Box component={Table.Tr} key={i.key}>
                      <Table.Td>
                        {i.header_editable ? (
                          <TextInput
                            w={130}
                            defaultValue={i.header}
                            size="xs"
                            errorProps={{ size: 'xs' }}
                            rightSection={
                              columnState[i.header] ? (
                                <Tooltip
                                  label={columnState[i.header]}
                                  withArrow
                                  multiline
                                  styles={() => ({ tooltip: { maxWidth: 400 } })}
                                >
                                  <Box c="red.5" component={IconAlertTriangle} size={14} />
                                </Tooltip>
                              ) : null
                            }
                            onChange={(e: any) => {
                              const next = [
                                ...columns.slice(0, index),
                                { ...columns[index], header: e.target.value },
                                ...columns.slice(index + 1)
                              ]
                              setColumns(next)
                              eventTracking(`Edit column name`)
                            }}
                          />
                        ) : (
                          <Typography w={130} lh="30px" truncate title={i.header} fz={14}>
                            {i.header || (
                              <Tooltip label="Column name cannot be empty" withArrow>
                                <IconAlertTriangle className="wd-text-yellow-500" size={14} />
                              </Tooltip>
                            )}
                          </Typography>
                        )}
                      </Table.Td>

                      <Table.Td>
                        {i.type_editable ? (
                          <Select
                            value={i.type}
                            data={dataTypeOptions}
                            size="xs"
                            maw={150}
                            onChange={(val) => {
                              setColumns((prev) => [
                                ...prev.slice(0, index),
                                { ...prev[index], type: val! },
                                ...prev.slice(index + 1)
                              ])
                              eventTracking(`Edit data type`)
                            }}
                          />
                        ) : (
                          <Typography variant="body-md" lh="30px">
                            {i.type}
                          </Typography>
                        )}
                      </Table.Td>

                      <Table.Td miw={70}>
                        <Group gap={8}>
                          <Checkbox
                            disabled={!isTargetingNewTable}
                            checked={isTargetingNewTable ? pkColumns.indexOf(index) > -1 : i.is_primary}
                            onChange={(e) => {
                              const checked = e.currentTarget.checked
                              if (checked) {
                                setPkColumns((prev) => [...prev, index])
                              } else {
                                setPkColumns((prev) => prev.filter((k) => k !== index))
                              }
                            }}
                          />
                          {pkColumns.indexOf(index) > -1 && <Badge>{pkColumns.indexOf(index) + 1}</Badge>}
                        </Group>
                      </Table.Td>

                      <Table.Td>
                        <Card withBorder shadow="none" p={4}>
                          <Group px={4} justify="space-between" wrap="nowrap">
                            <Group gap={8} wrap="nowrap">
                              {(i.values.length > 0 ? i.values.slice(0, 5) : [null]).map((j, index) => (
                                <Typography
                                  key={index}
                                  fz={12}
                                  sx={(theme) => ({
                                    borderRadius: 4,
                                    backgroundColor: theme.colors.carbon[4],
                                    color: theme.colors.carbon[7],
                                    maxWidth: 32,
                                    height: 20,
                                    overflow: 'hidden',
                                    whiteSpace: 'nowrap',
                                    padding: '0 8px',
                                    textOverflow: 'ellipsis'
                                  })}
                                >
                                  {j}
                                </Typography>
                              ))}
                            </Group>

                            <HoverCard position="bottom-end">
                              <HoverCard.Target>
                                <Box component={IconParagraphSpacing} sx={{ cursor: 'pointer' }} size={12} />
                              </HoverCard.Target>

                              <HoverCard.Dropdown>
                                <Stack>
                                  {i.values.map((j, index) => (
                                    <Typography key={index} variant="body-md" truncate maw={600}>
                                      {j}
                                    </Typography>
                                  ))}
                                </Stack>
                              </HoverCard.Dropdown>
                            </HoverCard>
                          </Group>
                        </Card>
                      </Table.Td>
                    </Box>
                  )
                })}
            </Table.Tbody>
          </Table>
        )}

        <FormErrorMessage message={error || getErrorMessage(previewLocalError, true)} closable autoScroll />
      </Stack>
    )
  }
)
