<script setup lang="ts">
import { RefreshRight, Tickets } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { devopsApi } from '../services/devopsApi'
import type { JenkinsBuildLog, PipelineStep, ReleaseRun, StepStatus } from '../types/devops'
import StatusPill from './StatusPill.vue'

const props = defineProps<{
  steps: PipelineStep[]
  run?: ReleaseRun
}>()

type StatusMeta = {
  label: string
  short: string
  className: string
  description: string
}

type OutputMode = 'step' | 'jenkins'
const maxRenderedLogLines = 520

const selectedStepNodeId = ref('')
const terminalOutputEl = ref<HTMLElement>()
const outputMode = ref<OutputMode>('step')
const logLoading = ref(false)
const logText = ref('')
const foldedLogLines = ref(0)
const logError = ref('')
const logMeta = ref<JenkinsBuildLog | null>(null)
const autoTail = ref(true)
let logTimer: ReturnType<typeof window.setTimeout> | undefined

const statusMeta: Record<StepStatus, StatusMeta> = {
  pending: {
    label: '等待中',
    short: '等',
    className: 'pending',
    description: '前置节点尚未完成，流程还没有执行到这里。',
  },
  queued: {
    label: '排队中',
    short: '排',
    className: 'queued',
    description: '任务已经提交，正在等待 Jenkins 或后端调度。',
  },
  running: {
    label: '运行中',
    short: '行',
    className: 'running',
    description: '当前节点正在执行，状态会随后端同步刷新。',
  },
  success: {
    label: '成功',
    short: '成',
    className: 'success',
    description: '节点已经完成且没有阻断后续流程。',
  },
  failed: {
    label: '失败',
    short: '败',
    className: 'failed',
    description: '节点执行失败，需要查看失败摘要和日志片段。',
  },
  skipped: {
    label: '跳过',
    short: '跳',
    className: 'skipped',
    description: '节点被后端判定为可跳过，通常不阻断发布。',
  },
  canceled: {
    label: '取消',
    short: '取',
    className: 'canceled',
    description: '流程被人工取消或由后端终止。',
  },
}

const statusLegend = Object.values(statusMeta)
const orderedSteps = computed(() => props.steps)
const outputOptions = computed(() => [
  { label: '节点输出', value: 'step' },
  { label: 'Jenkins 日志', value: 'jenkins', disabled: !canRequestJenkinsLog.value },
])

const currentStep = computed(() => {
  const active = orderedSteps.value.find((step) =>
    ['running', 'queued', 'failed'].includes(step.status),
  )

  if (active) {
    return active
  }

  return (
    orderedSteps.value
      .slice()
      .reverse()
      .find((step) => ['success', 'skipped', 'canceled'].includes(step.status)) ??
    orderedSteps.value[0]
  )
})

const selectedStep = computed(() => {
  if (!selectedStepNodeId.value) {
    return currentStep.value
  }

  return findStep(selectedStepNodeId.value) ?? currentStep.value
})

const stepStats = computed(() => {
  const total = orderedSteps.value.length
  const done = orderedSteps.value.filter((step) =>
    ['success', 'failed', 'skipped', 'canceled'].includes(step.status),
  ).length
  const running = orderedSteps.value.filter((step) => step.status === 'running').length
  const failed = orderedSteps.value.filter((step) => step.status === 'failed').length

  return { total, done, running, failed }
})

const progressPercent = computed(() => {
  if (stepStats.value.total === 0) {
    return 0
  }

  return Math.round((stepStats.value.done / stepStats.value.total) * 100)
})

const canRequestJenkinsLog = computed(() => Boolean(props.run?.id))

const hasJenkinsBuildNumber = computed(
  () => props.run?.jenkinsBuildNumber !== undefined && props.run?.jenkinsBuildNumber !== null,
)

const isActiveRun = computed(
  () => props.run?.status === 'queued' || props.run?.status === 'running',
)

const terminalTitle = computed(() => {
  if (outputMode.value === 'jenkins') {
    return hasJenkinsBuildNumber.value
      ? `Jenkins Build #${props.run?.jenkinsBuildNumber}`
      : 'Jenkins 输出日志'
  }

  return selectedStep.value?.name || '等待后端步骤'
})

const terminalSubtitle = computed(() => {
  if (outputMode.value === 'jenkins') {
    if (logMeta.value) {
      return `${logMeta.value.jobPath} · ${logMeta.value.start} -> ${logMeta.value.nextStart}`
    }

    const buildText = hasJenkinsBuildNumber.value ? `Build #${props.run?.jenkinsBuildNumber}` : ''
    const queueText = props.run?.jenkinsQueueId ? `Queue ${props.run.jenkinsQueueId}` : ''
    return [buildText, queueText].filter(Boolean).join(' · ') || '等待 Jenkins 日志来源'
  }

  if (!selectedStep.value) {
    return '等待后端步骤'
  }

  return `${selectedStep.value.owner} · 节点 ${selectedStep.value.nodeId}`
})

const logRefreshStateText = computed(() => {
  if (logLoading.value) {
    return '刷新中'
  }

  if (outputMode.value === 'jenkins' && autoTail.value && isActiveRun.value) {
    return '跟随中'
  }

  return '局部刷新'
})

const stepOutputText = computed(() => {
  const step = selectedStep.value

  if (!step) {
    return ''
  }

  return [
    `# ${step.name}`,
    `owner=${step.owner}`,
    `status=${statusMeta[step.status].label}`,
    `nodeId=${step.nodeId}`,
    `duration=${step.duration}`,
    step.startedAt ? `startedAt=${step.startedAt}` : '',
    step.finishedAt ? `finishedAt=${step.finishedAt}` : '',
    '',
    step.errorSummary ? `[error]\n${step.errorSummary}` : '',
    step.message ? `[message]\n${step.message}` : '',
    step.logExcerpt ? `[log]\n${step.logExcerpt}` : '',
    !step.errorSummary && !step.message && !step.logExcerpt ? step.summary : '',
  ]
    .filter((line) => line !== '')
    .join('\n')
})

const terminalText = computed(() => {
  if (outputMode.value !== 'jenkins') {
    return stepOutputText.value
  }

  if (!foldedLogLines.value) {
    return logText.value
  }

  return `... 已折叠前 ${foldedLogLines.value} 行日志，仅展示最近 ${maxRenderedLogLines} 行 ...\n${logText.value}`
})

const terminalEmptyTitle = computed(() => {
  if (outputMode.value === 'jenkins') {
    return canRequestJenkinsLog.value ? 'Jenkins 暂无日志内容' : '等待发布单'
  }

  return '暂无节点输出'
})

const terminalEmptyHint = computed(() => {
  if (outputMode.value === 'jenkins') {
    return canRequestJenkinsLog.value
      ? '正在通过后端读取 Jenkins progressive log；如果这里为空，需要后端返回对应 run 的构建日志。'
      : '触发发布后，这里会跟随当前 run 读取 Jenkins 构建日志。'
  }

  return '点击右侧节点查看该节点的执行消息、失败摘要或日志片段。'
})

function findStep(nodeId: string) {
  return props.steps.find((step) => step.nodeId === nodeId)
}

function statusClass(status: StepStatus): string {
  return statusMeta[status]?.className ?? 'pending'
}

function isSelectedStep(step: PipelineStep): boolean {
  return selectedStep.value?.nodeId === step.nodeId
}

function selectStep(step: PipelineStep) {
  selectedStepNodeId.value = step.nodeId

  if (step.owner === 'Jenkins') {
    outputMode.value = 'jenkins'
    void reloadJenkinsLog()
    return
  }

  outputMode.value = 'step'
}

function clearLogTimer() {
  if (logTimer !== undefined) {
    window.clearTimeout(logTimer)
    logTimer = undefined
  }
}

function resetJenkinsLog() {
  clearLogTimer()
  logLoading.value = false
  logText.value = ''
  foldedLogLines.value = 0
  logError.value = ''
  logMeta.value = null
}

async function reloadJenkinsLog() {
  await loadJenkinsLog(0, 'replace')
}

async function loadMoreJenkinsLog() {
  await loadJenkinsLog(logMeta.value?.nextStart ?? 0, 'append')
}

async function loadJenkinsLog(start: number, mode: 'replace' | 'append' | 'poll') {
  clearLogTimer()

  if (!props.run) {
    if (mode !== 'poll') {
      ElMessage.warning('当前没有可读取的发布单')
    }
    return
  }

  const runId = props.run.id
  logLoading.value = mode !== 'poll' || !logText.value
  logError.value = ''

  try {
    const log = await devopsApi.getJenkinsLog(runId, start)

    if (runId !== props.run?.id) {
      return
    }

    logMeta.value = log
    updateLogText(log.text, mode)
    scrollLogToBottom()
  } catch (error) {
    logError.value = error instanceof Error ? error.message : 'Jenkins 日志读取失败'
    if (mode !== 'poll') {
      ElMessage.error(logError.value)
    }
  } finally {
    if (runId === props.run?.id) {
      logLoading.value = false
      scheduleLogRefresh()
    }
  }
}

function updateLogText(nextText: string, mode: 'replace' | 'append' | 'poll') {
  const mergedText = mode === 'replace' ? nextText : `${logText.value}${nextText}`
  const lines = mergedText.split('\n')

  if (mode === 'replace') {
    foldedLogLines.value = 0
  }

  if (lines.length <= maxRenderedLogLines) {
    logText.value = mergedText
    return
  }

  const removed = lines.length - maxRenderedLogLines
  foldedLogLines.value += removed
  logText.value = lines.slice(removed).join('\n')
}

async function scrollLogToBottom() {
  if (!autoTail.value || outputMode.value !== 'jenkins') {
    return
  }

  await nextTick()
  const element = terminalOutputEl.value

  if (!element) {
    return
  }

  element.scrollTo({
    top: element.scrollHeight,
    behavior: 'smooth',
  })
}

function scheduleLogRefresh() {
  clearLogTimer()

  if (!autoTail.value || outputMode.value !== 'jenkins' || !canRequestJenkinsLog.value) {
    return
  }

  if (!isActiveRun.value && !logMeta.value?.hasMore) {
    return
  }

  logTimer = window.setTimeout(() => {
    void loadJenkinsLog(logMeta.value?.nextStart ?? 0, logText.value ? 'poll' : 'replace')
  }, 4500)
}

watch(
  () => `${props.run?.id ?? 'none'}:${props.run?.jenkinsBuildNumber ?? 'none'}`,
  () => {
    resetJenkinsLog()

    if (hasJenkinsBuildNumber.value) {
      outputMode.value = 'jenkins'
      void loadJenkinsLog(0, 'replace')
    } else if (outputMode.value === 'jenkins') {
      void loadJenkinsLog(0, 'replace')
    } else if (selectedStep.value?.owner !== 'Jenkins') {
      outputMode.value = 'step'
    }
  },
  { immediate: true },
)

watch(
  () => props.run?.status,
  () => {
    scheduleLogRefresh()
  },
)

watch(
  () => `${props.run?.id ?? 'none'}:${currentStep.value?.id ?? 'none'}:${currentStep.value?.status ?? 'none'}`,
  () => {
    if (selectedStepNodeId.value || currentStep.value?.owner !== 'Jenkins') {
      return
    }

    outputMode.value = 'jenkins'

    if (!logText.value && !logLoading.value) {
      void loadJenkinsLog(0, 'replace')
    }
  },
  { immediate: true },
)

watch(outputMode, () => {
  if (outputMode.value === 'jenkins' && !logText.value && !logLoading.value) {
    void loadJenkinsLog(0, 'replace')
  }
})

watch(autoTail, () => {
  scheduleLogRefresh()
})

onBeforeUnmount(() => {
  clearLogTimer()
})
</script>

<template>
  <div class="pipeline-viewer">
    <section class="execution-output">
      <div class="execution-toolbar">
        <div>
          <strong>{{ terminalTitle }}</strong>
          <span>{{ terminalSubtitle }}</span>
        </div>
        <div class="execution-actions">
          <ElSegmented v-model="outputMode" size="small" :options="outputOptions" />
          <span
            v-if="outputMode === 'jenkins'"
            class="refresh-state"
            :class="{ loading: logLoading }"
          >
            {{ logRefreshStateText }}
          </span>
          <ElSwitch
            v-if="outputMode === 'jenkins'"
            v-model="autoTail"
            size="small"
            inline-prompt
            active-text="跟随"
            inactive-text="暂停"
          />
          <ElButton
            v-if="outputMode === 'jenkins'"
            size="small"
            :icon="RefreshRight"
            :disabled="!canRequestJenkinsLog"
            :loading="logLoading"
            @click="reloadJenkinsLog"
          >
            重读
          </ElButton>
          <ElButton
            v-if="outputMode === 'jenkins' && logMeta?.hasMore"
            size="small"
            :loading="logLoading"
            @click="loadMoreJenkinsLog"
          >
            继续加载
          </ElButton>
        </div>
      </div>

      <pre v-if="terminalText" ref="terminalOutputEl" class="terminal-output">{{ terminalText }}</pre>
      <div v-else class="terminal-empty">
        <ElIcon><Tickets /></ElIcon>
        <strong>{{ terminalEmptyTitle }}</strong>
        <span>{{ terminalEmptyHint }}</span>
      </div>
      <div v-if="logError && outputMode === 'jenkins'" class="log-error">{{ logError }}</div>
    </section>

    <aside class="pipeline-side">
      <div class="flow-stats">
        <div>
          <small>流程进度</small>
          <strong>{{ progressPercent }}%</strong>
        </div>
        <div>
          <small>节点</small>
          <strong>{{ stepStats.done }}/{{ stepStats.total }}</strong>
        </div>
        <div>
          <small>运行</small>
          <strong>{{ stepStats.running }}</strong>
        </div>
        <div>
          <small>失败</small>
          <strong>{{ stepStats.failed }}</strong>
        </div>
        <div class="progress-track">
          <span :style="{ width: `${progressPercent}%` }" />
        </div>
      </div>

      <div class="legend">
        <span v-for="meta in statusLegend" :key="meta.className">
          <i class="dot" :class="meta.className" />{{ meta.label }}
        </span>
      </div>

      <div class="timeline-list">
        <button
          v-for="(step, index) in orderedSteps"
          :key="step.id"
          class="timeline-step"
          :class="[statusClass(step.status), { active: isSelectedStep(step) }]"
          type="button"
          @click="selectStep(step)"
        >
          <i class="timeline-index">{{ index + 1 }}</i>
          <span class="timeline-text">
            <strong>{{ step.name }}</strong>
            <small>{{ step.owner }} · {{ statusMeta[step.status].description }}</small>
          </span>
          <StatusPill :state="step.status" />
        </button>
      </div>

      <div v-if="selectedStep" class="node-detail">
        <div class="node-detail-head">
          <span>
            <strong>{{ selectedStep.name }}</strong>
            <small>{{ selectedStep.owner }} · 节点 {{ selectedStep.nodeId }}</small>
          </span>
          <StatusPill :state="selectedStep.status" />
        </div>
        <p>{{ selectedStep.summary }}</p>
        <dl>
          <dt>耗时</dt>
          <dd>{{ selectedStep.duration }}</dd>
          <dt>开始</dt>
          <dd>{{ selectedStep.startedAt || '-' }}</dd>
          <dt>结束</dt>
          <dd>{{ selectedStep.finishedAt || '-' }}</dd>
        </dl>
        <p v-if="selectedStep.message">
          <span>执行消息</span>
          {{ selectedStep.message }}
        </p>
        <p v-if="selectedStep.errorSummary">
          <span>失败摘要</span>
          {{ selectedStep.errorSummary }}
        </p>
        <pre v-if="selectedStep.logExcerpt">{{ selectedStep.logExcerpt }}</pre>
      </div>
    </aside>
  </div>
</template>

<style scoped>
.pipeline-viewer {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 330px;
  gap: 16px;
  height: clamp(430px, 58vh, 560px);
  min-height: 0;
  align-items: start;
}

.execution-output {
  display: flex;
  min-width: 0;
  height: 100%;
  min-height: 0;
  overflow: hidden;
  flex-direction: column;
  border: 1px solid #dfe6f0;
  border-radius: 8px;
  background: #0f172a;
  contain: layout paint;
}

.execution-toolbar {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
  padding: 12px 14px;
  border-bottom: 1px solid #1f2937;
  background: #111827;
  color: #dbeafe;
}

.execution-toolbar strong,
.execution-toolbar span {
  display: block;
}

.execution-toolbar strong {
  color: #f8fafc;
  font-size: 14px;
}

.execution-toolbar > div > span {
  margin-top: 4px;
  color: #94a3b8;
  font-size: 12px;
  line-height: 1.5;
}

.execution-actions {
  display: flex;
  flex: 0 0 auto;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
}

.refresh-state {
  display: inline-flex;
  height: 24px;
  align-items: center;
  gap: 6px;
  padding: 0 8px;
  border: 1px solid rgb(148 163 184 / 30%);
  border-radius: 999px;
  background: rgb(15 23 42 / 55%);
  color: #cbd5e1;
  font-size: 12px;
  white-space: nowrap;
}

.refresh-state::before {
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: #38bdf8;
  content: '';
}

.refresh-state.loading::before {
  animation: pulse 1s ease-in-out infinite;
  background: #60a5fa;
}

.execution-actions :deep(.el-segmented) {
  --el-segmented-bg-color: #1f2937;
  --el-segmented-item-selected-bg-color: #334155;
  --el-segmented-item-selected-color: #ffffff;
  --el-segmented-item-hover-color: #ffffff;
  color: #cbd5e1;
}

.terminal-output {
  flex: 1;
  min-height: 0;
  margin: 0;
  padding: 16px;
  overflow: auto;
  overscroll-behavior: contain;
  scroll-behavior: smooth;
  background:
    linear-gradient(rgb(255 255 255 / 3%) 1px, transparent 1px),
    #0f172a;
  background-size: 100% 24px;
  color: #dbeafe;
  font-size: 12px;
  line-height: 1.65;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.terminal-empty {
  display: flex;
  flex: 1;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 8px;
  padding: 28px;
  color: #94a3b8;
  text-align: center;
}

.terminal-empty .el-icon {
  color: #38bdf8;
  font-size: 28px;
}

.terminal-empty strong {
  color: #e5edf8;
  font-size: 14px;
}

.terminal-empty span {
  max-width: 420px;
  font-size: 13px;
  line-height: 1.6;
}

.log-error {
  padding: 10px 14px;
  border-top: 1px solid #7f1d1d;
  background: #fef2f2;
  color: #991b1b;
  font-size: 12px;
}

.pipeline-side {
  position: sticky;
  top: 86px;
  display: flex;
  height: 100%;
  max-height: 100%;
  min-width: 0;
  overflow: auto;
  overscroll-behavior: contain;
  flex-direction: column;
  gap: 12px;
  padding-right: 2px;
  contain: layout paint;
}

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 12px;
  color: #596578;
  font-size: 12px;
}

.legend span {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.dot {
  width: 8px;
  height: 8px;
  border-radius: 999px;
}

.dot.success,
.timeline-step.success .timeline-index {
  background: #18a058;
}

.dot.running,
.timeline-step.running .timeline-index {
  background: #1d5fd0;
}

.dot.queued,
.timeline-step.queued .timeline-index {
  background: #b7791f;
}

.dot.failed,
.timeline-step.failed .timeline-index {
  background: #d03050;
}

.dot.pending,
.dot.skipped,
.dot.canceled,
.timeline-step.pending .timeline-index,
.timeline-step.skipped .timeline-index,
.timeline-step.canceled .timeline-index {
  background: #9aa4b2;
}

.flow-stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
  padding: 12px;
  border: 1px solid #e3e8f2;
  border-radius: 8px;
  background: #ffffff;
}

.flow-stats small,
.flow-stats strong {
  display: block;
}

.flow-stats small {
  color: #748093;
  font-size: 12px;
}

.flow-stats strong {
  margin-top: 4px;
  color: #172033;
  font-size: 16px;
}

.progress-track {
  grid-column: 1 / -1;
  height: 6px;
  overflow: hidden;
  border-radius: 999px;
  background: #e8eef7;
}

.progress-track span {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: #1d5fd0;
}

.timeline-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.timeline-step {
  display: flex;
  width: 100%;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid #e7edf5;
  border-radius: 8px;
  background: #ffffff;
  color: inherit;
  cursor: pointer;
  text-align: left;
  transition:
    border-color 0.18s ease,
    background-color 0.18s ease,
    box-shadow 0.18s ease,
    transform 0.18s ease;
}

.timeline-step.active {
  border-color: #1d5fd0;
  background: #f3f7ff;
  box-shadow: 0 0 0 2px rgb(29 95 208 / 10%);
}

.timeline-step:hover {
  transform: translateY(-1px);
}

.timeline-index {
  display: inline-flex;
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  color: #ffffff;
  font-size: 12px;
  font-style: normal;
  font-weight: 700;
}

.timeline-text {
  min-width: 0;
  flex: 1 1 auto;
}

.timeline-step strong,
.timeline-step small {
  display: block;
}

.timeline-step strong {
  overflow: hidden;
  color: #182033;
  font-size: 13px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.timeline-step small {
  margin-top: 3px;
  color: #748093;
  font-size: 12px;
  line-height: 1.4;
}

.node-detail {
  padding: 12px;
  border: 1px solid #d9e5ff;
  border-radius: 8px;
  background: #f7faff;
}

.node-detail-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}

.node-detail-head strong,
.node-detail-head small {
  display: block;
}

.node-detail-head strong {
  color: #172033;
  font-size: 14px;
}

.node-detail-head small {
  margin-top: 4px;
  color: #748093;
  font-size: 12px;
}

.node-detail p {
  margin: 6px 0 0;
  color: #596578;
  font-size: 13px;
  line-height: 1.6;
}

.node-detail p span {
  display: inline-block;
  margin-right: 8px;
  color: #8a5a12;
  font-weight: 600;
}

.node-detail dl {
  display: grid;
  grid-template-columns: 46px minmax(0, 1fr);
  gap: 6px 10px;
  margin: 10px 0 0;
  padding: 10px;
  border-radius: 6px;
  background: #ffffff;
  font-size: 12px;
}

.node-detail dt {
  color: #7b8798;
}

.node-detail dd {
  margin: 0;
  overflow: hidden;
  color: #172033;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.node-detail pre {
  max-height: 160px;
  margin: 10px 0 0;
  padding: 10px;
  overflow: auto;
  border-radius: 6px;
  background: #172033;
  color: #eef4ff;
  font-size: 12px;
  line-height: 1.5;
  white-space: pre-wrap;
}

@media (max-width: 960px) {
  .pipeline-viewer {
    grid-template-columns: 1fr;
    height: auto;
  }

  .execution-toolbar {
    flex-direction: column;
  }

  .execution-actions {
    justify-content: flex-start;
  }

  .execution-output {
    height: min(520px, 70vh);
  }

  .pipeline-side {
    position: static;
    height: auto;
    max-height: none;
    overflow: visible;
  }
}

@keyframes pulse {
  0%,
  100% {
    opacity: 0.45;
    transform: scale(0.9);
  }

  50% {
    opacity: 1;
    transform: scale(1.2);
  }
}
</style>
