import {
  ActionIcon,
  Box,
  Button,
  Card,
  Divider,
  Group,
  HoverCard,
  Loader,
  SegmentedControl,
  Stack,
  Typography
} from '@tidbcloud/uikit'
import { FormNumberInput, FormRadioGroup, FormSelect, FormTextInput, TransferTree } from '@tidbcloud/uikit/biz'
import { IconDatabase01, IconInfoCircle, IconLayoutGrid02, IconRefreshCw01 } from '@tidbcloud/uikit/icons'
import { capitalize, pick, uniqBy } from 'lodash-es'
import { observer } from 'mobx-react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useFormContext } from 'react-hook-form'
import { useQuery } from 'react-query'

import { FormErrorMessage } from 'dbaas/components/Form'
import { DmImportMode, EssentialDMTypeOptions } from 'dbaas/features/DataMigration'
import type { EssentialDMFormData } from 'dbaas/screens/DataMigration/CreateDataMigrationEssential.types'
import type { EssentialStep1FormData } from 'dbaas/screens/DataMigration/steps/Step1/EssentialDMStep1'
import { buildEssentialConnProfile } from 'dbaas/screens/DataMigration/steps/Step1/helper'
import {
  migrationServiceGetSourceTables,
  useMigrationServicePrecheck
} from 'dbaas/services/api-clients/legacy/serverless'
import type {
  BlockAllowRulesTable,
  MigrationServicePrecheckBody,
  Source
} from 'dbaas/services/api-clients/legacy/serverless/interface'
import { TaskMode } from 'dbaas/services/api-clients/legacy/serverless/interface/taskMode'
import { getErrorMessage } from 'dbaas/services/errorCodes'
import useStores from 'dbaas/stores/useStores'

import { flatTree2Keys } from './utils'

export interface EssentialStep2FormData {
  full_instance_migrate: boolean
  migration_mode: TaskMode
  sources: Source[]
  precheckId: string
  selectObjectMethod: 'all' | 'customize'
  selectedObjectKeys: string[]
  start_position_method: 'gtid' | 'binlog' | 'current'
  binlog_name?: string
  binlog_pos?: number
  binlog_gtid?: string
  import_mode?: DmImportMode
}

type RuleTable = BlockAllowRulesTable & { key: string }

const Step2 = ({
  onCancel,
  onGoPrev,
  onFinish
}: {
  onFinish: () => void
  onCancel: () => void
  onGoPrev: () => void
}) => {
  const {
    store: { tenantData, activeProjectId, currentCluster }
  } = useStores()
  const { handleSubmit, setValue, watch, getValues } = useFormContext<EssentialDMFormData>()
  const formData = getValues() as EssentialStep1FormData & EssentialStep2FormData
  const migrationMode = watch('migration_mode')
  const isFull = migrationMode === TaskMode.MODE_ALL
  const isIncremental = migrationMode === TaskMode.MODE_INCREMENTAL
  const startPositionMethod = watch('start_position_method')
  const selectObjectMethod = watch('selectObjectMethod') ?? 'all'
  const setSelectObjectMethod = (value: EssentialStep2FormData['selectObjectMethod']) => {
    setValue('selectObjectMethod', value)
  }
  const precheck = useMigrationServicePrecheck()
  const [errorMessage, setErrorMessage] = useState('')

  const selectedObjectKeys = watch('selectedObjectKeys')
  const targetTables = useRef<string[]>([])
  useEffect(() => {
    targetTables.current = selectedObjectKeys
  }, [selectedObjectKeys])
  const setSelectedObjectKeys = (keys: string[]) => {
    setValue('selectedObjectKeys', keys)
  }
  const [rules, setRules] = useState<{
    do_dbs?: string[]
    do_tables?: RuleTable[]
  }>({
    do_dbs: formData.sources?.[0]?.ba_rules?.do_dbs ?? [],
    do_tables:
      formData.sources?.[0]?.ba_rules?.do_tables
        ?.map((i) => ({ ...i, key: [i.schema, i.table].join(',') }))
        ?.filter((i) => Boolean(i.schema) && Boolean(i.table)) ?? []
  })

  const selectedSourceTables = useRef<string[]>([])
  const selectedTargetTables = useRef<string[]>([])
  const fullDbs = useRef<Set<string>>(new Set())

  const updateRules = () => {
    setRules((prev) => {
      const tableSet = new Set(targetTables.current.filter((i) => i.includes(',')))
      const do_dbs: string[] = targetTables.current
        .filter((i) => !i.includes(','))
        .filter((i) => {
          const schema = schemaGroups.find((s) => s.schema_name === i)
          if (!schema) return false
          if (!fullDbs.current.has(schema.schema_name)) return false
          return schema.tables.every((t) => tableSet.has([t.schema_name, t.table_name].join(',')))
        })

      const do_tables: RuleTable[] = uniqBy(
        targetTables.current
          .filter((i) => i.includes(','))
          .filter((i) => !do_dbs.includes(i.split(',')?.[0]))
          .map((i) => {
            const [schema, table] = i.split(',')
            return { schema, table, key: i }
          }),
        'key'
      )

      if (do_dbs.length) {
        do_tables.push(...do_dbs.map((db) => ({ schema: db, table: '~.*', key: db })))
      }

      return {
        ...prev,
        do_dbs,
        do_tables
      }
    })
  }

  const {
    data,
    isFetching: isLoadingSourceTable,
    refetch
  } = useQuery(
    '/source-tables',
    () =>
      migrationServiceGetSourceTables(
        tenantData.id,
        activeProjectId,
        currentCluster?.id!,
        {
          conn_profile: buildEssentialConnProfile({
            connect_method: formData.connect_method,
            endpoint_id: formData.endpoint_id,
            host: formData.host || formData.service_endpoint,
            port: formData.port,
            username: formData.username,
            password: formData.password,
            ssl: formData.ssl,
            ssl_ca: formData.ssl_ca,
            ssl_cert: formData.ssl_cert,
            ssl_key: formData.ssl_key
          })
        },
        { timeout: 60 * 1000 }
      ),
    {
      enabled: Boolean(formData.connect_method),
      refetchOnWindowFocus: false,
      refetchOnReconnect: false
    }
  )

  const schemaGroups = useMemo(() => {
    if (!data?.tables) {
      return [] as Array<{
        schema_name: string
        tables: Array<{ schema_name: string; table_name: string }>
      }>
    }

    const grouped = new Map<string, Set<string>>()
    data.tables.forEach(({ schema, table }) => {
      if (!schema) return
      const tableSet = grouped.get(schema) ?? new Set<string>()
      if (table) {
        tableSet.add(table)
      }
      grouped.set(schema, tableSet)
    })

    return Array.from(grouped.entries())
      .map(([schemaName, tables]) => ({
        schema_name: schemaName,
        tables: Array.from(tables)
          .sort((a, b) => a.localeCompare(b))
          .map((tableName) => ({ schema_name: schemaName, table_name: tableName }))
      }))
      .sort((a, b) => a.schema_name.localeCompare(b.schema_name))
  }, [data?.tables])

  const dataSource = useMemo(() => {
    return schemaGroups.map((schema) => ({
      title: schema.schema_name,
      key: schema.schema_name,
      icon: <Box component={IconDatabase01} size={14} sx={{ position: 'relative', top: 2, marginRight: 4 }} />,
      isLeaf: false,
      children:
        schema.tables?.map((table) => ({
          title: table.table_name,
          key: `${table.schema_name},${table.table_name}`,
          icon: <Box component={IconLayoutGrid02} size={14} sx={{ position: 'relative', top: 2, marginRight: 4 }} />,
          isLeaf: true
        })) ?? []
    }))
  }, [schemaGroups])

  const onSubmit = async (values: EssentialStep2FormData) => {
    if (!values.full_instance_migrate && selectedObjectKeys.length === 0) {
      setErrorMessage(
        'No selected objects. You have to pick some in the source database and move them to the right side.'
      )
      return
    }

    try {
      const baRules = values.full_instance_migrate
        ? undefined
        : {
            do_dbs: rules.do_dbs ?? [],
            do_tables: rules.do_tables?.map(({ schema, table }) => ({ schema, table })) ?? []
          }

      const payload: MigrationServicePrecheckBody = {
        name: formData.name,
        sources: [
          {
            ba_rules: baRules,
            conn_profile: buildEssentialConnProfile({
              connect_method: formData.connect_method,
              endpoint_id: formData.endpoint_id,
              host: formData.host || formData.service_endpoint,
              port: formData.port,
              username: formData.username,
              password: formData.password,
              ssl: formData.ssl,
              ssl_ca: formData.ssl_ca,
              ssl_cert: formData.ssl_cert,
              ssl_key: formData.ssl_key
            }),
            source_type_id: formData.data_source,
            ...pick(values, ['binlog_pos', 'binlog_name', 'binlog_gtid'])
          }
        ],
        target: {
          user: formData.cluster_username,
          password: formData.cluster_password
        },
        mode: values.migration_mode,
        full_instance_migration: values.full_instance_migrate
      }
      const res = await precheck.mutateAsync({
        orgId: tenantData.id,
        projectId: activeProjectId,
        clusterId: currentCluster?.id!,
        data: payload
      })

      setValue('sources', payload.sources ?? [])
      setValue('precheckId', res.id ?? '')
      onFinish()
    } catch (e) {
      setErrorMessage(getErrorMessage(e, true))
    }
  }

  return (
    <Card withBorder shadow="none" p={0}>
      <form onSubmit={handleSubmit((values) => onSubmit(values as EssentialStep2FormData))}>
        <Stack m={24}>
          <Typography variant="headline-sm">Migration Type</Typography>

          <Group align="flex-start">
            <Typography variant="label-lg" miw={240}>
              Migration process
            </Typography>
            <FormRadioGroup
              name="migration_mode"
              data={EssentialDMTypeOptions}
              styles={{ root: { '--mantine-spacing-xs': '4px' } }}
            />
          </Group>

          <Group align="flex-start">
            <Group gap={4} miw={240}>
              <Typography variant="label-lg">Existing data migration mode</Typography>
              <HoverCard shadow="md" withArrow>
                <HoverCard.Target>
                  <IconInfoCircle size={14} />
                </HoverCard.Target>
                <HoverCard.Dropdown maw={400}>
                  <Typography variant="body-lg">
                    Physical migration mode is faster but requires the target tables that need data migration to be
                    empty. Logical mode is slower but allows migration into tables with existing data.
                  </Typography>
                </HoverCard.Dropdown>
              </HoverCard>
            </Group>

            <FormRadioGroup
              name="import_mode"
              data={[
                {
                  value: DmImportMode.Physical,
                  label: 'Physical',
                  disabled: true,
                  tooltip: 'Only logical mode is supported for now, physical mode will be supported in the future.'
                },
                { value: DmImportMode.Logical, label: 'Logical' }
              ]}
              styles={{ root: { '--mantine-spacing-xs': '4px' } }}
            />
          </Group>

          {isIncremental && !isFull && (
            <Stack maw={500}>
              <Group gap={8}>
                <Typography variant="title-lg">Start Position</Typography>
                <HoverCard shadow="md" withArrow>
                  <HoverCard.Target>
                    <IconInfoCircle size={14} />
                  </HoverCard.Target>
                  <HoverCard.Dropdown>
                    <Typography variant="body-lg" maw={300}>
                      Support migration of incremental data generated after the DM job starts, or from user-specified
                      binlog position or GTID. Refer to{' '}
                      <a
                        href="https://docs.pingcap.com/tidbcloud/migrate-incremental-data-from-mysql-using-data-migration#step-3-choose-migration-job-type"
                        target="_blank"
                        rel="noreferrer"
                      >
                        documentation
                      </a>{' '}
                      for details.
                    </Typography>
                  </HoverCard.Dropdown>
                </HoverCard>
              </Group>
              <Stack>
                <FormSelect
                  name="start_position_method"
                  size="xs"
                  rules={{ required: 'Required' }}
                  placeholder="Choose when the incremental migration job starts"
                  data={[
                    {
                      label: 'The earliest existing binlog position currently available on the source',
                      value: 'current'
                    },
                    { label: 'Specify GTID', value: 'gtid' },
                    { label: 'Specify binlog file name and position', value: 'binlog' }
                  ]}
                  miw={330}
                />
                {startPositionMethod === 'binlog' && (
                  <>
                    <FormTextInput
                      name="binlog_name"
                      rules={{ required: 'Required' }}
                      size="xs"
                      label="Binlog File Name"
                      placeholder="Enter the binlog file name"
                    />
                    <FormNumberInput
                      name="binlog_pos"
                      size="xs"
                      label="Binlog Position"
                      placeholder="Enter the binlog position"
                    />
                  </>
                )}
                {startPositionMethod === 'gtid' && (
                  <FormTextInput
                    name="binlog_gtid"
                    size="xs"
                    label="GTID"
                    placeholder="Enter the GTID"
                    rules={{ required: 'Required' }}
                  />
                )}
              </Stack>
            </Stack>
          )}
        </Stack>

        <Divider />

        <Stack m={24}>
          <Typography variant="headline-sm">Select Objects to Migrate</Typography>

          <div>
            <SegmentedControl
              size="sm"
              value={selectObjectMethod}
              onChange={(val) => {
                setSelectObjectMethod(val as any)
                setValue('full_instance_migrate', val === 'all')
                if (val === 'all') {
                  setSelectedObjectKeys([])
                }
              }}
              miw={240}
              data={(['all', 'customize'] as const).map((i) => ({ label: capitalize(i), value: i }))}
            />
          </div>

          {selectObjectMethod === 'customize' && (
            <TransferTree
              className="wd-h-[400px]"
              dataSource={dataSource}
              onChange={(tree) => setSelectedObjectKeys(flatTree2Keys(tree))}
              treeProps={{ showIcon: true, virtual: true }}
              initialTargetKeys={formData.selectedObjectKeys}
              sourceHeader={
                <Group justify="space-between" p={16} sx={(theme) => ({ backgroundColor: theme.colors.carbon[3] })}>
                  <b>Source Database</b>
                  {isLoadingSourceTable ? (
                    <Loader size="xs" />
                  ) : (
                    <ActionIcon variant="default" onClick={() => refetch()}>
                      <IconRefreshCw01 size={14} />
                    </ActionIcon>
                  )}
                </Group>
              }
              targetHeader={
                <Group justify="space-between" p={16} sx={(theme) => ({ backgroundColor: theme.colors.carbon[3] })}>
                  <Typography fw={700} lh="28px">
                    Selected Objects
                  </Typography>
                </Group>
              }
              onCheckLeft={(selected: string[], info) => {
                selectedSourceTables.current = selected
                if (info.node.isLeaf) {
                  return
                }
                if (info.checked) {
                  fullDbs.current.add(info.node.key as string)
                } else {
                  fullDbs.current.delete(info.node.key as string)
                }
              }}
              onCheckRight={(selected: string[]) => {
                selectedTargetTables.current = selected as string[]
              }}
              onMoveRight={() => {
                targetTables.current = [...new Set([...targetTables.current, ...selectedSourceTables.current])]
                updateRules()
              }}
              onMoveLeft={() => {
                targetTables.current = [
                  ...new Set([...targetTables.current].filter((i) => !selectedTargetTables.current.includes(i)))
                ]
                updateRules()
                selectedTargetTables.current = []
              }}
            />
          )}
        </Stack>

        <Divider />

        {errorMessage && (
          <Box m={24}>
            <FormErrorMessage message={errorMessage} />
          </Box>
        )}

        <Group justify="flex-end" m={24}>
          <Button variant="default" type="button" onClick={onCancel} data-mp-event="mp-cancel-1691461016963">
            Cancel
          </Button>

          <Button variant="outline" type="button" onClick={onGoPrev} data-mp-event="mp-previous-1691461016965">
            Previous
          </Button>

          <Button type="submit" loading={precheck.isLoading} data-mp-event="mp-next-1691461016966">
            Next
          </Button>
        </Group>
      </form>
    </Card>
  )
}

export const EssentialDMStep2 = observer(Step2)
