<!--
  组件名称：DeployExecutions.vue
  组件描述：自动化部署测试 - 部署执行中心（一键执行 + WebSocket 实时日志）

  @author czj
  @date 2026-08-12
-->

<template>
  <div class="deploy-executions-page">
    <div class="page-header">
      <h2>部署执行</h2>
      <el-button type="primary" @click="openSelectScriptDialog">
        <el-icon><VideoPlay /></el-icon>执行部署
      </el-button>
    </div>

    <el-card shadow="never">
      <el-table :data="executions" v-loading="loading" stripe style="width: 100%">
        <el-table-column prop="scriptName" label="脚本" min-width="150" />
        <el-table-column prop="serverName" label="目标服务器" min-width="130" />
        <el-table-column label="状态" width="110">
          <template #default="{ row }">
            <el-tag :type="statusType(row.status)" size="small">
              {{ statusLabel(row.status) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="开始时间" width="170">
          <template #default="{ row }">
            {{ formatTime(row.startTime) || '-' }}
          </template>
        </el-table-column>
        <el-table-column label="耗时" width="100">
          <template #default="{ row }">
            {{ row.duration ? row.duration + 's' : '-' }}
          </template>
        </el-table-column>
        <el-table-column label="退出码" width="90">
          <template #default="{ row }">
            <span v-if="row.exitCode !== null">{{ row.exitCode }}</span>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="200" fixed="right">
          <template #default="{ row }">
            <el-button link type="primary" size="small" @click="openDetail(row)">查看日志</el-button>
            <el-button v-if="row.status === 'running' || row.status === 'pending'" link type="warning" size="small" @click="handleCancel(row)">取消</el-button>
            <el-popconfirm title="确定删除此执行记录？" @confirm="handleDelete(row)">
              <template #reference>
                <el-button link type="danger" size="small">删除</el-button>
              </template>
            </el-popconfirm>
          </template>
        </el-table-column>
      </el-table>
    </el-card>

    <!-- 选择脚本并执行弹窗 -->
    <el-dialog v-model="scriptDialogVisible" title="选择要执行的部署脚本" width="520px">
      <el-form label-position="top">
        <el-form-item label="部署脚本" required>
          <el-select v-model="selectedScriptId" placeholder="请选择部署脚本" style="width: 100%" filterable>
            <el-option v-for="s in scripts" :key="s.id" :label="s.name + ' (' + s.serverName + ')'" :value="s.id" />
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="scriptDialogVisible = false">取消</el-button>
        <el-button type="primary" :loading="running" @click="handleRun">开始执行</el-button>
      </template>
    </el-dialog>

    <!-- 实时日志弹窗 -->
    <el-dialog v-model="logDialogVisible" title="部署执行日志" width="860px" top="5vh" :close-on-click-modal="false">
      <div class="exec-status-bar">
        <el-tag :type="statusType(currentStatus)" size="small">
          {{ statusLabel(currentStatus) }}
        </el-tag>
        <span class="exec-id">执行ID: {{ currentExecutionId }}</span>
        <el-button v-if="currentStatus === 'running' || currentStatus === 'pending'" type="warning" size="small" @click="handleCancel()">取消执行</el-button>
      </div>
      <div ref="logViewerRef" class="log-viewer">
        <div v-if="logs.length === 0" class="log-empty">等待执行日志...</div>
        <div v-for="(line, idx) in logs" :key="idx" :class="['log-line', 'log-' + line.level]">
          <span class="log-time">{{ formatTime(line.timestamp) }}</span>
          <span v-if="line.level === 'INPUT'" class="log-badge">[应答]</span>
          <span v-else-if="line.level === 'OUTPUT'" class="log-badge">[输出]</span>
          <span v-else class="log-badge">[{{ line.level }}]</span>
          <span class="log-msg">{{ line.message }}</span>
        </div>
      </div>
      <template #footer>
        <el-button @click="logDialogVisible = false">关闭</el-button>
        <el-button type="primary" @click="openDetailById(currentExecutionId)">查看完整日志</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup lang="ts">
/**
 * 部署执行中心：选择脚本一键执行，实时查看部署进度日志
 */

import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { VideoPlay } from '@element-plus/icons-vue'
import {
  listScripts,
  listExecutions,
  createExecution,
  runExecution,
  cancelExecution,
  deleteExecution,
  createDeployLogStream,
} from '@/api/deploy'
import type { DeployScript, DeployExecution, DeployMessage } from '@/types/deploy'

const router = useRouter()

const loading = ref(false)
const executions = ref<DeployExecution[]>([])
const scripts = ref<DeployScript[]>([])

const scriptDialogVisible = ref(false)
const selectedScriptId = ref('')
const running = ref(false)

const logDialogVisible = ref(false)
const logs = ref<Array<{ timestamp: string; level: string; message: string }>>([])
const currentStatus = ref('pending')
const currentExecutionId = ref('')
const logViewerRef = ref<HTMLElement>()
let ws: WebSocket | null = null

/** 加载执行记录列表 */
async function loadExecutions() {
  loading.value = true
  try {
    const res = await listExecutions({ limit: 100 })
    executions.value = res.items
  } catch (e: any) {
    ElMessage.error('加载执行记录失败: ' + (e.message || ''))
  } finally {
    loading.value = false
  }
}

/** 加载脚本列表 */
async function loadScripts() {
  try {
    const res = await listScripts({ limit: 200 })
    scripts.value = res.items
  } catch (_e) {
    // 非关键错误
  }
}

/** 打开选择脚本弹窗 */
function openSelectScriptDialog() {
  selectedScriptId.value = ''
  scriptDialogVisible.value = true
}

/** 开始执行部署 */
async function handleRun() {
  if (!selectedScriptId.value) {
    ElMessage.warning('请选择部署脚本')
    return
  }
  running.value = true
  try {
    const created = await createExecution({ scriptId: selectedScriptId.value })
    scriptDialogVisible.value = false
    await openLogWindow(created.id)
    // 触发执行
    await runExecution(created.id)
    await loadExecutions()
  } catch (e: any) {
    ElMessage.error('执行失败: ' + (e.message || ''))
  } finally {
    running.value = false
  }
}

/** 打开实时日志窗口并连接 WebSocket */
async function openLogWindow(executionId: string) {
  currentExecutionId.value = executionId
  currentStatus.value = 'running'
  logs.value = []
  logDialogVisible.value = true

  // 断开旧连接
  if (ws) {
    ws.close()
    ws = null
  }

  ws = createDeployLogStream(
    executionId,
    (msg: DeployMessage) => {
      handleWsMessage(msg)
    },
    () => {
      // onError
    },
    () => {
      // onClose
    },
  )
  await nextTick()
  scrollToBottom()
}

/** 处理 WebSocket 消息 */
function handleWsMessage(msg: DeployMessage) {
  const data = msg.data || {}
  switch (msg.type) {
    case 'deploy_start':
      currentStatus.value = 'running'
      logs.value.push({ timestamp: formatNow(), level: 'INFO', message: data.message || '部署开始...' })
      break
    case 'deploy_output':
      logs.value.push({ timestamp: formatNow(), level: 'OUTPUT', message: data.message || '' })
      break
    case 'deploy_stderr':
      logs.value.push({ timestamp: formatNow(), level: 'ERROR', message: data.message || '' })
      break
    case 'deploy_answer_sent':
      logs.value.push({ timestamp: formatNow(), level: 'INPUT', message: `已发送应答: ${data.answer || ''}` })
      break
    case 'deploy_complete':
      currentStatus.value = 'completed'
      logs.value.push({ timestamp: formatNow(), level: 'INFO', message: `部署完成 (退出码 ${data.exitCode}, 耗时 ${data.duration}s)` })
      if (ws) ws.close()
      break
    case 'deploy_error':
      currentStatus.value = data.exitCode === -1 ? 'cancelled' : 'failed'
      logs.value.push({ timestamp: formatNow(), level: 'ERROR', message: data.error || data.message || '部署失败' })
      if (ws) ws.close()
      break
  }
  scrollToBottom()
}

/** 滚动到底部 */
function scrollToBottom() {
  nextTick(() => {
    if (logViewerRef.value) {
      logViewerRef.value.scrollTop = logViewerRef.value.scrollHeight
    }
  })
}

/** 取消执行 */
async function handleCancel(row?: DeployExecution) {
  const id = row ? row.id : currentExecutionId.value
  if (!id) return
  try {
    await cancelExecution(id)
    ElMessage.success('已发送取消指令')
    await loadExecutions()
  } catch (e: any) {
    ElMessage.error('取消失败: ' + (e.message || ''))
  }
}

/** 打开执行详情 */
function openDetail(row: DeployExecution) {
  router.push(`/deploy/executions/${row.id}`)
}

/** 通过ID打开执行详情 */
function openDetailById(id: string) {
  router.push(`/deploy/executions/${id}`)
}

/** 删除执行记录 */
async function handleDelete(row: DeployExecution) {
  try {
    await deleteExecution(row.id)
    ElMessage.success('执行记录已删除')
    await loadExecutions()
  } catch (e: any) {
    ElMessage.error('删除失败: ' + (e.message || ''))
  }
}

/** 状态标签类型 */
function statusType(status: string) {
  const map: Record<string, string> = { completed: 'success', running: 'primary', pending: 'info', failed: 'danger', cancelled: 'warning' }
  return map[status] || 'info'
}

/** 状态标签文字 */
function statusLabel(status: string) {
  const map: Record<string, string> = { completed: '已完成', running: '执行中', pending: '等待中', failed: '失败', cancelled: '已取消' }
  return map[status] || status
}

/** 格式化时间 */
function formatTime(t: string) {
  return t?.replace('T', ' ').substring(0, 19) || ''
}

/** 当前时间字符串 */
function formatNow() {
  const d = new Date()
  const p = (n: number) => String(n).padStart(2, '0')
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
}

onMounted(() => {
  loadExecutions()
  loadScripts()
})

onBeforeUnmount(() => {
  if (ws) {
    ws.close()
    ws = null
  }
})
</script>

<style scoped>
.deploy-executions-page {
  padding: 0;
}

.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
}

.page-header h2 {
  font-size: 20px;
  font-weight: 600;
  color: #303133;
  margin: 0;
}

.exec-status-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
}

.exec-id {
  font-size: 12px;
  color: #909399;
  flex: 1;
}

.log-viewer {
  height: 480px;
  overflow-y: auto;
  background: #1e1e1e;
  border-radius: 4px;
  padding: 12px;
  font-family: 'Consolas', 'Courier New', monospace;
  font-size: 13px;
  line-height: 1.6;
}

.log-empty {
  color: #555;
  text-align: center;
  padding-top: 40px;
}

.log-line {
  white-space: pre-wrap;
  word-break: break-all;
}

.log-time {
  color: #888;
  margin-right: 8px;
}

.log-badge {
  margin-right: 6px;
  font-weight: 600;
}

.log-INFO .log-badge {
  color: #4fc1ff;
}

.log-OUTPUT {
  color: #d4d4d4;
}

.log-OUTPUT .log-badge {
  color: #6a9955;
}

.log-INPUT {
  color: #d7ba7d;
}

.log-INPUT .log-badge {
  color: #d7ba7d;
}

.log-ERROR {
  color: #f44747;
}

.log-ERROR .log-badge {
  color: #f44747;
}
</style>