import { ActionIcon, Box, Flex, HoverCard, Input, Menu, Skeleton, Typography, notifier } from '@tidbcloud/uikit'
import { Tree } from '@tidbcloud/uikit/biz'
import { useDebouncedValue } from '@tidbcloud/uikit/hooks'
import {
  IconBrackets,
  IconCalendar,
  IconDatabase01,
  IconDatabaseSystem,
  IconDotsHorizontal,
  IconLayoutGrid02,
  IconNumberic01,
  IconRefreshCw02,
  IconString01,
  IconVector,
  IconXClose
} from '@tidbcloud/uikit/icons'
import { observer } from 'mobx-react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useHistory } from 'react-router-dom'

import { AccessCheck } from 'dbaas/contexts/access-control'
import {
  createSqlEditorFile,
  getDatabaseList,
  getTablesByDbName,
  getColumnsByTableName,
  DtoDatabaseItem,
  EntityTablesMeta,
  searchSchemaInfo,
  DtoSchemaSearchItem
} from 'dbaas/services'
import useStores from 'dbaas/stores/useStores'
import { eventTracking } from 'dbaas/utils/tracking'

import { useSQLEditorContext } from '../context/editor-context'
import { preTips, useAffixTips } from '../SqlFiles/fileContent'
import { CLICK_TYPE, SqlFile } from '../types'

import DbErrorMsg, { ErrorInfo } from './DbErrorMsg'
import { useStyles } from './index.styles'

const MAX_DEFAULT_LIMIT = 100
const MAX_SCHEMA_ITEM = 1000

interface TableItem extends EntityTablesMeta {
  name: string
  isLeaf: boolean
  loaded: boolean
  key: string
  children?: any[]
}

interface DBItem extends DtoDatabaseItem {
  key?: string
  loaded?: boolean
  isLeaf: boolean
  tables: TableItem[]
  children?: TableItem[]
}

const DatabaseList = () => {
  const {
    store: { activeProjectId, tenantData, currentCluster }
  } = useStores()
  const {
    dbList,
    setDbList,
    clusterId,
    systemShow,
    setNewGeneSql,
    sqlFiles,
    setSqlFiles,
    editedSqlFileRef,
    setEditedSqlFile,
    setIsRunAll,
    isDbNeedFresh,
    setIsDbNeedFresh
  } = useSQLEditorContext()
  const [loading, setLoading] = useState(true)
  const [dbError, setDbError] = useState(false)
  const [dbErrorMsg, setDbErrorMsg] = useState<ErrorInfo | undefined>()
  const ref = useRef<HTMLDivElement>(null)
  const [filter, setFilter] = useState('')
  const filterRef = useRef('')
  const [debouncedSearchText] = useDebouncedValue(filter, 500)
  const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([])
  const [autoExpandParent, setAutoExpandParent] = useState(false)
  const [isInit, setIsInit] = useState(true)
  const sqlFileRef = useRef<SqlFile[]>([])
  const expandedKeysRef = useRef<React.Key[]>([])
  const loadedRef = useRef(false)
  const { classes } = useStyles()
  const affixTips = useAffixTips()
  const history = useHistory()
  const importActionAccessCheckProps = currentCluster?.computed.isServerless
    ? {
        id: 'TidbxImportAction' as const,
        scopeProps: { tidbId: clusterId }
      }
    : {
        id: 'ClusterImportAction' as const
      }

  useEffect(() => {
    if (isDbNeedFresh) {
      setLoading(true)
      getAll()
      setIsDbNeedFresh(false)
    }
  }, [isDbNeedFresh])

  useEffect(() => {
    sqlFileRef.current = sqlFiles
  }, [sqlFiles])

  useEffect(() => {
    if (!isInit) {
      getAll()
    }
  }, [systemShow])

  useEffect(() => {
    expandedKeysRef.current = expandedKeys
  }, [expandedKeys])

  const formatDb = (data: DBItem[]) => {
    const importData = JSON.parse(localStorage.getItem('chat2queryDefaultData') || '{}')
    const keys: any = new Set()

    return data.map((db, index) => {
      if (importData.database) {
        if (db.name === importData.database) {
          keys.add(db.name)
        }
      }

      return {
        name: db.name,
        isLeaf: !db.has_table,
        has_table: db.has_table,
        loaded: false,
        key: db.name,
        tables: db.tables ?? [],
        children: (db.tables || []).map((table, idx) => {
          if (table.name === importData.table) {
            keys.add(`${db.name}:${table.name}`)
          }

          if (
            importData.database &&
            idx === (db.tables || []).length - 1 &&
            index === data.length - 1 &&
            !expandedKeys.length
          ) {
            if (Array.from(keys).length) {
              setAutoExpandParent(false)
              setExpandedKeys(Array.from(keys))
              localStorage.setItem('Chat2querySelectedKeys', JSON.stringify(Array.from(keys)))
            }
          }

          localStorage.setItem('chat2queryDefaultData', '')

          return {
            name: table.name,
            isLeaf: false,
            loaded: false,
            key: `${db.name}:${table.name}`,
            columns: table.columns,
            children: (table.columns || []).map((meta) => {
              return {
                title: (
                  <Typography variant="body-md" c="carbon.7" lh="28px" truncate>
                    {meta.col}
                  </Typography>
                ),
                isLeaf: true,
                name: meta.col,
                key: `${db.name}:${table.name}:${meta.col}`,
                icon: getSchemaIcon(meta.data_type!),
                type: meta.data_type
              }
            })
          }
        })
      }
    })
  }

  const getAll = async () => {
    setDbList([])
    setLoading(true)
    setDbError(false)

    try {
      const res = await getDatabaseList(tenantData.id, activeProjectId, clusterId)
      setIsInit(false)
      setLoading(false)
      if (res.code !== 200) {
        setDbError(true)
        setDbErrorMsg(
          res.message
            ? {
                code: res.code as number,
                message: res.message
              }
            : undefined
        )
        return
      }

      const dbs = formatDb((res.data || []) as DBItem[])
      setDbList(dbs)
      loadedRef.current = true
    } catch (e: any) {
      setLoading(false)
      setDbError(true)
      setDbErrorMsg(
        e.response?.data?.message
          ? {
              code: e.response.data?.code,
              message: e.response.data.message
            }
          : undefined
      )
    }
  }

  useEffect(() => {
    const importData = localStorage.getItem('chat2queryDefaultData')
    if (importData) {
      return
    }

    const keys = localStorage.getItem('Chat2querySelectedKeys') || '[]'
    const keyData = JSON.parse(keys)
    setExpandedKeys(keyData)
  }, [dbList])

  const schemaClick = (dbName: string) => {
    setNewGeneSql(`USE ${dbName};`)
  }

  const toggleExpand = (clickType: number, dbKey: string, tabKey?: string) => {
    if (clickType === CLICK_TYPE.DOUBLE_CLICK) {
      // fix: db click, the tree will be quickly expanded and shrinked
      return
    }
    const keys = new Set(expandedKeysRef.current)

    if (tabKey) {
      if (!keys.delete(tabKey)) {
        keys.add(tabKey)
      }
    } else {
      keys.has(dbKey) ? keys.delete(dbKey) : keys.add(dbKey)
    }

    onExpand(Array.from(keys))
  }

  const toImportData = (dbName: string) => {
    history.push(`/clusters/${clusterId}/imports/create-import?from=local&db=${dbName}`)
  }

  const exploreDataHandler = (dbName: string, tables: { name: string }[]) => {
    tableClick(dbName, tables.length ? tables[0].name : '', MAX_DEFAULT_LIMIT)
  }

  const tableClick = async (dbName: string, tabName: string, limit?: number) => {
    const name = 'New query'
    let sql = `USE ${dbName};`
    if (tabName) {
      sql += `\nSELECT * FROM \`${dbName}\`.\`${tabName}\`${limit ? ' LIMIT ' + limit : ''};`
    }
    const content = `${preTips}\n${affixTips}\n${sql}`
    const createRes = await createSqlEditorFile(tenantData.id, activeProjectId, clusterId, {
      name,
      content
    })

    if (createRes.code !== 200) {
      notifier.error(createRes.message)
      return
    }

    const file: SqlFile = {
      id: createRes.data || 0,
      content,
      name
    }

    const files: any = sqlFileRef.current.slice()
    files.unshift(file)

    setSqlFiles(files)
    setEditedSqlFile(file)
    editedSqlFileRef.current = file

    setIsRunAll(true)
  }

  const getSchemaIcon = (typeRaw: string) => {
    const type = typeRaw?.toUpperCase()
    if (!type) {
      return <IconString01 />
    }
    if (
      [
        'BIT',
        'BOOLEAN',
        'TINYINT',
        'SMALLINT',
        'MEDIUMINT',
        'INTEGER',
        'INT',
        'BIGINT',
        'FLOAT',
        'DOUBLE',
        'DECIMAL',
        'NUMERIC'
      ].includes(type)
    ) {
      return <IconNumberic01 />
    }

    if (
      [
        'CHAR',
        'VARCHAR',
        'TEXT',
        'TINYTEXT',
        'MEDIUMTEXT',
        'LONGTEXT',
        'BINARY',
        'VARBINARY',
        'BLOB',
        'TINYBLOB',
        'MEDIUMBLOB',
        'LONGBLOB',
        'ENUM',
        'SET'
      ].includes(type)
    ) {
      return <IconString01 />
    }

    if (['DATE', 'TIME', 'DATETIME', 'TIMESTAMP', 'YEAR'].includes(type)) {
      return <IconCalendar />
    }

    if (type === 'JSON') {
      return <IconBrackets />
    }

    if (['VECTOR<FLOAT>', 'VECTOR<FLOAT16>', 'VECTOR'].includes(type)) {
      return <IconVector color="#999" strokeWidth={2} />
    }
  }

  const refresh = () => {
    if (filterRef.current) {
      clearFilter()
    } else {
      getAll()
    }
  }

  const handleFilter = () => {
    if (!debouncedSearchText) {
      getAll()
      return
    }

    const newExpandedKeys: any = new Set()

    if (debouncedSearchText !== filterRef.current) {
      return
    }

    setLoading(true)
    setDbError(false)

    setExpandedKeys([])

    const databases = searchData('database')
    const tables = searchData('table')
    Promise.all([databases, tables])
      .then(([dbRes, tableRes]) => {
        setLoading(false)
        if (dbRes.code === 200 && tableRes.code === 200) {
          const dbData = dbRes.data || []
          const tableData = tableRes.data || []

          const treeMap = new Map<string, DBItem>()

          dbData.forEach((db: DtoSchemaSearchItem) => {
            if (!db.database) {
              return
            }

            treeMap.set(db.database, {
              name: db.database,
              key: `${db.database}`,
              isLeaf: !db.has_table,
              has_table: db.has_table,
              loaded: false,
              tables: [],
              children: []
            })
          })

          const tableMap = new Map<string, DBItem>()

          tableData.forEach((tb) => {
            if (!tb.database || !tb.table) {
              return
            }

            if (treeMap.has(tb.database)) {
              return
            }

            const parentDb = tableMap.get(tb.database)
            if (parentDb) {
              parentDb.tables.push({
                name: tb.table,
                isLeaf: false,
                loaded: false,
                key: `${tb.database}:${tb.table}`,
                columns: [],
                children: []
              })
            } else {
              tableMap.set(tb.database, {
                name: tb.database,
                isLeaf: false,
                has_table: true,
                key: `${tb.database}`,
                tables: [
                  {
                    name: tb.table,
                    isLeaf: false,
                    loaded: false,
                    columns: [],
                    children: [],
                    key: `${tb.database}:${tb.table}`
                  }
                ]
              })
            }
          })

          const tableArr = Array.from(tableMap.values())
          const datas = Array.from(treeMap.values())
          datas.push(...tableArr)
          const dbs = formatDb(datas)
          setDbList(dbs)
        } else {
          setDbError(true)
        }
      })
      .finally(() => {
        setLoading(false)
      })

    setAutoExpandParent(false)
    setExpandedKeys(Array.from(newExpandedKeys))
  }

  const searchData = async (type) => {
    try {
      const res = await searchSchemaInfo(tenantData.id, activeProjectId, clusterId, {
        keyword: debouncedSearchText,
        type
      })
      return res
    } catch (_e) {
      setDbError(true)
      return {}
    }
  }

  useEffect(() => {
    handleFilter()
  }, [debouncedSearchText])

  const filterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target
    const val = value.trim().toLowerCase()

    localStorage.setItem('Chat2querySelectedKeys', '[]')

    setFilter(val)
    filterRef.current = val

    if (!val) {
      setTimeout(() => {
        clearFilter()
      }, 300)
      return
    }
  }

  const clearFilter = () => {
    setFilter('')
    filterRef.current = ''
  }

  const onExpand = (keys: React.Key[]) => {
    setAutoExpandParent(false)
    const filters = keys.filter((key: string) => {
      const arrs = `${key}`.split(':')
      if (keys.includes(arrs[0])) {
        return true
      }
      return false
    })
    setExpandedKeys(filters)
    localStorage.setItem('Chat2querySelectedKeys', JSON.stringify(filters))
  }

  const DotsIcon = <IconDotsHorizontal size={16} />

  const treeData = useMemo(() => {
    const systemDatabse = ['INFORMATION_SCHEMA', 'PERFORMANCE_SCHEMA', 'MYSQL']
    return dbList.map((db, index) => {
      const children = (db.children || []).slice(0, MAX_SCHEMA_ITEM).map((table: any, idx) => ({
        ...table,
        title: (
          <>
            <Typography
              c="carbon.7"
              lh="28px"
              sx={{
                flex: 1
              }}
              truncate
              onDoubleClick={() => schemaClick(db.name)}
              onClick={(e) => toggleExpand(e.detail, `${index}`, `${index}-${idx}`)}
            >
              {table.name}
            </Typography>
            <Menu
              position="bottom-end"
              offset={-1}
              styles={(theme) => ({
                itemLabel: {
                  color: theme.colors.carbon[8]
                }
              })}
              withinPortal={false}
            >
              <Menu.Target>
                <ActionIcon
                  aria-label={`Table \`${table.name}\` actions`}
                  variant="subtle"
                  className="action-icon"
                  sx={{ textAlign: 'center' }}
                >
                  {DotsIcon}
                </ActionIcon>
              </Menu.Target>
              <Menu.Dropdown>
                <Menu.Item
                  onClick={() => {
                    tableClick(db.name, table.name, MAX_DEFAULT_LIMIT)
                  }}
                >
                  Show 100 records
                </Menu.Item>
              </Menu.Dropdown>
            </Menu>
          </>
        ),
        icon: <IconLayoutGrid02 />
      }))

      if ((db.children ?? []).length >= MAX_SCHEMA_ITEM) {
        children.push({
          name: 'limit',
          isLeaf: true,
          title: (
            <Typography c="carbon.7" fz={11} py={6}>
              Top {MAX_SCHEMA_ITEM} items above, please search
            </Typography>
          ),
          icon: null
        })
      }

      // database level
      return {
        ...db,
        title: (
          <>
            <Typography
              variant="body-md"
              c="carbon.7"
              lh="28px"
              sx={{
                flex: 1
              }}
              truncate
              onDoubleClick={() => schemaClick(db.name)}
              onClick={(e) => toggleExpand(e.detail, `${index}`)}
            >
              {db.name}
            </Typography>
            <Menu
              position="bottom-end"
              offset={-1}
              styles={(theme) => ({
                itemLabel: {
                  color: theme.colors.carbon[8]
                }
              })}
              withinPortal={false}
            >
              <Menu.Target>
                <ActionIcon
                  aria-label={`Database \`${db.name}\` actions`}
                  variant="subtle"
                  className="action-icon"
                  sx={{ textAlign: 'center' }}
                >
                  {DotsIcon}
                </ActionIcon>
              </Menu.Target>
              <Menu.Dropdown>
                <Menu.Item
                  onClick={() => {
                    exploreDataHandler(db.name, db.children || [])
                  }}
                >
                  Explore data
                </Menu.Item>
                <AccessCheck {...importActionAccessCheckProps}>
                  {({ accessible }) => (
                    <Menu.Item
                      disabled={!accessible}
                      onClick={() => {
                        toImportData(db.name)
                      }}
                    >
                      Import data
                    </Menu.Item>
                  )}
                </AccessCheck>
              </Menu.Dropdown>
            </Menu>
          </>
        ),
        icon: systemDatabse.includes(db.name.toUpperCase()) ? (
          <IconDatabaseSystem strokeWidth={2} />
        ) : (
          <IconDatabase01 />
        ),
        children
      }
    })
  }, [dbList])

  const loadDbDataSync = (node: any) => {
    const pos = node.pos.split('-')
    const db = dbList[pos[1]]

    return new Promise<void>(async (resolve, reject) => {
      if (node.children?.length) {
        resolve()
        return
      }

      const dbCurrent = dbList.slice()

      if (pos.length === 3) {
        getColumnsByTableName(tenantData.id, activeProjectId, clusterId, db.name, node.name).then((res) => {
          if (res.code !== 200) {
            reject()
          } else {
            const columns = res.data || []
            dbCurrent[pos[1]].tables[pos[2]].columns = columns
            const dbs = formatDb(dbCurrent)
            setDbList(dbs)
            resolve()
          }
        })
      } else if (pos.length === 2) {
        getTablesByDbName(tenantData.id, activeProjectId, clusterId, db.name).then((res) => {
          if (res.code !== 200) {
            reject()
          } else {
            const tables = res.data || []
            dbCurrent[pos[1]].tables = tables.map((table: any) => {
              return {
                name: table,
                columns: []
              }
            })
            const dbs = formatDb(dbCurrent)
            setDbList(dbs)
            resolve()
          }
        })
      }
    })
  }

  return (
    <Flex direction="column" h="100%">
      <Flex mt={12} pl={8} pr={16}>
        <HoverCard shadow="md" withArrow position="left">
          <HoverCard.Target>
            <ActionIcon aria-label="Refresh" variant="default" size={32}>
              <IconRefreshCw02 size={16} onClick={refresh} />
            </ActionIcon>
          </HoverCard.Target>
          <HoverCard.Dropdown>Refresh</HoverCard.Dropdown>
        </HoverCard>
        <Box
          sx={{
            flex: 1,
            position: 'relative',
            marginLeft: 8
          }}
        >
          <Input
            maxLength={64}
            placeholder="Search"
            value={filter}
            styles={() => ({
              input: {
                height: 32,
                minHeight: 32,
                fontSize: 14
              }
            })}
            onFocus={() => eventTracking('SQL Editor Search File Focus')}
            onInput={filterChange}
          />
          {filter && (
            <ActionIcon
              aria-label="Clear Filter"
              sx={(theme) => ({
                position: 'absolute',
                right: 2,
                top: 1,
                color: theme.colors.carbon[7]
              })}
            >
              <IconXClose size={18} onClick={clearFilter} />
            </ActionIcon>
          )}
        </Box>
      </Flex>

      {loading && (
        <Box m="24px 16px 24px 8px" sx={{ overflow: 'hidden' }}>
          {Array(20)
            .fill(0)
            .map((_, idx) => (
              <Skeleton mb={24} height={16} key={idx} />
            ))}
        </Box>
      )}

      <Box
        ref={ref}
        sx={{
          position: 'relative',
          flex: 1,
          overflowY: 'auto',
          marginTop: 8,
          paddingLeft: 12,
          paddingRight: 8,
          zIndex: 1
        }}
      >
        {!loading && !dbError && (
          <Tree
            className={classes.dbListTree}
            showIcon
            showLine={false}
            treeData={treeData}
            autoExpandParent={autoExpandParent}
            expandedKeys={expandedKeys as any[]}
            onExpand={onExpand}
            loadedKeys={[]}
            loadData={loadDbDataSync}
          />
        )}

        {!dbError && !dbList.length && !loading && (
          <Typography variant="body-lg" mt={32} sx={{ fontStyle: 'italic' }}>
            {debouncedSearchText ? 'No matching databases or tables' : 'No user-defined database available'}
          </Typography>
        )}

        {!!dbError && !dbList.length && !loading && <DbErrorMsg errInfo={dbErrorMsg} />}
      </Box>
    </Flex>
  )
}

export default observer(DatabaseList)
