import { Flex, HoverCard, Typography } from '@tidbcloud/uikit'
import { IconKeyboard01, IconInfoCircle } from '@tidbcloud/uikit/icons'
import clsx from 'clsx'
import { cloneDeep } from 'lodash-es'
import { useEffect, useState, useRef } from 'react'

import { useHelpCenterContext } from 'dbaas/screens/HelpCenter/context'
import { EmptyRunTips } from 'dbaas/screens/SqlEditor/MessageLogs/EmptyTips'
import strip from 'dbaas/screens/SqlEditor/StripComments'
import { AI_CODE, RequestStatus } from 'dbaas/screens/SqlEditor/types'
import AIAggModal from 'dbaas/screens/SqlEditorV2/Editor/AIAggModal'
import { dataApiSQLExecute } from 'dbaas/services/dataservice'
import { getErrorMessage } from 'dbaas/services/errorCodes'
import { eventTracking } from 'dbaas/utils/tracking'

import { AutoSaveStateEnum, useDataApiContext } from '../context'
import { useTestEndpoint, getArgs } from '../helpers'
import { SqlRes, EndPoint, ParamType, SaveResStatus, SaveResType } from '../types'

import ClusterList from './ClusterList'
import EditContent from './EditContent'
import { useStyles } from './editor.styles'
import { useDataserviceEditorStyles } from './index.styles'
import RunBtn from './RunBtn'
import SchemaList from './SchemaList'

const Editor = (props: { onRun: () => void }) => {
  const [isRunning, setIsRunning] = useState(false)
  const isSqlInit = useRef(true)

  const {
    layout: { mainPanel },
    orgId,
    projectId,
    setSqlResultList,
    dbList,
    setIsDbNeedFresh,
    editEndpoint,
    setEditEndpoint,
    setSqlText,
    sqlTextRef,
    paramsList,
    setParamsList,
    batchParamsList,
    setBatchParamsList,
    panelOpenedKey,
    setPanelOpenedKey,
    editEndpointRef,
    AISetting,
    setAISetting,
    ParamsFormMethods,
    isAutoSaveClose,
    setInitialText,
    initialText,
    initialTextRef,
    autoSaveStateRef,
    setAutoSaveState,
    setAutoSaveErrorMsg,
    createSession,
    editorContentRef,
    systemValues
  } = useDataApiContext()
  const { dataServiceGuideStep } = useHelpCenterContext()
  const saveSqlTimer = useRef<ReturnType<typeof setTimeout>>()
  const paramsListRef = useRef<ParamType[]>([])
  const [isAggVisible, setIsAggVisible] = useState(false)
  const { testEndpoint, saveEndpoint } = useTestEndpoint()
  const dbReg = /[uU][sS][eE]\s+(\S*)\s*;/
  const { classes } = useStyles()
  const { classes: styles } = useDataserviceEditorStyles()

  useEffect(() => {
    window.addEventListener('keydown', keydownHandler, true)

    return () => {
      saveSqlTimer.current && clearTimeout(saveSqlTimer.current)
      window.removeEventListener('keydown', keydownHandler, true)
    }
  }, [])

  useEffect(() => {
    clearTimeout(saveSqlTimer.current)
    saveSqlAuto()
  }, [editEndpoint?.id])

  useEffect(() => {
    if (!AISetting.chat2query_init) {
      setIsAggVisible(true)
      return
    }
  }, [AISetting])

  useEffect(() => {
    paramsListRef.current = paramsList
  }, [paramsList])

  useEffect(() => {
    if (isAutoSaveClose) {
      saveSqlTimer.current && clearTimeout(saveSqlTimer.current)
    } else {
      saveSqlAuto()
    }
  }, [isAutoSaveClose])

  const saveSqlAuto = () => {
    saveSqlTimer.current && clearTimeout(saveSqlTimer.current)
    saveSqlTimer.current = setTimeout(async () => {
      if (!editEndpointRef.current || editEndpointRef.current.id === -1) {
        return
      }
      const data = {
        ...editEndpointRef.current,
        sql_template: sqlTextRef.current
      } as EndPoint

      if (!editEndpointRef.current?.settings) {
        return
      }

      if (autoSaveStateRef.current !== AutoSaveStateEnum.Failed) {
        setAutoSaveState(AutoSaveStateEnum.Saving)
        autoSaveStateRef.current = AutoSaveStateEnum.Saving
      }

      const res = (await saveEndpoint(
        {
          ...data,
          endpoint: data.endpoint && data.endpoint[0] === '/' ? data.endpoint : '',
          sql_template: editorContentRef.current?.getFormatContent() || ''
        },
        undefined,
        false
      )) as SaveResType

      if (res.status === SaveResStatus.SUCCESS) {
        setAutoSaveState(AutoSaveStateEnum.Saved)
        autoSaveStateRef.current = AutoSaveStateEnum.Saved
      } else {
        setAutoSaveState(AutoSaveStateEnum.Failed)
        autoSaveStateRef.current = AutoSaveStateEnum.Failed
        setAutoSaveErrorMsg(res.message ?? '')
      }
      saveSqlAuto()
    }, 5000)
  }

  useEffect(() => {
    if (editEndpoint?.type !== 'chat2sql') {
      return
    }

    isSqlInit.current = true
    const sql = editEndpoint?.sql_template || ''
    setSqlText(sql)
    sqlTextRef.current = sql
    setInitialText(sql)
    initialTextRef.current = sql
  }, [editEndpoint?.id])

  const keydownHandler = (e: KeyboardEvent) => {
    if (e.key === 'F5') {
      testEndpoint()
      return
    }

    if (isCompositionKey(e) || isRunAllCompositionKey(e)) {
      if (isRunning || !editEndpointRef.current?.cluster_id || editEndpointRef.current.cluster_id === '0') {
        return
      }

      e.preventDefault()
      let isAll = true

      if (isRunAllCompositionKey(e)) {
        runSqlHandler(true)
      } else {
        runSqlHandler(false)
        isAll = false
      }

      eventTracking('Data Service Executed', {
        source: 'Hot Key',
        key: isAll ? 'run all' : 'run single line'
      })
    }
  }

  const isCompositionKey = (e: KeyboardEvent) => {
    return (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'enter'
  }

  const isRunAllCompositionKey = (e: KeyboardEvent) => {
    return (e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'enter'
  }

  const getLineNumByPos = (pos: number, texts: string[]) => {
    let len = 0
    let lineNum = 0

    for (let i = 0; i < texts.length; i++) {
      if (len + texts[i].length + 1 > pos) {
        lineNum = i
        break
      } else {
        len += texts[i].length + 1
      }
    }

    return lineNum
  }

  const isArgsValid = async () => {
    try {
      const res = await ParamsFormMethods.trigger()
      if (res === false) {
        setPanelOpenedKey('Params')
        return false
      }
      return true
    } catch (_e) {
      return false
    }
  }

  const runSqlHandler = async (isRunAll = false) => {
    const sessionId = editEndpointRef.current?.session_id
    if (isRunning || !sessionId) {
      return
    }

    const content = editorContentRef.current?.getOriContent() || ''
    const selectContent = editorContentRef.current?.getSelectText() || ''
    const text = (isRunAll ? content : selectContent) || ' '
    if (!text.trim()) {
      return
    }

    if (editEndpointRef.current?.type !== 'system-data') {
      const isParamsValid = await isArgsValid()
      if (!isParamsValid) {
        return
      }
    }

    setIsRunning(true)
    props.onRun && props.onRun()
    let sqls: any[] = []

    let lineNum
    const arrs = content.split('\n')
    if (isRunAll) {
      lineNum = arrs.length - 1
    } else {
      lineNum = getLineNumByPos(editorContentRef.current?.getCursorPosing() || 0, arrs)
    }

    const beforeDatabase = getDatabaseName(lineNum)

    const file2AiMap = editorContentRef.current?.getFileAiMap() || {}
    const maps = Object.keys(file2AiMap)
    if (!isRunAll && maps.length) {
      const { start, end } = editorContentRef.current?.getSelectSqlLine() || { start: 0, end: 0 }
      const texts = sqlTextRef.current.split('\n')
      let cur = start

      while (cur <= end) {
        const e: any = editorContentRef.current?.getSelectionTar()

        const { end } = getSqlLineNum(e.view.observer.dom.cmView.children || [], cur, texts, true)

        let isAi = false
        for (let key of maps) {
          const file = file2AiMap[key]
          if (file.end_line >= end && end >= file.start_line) {
            isAi = true
            const formatText = removeComment(texts.slice(cur, end + 1).join(' \n'))
            if (formatText) {
              sqls = [
                {
                  sql: formatText,
                  chat2querySid: file.chat2query_sid
                }
              ]
            }

            break
          }
        }
        if (!isAi) {
          sqls = [
            {
              sql: removeComment(texts.slice(cur, end + 1).join(' \n')),
              chat2querySid: ''
            }
          ]
        }
        cur = end + 1
      }
    } else {
      const noCommentText = removeComment(text)
      sqls = [noCommentText]
    }

    if (!sqls.length || !(typeof sqls[0] === 'string' ? sqls[0] : sqls[0].sql || '').trim()) {
      setIsRunning(false)
      return
    }

    const result = {
      status: RequestStatus.Loading,
      sql: typeof sqls[0] === 'object' ? sqls[0].sql : sqls[0],
      res: {},
      chat2querySid: typeof sqls[0] === 'object' ? sqls[0].chat2querySid : ''
    } as any as SqlRes & { chat2querySid: string }

    setSqlResultList(result)

    let hasDDL = false

    const ddl = ['create', 'drop', 'alter', 'truncate', 'rename', 'comment', 'grant', 'revoke']

    eventTracking('Data Service Run Sql Start', {
      sql: result.sql,
      sessionId
    })
    const database = getDatabaseName(0, [result.sql])
    let res
    try {
      res = (await execSql(result.sql, sessionId, database || beforeDatabase, result.chat2querySid)) as any

      if (res.code === AI_CODE.SESSION_EXPIRED) {
        try {
          const newSessionId = await createSession(editEndpointRef.current?.id || 0)

          if (newSessionId) {
            res = await execSql(result.sql, newSessionId, database || beforeDatabase, result.chat2querySid)
          }
        } catch (e: any) {
          throw new Error(e)
        }
      } else if (res.code !== 200) {
        throw new Error(res.message)
      }

      eventTracking('Data Service Run Sql Success', {
        sql: result.sql,
        sessionId
      })
    } catch (e) {
      res = getErrorMessage(e)
    }

    const ddlCallback = () => {
      if (hasDDL) {
        setIsDbNeedFresh(true)
      }
    }

    const execRes = { ...result }

    if (res && res.code === 200) {
      execRes.res = (res.data && res.data.length && res.data[0]) || {}
      execRes.res && (execRes.res.args = res.args)
      execRes.status = RequestStatus.Success

      if (ddl.some((i) => result.sql.trim().toLowerCase().startsWith(i))) {
        hasDDL = true
      }
    } else {
      execRes.status = RequestStatus.Error
      execRes.res = res
    }
    setSqlResultList(execRes)

    setIsRunning(false)
    ddlCallback()
  }

  const removeComment = (text: string) => {
    if (!text) {
      return ''
    }

    const res = strip(text, {
      language: 'sql',
      keepProtected: true
    })

    return res
  }

  const execSql = async (sqlLine: string, sessionId: string, database?: string, _chat2querySid = '') => {
    return new Promise(async (resolve, _reject) => {
      try {
        const sql = sqlLine.replaceAll('\n', ' ')
        const { sysArgs } = getArgs(
          paramsListRef.current,
          batchParamsList,
          !!editEndpointRef.current?.settings?.enable_pagination,
          systemValues
        )
        const args =
          editEndpointRef.current?.type === 'system-data'
            ? []
            : [(paramsListRef.current ?? []).concat(sysArgs as ParamType[])]

        const res = await dataApiSQLExecute(
          orgId,
          projectId,
          `${editEndpointRef.current?.dataappp_id}`,
          `${editEndpointRef.current?.id}`,
          {
            cluster_id: editEndpointRef.current?.cluster_id || '',
            database: database || '',
            session_id: sessionId,
            settings: editEndpointRef.current?.settings,
            args,
            sql
          },
          { timeout: 30 * 1000 }
        )
        resolve({
          ...res,
          args
        })

        const dbs = sql.match(dbReg) || []
        let db = ''
        if (dbs.length > 1) {
          db = (dbs[1] || '').trim()
        }

        const index = dbList.findIndex((item) => item.name === db)
        if (index !== -1 && editEndpoint?.type !== 'system-data') {
          const val = {
            ...editEndpointRef.current,
            database: db
          } as EndPoint
          setEditEndpoint(val)
          editEndpointRef.current = val
        }
      } catch (e) {
        if (JSON.stringify(e).includes('Error: timeout of')) {
          resolve({
            message: 'Query timeout expired'
          })
        } else {
          resolve({
            message: 'Execute error, Please retry in a short while.'
          })
        }
      }
    })
  }

  const isComment = (domList: any[], index: number) => {
    if (index < 0) {
      return true
    }

    const list = domList[index]?.children || []
    return list.length === 1 && list[0]?.mark?.class === 'ͼ16'
  }

  const getSqlLineNum = (domList: any[], index: number, texts: string[], isBlock = false) => {
    let start = index - 1
    let end = index
    const len = texts.length

    const beforeText = removeComment(texts.slice(0, index).join('\n')).trim()

    if (!(isBlock && (beforeText?.length && beforeText[beforeText.length - 1]) === ';')) {
      while (start > 0) {
        if (isComment(domList, start)) {
          start--
        } else {
          const cur = (texts[start] || '').trim()
          if (cur.length && cur[cur.length - 1] === ';' && start !== domList.length - 1) {
            break
          } else {
            start--
          }
        }
      }
    }

    start = Math.max(start, 0)
    const startLine = (texts[0] || '').trim()
    if (start) {
      start++
    }

    while (end < len) {
      if (isComment(domList, end)) {
        end++
      } else {
        const cur = (texts[end] || '').trim()
        if (cur.length && cur[cur.length - 1] === ';') {
          break
        } else {
          end++
        }
      }
    }

    if (start === 0 && startLine[startLine.length - 1] === ';' && end) {
      start++
    }

    return {
      start,
      end
    }
  }

  const getDatabaseName = (lineNum: number, sqlArrs?: string[]) => {
    if (editEndpoint?.type === 'system-data') {
      return editEndpointRef.current?.database
    }

    let name = ''
    let arrs: string[] = []

    if (sqlArrs) {
      arrs = sqlArrs
    } else {
      const texts = removeComment(
        sqlTextRef.current
          .split('\n')
          .slice(0, lineNum + 1)
          .join('\n')
      )
      arrs = texts.split(';').join(';\n').split('\n')
    }

    for (let i = 0; i < arrs.length; i++) {
      const cur = arrs[i] || ''
      const res = cur.match(dbReg) || []
      if (res.length > 1) {
        name = (res[1] || '').trim()
      }
    }
    if (name) {
      name = name[name.length - 1] === ';' ? name.slice(0, name.length - 1) : name
    }
    return name
  }

  const paramsChangeHandler = (params: ParamType[], syncPanel?: boolean) => {
    if (editEndpoint?.type === 'system-data') {
      return
    }

    setParamsList(params)
    setBatchParamsList(cloneDeep(params))
    if (syncPanel && panelOpenedKey !== 'Params' && !isSqlInit.current && dataServiceGuideStep === 0) {
      setPanelOpenedKey('Params')
    }
  }

  return (
    <div className={clsx(styles.sqlEditor, { [styles.hidden]: mainPanel.hidden })}>
      <Flex
        sx={{
          position: 'absolute',
          top: 14,
          left: 16,
          right: 16,
          zIndex: 1
        }}
        justify="space-between"
        align="center"
      >
        <Flex sx={{ flex: 1 }}>
          {editEndpoint?.type === 'system-data' && (
            <Flex
              bg="carbon.3"
              px={8}
              align="center"
              mr={8}
              sx={(t) => ({
                borderRadius: t.defaultRadius
              })}
            >
              <Typography mr={4} variant="label-sm">
                Playground
              </Typography>
              <HoverCard withArrow width={400} position="top" shadow="md">
                <HoverCard.Target>
                  <Flex sx={{ cursor: 'pointer' }}>
                    <IconInfoCircle size={14} />
                  </Flex>
                </HoverCard.Target>
                <HoverCard.Dropdown>
                  <Typography>
                    SQL statements and parameters used in this Playground (SQL Editor) are not saved and are intended
                    solely for development and testing purposes.
                  </Typography>
                </HoverCard.Dropdown>
              </HoverCard>
            </Flex>
          )}

          <ClusterList />

          {editEndpoint?.type === 'system-data' && <SchemaList />}

          <RunBtn running={isRunning} onClick={(isRunAll: boolean) => runSqlHandler(isRunAll)} />
        </Flex>
        <HoverCard withArrow width={400} position="top" shadow="md">
          <HoverCard.Target>
            <Flex sx={{ cursor: 'pointer' }}>
              <IconKeyboard01 size={20} />
            </Flex>
          </HoverCard.Target>
          <HoverCard.Dropdown>
            <EmptyRunTips />
          </HoverCard.Dropdown>
        </HoverCard>
      </Flex>

      <div
        key={editEndpoint?.id}
        data-testid="endpoint-editor-container"
        className={clsx(classes.editorContainer, classes.editorContainerNoTransform)}
      >
        <EditContent
          ref={editorContentRef}
          content={editEndpoint?.sql_template || ''}
          isAiAllowed={AISetting.is_privacy_allowed}
          cluster_id={editEndpoint?.cluster_id}
          session_id={editEndpoint?.session_id}
          file_id={editEndpoint?.id}
          endpoint_type={editEndpointRef.current?.type}
          selected_database={editEndpointRef.current?.database}
          dbList={dbList}
          initialText={initialText}
          paramsList={paramsList}
          endpointPath={editEndpoint?.endpoint ?? ''}
          onParamsChange={paramsChangeHandler}
          onSqlChange={(sql: string) => {
            setSqlText(sql)
            sqlTextRef.current = sql
            isSqlInit.current = false
          }}
        />
      </div>
      {isAggVisible && (
        <AIAggModal
          onReset={(isAccept) => {
            setAISetting({
              chat2query_init: true,
              is_privacy_allowed: isAccept,
              privacy_box_count: 1
            })
          }}
        />
      )}
    </div>
  )
}

export default Editor
