<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { Connection, RefreshRight, TopRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import * as echarts from 'echarts/core'
import { LineChart } from 'echarts/charts'
import {
  GridComponent,
  LegendComponent,
  TooltipComponent,
} from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'
import http, { apiErrorMessage } from '../api/http'

echarts.use([
  LineChart,
  GridComponent,
  LegendComponent,
  TooltipComponent,
  CanvasRenderer,
])

type ReportStatus = 'ready' | 'not_configured' | 'unavailable'
type ReportRange = '15m' | '1h' | '24h' | '7d'

interface PerformanceHeadline {
  requestCount?: number
  requestP95Milliseconds?: number
  serverErrorRatePercent?: number
  databaseP95Milliseconds?: number
  slowDatabaseCommandCount?: number
  failedDatabaseCommandCount?: number
}

interface TimelinePoint {
  timestamp: string
  requestsPerSecond?: number
  requestP95Milliseconds?: number
}

interface RankingItem {
  name: string
  p95Milliseconds?: number
  requestCount?: number
  exceptionalCount?: number
}

interface PerformanceReport {
  status: ReportStatus
  range: ReportRange
  from?: string
  to?: string
  generatedAt: string
  dataSource: string
  dashboardUrl?: string
  detail?: string
  headline?: PerformanceHeadline
  timeline: TimelinePoint[]
  endpoints: RankingItem[]
  databaseQueries: RankingItem[]
}

const rangeOptions: { value: ReportRange; label: string }[] = [
  { value: '15m', label: '15 分钟' },
  { value: '1h', label: '1 小时' },
  { value: '24h', label: '24 小时' },
  { value: '7d', label: '7 天' },
]

const range = ref<ReportRange>('1h')
const loading = ref(true)
const report = ref<PerformanceReport>()
const chartElement = ref<HTMLElement>()
let chart: echarts.ECharts | undefined
let resizeObserver: ResizeObserver | undefined

const hasSamples = computed(() =>
  Boolean(report.value?.headline) &&
  (report.value?.timeline.length ?? 0) > 0,
)

const operatingNote = computed(() => {
  const headline = report.value?.headline
  if (!headline) return '等待指标样本'
  if ((headline.failedDatabaseCommandCount ?? 0) > 0)
    return '存在失败的数据库命令'
  if ((headline.serverErrorRatePercent ?? 0) >= 1)
    return '服务端错误率需要关注'
  if ((headline.slowDatabaseCommandCount ?? 0) > 0)
    return '存在超过阈值的慢查询'
  return '当前采样窗口内未见明显异常'
})

function formatNumber(value?: number, digits = 0) {
  if (value == null || !Number.isFinite(value)) return '—'
  return value.toLocaleString('zh-CN', {
    maximumFractionDigits: digits,
    minimumFractionDigits: digits,
  })
}

function formatTime(value?: string) {
  if (!value) return '—'
  return new Date(value).toLocaleString('zh-CN', {
    hour12: false,
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
  })
}

function formatAxisTime(value: string) {
  return new Date(value).toLocaleString('zh-CN', {
    hour12: false,
    month: range.value === '7d' ? '2-digit' : undefined,
    day: range.value === '7d' ? '2-digit' : undefined,
    hour: '2-digit',
    minute: '2-digit',
  })
}

function metricWidth(value?: number, rows: RankingItem[] = []) {
  if (value == null || value <= 0) return '0%'
  const maximum = Math.max(
    ...rows.map((row) => row.p95Milliseconds ?? 0),
    value,
  )
  return `${Math.max(4, value / maximum * 100)}%`
}

async function loadReport() {
  loading.value = true
  try {
    const { data } = await http.get<PerformanceReport>(
      '/operations/performance',
      { params: { range: range.value } },
    )
    report.value = data
    await nextTick()
    renderChart()
  } catch (error) {
    report.value = {
      status: 'unavailable',
      range: range.value,
      generatedAt: new Date().toISOString(),
      dataSource: 'prometheus',
      detail: apiErrorMessage(error),
      timeline: [],
      endpoints: [],
      databaseQueries: [],
    }
    ElMessage.error(apiErrorMessage(error))
    disposeChart()
  } finally {
    loading.value = false
  }
}

function renderChart() {
  disposeChart()
  if (!chartElement.value || !report.value?.timeline.length) return
  chart = echarts.init(chartElement.value)
  const times = report.value.timeline.map((point) =>
    formatAxisTime(point.timestamp),
  )
  chart.setOption({
    animationDuration: 420,
    color: ['#1f7468', '#b47722'],
    grid: { left: 50, right: 54, top: 54, bottom: 38 },
    legend: {
      top: 4,
      left: 0,
      itemWidth: 18,
      itemHeight: 3,
      textStyle: {
        color: '#53636d',
        fontFamily: '"Cascadia Mono", Consolas, monospace',
        fontSize: 10,
      },
    },
    tooltip: {
      trigger: 'axis',
      backgroundColor: 'rgba(30, 41, 50, 0.94)',
      borderWidth: 0,
      textStyle: { color: '#fff', fontSize: 12 },
      valueFormatter: (value: unknown) =>
        typeof value === 'number' ? value.toFixed(2) : '—',
    },
    xAxis: {
      type: 'category',
      boundaryGap: false,
      data: times,
      axisLine: { lineStyle: { color: '#cbd4d7' } },
      axisTick: { show: false },
      axisLabel: { color: '#79878f', fontSize: 10, hideOverlap: true },
    },
    yAxis: [
      {
        type: 'value',
        name: '请求 / 秒',
        nameTextStyle: { color: '#63727b', fontSize: 10 },
        splitLine: { lineStyle: { color: '#e8edef', type: 'dashed' } },
        axisLabel: { color: '#79878f', fontSize: 10 },
      },
      {
        type: 'value',
        name: 'P95 / ms',
        nameTextStyle: { color: '#8b6b36', fontSize: 10 },
        splitLine: { show: false },
        axisLabel: { color: '#8b6b36', fontSize: 10 },
      },
    ],
    series: [
      {
        name: '请求速率',
        type: 'line',
        smooth: 0.22,
        symbol: 'none',
        lineStyle: { width: 2 },
        areaStyle: { color: 'rgba(31, 116, 104, 0.10)' },
        data: report.value.timeline.map(
          (point) => point.requestsPerSecond ?? null,
        ),
      },
      {
        name: '接口 P95',
        type: 'line',
        yAxisIndex: 1,
        smooth: 0.22,
        symbol: 'none',
        lineStyle: { width: 1.5 },
        data: report.value.timeline.map(
          (point) => point.requestP95Milliseconds ?? null,
        ),
      },
    ],
  })
  resizeObserver = new ResizeObserver(() => chart?.resize())
  resizeObserver.observe(chartElement.value)
}

function disposeChart() {
  resizeObserver?.disconnect()
  resizeObserver = undefined
  chart?.dispose()
  chart = undefined
}

watch(range, loadReport)
onMounted(loadReport)
onUnmounted(disposeChart)
</script>

<template>
  <section class="performance-panel" aria-labelledby="performance-title">
    <header class="performance-heading">
      <div>
        <span>PERFORMANCE RAIL / {{ report?.dataSource?.toUpperCase() || 'PROMETHEUS' }}</span>
        <h3 id="performance-title">系统性能</h3>
        <p>从接口进入数据库，定位响应时间消耗在哪里。</p>
      </div>
      <div class="performance-actions">
        <div class="range-switch" aria-label="性能报表时间范围">
          <button
            v-for="item in rangeOptions"
            :key="item.value"
            type="button"
            :class="{ active: range === item.value }"
            :aria-pressed="range === item.value"
            @click="range = item.value"
          >
            {{ item.label }}
          </button>
        </div>
        <el-button
          circle
          :icon="RefreshRight"
          :loading="loading"
          aria-label="刷新性能报表"
          @click="loadReport"
        />
      </div>
    </header>

    <div v-if="loading && !report" class="performance-loading">
      <el-skeleton :rows="5" animated />
    </div>

    <div
      v-else-if="report?.status !== 'ready'"
      :class="`is-${report?.status || 'unavailable'}`"
      class="source-state"
    >
      <el-icon><Connection /></el-icon>
      <div>
        <strong>
          {{ report?.status === 'not_configured' ? '等待连接性能数据源' : '性能数据暂不可用' }}
        </strong>
        <p>{{ report?.detail }}</p>
        <small>
          业务接口继续正常运行；报表不会回退读取教务业务数据库。
        </small>
      </div>
      <a
        v-if="report?.dashboardUrl"
        :href="report.dashboardUrl"
        target="_blank"
        rel="noreferrer"
      >
        打开监控平台
        <el-icon><TopRight /></el-icon>
      </a>
    </div>

    <template v-else>
      <div class="performance-strip">
        <div class="strip-lead">
          <span>采样结论</span>
          <strong>{{ operatingNote }}</strong>
          <small>
            {{ formatTime(report.from) }} — {{ formatTime(report.to) }}
          </small>
        </div>
        <dl>
          <div>
            <dt>请求总量</dt>
            <dd>{{ formatNumber(report.headline?.requestCount) }}</dd>
          </div>
          <div>
            <dt>接口 P95</dt>
            <dd>{{ formatNumber(report.headline?.requestP95Milliseconds, 1) }}<small>ms</small></dd>
          </div>
          <div>
            <dt>5xx 比例</dt>
            <dd>{{ formatNumber(report.headline?.serverErrorRatePercent, 2) }}<small>%</small></dd>
          </div>
          <div>
            <dt>数据库 P95</dt>
            <dd>{{ formatNumber(report.headline?.databaseP95Milliseconds, 1) }}<small>ms</small></dd>
          </div>
          <div>
            <dt>慢查询</dt>
            <dd>{{ formatNumber(report.headline?.slowDatabaseCommandCount) }}</dd>
          </div>
          <div>
            <dt>查询失败</dt>
            <dd>{{ formatNumber(report.headline?.failedDatabaseCommandCount) }}</dd>
          </div>
        </dl>
      </div>

      <div v-if="hasSamples" class="performance-rail">
        <div ref="chartElement" class="rail-chart" aria-label="请求速率与接口 P95 趋势图" />
      </div>
      <div v-else class="samples-empty">
        数据源已连接，但该时间范围内尚未收到请求指标。
      </div>

      <div class="rankings">
        <article class="ranking-board">
          <div class="ranking-heading">
            <div>
              <span>HTTP ROUTES</span>
              <h4>接口耗时排行</h4>
            </div>
            <small>P95 / 请求量 / 5xx</small>
          </div>
          <div v-if="report.endpoints.length" class="ranking-list">
            <div
              v-for="(item, index) in report.endpoints"
              :key="item.name"
              class="ranking-row"
            >
              <b>{{ String(index + 1).padStart(2, '0') }}</b>
              <div>
                <strong :title="item.name">{{ item.name }}</strong>
                <span>
                  <i
                    :style="{ width: metricWidth(item.p95Milliseconds, report.endpoints) }"
                  />
                </span>
              </div>
              <dl>
                <dd>{{ formatNumber(item.p95Milliseconds, 1) }} ms</dd>
                <dt>{{ formatNumber(item.requestCount) }} 次 · {{ formatNumber(item.exceptionalCount) }} 错误</dt>
              </dl>
            </div>
          </div>
          <p v-else class="ranking-empty">该范围内没有接口指标。</p>
        </article>

        <article class="ranking-board database-board">
          <div class="ranking-heading">
            <div>
              <span>DATABASE QUERIES</span>
              <h4>数据库查询排行</h4>
            </div>
            <small>P95 / 调用量 / 慢查询</small>
          </div>
          <div v-if="report.databaseQueries.length" class="ranking-list">
            <div
              v-for="(item, index) in report.databaseQueries"
              :key="item.name"
              class="ranking-row"
            >
              <b>{{ String(index + 1).padStart(2, '0') }}</b>
              <div>
                <strong :title="item.name">{{ item.name }}</strong>
                <span>
                  <i
                    :style="{ width: metricWidth(item.p95Milliseconds, report.databaseQueries) }"
                  />
                </span>
              </div>
              <dl>
                <dd>{{ formatNumber(item.p95Milliseconds, 1) }} ms</dd>
                <dt>{{ formatNumber(item.requestCount) }} 次 · {{ formatNumber(item.exceptionalCount) }} 慢</dt>
              </dl>
            </div>
          </div>
          <p v-else class="ranking-empty">该范围内没有数据库指标。</p>
        </article>
      </div>

      <footer class="performance-foot">
        <span>生成于 {{ formatTime(report.generatedAt) }} · 页面数据采用短时缓存</span>
        <a
          v-if="report.dashboardUrl"
          :href="report.dashboardUrl"
          target="_blank"
          rel="noreferrer"
        >
          查看原始调用链
          <el-icon><TopRight /></el-icon>
        </a>
      </footer>
    </template>
  </section>
</template>

<style scoped>
.performance-panel {
  --ink: #1e2932;
  --muted: #66747e;
  --line: #d8dee1;
  --paper: #f6f8f8;
  --panel: #fff;
  --signal: #1f7468;
  --warning: #b47722;
  --danger: #b1463e;
  background: var(--panel);
  border: 1px solid var(--line);
  color: var(--ink);
  min-width: 0;
  overflow: hidden;
}

.performance-heading {
  align-items: flex-end;
  background:
    linear-gradient(90deg, rgba(31, 116, 104, 0.06), transparent 38%),
    var(--panel);
  border-bottom: 1px solid var(--line);
  display: flex;
  justify-content: space-between;
  padding: 20px 22px 18px;
}

.performance-heading span,
.ranking-heading span,
.strip-lead > span {
  color: var(--signal);
  font-family: "Cascadia Mono", Consolas, monospace;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .12em;
}

.performance-heading h3 {
  font-family: "Noto Serif SC", "Source Han Serif SC", serif;
  font-size: 21px;
  margin: 5px 0 3px;
}

.performance-heading p {
  color: var(--muted);
  font-size: 12px;
  margin: 0;
}

.performance-actions {
  align-items: center;
  display: flex;
  gap: 10px;
}

.range-switch {
  background: #edf1f1;
  display: flex;
  padding: 3px;
}

.range-switch button {
  background: transparent;
  border: 0;
  color: #68767e;
  cursor: pointer;
  font-size: 11px;
  padding: 7px 11px;
  transition: background .16s ease, color .16s ease;
}

.range-switch button.active {
  background: var(--ink);
  color: #fff;
}

.range-switch button:focus-visible {
  outline: 2px solid var(--signal);
  outline-offset: 2px;
}

.performance-loading {
  padding: 32px;
}

.source-state {
  align-items: flex-start;
  background: #f8faf9;
  display: grid;
  gap: 16px;
  grid-template-columns: auto minmax(0, 1fr) auto;
  margin: 22px;
  padding: 24px;
}

.source-state > .el-icon {
  background: #e7efed;
  color: var(--signal);
  font-size: 22px;
  padding: 12px;
}

.source-state strong {
  display: block;
  font-size: 15px;
  margin: 2px 0 6px;
}

.source-state p,
.source-state small {
  color: var(--muted);
  line-height: 1.6;
  margin: 0;
}

.source-state p { font-size: 12px; }
.source-state small { font-size: 10px; }
.source-state.is-unavailable > .el-icon { background: #f6e9e8; color: var(--danger); }

.source-state a,
.performance-foot a {
  align-items: center;
  color: var(--signal);
  display: inline-flex;
  font-size: 11px;
  gap: 4px;
  text-decoration: none;
}

.performance-strip {
  border-bottom: 1px solid var(--line);
  display: grid;
  grid-template-columns: 230px minmax(0, 1fr);
}

.strip-lead {
  background: var(--ink);
  color: #fff;
  padding: 18px 20px;
}

.strip-lead > span { color: #84c7bc; }
.strip-lead strong { display: block; font-size: 13px; margin: 9px 0 13px; }
.strip-lead small { color: #aebbc2; font-family: "Cascadia Mono", Consolas, monospace; font-size: 9px; }

.performance-strip > dl {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  margin: 0;
}

.performance-strip dl > div {
  border-right: 1px solid #e5eaec;
  padding: 16px 14px;
}

.performance-strip dl > div:last-child { border-right: 0; }
.performance-strip dt { color: var(--muted); font-size: 10px; margin-bottom: 9px; }
.performance-strip dd { font-family: "Cascadia Mono", Consolas, monospace; font-size: 18px; margin: 0; }
.performance-strip dd small { color: var(--muted); font-size: 9px; margin-left: 3px; }

.performance-rail {
  border-bottom: 1px solid var(--line);
  padding: 16px 20px 8px;
}

.rail-chart { height: 260px; width: 100%; }

.samples-empty,
.ranking-empty {
  color: var(--muted);
  font-size: 12px;
  margin: 0;
  padding: 44px 22px;
  text-align: center;
}

.samples-empty { border-bottom: 1px solid var(--line); }

.rankings {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.ranking-board {
  border-right: 1px solid var(--line);
  min-width: 0;
  padding: 18px 20px 20px;
}

.ranking-board:last-child { border-right: 0; }

.ranking-heading {
  align-items: flex-start;
  display: flex;
  justify-content: space-between;
  margin-bottom: 14px;
}

.ranking-heading h4 {
  font-family: "Noto Serif SC", "Source Han Serif SC", serif;
  font-size: 15px;
  margin: 4px 0 0;
}

.ranking-heading > small {
  color: #89959b;
  font-family: "Cascadia Mono", Consolas, monospace;
  font-size: 9px;
  margin-top: 5px;
}

.ranking-list { display: grid; gap: 1px; }

.ranking-row {
  align-items: center;
  background: #f8fafa;
  display: grid;
  gap: 11px;
  grid-template-columns: 26px minmax(0, 1fr) 116px;
  min-height: 50px;
  padding: 7px 10px;
}

.ranking-row > b {
  color: #9aa6ab;
  font-family: "Cascadia Mono", Consolas, monospace;
  font-size: 10px;
}

.ranking-row > div { min-width: 0; }
.ranking-row strong {
  display: block;
  font-family: "Cascadia Mono", Consolas, monospace;
  font-size: 10px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ranking-row > div > span {
  background: #dfe7e7;
  display: block;
  height: 3px;
  margin-top: 8px;
  overflow: hidden;
}

.ranking-row i {
  background: var(--signal);
  display: block;
  height: 100%;
}

.database-board .ranking-row i { background: var(--warning); }

.ranking-row dl {
  margin: 0;
  text-align: right;
}

.ranking-row dd {
  font-family: "Cascadia Mono", Consolas, monospace;
  font-size: 10px;
  margin: 0 0 3px;
}

.ranking-row dt { color: var(--muted); font-size: 9px; }

.performance-foot {
  align-items: center;
  background: var(--paper);
  border-top: 1px solid var(--line);
  color: var(--muted);
  display: flex;
  font-size: 10px;
  justify-content: space-between;
  padding: 11px 20px;
}

@media (prefers-reduced-motion: reduce) {
  .range-switch button { transition: none; }
}

@media (max-width: 1120px) {
  .performance-strip { grid-template-columns: 190px minmax(0, 1fr); }
  .performance-strip > dl { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .performance-strip dl > div:nth-child(3) { border-right: 0; }
  .performance-strip dl > div:nth-child(-n+3) { border-bottom: 1px solid #e5eaec; }
}

@media (max-width: 820px) {
  .performance-heading { align-items: flex-start; gap: 16px; }
  .performance-actions { align-items: flex-end; flex-direction: column-reverse; }
  .performance-strip { display: block; }
  .rankings { grid-template-columns: 1fr; }
  .ranking-board { border-bottom: 1px solid var(--line); border-right: 0; }
  .ranking-board:last-child { border-bottom: 0; }
}

@media (max-width: 560px) {
  .performance-heading { display: block; padding: 17px 14px; }
  .performance-actions { align-items: stretch; flex-direction: row; margin-top: 14px; }
  .range-switch { flex: 1; overflow-x: auto; }
  .range-switch button { flex: 1 0 auto; padding-inline: 9px; }
  .source-state { grid-template-columns: auto 1fr; margin: 12px; padding: 16px; }
  .source-state a { grid-column: 2; }
  .performance-strip > dl { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .performance-strip dl > div,
  .performance-strip dl > div:nth-child(3) { border-bottom: 1px solid #e5eaec; border-right: 1px solid #e5eaec; }
  .performance-strip dl > div:nth-child(even) { border-right: 0; }
  .performance-strip dl > div:nth-last-child(-n+2) { border-bottom: 0; }
  .performance-rail { padding-inline: 8px; }
  .rail-chart { height: 230px; }
  .ranking-board { padding-inline: 12px; }
  .ranking-heading > small { display: none; }
  .ranking-row { grid-template-columns: 22px minmax(0, 1fr) 96px; padding-inline: 7px; }
  .performance-foot { align-items: flex-start; gap: 8px; }
}
</style>
