<template>
  <div class="terminal_tab_container">
    <div
      ref="terminalRef"
      class="terminal_container"
      @contextmenu="handleRightClick"
    />
    <!-- <div class="terminal_command_history">
      <CommandHistory :list="commandHistoryList" />
    </div> -->
  </div>
</template>

<script setup>
import { ref, onMounted, computed, onBeforeUnmount, getCurrentInstance, watch, nextTick } from 'vue'
// import CommandHistory from './command_history.vue'
import { Terminal } from '@xterm/xterm'
import '@xterm/xterm/css/xterm.css'
import { FitAddon } from '@xterm/addon-fit'
import { SearchAddon } from '@xterm/addon-search'
// import { SearchBarAddon } from 'xterm-addon-search-bar'
import { WebLinksAddon } from '@xterm/addon-web-links'
import socketIo from 'socket.io-client'
import themeList from 'xterm-theme'
import { terminalStatus } from '@/utils/enum'

const { CONNECTING, RECONNECTING, CONNECT_SUCCESS, CONNECT_FAIL } = terminalStatus

const { io } = socketIo
const { proxy: { $api, $store, $serviceURI, $notification, $router, $message } } = getCurrentInstance()

const props = defineProps({
  hostObj: {
    required: true,
    type: Object
  }
})

const emit = defineEmits(['inputCommand', 'cdCommand', 'ping-data',])

const socket = ref(null)
// const commandHistoryList = ref([])
const term = ref(null)
const command = ref('')
const timer = ref(null)
const pingTimer = ref(null)
const fitAddon = ref(null)
// const searchBar = ref(null)
const hasRegisterEvent = ref(false)

const socketConnected = ref(false)
const curStatus = ref(CONNECTING)
const terminal = ref(null)
const terminalRef = ref(null)

const token = computed(() => $store.token)
const theme = computed(() => themeList[$store.terminalConfig.theme])
const fontSize = computed(() => $store.terminalConfig.fontSize)
const background = computed(() => $store.terminalConfig.background)
const hostObj = computed(() => props.hostObj)
const hostId = computed(() => hostObj.value.id)
const host = computed(() => hostObj.value.host)
const menuCollapse = computed(() => $store.menuCollapse)
const quickCopy = computed(() => $store.terminalConfig.quickCopy)
const quickPaste = computed(() => $store.terminalConfig.quickPaste)
const autoExecuteScript = computed(() => $store.terminalConfig.autoExecuteScript)

watch(menuCollapse, () => {
  nextTick(() => {
    handleResize()
  })
})

watch(theme, () => {
  nextTick(() => {
    if (!background.value) terminal.value.options.theme = theme.value
    else terminal.value.options.theme = { ...theme.value, background: '#00000080' }
  })
})

watch(fontSize, () => {
  nextTick(() => {
    terminal.value.options.fontSize = fontSize.value
    // fitAddon.value.fit()
    handleResize()
  })
})

watch(background, (newVal) => {
  nextTick(() => {
    if (newVal) {
      // terminal.value.options.theme.background = '#00000080'
      terminal.value.options.theme = { ...theme.value, background: '#00000080' }
      terminalRef.value.style.backgroundImage = `url(${ background.value })`
      terminalRef.value.style.backgroundImage = `url(${ background.value })`
      // terminalRef.value.style.backgroundImage = `linear-gradient(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15)), url(${ background.value })`
    } else {
      terminal.value.options.theme = theme.value
      terminalRef.value.style.backgroundImage = null
    }
  })
}, { immediate: true })

watch(curStatus, () => {
  console.warn(`status: ${ curStatus.value }`)
  hostObj.value.status = curStatus.value
})

const getCommand = async () => {
  let { data } = await $api.getCommand(hostId.value)
  if (data) command.value = data
}

const connectIO = () => {
  socket.value = io($serviceURI, {
    path: '/terminal',
    forceNew: false,
    reconnectionAttempts: 1
  })
  socket.value.on('connect', () => {
    console.log('/terminal socket已连接：', hostId.value)

    socketConnected.value = true
    socket.value.emit('create', { hostId: hostId.value, token: token.value })
    socket.value.on('connect_terminal_success', () => {
      if (hasRegisterEvent.value) return // 以下事件连接成功后仅可注册一次, 否则会多次触发. 除非socket重连
      hasRegisterEvent.value = true

      socket.value.on('output', (str) => {
        term.value.write(str)
        terminalText.value += str
      })

      socket.value.on('connect_shell_success', () => {
        curStatus.value = CONNECT_SUCCESS
        onResize()
        onFindText()
        onWebLinks()
        if (command.value) socket.value.emit('input', command.value + '\n')
      })

      // socket.value.on('terminal_command_history', (data) => {
      //   console.log(data)
      //   commandHistoryList.value = data
      // })
    })

    if (pingTimer.value) clearInterval(pingTimer.value)
    pingTimer.value = setInterval(() => {
      socket.value.emit('get_ping', host.value)
    }, 3000)
    socket.value.emit('get_ping', host.value) // 获取服务端到客户端的ping值
    socket.value.on('ping_data', (pingMs) => {
      emit('ping-data', Object.assign({ ip: host.value }, pingMs))
    })

    socket.value.on('token_verify_fail', () => {
      $notification({ title: 'Error', message: 'token校验失败，请重新登录', type: 'error' })
      $router.push('/login')
    })

    socket.value.on('connect_close', () => {
      if (curStatus.value === CONNECT_FAIL) return // 连接失败不需要自动重连
      curStatus.value = RECONNECTING
      console.warn('连接断开,3秒后自动重连: ', hostId.value)
      term.value.write('\r\n连接断开,3秒后自动重连...\r\n')
      socket.value.emit('reconnect_terminal')
    })

    socket.value.on('reconnect_terminal_success', () => {
      curStatus.value = CONNECT_SUCCESS
    })

    socket.value.on('create_fail', (message) => {
      curStatus.value = CONNECT_FAIL
      console.error('n创建失败:', hostId.value, message)
      term.value.write(`\r\n创建失败: ${ message }\r\n`)
    })

    socket.value.on('connect_fail', (message) => {
      curStatus.value = CONNECT_FAIL
      console.error('连接失败:', hostId.value, message)
      term.value.write(`\r\n连接失败: ${ message }\r\n`)
    })
  })

  socket.value.on('disconnect', () => {
    console.warn('terminal websocket 连接断开')
    socket.value.removeAllListeners() // 取消所有监听
    // socket.value.off('output') // 取消output监听,取消onData输入监听，重新注册
    curStatus.value = CONNECT_FAIL
    socketConnected.value = false
    term.value.write('\r\nError: 与面板socket连接断开。请关闭此tab，并检查本地与面板连接是否稳定\r\n')
  })

  socket.value.on('connect_error', (err) => {
    console.error('terminal websocket 连接错误：', err)
    $notification({
      title: '终端连接失败',
      message: '请检查socket服务是否正常',
      type: 'error'
    })
  })
}

const createLocalTerminal = () => {
  let terminalInstance = new Terminal({
    rendererType: 'dom',
    bellStyle: 'sound',
    convertEol: true,
    cursorBlink: true,
    disableStdin: false,
    minimumContrastRatio: 7,
    allowTransparency: true,
    fontFamily: 'Cascadia Code, Menlo, monospace',
    fontSize: fontSize.value,
    theme: theme.value
    // {
    //   foreground: '#ECECEC',
    //   background: '#000000', // 'transparent',
    //   // cursor: 'help',
    //   selection: '#ff9900',
    //   lineHeight: 20
    // }
  })
  term.value = terminalInstance
  terminalInstance.open(terminalRef.value)
  terminalInstance.writeln('\x1b[1;32mWelcome to EasyNode terminal\x1b[0m.')
  terminalInstance.writeln('\x1b[1;32mAn experimental Web-SSH Terminal\x1b[0m.')
  terminalInstance.focus()
  onSelectionChange()
  terminal.value = terminalInstance
}

const onResize = () => {
  fitAddon.value = new FitAddon()
  term.value.loadAddon(fitAddon.value)
  fitAddon.value.fit()
  let { rows, cols } = term.value
  socket.value.emit('resize', { rows, cols })
  window.addEventListener('resize', handleResize)
}

const handleResize = () => {
  if (timer.value) clearTimeout(timer.value)
  timer.value = setTimeout(async () => {
    // 由于非当前的el-tab-pane的display属性为none, 调用fitAddon.value?.fit()时无法获取宽高，因此先展示，再fit，最后再隐藏
    let temp = []
    let panes = Array.from(document.getElementsByClassName('el-tab-pane'))
    panes.forEach((item, index) => {
      temp[index] = item.style.display
      item.style.display = 'block'
    })
    fitAddon.value?.fit()
    let { rows, cols } = term.value
    socket.value?.emit('resize', { rows, cols })

    panes.forEach((item, index) => {
      item.style.display = temp[index]
    })
  }, 200)
}

const onWebLinks = () => {
  term.value.loadAddon(new WebLinksAddon())
}

// :TODO: 重写终端搜索功能
const onFindText = () => {
  const searchAddon = new SearchAddon()
  // searchBar.value = new SearchBarAddon({ searchAddon })
  term.value.loadAddon(searchAddon)
  // term.value.loadAddon(searchBar.value)
}

const onSelectionChange = () => {
  term.value.onSelectionChange(() => {
    if (!quickCopy.value) return
    let str = term.value.getSelection()
    if (!str) return
    const text = new Blob([str,], { type: 'text/plain' })
    const item = new ClipboardItem({
      'text/plain': text
    })
    navigator.clipboard.write([item,])
  })
}

const terminalText = ref(null)
const enterTimer = ref(null)

function filterAnsiSequences(str) {
  // 使用正则表达式移除ANSI转义序列
  // return str.replace(/\x1b\[[0-9;]*m|\x1b\[?[\d;]*[A-HJKSTfmin]/g, '')
  // eslint-disable-next-line
  return str.replace(/\x1b\[[0-9;]*[mGK]|(\x1b\][0-?]*[0-7;]*\x07)|(\x1b[\[\]()#%;][0-9;?]*[0-9A-PRZcf-ntqry=><])/g, '')
}

// 处理 Backspace，删除前一个字符
function applyBackspace(text) {
  let result = []
  for (let i = 0; i < text.length; i++) {
    if (text[i] === '\b') {
      if (result.length > 0) {
        result.pop()
      }
    } else {
      result.push(text[i])
    }
  }
  return result.join('')
}

function extractLastCdPath(text) {
  const regex = /cd\s+([^\s]+)(?=\s|$)/g
  let lastMatch
  let match
  regex.lastIndex = 0
  while ((match = regex.exec(text)) !== null) {
    lastMatch = match
  }
  return lastMatch ? lastMatch[1] : null
}

const onData = () => {
  // term.value.off('data', listenerInput)
  term.value.onData((key) => {
    if (socketConnected.value === false) return
    let acsiiCode = key.codePointAt()
    // console.log(acsiiCode)
    if (acsiiCode === 22) return handlePaste() // Ctrl + V
    // if (acsiiCode === 6) return searchBar.value.show() // Ctrl + F
    enterTimer.value = setTimeout(() => {
      if (enterTimer.value) clearTimeout(enterTimer.value)
      if (key === '\r') { // Enter
        if (curStatus.value === CONNECT_FAIL) { // 连接失败&&未正在连接，按回车可触发重连
          curStatus.value = CONNECTING
          term.value.write('\r\n连接中...\r\n')
          socket.value.emit('reconnect_terminal')
          return
        }
        if (curStatus.value === CONNECT_SUCCESS) {
          let cleanText = applyBackspace(filterAnsiSequences(terminalText.value))
          const lines = cleanText.split('\n')
          // console.log('lines: ', lines)
          const lastLine = lines[lines.length - 1].trim()
          // console.log('lastLine: ', lastLine)
          // 截取最后一个提示符后的内容（'$'或'#'后的内容）
          const commandStartIndex = lastLine.lastIndexOf('#') + 1
          const commandText = lastLine.substring(commandStartIndex).trim()
          // console.log('Processed command: ', commandText)
          // eslint-disable-next-line
          const cdPath = extractLastCdPath(commandText)

          if (cdPath) {
            console.log('cd command path:', cdPath)
            let firstChar = cdPath.charAt(0)
            if (!['/',].includes(firstChar)) return console.log('err fullpath:', cdPath) // 后端依赖不支持 '~'
            emit('cdCommand', cdPath)
          }
          terminalText.value = ''
        }
      }
    })
    if (curStatus.value !== CONNECT_SUCCESS) return
    emit('inputCommand', key)
    socket.value.emit('input', key)
  })
}

const handleRightClick = async (e) => {
  if (!quickPaste.value) return
  e.preventDefault()
  try {
    const clipboardText = await navigator.clipboard.readText()
    if (!clipboardText) return
    // 移除多余空格与换行符
    const formattedText = clipboardText.trim().replace(/\s+/g, ' ')
    // console.log(formattedText)
    if (formattedText.includes('rm -rf /')) return $message.warning(`高危指令,禁止粘贴: ${ formattedText }`)
    const safeText = formattedText.replace(/\r?\n|\r/g, '')
    // console.log(safeText)
    socket.value.emit('input', safeText)
  } catch (error) {
    $message.warning('右键默认粘贴行为,需要https支持')
  }
}

const handleClear = () => {
  term.value.clear()
}

const handlePaste = async () => {
  if (!quickPaste.value) return
  let key = await navigator.clipboard.readText()
  emit('inputCommand', key)
  socket.value.emit('input', key)
  term.value.focus()
}

const focusTab = () => {
  term.value.blur()
  setTimeout(() => {
    term.value.focus()
  }, 200)
}

const inputCommand = (command) => {
  command = command + (autoExecuteScript.value ? '\n' : '')
  socket.value.emit('input', command)
}

onMounted(async () => {
  createLocalTerminal()
  await getCommand()
  connectIO()
  await nextTick()
  onData()
})

onBeforeUnmount(() => {
  socket.value?.close()
  window.removeEventListener('resize', handleResize)
  clearInterval(pingTimer.value)
})

defineExpose({
  focusTab,
  handleResize,
  inputCommand,
  handleClear
})
</script>

<style lang="scss" scoped>
.terminal_tab_container {
  min-height: 200px;
  position: relative;
  .terminal_container {
    background-size: 100% 100%;
    background-repeat: no-repeat;

    :deep(.xterm) {
      height: 100%;
    }

    :deep(.xterm-viewport) {
      overflow-y: auto;
    }

    :deep(.xterm-screen) {
      padding: 0 0 0 10px;
    }
  }
  .terminal_command_history {
    width: 200px;
    height: 100%;
    overflow: auto;
    position: absolute;
    top: 0;
    right: 0;
    z-index: 1;
    background-color: #fff;
    border-radius: 6px
  }
}
</style>

<style lang="scss">
.terminals {
  .el-tabs__header {
    padding-left: 55px;
  }
}
</style>
