import React, { useCallback, useEffect, useMemo, useState } from 'react'

import { CopyButton } from '@mantine/core'
import {
  IconCheck,
  IconChevronDown,
  IconChevronRight,
  IconCopy
} from '@tabler/icons-react'
import {
  Accordion,
  ActionIcon,
  Anchor,
  Badge,
  Button,
  Card,
  Checkbox,
  Collapse,
  Group,
  List,
  Modal,
  Prism,
  Stack,
  Table,
  Tabs,
  Text,
  Title,
  Tooltip as UIKitTooltip,
  UnstyledButton
} from '@tidbcloud/uikit'
import { MRT_ColumnDef, ProTable } from '@tidbcloud/uikit/biz'
import { IconArrowCircleLeft, IconInfoCircle } from '@tidbcloud/uikit/icons'
import { useNavigate, useParams } from 'react-router-dom'
import useSWR from 'swr'

import { useCurCluster, useCurOrg } from '~/hooks/state'
import { formatDateTime } from '~/utils/date-time'

import { getResultDetail } from '../api'
import {
  CHECK_ITEM_DESCRIPTIONS,
  CHECK_ITEM_DISPLAY_NAMES
} from '../components/CheckItemBadge'
import type { HealthcheckHealthCheckItemReport } from '../types'

const REPORT_TOOLTIP_MAX_WIDTH = 320
// The controller persists observations on a 30-second default cadence. Polling
// faster only repeats full report/S3 reads without exposing newer state.
const REPORT_REFRESH_INTERVAL_MS = 30 * 1000

const reportStatusColorMap: Record<string, string> = {
  pending: 'gray',
  running: 'blue',
  success: 'green',
  partial: 'yellow',
  partial_failed: 'orange',
  failed: 'red'
}

const itemStatusColorMap: Record<string, string> = {
  pending: 'blue',
  running: 'blue',
  waiting: 'blue',
  success: 'green',
  ready: 'green',
  failed: 'red',
  timed_out: 'red'
}

const Tooltip: React.FC<React.ComponentProps<typeof UIKitTooltip>> = (
  props
) => (
  <UIKitTooltip
    multiline
    {...props}
    styles={{
      tooltip: {
        maxWidth: REPORT_TOOLTIP_MAX_WIDTH,
        whiteSpace: 'normal',
        wordBreak: 'break-word'
      }
    }}
  />
)

// ── Dashboard URL helpers ─────────────────────────────────────────
function buildDashboardBase(org: any, cluster: any) {
  return (
    `dashboard/cloud/ngm.html?provider=${cluster.provider}` +
    `&region=${cluster.region}` +
    `&orgName=${org.name}` +
    `&orgId=${org.externalID}` +
    `&projectId=${cluster.projectID}` +
    `&clusterId=${cluster.id}` +
    `&clusterName=${cluster.name}` +
    `&deployType=${cluster.deployType}`
  )
}

// ── Types for slow_query_hugeResourceUsage decoded content ──────────
interface SlowQueryDigestDetail {
  conn_id?: string
  link?: string
  mem_max: number
  query_time: number
  source?: string
  time: string
}

interface SlowQueryResult {
  avg_max_mem_b: number
  avg_query_time_s: number
  digest: string
  digest_detail?: SlowQueryDigestDetail[]
  hit_mem_records: number
  hit_query_time_records: number
  hit_records: number
  max_max_mem_b: number
  max_query_time_s: number
  sample_query: string
}

interface SlowQueryContent {
  checkType: string
  comment: string
  description: string
  name: string
  params: {
    memMaxThreshold: number
    queryTimeThreshold: number
  }
  resultJsonUrl: string
  results: SlowQueryResult[]
  status: string
  summary: {
    distinctDigestHugeResourceUsage: number
    hitMemMaxThresholdDigest: number
    hitQueryTimeThresholdDigest: number
    hugeResourceUsageRecords: number
    totalRecords: number
  }
}

// ── Types for tablefullscan_slow_query decoded content ───────────────
interface TableFullScanSample {
  act_rows: number
  db: string
  latency_s: number
  time: number
  time_formatted: string
  connection_id?: string | number
}

interface TableFullScanResult {
  'anomaly_logs(scan>=10000rows)': number
  digest: string
  max_scanned_rows: number
  p50_latency_s: number
  p50_scanned_rows: number
  p80_latency_s: number
  p80_scanned_rows: number
  p95_latency_s: number
  p95_scanned_rows: number
  p99_latency_s: number
  p99_scanned_rows: number
  query: string
  samples: TableFullScanSample[]
  slow_count: number
  total_latency_s: number
  total_scanned_rows: number
  with_table_full_scan_count: number
  without_table_full_scan_count: number
}

interface TableFullScanContent {
  description: string
  name: string
  params: Record<string, unknown>
  results: TableFullScanResult[]
  status: string
  summary: Record<string, unknown>
}

// ── Types for slowlog_top_digest decoded content ────────────────────
type SlowLogTopDigestDimension =
  | 'count'
  | 'total_latency'
  | 'total_keys'
  | 'process_keys'
  | 'request_count'
  | 'sum_ru'

type SlowLogTopDigestResultKey =
  | 'top_by_count'
  | 'top_by_total_latency'
  | 'top_by_total_keys'
  | 'top_by_process_keys'
  | 'top_by_request_count'
  | 'top_by_sum_ru'

interface SlowLogTopDigestRow {
  rank: number
  digest: string
  query: string
  slow_count: number
  total_latency_s: number
  total_keys: number
  process_keys: number
  request_count: number
  sum_ru?: number
  max_latency_s: number
  latest_time?: number | string
}

interface SlowLogTopDigestContent {
  description?: string
  name: string
  params?: {
    dimensions?: string[]
    topN?: number
    count?: number
    min_count?: number
    min_total_keys?: number
    ignore_dml?: boolean
    dimension_count_thresholds?: Record<string, number>
    exclude_internal?: boolean
  }
  results?: Partial<Record<SlowLogTopDigestResultKey, SlowLogTopDigestRow[]>>
  status: string
  summary?: {
    dimensions?: string[]
    topN?: number
    count?: number
    min_count?: number
    min_total_keys?: number
    ignore_dml?: boolean
    dimension_count_thresholds?: Record<string, number>
    exclude_internal?: boolean
    result_lists?: number
    total_result_rows?: number
  }
  error?: string
}

// ── Types for sql_tikv_high_duration / sql_tikv_high_cpu decoded content ──
interface TiKVHotspotSlowLogEvidence {
  count?: number
  query?: string
  max_query_time_s?: number
  avg_query_time_s?: number
  max_mem_bytes?: number
  avg_mem_bytes?: number
  max_process_keys?: number
  max_total_keys?: number
  max_request_count?: number
  cop_proc_addr_match?: boolean
  cop_wait_addr_match?: boolean
  cop_proc_addr_match_count?: number
  cop_wait_addr_match_count?: number
  cop_proc_max_s?: number
  cop_wait_max_s?: number
  matched_cop_proc_max_s?: number
  matched_cop_wait_max_s?: number
  sample_conn_id?: string
  sample_time?: number | string
  sample_db?: string
  sample_query?: string
}

interface TiKVHotspotCandidate {
  digest: string
  rank?: number
  topsql_rank?: number
  cpu_time_seconds?: number
  cpu_time_minutes?: number
  digest_topsql_cpu_share_percent?: number
  matched_topsql_cpu_percent_threshold?: boolean
  dominance_cutoff?: boolean
  is_slow_query?: boolean
  slow_log?: TiKVHotspotSlowLogEvidence
  links?: {
    top_sql?: string
    slow_query_list?: string
    slow_query_detail?: string
  }
}

interface TiKVHotspotWindow {
  window_id?: string
  rank?: number
  decision: string
  node_severity?: string
  confidence?: {
    level?: string
    positive_reasons?: string[]
    negative_reasons?: string[]
  }
  node_anomaly?: {
    type?: string
    cluster_id?: string
    instance?: string
    source_instance?: string
    window_start?: number
    window_end?: number
    window_duration_seconds?: number
    anomaly_duration_seconds?: number
    sample_count?: number
    max_duration_ms?: number
    avg_duration_ms?: number
    max_cpu_percent?: number
    avg_cpu_percent?: number
    p50_cluster_cpu_percent?: number
    max_relative_delta_percent?: number
    rule_tag?: string
    links?: {
      metrics?: string
    }
  }
  topsql_summary?: {
    topsql_total_cpu_seconds?: number
    topn_topsql_cpu_seconds?: number
    topn_topsql_share_percent?: number
    others_topsql_cpu_seconds?: number
    attribution_window_start?: number
    attribution_window_end?: number
    attribution_window_duration_seconds?: number
  }
  sql_candidates?: TiKVHotspotCandidate[]
  non_sql_counter_evidence?: {
    profile?: string
    result?: string
    note?: string
  }
  links?: {
    top_sql?: string
  }
  data_errors?: Record<string, string>
}

interface TiKVHotspotDigestSummary {
  digest: string
  affected_windows?: number
  affected_instances?: number
  instances?: string[]
  max_severity?: string
  total_topsql_cpu_seconds?: number
  avg_topsql_share_percent?: number
  max_topsql_share_percent?: number
  max_duration_ms?: number | null
  max_cpu_percent?: number | null
  slow_log_count?: number
  first_seen?: number | string
  last_seen?: number | string
  representative_window_id?: string
  representative_links?: {
    top_sql?: string
    slow_query_list?: string
    slow_query_detail?: string
  }
}

interface TiKVHotspotContent {
  name: 'sql_tikv_high_duration' | 'sql_tikv_high_cpu'
  description?: string
  status: string
  params?: Record<string, unknown>
  summary?: {
    node_anomaly_windows?: number
    sql_candidate_windows?: number
    sql_likely_windows?: number
    sql_possible_windows?: number
    non_sql_likely_windows?: number
    not_attributed_windows?: number
    unknown_windows?: number
    sql_candidates?: number
    distinct_sql_digests?: number
    data_quality?: {
      core_metrics_available?: boolean
      topsql_available?: boolean
      slow_log_available?: boolean
      counter_evidence_profile?: string
      truncated?: boolean
      truncated_windows?: number
    }
  }
  digest_summary?: TiKVHotspotDigestSummary[]
  results?: TiKVHotspotWindow[]
  error?: string
}

// ── Types for metrics_healthcheck decoded content ─────────────────────
interface MetricsResult {
  queryName: string
  timeAggregation: string
  threshold: string
  status: string
  max: string
  avg: string
  link: string
}

interface MetricsHealthCheckContent {
  name: string
  description: string
  checkType: string
  status: string
  resultJsonUrl?: string
  summary: {
    totalMetrics: number
    needAttentionCount: number
  }
  results: MetricsResult[]
}

// ── Types for config_check decoded content ────────────────────────────
interface ConfigParameterChange {
  name: string
  from: string
  to: string
  risk_level: string
  component: string
}

interface ConfigChange {
  change_id: string
  applied_at: string
  reason: string
  applicant: string
  filename: string
  component: string
  parameter_changes: ConfigParameterChange[]
}

interface ConfigCheckContent {
  cluster_id?: string
  report_id?: string
  check_time?: string
  time_range_start?: string
  time_range_end?: string
  total_changes: number
  changes: ConfigChange[]
}

// ── Types for disk_check decoded content ──────────────────────────────
interface DiskCaseEvent {
  start_time: string
  end_time: string
  cluster_id: string
  disk_type: string
  duration: string
  jitter_level: string
  impact_scope: string
  root_cause: string
  jitter_impact: string
  disk_monitoring_link: string
  business_monitoring_link: string
}

interface DiskCaseTracking {
  tidb_issues: DiskCaseEvent[]
  jitter_events: DiskCaseEvent[]
}

interface DiskStatistic {
  disk_type: string
  impact_type: string
  jitter_events_count: number
  tikv_issues_count: number
  tiflash_issues_count: number
}

interface DiskCheckContent {
  cluster_id?: string
  report_id?: string
  check_time?: string
  time_range_start?: string
  time_range_end?: string
  params?: Record<string, number>
  summary?: {
    total_disks?: Record<string, number>
    statistics?: DiskStatistic[]
  }
  case_tracking?: {
    impacted?: DiskCaseTracking
    non_impacted?: DiskCaseTracking
  }
}

// ── Helpers ─────────────────────────────────────────────────────────
function decodeContent(content?: unknown): unknown | null {
  if (!content) return null
  if (typeof content === 'object' && !Array.isArray(content)) return content
  if (Array.isArray(content) && content.length > 0) {
    try {
      const bytes = new Uint8Array(content)
      const text = new TextDecoder().decode(bytes)
      return JSON.parse(text)
    } catch {
      return null
    }
  }
  return null
}

function formatMemBytes(bytes: number): string {
  if (bytes >= 1024 * 1024 * 1024)
    return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`
  if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MiB`
  if (bytes >= 1024) return `${(bytes / 1024).toFixed(2)} KB`
  return `${bytes} B`
}

function formatNumber(n: number): string {
  return n.toLocaleString()
}

function formatMetricNumber(value: unknown, maximumFractionDigits = 0): string {
  const numeric = Number(value)
  if (!Number.isFinite(numeric)) return '-'
  return numeric.toLocaleString(undefined, { maximumFractionDigits })
}

function formatMetricPercent(
  value: unknown,
  maximumFractionDigits = 1
): string {
  const formatted = formatMetricNumber(value, maximumFractionDigits)
  return formatted === '-' ? '-' : `${formatted}%`
}

function formatUnixTime(value?: number | string): string {
  const numeric = Number(value)
  if (!Number.isFinite(numeric)) return '-'
  return formatDateTime(numeric > 1e12 ? numeric / 1000 : numeric)
}

function formatParamDisplayValue(value: unknown): string {
  if (typeof value === 'boolean') return value ? 'Yes' : 'No'
  if (Array.isArray(value)) return value.map(String).join(', ')
  if (typeof value === 'number') return formatMetricNumber(value, 3)
  if (value == null || value === '') return '-'
  return String(value)
}

function formatHotspotParamValue(key: string, value: unknown): string {
  if (key === 'slow_log_match_policy') {
    const labels: Record<string, string> = {
      require: 'Query slow logs',
      annotate: 'Query slow logs as annotation',
      none: 'Do not query slow logs'
    }
    return labels[String(value)] ?? formatParamDisplayValue(value)
  }
  return formatParamDisplayValue(value)
}

const SLOW_LOG_TOP_DIGEST_DIMENSION_META: Record<
  SlowLogTopDigestDimension,
  {
    label: string
    resultKey: SlowLogTopDigestResultKey
    metricColumnId: keyof Pick<
      SlowLogTopDigestRow,
      | 'slow_count'
      | 'total_latency_s'
      | 'total_keys'
      | 'process_keys'
      | 'request_count'
      | 'sum_ru'
    >
  }
> = {
  count: {
    label: 'Count',
    resultKey: 'top_by_count',
    metricColumnId: 'slow_count'
  },
  total_latency: {
    label: 'Total Latency',
    resultKey: 'top_by_total_latency',
    metricColumnId: 'total_latency_s'
  },
  total_keys: {
    label: 'SUM Total Keys',
    resultKey: 'top_by_total_keys',
    metricColumnId: 'total_keys'
  },
  process_keys: {
    label: 'SUM Process Keys',
    resultKey: 'top_by_process_keys',
    metricColumnId: 'process_keys'
  },
  request_count: {
    label: 'SUM Request Count',
    resultKey: 'top_by_request_count',
    metricColumnId: 'request_count'
  },
  sum_ru: {
    label: 'SUM RU',
    resultKey: 'top_by_sum_ru',
    metricColumnId: 'sum_ru'
  }
}

const SLOW_LOG_TOP_DIGEST_RESULT_ORDER: SlowLogTopDigestDimension[] = [
  'count',
  'total_latency',
  'total_keys',
  'process_keys',
  'request_count',
  'sum_ru'
]

function isSlowLogTopDigestDimension(
  value: string
): value is SlowLogTopDigestDimension {
  return value in SLOW_LOG_TOP_DIGEST_DIMENSION_META
}

function formatSlowLogTopDigestDimension(value: string): string {
  return isSlowLogTopDigestDimension(value)
    ? SLOW_LOG_TOP_DIGEST_DIMENSION_META[value].label
    : value
}

function getSlowLogTopDigestCountThresholdEntries(
  content: SlowLogTopDigestContent
) {
  const thresholds: Record<string, number> = {
    ...((content.params?.dimension_count_thresholds ??
      content.summary?.dimension_count_thresholds ??
      {}) as Record<string, number>)
  }
  const legacySumRuCount = Number(
    content.params?.count ?? content.summary?.count
  )
  if (
    !thresholds.sum_ru &&
    Number.isFinite(legacySumRuCount) &&
    legacySumRuCount > 0
  ) {
    thresholds.sum_ru = legacySumRuCount
  }

  return SLOW_LOG_TOP_DIGEST_RESULT_ORDER.map((dimension) => ({
    dimension,
    value: Number(thresholds[dimension] ?? 0)
  })).filter(({ value }) => Number.isFinite(value) && value > 0)
}

function getSlowLogTopDigestNumberParam(
  content: SlowLogTopDigestContent,
  key: 'min_count' | 'min_total_keys'
) {
  const value = Number(content.params?.[key] ?? content.summary?.[key] ?? 0)
  return Number.isFinite(value) && value > 0 ? value : 0
}

function buildSlowQueryDigestUrl(
  slowQueryUrlBase: string,
  digest: string,
  fromTs: number | null,
  toTs: number | null
) {
  const timeParams =
    fromTs && toTs
      ? `?from=${fromTs}&to=${toTs}&digest=${digest}`
      : `?digest=${digest}`
  return `${slowQueryUrlBase}${timeParams}`
}

const DIGEST_TEXT_STYLE: React.CSSProperties = {
  display: 'block',
  flex: '0 1 auto',
  minWidth: 0,
  overflow: 'hidden',
  textOverflow: 'ellipsis',
  whiteSpace: 'nowrap'
}

const DIGEST_WITH_COPY_STYLE: React.CSSProperties = {
  display: 'inline-flex',
  alignItems: 'center',
  gap: 4,
  maxWidth: '100%',
  minWidth: 0,
  verticalAlign: 'middle'
}

const DigestLinkWithCopy: React.FC<{
  digest: string
  fromTs: number | null
  toTs: number | null
  slowQueryUrlBase: string
}> = ({ digest, fromTs, toTs, slowQueryUrlBase }) => {
  if (!digest) return <>-</>

  const url = buildSlowQueryDigestUrl(slowQueryUrlBase, digest, fromTs, toTs)

  return (
    <div style={DIGEST_WITH_COPY_STYLE}>
      <Anchor
        href={url}
        target='_blank'
        size='sm'
        title={digest}
        style={DIGEST_TEXT_STYLE}
      >
        {digest}
      </Anchor>
      <CopyButton value={digest} timeout={1500}>
        {({ copied, copy }) => (
          <Tooltip label={copied ? 'Copied' : 'Copy digest'}>
            <ActionIcon
              aria-label='Copy digest'
              className='health-check-digest-copy'
              color={copied ? 'teal' : 'gray'}
              size='xs'
              variant='subtle'
              onClick={(event) => {
                event.preventDefault()
                event.stopPropagation()
                copy()
              }}
            >
              {copied ? <IconCheck size={14} /> : <IconCopy size={14} />}
            </ActionIcon>
          </Tooltip>
        )}
      </CopyButton>
    </div>
  )
}

const DigestExternalLinkWithCopy: React.FC<{
  digest: string
  url?: string
}> = ({ digest, url }) => {
  if (!digest) return <>-</>

  const content = url ? (
    <Anchor
      href={url}
      target='_blank'
      size='sm'
      title={digest}
      style={DIGEST_TEXT_STYLE}
    >
      {digest}
    </Anchor>
  ) : (
    <Text size='sm' title={digest} style={DIGEST_TEXT_STYLE}>
      {digest}
    </Text>
  )

  return (
    <div style={DIGEST_WITH_COPY_STYLE}>
      {content}
      <CopyButton value={digest} timeout={1500}>
        {({ copied, copy }) => (
          <Tooltip label={copied ? 'Copied' : 'Copy digest'}>
            <ActionIcon
              aria-label='Copy digest'
              className='health-check-digest-copy'
              color={copied ? 'teal' : 'gray'}
              size='xs'
              variant='subtle'
              onClick={(event) => {
                event.preventDefault()
                event.stopPropagation()
                copy()
              }}
            >
              {copied ? <IconCheck size={14} /> : <IconCopy size={14} />}
            </ActionIcon>
          </Tooltip>
        )}
      </CopyButton>
    </div>
  )
}

function toUnixSeconds(isoStr?: string): number | null {
  if (!isoStr) return null
  const ts = new Date(isoStr).getTime()
  if (isNaN(ts)) return null
  return Math.floor(ts / 1000)
}

// ── Main page ───────────────────────────────────────────────────────
const HealthCheckDetail: React.FC = () => {
  const navigate = useNavigate()
  const { orgId, cid, taskId } = useParams<'orgId' | 'cid' | 'taskId'>()

  const { curOrg } = useCurOrg()
  const { curCluster } = useCurCluster()

  const oid = curOrg!.id!
  const clusterId = curCluster!.id!

  const dashboardBase = buildDashboardBase(curOrg, curCluster)
  const slowQueryUrlBase = dashboardBase + '#/slow_query'
  const slowQueryDetailUrlBase = dashboardBase + '#/slow_query/detail'

  const { data: reportRes, error } = useSWR(
    taskId ? `/healthcheck/result/${oid}/${clusterId}/${taskId}` : null,
    () => getResultDetail(oid, clusterId, taskId!),
    {
      refreshInterval: (latestReport) => {
        const status = latestReport?.data?.status
        return status === 'pending' ||
          status === 'running' ||
          status === 'partial'
          ? REPORT_REFRESH_INTERVAL_MS
          : 0
      }
    }
  )

  const report = reportRes?.data
  const loading = !reportRes && !error

  useEffect(() => {
    const main = document.querySelector<HTMLElement>('.mantine-AppShell-main')
    if (!main) return

    const previousWidth = main.style.width
    main.style.width = '100%'

    return () => {
      main.style.width = previousWidth
    }
  }, [])

  // Time range from report API response (snake_case fallback)
  const reportAny = report as any
  const reportStatus = report?.status ?? reportAny?.status
  const reportErrorMsg = report?.errorMsg ?? reportAny?.error_msg
  const completionReason =
    report?.completionReason ?? reportAny?.completion_reason
  const timeRangeStart =
    reportAny?.timeRangeAStart ?? reportAny?.time_range_a_start
  const timeRangeEnd = reportAny?.timeRangeAEnd ?? reportAny?.time_range_a_end
  const fromTs = toUnixSeconds(timeRangeStart)
  const toTs = toUnixSeconds(timeRangeEnd)

  const handleExportJSON = useCallback(() => {
    if (!report) return
    const json = JSON.stringify(report, null, 2)
    const blob = new Blob([json], { type: 'application/json' })
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = `health-check-report-${taskId ?? 'unknown'}.json`
    a.click()
    URL.revokeObjectURL(url)
  }, [report, taskId])

  return (
    <div className='health-check-report'>
      <style>
        {`
          .health-check-report .health-check-digest-copy {
            opacity: 0;
            transition: opacity 120ms ease;
          }

          .health-check-report tr:hover .health-check-digest-copy,
          .health-check-report .health-check-digest-copy:focus {
            opacity: 1;
          }
        `}
      </style>
      <Group
        justify='space-between'
        mb={report?.createdAt ? 'xs' : 'lg'}
        align='center'
      >
        <Group gap='xs' align='center'>
          <ActionIcon
            variant='subtle'
            aria-label='back'
            onClick={() =>
              navigate(`/orgs/${orgId}/clusters/${cid}/health-check`)
            }
            title='Back to Health Check'
          >
            <IconArrowCircleLeft size={20} />
          </ActionIcon>
          <Title order={3}>Health Check Report</Title>
        </Group>
        {report && (
          <Button variant='light' size='compact-sm' onClick={handleExportJSON}>
            Export JSON
          </Button>
        )}
      </Group>

      {(reportStatus || report?.createdAt || reportAny?.created_at) && (
        <Group gap='sm' style={{ marginLeft: 38 }}>
          {reportStatus && (
            <Badge
              color={reportStatusColorMap[reportStatus] ?? 'gray'}
              variant='dot'
            >
              {reportStatus}
            </Badge>
          )}
          {(report?.createdAt || reportAny?.created_at) && (
            <Text size='sm' c='dimmed'>
              Created at{' '}
              {formatDateTime(
                new Date(report?.createdAt ?? reportAny?.created_at).getTime() /
                  1000
              )}
            </Text>
          )}
        </Group>
      )}
      {timeRangeStart && timeRangeEnd && (
        <Text size='sm' c='dimmed' mb='md' style={{ marginLeft: 38 }}>
          Time Range:{' '}
          {formatDateTime(new Date(timeRangeStart).getTime() / 1000)}
          {' ~ '}
          {formatDateTime(new Date(timeRangeEnd).getTime() / 1000)}
        </Text>
      )}

      {loading && <Text>Loading report...</Text>}

      {error && (
        <Card withBorder p='lg'>
          <Text c='red'>Failed to load report. Please try again later.</Text>
        </Card>
      )}

      {report && reportErrorMsg && (
        <Card withBorder p='md' mb='md'>
          <Text c='red' fw={600}>
            {reportStatus === 'partial_failed'
              ? 'Report completed with errors'
              : reportStatus === 'partial'
              ? 'Report in progress with errors'
              : 'Report failed'}
          </Text>
          <Text size='sm' c='red'>
            {reportErrorMsg}
          </Text>
          {completionReason && (
            <Text size='xs' c='dimmed' mt={4}>
              Completion reason: {completionReason}
            </Text>
          )}
        </Card>
      )}

      {report && (
        <div>
          {!report.items || report.items.length === 0 ? (
            <Card withBorder p='lg'>
              <Text c='dimmed'>No check items in this report.</Text>
            </Card>
          ) : (
            <Accordion
              multiple
              defaultValue={report.items.map(
                (item, idx) => item.name ?? String(idx)
              )}
              variant='contained'
              chevronPosition='left'
              styles={{
                root: { backgroundColor: '#fff' },
                item: { padding: '16px 12px 16px 4px' }
              }}
            >
              {report.items.map((item, idx) => {
                const displayName =
                  CHECK_ITEM_DISPLAY_NAMES[item.name ?? ''] ??
                  item.name ??
                  'Unknown Item'
                const decoded = decodeContent(item.content)
                const apiDesc =
                  decoded &&
                  typeof decoded === 'object' &&
                  'description' in decoded
                    ? (decoded as { description?: string }).description
                    : undefined
                const description =
                  apiDesc || CHECK_ITEM_DESCRIPTIONS[item.name ?? '']
                return (
                  <Accordion.Item
                    key={item.name ?? idx}
                    value={item.name ?? String(idx)}
                  >
                    <Accordion.Control>
                      <div>
                        <Group gap='sm'>
                          <Text fw={600}>{displayName}</Text>
                          <Badge
                            color={
                              itemStatusColorMap[item.status ?? ''] ?? 'gray'
                            }
                            variant='dot'
                            size='sm'
                          >
                            {item.status}
                          </Badge>
                        </Group>
                        {description && (
                          <Text size='xs' c='dimmed' mt={4}>
                            {description}
                          </Text>
                        )}
                      </div>
                    </Accordion.Control>
                    <Accordion.Panel>
                      <div style={{ marginLeft: 28 }}>
                        <CheckItemSection
                          item={item}
                          fromTs={fromTs}
                          toTs={toTs}
                          slowQueryUrlBase={slowQueryUrlBase}
                          slowQueryDetailUrlBase={slowQueryDetailUrlBase}
                        />
                      </div>
                    </Accordion.Panel>
                  </Accordion.Item>
                )
              })}
            </Accordion>
          )}
        </div>
      )}
    </div>
  )
}

// ── Check item section ──────────────────────────────────────────────
const CheckItemSection: React.FC<{
  item: HealthcheckHealthCheckItemReport
  fromTs: number | null
  toTs: number | null
  slowQueryUrlBase: string
  slowQueryDetailUrlBase: string
}> = ({ item, fromTs, toTs, slowQueryUrlBase, slowQueryDetailUrlBase }) => {
  const decoded = useMemo(() => decodeContent(item.content), [item.content])

  const isMetricsHealthCheck = item.name === 'metrics_healthcheck'
  const isDiskCheck = item.name === 'disk_check'
  const isConfigCheck = item.name === 'config_check'
  const isSlowQueryHugeResource = item.name === 'slow_query_hugeResourceUsage'
  const isTableFullScan = item.name === 'tablefullscan_slow_query'
  const isSlowLogTopDigest = item.name === 'slowlog_top_digest'
  const isTiKVHotspot =
    item.name === 'sql_tikv_high_duration' || item.name === 'sql_tikv_high_cpu'

  return (
    <div>
      {item.errorMsg && (
        <Text size='sm' c='red' mb='sm'>
          {item.errorMsg}
        </Text>
      )}

      {isMetricsHealthCheck && decoded ? (
        <MetricsHealthCheckSection
          content={decoded as MetricsHealthCheckContent}
        />
      ) : isDiskCheck && decoded ? (
        <DiskCheckSection content={decoded as DiskCheckContent} />
      ) : isConfigCheck && decoded ? (
        <ConfigCheckSection content={decoded as ConfigCheckContent} />
      ) : isSlowQueryHugeResource && decoded ? (
        <SlowQueryHugeResourceSection
          content={decoded as SlowQueryContent}
          fromTs={fromTs}
          toTs={toTs}
          slowQueryUrlBase={slowQueryUrlBase}
        />
      ) : isTableFullScan && decoded ? (
        <TableFullScanSection
          content={decoded as TableFullScanContent}
          fromTs={fromTs}
          toTs={toTs}
          slowQueryUrlBase={slowQueryUrlBase}
          slowQueryDetailUrlBase={slowQueryDetailUrlBase}
        />
      ) : isSlowLogTopDigest && decoded ? (
        <SlowLogTopDigestSection
          content={decoded as SlowLogTopDigestContent}
          fromTs={fromTs}
          toTs={toTs}
          slowQueryUrlBase={slowQueryUrlBase}
        />
      ) : isTiKVHotspot && decoded ? (
        <SQLTiKVHotspotSection content={decoded as TiKVHotspotContent} />
      ) : decoded !== null ? (
        <pre
          style={{
            background: '#f5f5f5',
            padding: 12,
            borderRadius: 4,
            overflow: 'auto',
            maxHeight: 400,
            fontSize: 12,
            margin: 0
          }}
        >
          {JSON.stringify(decoded, null, 2)}
        </pre>
      ) : (
        !item.errorMsg && (
          <Text size='sm' c='dimmed'>
            No content
          </Text>
        )
      )}
    </div>
  )
}

const PAGE_SIZE = 10

// ── Metrics Health Check renderer ────────────────────────────────────
const MetricsMethodologySection: React.FC = () => {
  const [opened, setOpened] = useState(false)

  return (
    <div>
      <Anchor
        component='button'
        type='button'
        size='xs'
        onClick={() => setOpened((o) => !o)}
      >
        {opened
          ? 'Hide methodology details'
          : 'Learn how metrics are calculated and compared'}
      </Anchor>
      <Collapse in={opened}>
        <Card withBorder mt='xs' p='md' bg='gray.0'>
          <Stack gap='sm'>
            <div>
              <Text size='sm' fw={600} mb={4}>
                Metric Name &amp; Spatial Percentile
              </Text>
              <Text size='xs'>
                The suffix in the metric name (e.g. TiDB_CPU_P80) denotes a{' '}
                <b>spatial percentile</b> across all nodes of that component. At
                each point in time, values from all nodes are sorted in
                ascending order and the Pxx value is taken. For example,
                TiDB_CPU_P80 means the P80 CPU usage among all TiDB nodes at
                each moment.
              </Text>
            </div>

            <div>
              <Text size='sm' fw={600} mb={4}>
                Time Aggregation
              </Text>
              <Text size='xs'>
                The queried time-series data is aggregated into a single value
                over the inspection range:
              </Text>
              <List size='xs' mt={4} spacing={2}>
                <List.Item>
                  <b>AVG</b> — arithmetic mean of all data points in the range.
                </List.Item>
                <List.Item>
                  <b>P90</b> — the top and bottom 5% of data points are trimmed
                  (removing outlier peaks and troughs), then the remaining
                  points are sorted in ascending order and the P90 value is
                  taken. This is effectively a{' '}
                  <b>time-dimension P90 with outlier filtering</b>.
                </List.Item>
              </List>
            </div>

            <div>
              <Text size='sm' fw={600} mb={4}>
                Max / Avg Columns
              </Text>
              <Text size='xs'>
                These are the maximum and average of the queried PromQL data
                points within the spatial percentile defined by the metric. They
                reflect the range of values observed during the inspection
                period.
              </Text>
            </div>

            <div>
              <Text size='sm' fw={600} mb={4}>
                Baseline Comparison
              </Text>
              <List size='xs' spacing={2}>
                <List.Item>
                  <b>Inspection range</b>: queried with a fixed <b>4-minute</b>{' '}
                  step.
                </List.Item>
                <List.Item>
                  <b>Baseline</b>: the preceding <b>14 days</b> of data, queried
                  with a <b>30-minute</b> step.
                </List.Item>
                <List.Item>
                  Both are aggregated using the same Time Aggregation method.
                  The <b>change rate</b> between the current value and baseline
                  is compared against the threshold — if exceeded, the metric is
                  flagged as &quot;Need Attention&quot;.
                </List.Item>
              </List>
            </div>
          </Stack>
        </Card>
      </Collapse>
    </div>
  )
}

const MetricsHealthCheckSection: React.FC<{
  content: MetricsHealthCheckContent
}> = ({ content }) => {
  const summary = content.summary ?? { totalMetrics: 0, needAttentionCount: 0 }
  const allResults = useMemo(
    () =>
      [...(content.results ?? [])].sort((a, b) =>
        a.queryName.localeCompare(b.queryName)
      ),
    [content.results]
  )
  const [onlyAnomalies, setOnlyAnomalies] = useState(false)
  const filtered = useMemo(
    () =>
      onlyAnomalies
        ? allResults.filter((r) => r.status === 'Need Attention')
        : allResults,
    [allResults, onlyAnomalies]
  )
  const [page, setPage] = useState(1)
  const totalPages = Math.ceil(filtered.length / PAGE_SIZE)
  const pagedResults = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)

  const needAttention = allResults.filter((r) => r.status === 'Need Attention')

  const columns = useMemo(
    (): MRT_ColumnDef<MetricsResult>[] => [
      {
        id: 'queryName',
        header: 'Metric Name',
        size: 180,
        accessorFn: (row) => row.queryName
      },
      {
        id: 'timeAggregation',
        header: 'Time Aggregation',
        size: 130,
        accessorFn: (row) => row.timeAggregation
      },
      {
        id: 'status',
        header: 'Status',
        size: 140,
        accessorFn: (row) => (
          <Badge
            color={row.status === 'Need Attention' ? 'orange' : 'green'}
            variant='dot'
            size='sm'
          >
            {row.status}
          </Badge>
        )
      },
      {
        id: 'threshold',
        header: 'Change Rate',
        size: 120,
        accessorFn: (row) => {
          const val = parseFloat(row.threshold)
          const color =
            row.status === 'Need Attention'
              ? 'orange'
              : val > 0
              ? 'red'
              : val < 0
              ? 'green'
              : undefined
          return (
            <Text
              size='sm'
              c={color}
              fw={row.status === 'Need Attention' ? 600 : 400}
            >
              {row.threshold}
            </Text>
          )
        }
      },
      {
        id: 'max',
        header: 'Max',
        size: 120,
        accessorFn: (row) => row.max
      },
      {
        id: 'avg',
        header: 'Avg',
        size: 120,
        accessorFn: (row) => row.avg
      },
      {
        id: 'link',
        header: 'Grafana',
        size: 80,
        accessorFn: (row) =>
          row.link ? (
            <Anchor href={row.link} target='_blank' size='sm'>
              View
            </Anchor>
          ) : (
            '-'
          )
      }
    ],
    []
  )

  return (
    <Stack gap={32}>
      {/* Summary */}
      <div>
        <Text size='sm' fw={500} mb='xs'>
          Summary
        </Text>
        <Group gap='md' wrap='wrap' style={{ rowGap: 4 }}>
          <Group gap={6}>
            <Text size='sm'>Total Metrics:</Text>
            <Text size='sm' fw={500}>
              {summary.totalMetrics}
            </Text>
          </Group>
          <Group gap={6}>
            <Text size='sm'>Need Attention:</Text>
            <Text
              size='sm'
              fw={500}
              c={summary.needAttentionCount > 0 ? 'orange' : undefined}
            >
              {summary.needAttentionCount}
            </Text>
          </Group>
        </Group>
        {needAttention.length > 0 && (
          <Text size='xs' c='orange' mt='xs'>
            {needAttention
              .map((r) => `${r.queryName} (${r.timeAggregation})`)
              .join(', ')}{' '}
            — change rate exceeded threshold
          </Text>
        )}
      </div>

      {/* Results table */}
      <Stack gap='xs'>
        <Group gap='md' align='center'>
          <Text size='sm' fw={500}>
            Results ({filtered.length})
          </Text>
          <Checkbox
            label='Need Attention only'
            size='xs'
            checked={onlyAnomalies}
            onChange={(e) => {
              setOnlyAnomalies(e.currentTarget.checked)
              setPage(1)
            }}
          />
        </Group>
        <MetricsMethodologySection />
        {filtered.length > 0 ? (
          <ProTable<MetricsResult>
            columns={columns}
            data={pagedResults}
            manualPagination
            pagination={
              totalPages > 1
                ? {
                    total: totalPages,
                    value: page,
                    onChange: setPage,
                    wrapperProps: { justify: 'flex-end' }
                  }
                : undefined
            }
          />
        ) : (
          <Text size='sm' c='dimmed' mt='sm'>
            No metrics results available.
          </Text>
        )}
      </Stack>
    </Stack>
  )
}

// ── Disk Check renderer ──────────────────────────────────────────────
const JITTER_LEVEL_COLORS: Record<string, string> = {
  critical: 'red',
  major: 'orange',
  warning: 'yellow'
}

const DiskCaseEventTable: React.FC<{
  events: DiskCaseEvent[]
  title: string
}> = ({ events, title }) => {
  const [page, setPage] = useState(1)
  const totalPages = Math.ceil(events.length / PAGE_SIZE)
  const pagedEvents = events.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)

  const columns = useMemo(
    (): MRT_ColumnDef<DiskCaseEvent>[] => [
      {
        id: 'time',
        header: 'Time Range',
        size: 200,
        accessorFn: (row) => (
          <div>
            <div>
              {formatDateTime(new Date(row.start_time).getTime() / 1000)} ~
            </div>
            <div>{formatDateTime(new Date(row.end_time).getTime() / 1000)}</div>
          </div>
        )
      },
      {
        id: 'duration',
        header: 'Duration',
        size: 90,
        accessorFn: (row) => row.duration
      },
      {
        id: 'disk_type',
        header: 'Disk Type',
        size: 90,
        accessorFn: (row) => row.disk_type.toUpperCase()
      },
      {
        id: 'jitter_level',
        header: 'Level',
        size: 100,
        accessorFn: (row) => (
          <Badge
            color={JITTER_LEVEL_COLORS[row.jitter_level] ?? 'gray'}
            variant='dot'
            size='sm'
          >
            {row.jitter_level}
          </Badge>
        )
      },
      {
        id: 'impact_scope',
        header: 'Impact Scope',
        size: 150,
        accessorFn: (row) => row.impact_scope || '-'
      },
      {
        id: 'root_cause',
        header: 'Root Cause',
        size: 220,
        accessorFn: (row) => row.root_cause || '-'
      },
      {
        id: 'jitter_impact',
        header: 'Impact Detail',
        size: 280,
        accessorFn: (row) =>
          row.jitter_impact ? (
            <Text size='xs' style={{ whiteSpace: 'pre-wrap' }}>
              {row.jitter_impact.replace(/^- /, '')}
            </Text>
          ) : (
            '-'
          )
      }
    ],
    []
  )

  if (events.length === 0) return null

  return (
    <div>
      <Text size='sm' fw={500} mb='xs'>
        {title} ({events.length})
      </Text>
      <ProTable<DiskCaseEvent>
        columns={columns}
        data={pagedEvents}
        manualPagination
        pagination={
          totalPages > 1
            ? {
                total: totalPages,
                value: page,
                onChange: setPage,
                wrapperProps: { justify: 'flex-end' }
              }
            : undefined
        }
      />
    </div>
  )
}

const DiskCheckSection: React.FC<{
  content: DiskCheckContent
}> = ({ content }) => {
  const summary = content.summary
  const caseTracking = content.case_tracking
  const impacted = caseTracking?.impacted
  const nonImpacted = caseTracking?.non_impacted

  const impactedCount =
    (impacted?.tidb_issues?.length ?? 0) +
    (impacted?.jitter_events?.length ?? 0)
  const nonImpactedCount =
    (nonImpacted?.tidb_issues?.length ?? 0) +
    (nonImpacted?.jitter_events?.length ?? 0)
  const totalEvents = impactedCount + nonImpactedCount

  if (totalEvents === 0 && !summary?.statistics?.length) {
    return (
      <Text size='sm' c='dimmed'>
        No disk anomalies detected during this time range.
      </Text>
    )
  }

  return (
    <Stack gap='lg'>
      {/* Summary statistics */}
      {summary?.statistics && summary.statistics.length > 0 && (
        <div>
          <Text size='sm' fw={500} mb='xs'>
            Summary
          </Text>
          {summary.total_disks && (
            <Group gap='md' mb='sm'>
              {Object.entries(summary.total_disks).map(([key, count]) => (
                <Group gap={4} key={key}>
                  <Text size='xs' c='dimmed'>
                    {key.toUpperCase()}:
                  </Text>
                  <Text size='xs' fw={500}>
                    {count}
                  </Text>
                </Group>
              ))}
            </Group>
          )}
          <Table
            withTableBorder
            withColumnBorders
            fz='xs'
            styles={{ table: { maxWidth: 560 } }}
          >
            <Table.Thead>
              <Table.Tr>
                <Table.Th>Disk Type</Table.Th>
                <Table.Th>Impact</Table.Th>
                <Table.Th>Jitter Events</Table.Th>
                <Table.Th>TiKV Issues</Table.Th>
                <Table.Th>TiFlash Issues</Table.Th>
              </Table.Tr>
            </Table.Thead>
            <Table.Tbody>
              {summary.statistics.map((s, i) => (
                <Table.Tr key={i}>
                  <Table.Td>{s.disk_type.toUpperCase()}</Table.Td>
                  <Table.Td>
                    <Badge
                      color={s.impact_type === 'Impacted' ? 'red' : 'gray'}
                      variant='light'
                      size='xs'
                    >
                      {s.impact_type}
                    </Badge>
                  </Table.Td>
                  <Table.Td>{s.jitter_events_count}</Table.Td>
                  <Table.Td>{s.tikv_issues_count}</Table.Td>
                  <Table.Td>{s.tiflash_issues_count}</Table.Td>
                </Table.Tr>
              ))}
            </Table.Tbody>
          </Table>
        </div>
      )}

      {/* Case tracking */}
      {totalEvents > 0 && (
        <Tabs defaultValue='impacted'>
          <Tabs.List>
            <Tabs.Tab value='impacted'>
              <Group gap={6}>
                Impacted
                {impactedCount > 0 && (
                  <Badge color='red' variant='light' size='xs' circle>
                    {impactedCount}
                  </Badge>
                )}
              </Group>
            </Tabs.Tab>
            <Tabs.Tab value='non_impacted'>
              <Group gap={6}>
                Non-Impacted
                {nonImpactedCount > 0 && (
                  <Badge color='gray' variant='light' size='xs' circle>
                    {nonImpactedCount}
                  </Badge>
                )}
              </Group>
            </Tabs.Tab>
          </Tabs.List>

          <Tabs.Panel value='impacted' pt='md'>
            {impactedCount === 0 ? (
              <Text size='sm' c='dimmed'>
                No impacted events.
              </Text>
            ) : (
              <Stack gap='lg'>
                <DiskCaseEventTable
                  events={impacted?.tidb_issues ?? []}
                  title='TiDB Issues'
                />
                <DiskCaseEventTable
                  events={impacted?.jitter_events ?? []}
                  title='Jitter Events'
                />
              </Stack>
            )}
          </Tabs.Panel>

          <Tabs.Panel value='non_impacted' pt='md'>
            {nonImpactedCount === 0 ? (
              <Text size='sm' c='dimmed'>
                No non-impacted events.
              </Text>
            ) : (
              <Stack gap='lg'>
                <DiskCaseEventTable
                  events={nonImpacted?.tidb_issues ?? []}
                  title='TiDB Issues'
                />
                <DiskCaseEventTable
                  events={nonImpacted?.jitter_events ?? []}
                  title='Jitter Events'
                />
              </Stack>
            )}
          </Tabs.Panel>
        </Tabs>
      )}
    </Stack>
  )
}

// ── Config Check renderer ────────────────────────────────────────────
const RISK_COLORS: Record<string, string> = {
  high: 'red',
  medium: 'orange',
  low: 'blue'
}

const ConfigCheckSection: React.FC<{
  content: ConfigCheckContent
}> = ({ content }) => {
  const changes = content.changes ?? []

  return (
    <Stack gap='md'>
      {changes.length > 0 ? (
        changes.map((change, idx) => (
          <Card key={change.change_id || idx} withBorder p='md'>
            <Group gap='xl' mb='sm' wrap='wrap'>
              <div>
                <Text size='xs' c='dimmed'>
                  Component
                </Text>
                <Text size='sm' fw={500}>
                  {change.component}
                </Text>
              </div>
              <div>
                <Text size='xs' c='dimmed'>
                  Applied At
                </Text>
                <Text size='sm'>
                  {formatDateTime(new Date(change.applied_at).getTime() / 1000)}
                </Text>
              </div>
              <div>
                <Text size='xs' c='dimmed'>
                  Applicant
                </Text>
                <Text size='sm'>{change.applicant || '-'}</Text>
              </div>
              {change.reason && (
                <div>
                  <Text size='xs' c='dimmed'>
                    Reason
                  </Text>
                  <Text size='sm'>{change.reason}</Text>
                </div>
              )}
            </Group>

            {change.parameter_changes.length > 0 && (
              <ProTable<ConfigParameterChange>
                columns={[
                  {
                    id: 'name',
                    header: 'Parameter',
                    accessorFn: (row) => row.name
                  },
                  {
                    id: 'component',
                    header: 'Component',
                    size: 100,
                    accessorFn: (row) => row.component
                  },
                  {
                    id: 'from',
                    header: 'From',
                    accessorFn: (row) => (
                      <Text size='sm' style={{ fontFamily: 'monospace' }}>
                        {row.from}
                      </Text>
                    )
                  },
                  {
                    id: 'to',
                    header: 'To',
                    accessorFn: (row) => (
                      <Text size='sm' style={{ fontFamily: 'monospace' }}>
                        {row.to}
                      </Text>
                    )
                  },
                  {
                    id: 'risk_level',
                    header: 'Risk',
                    size: 100,
                    accessorFn: (row) => (
                      <Badge
                        color={RISK_COLORS[row.risk_level] ?? 'gray'}
                        variant='dot'
                        size='sm'
                      >
                        {row.risk_level}
                      </Badge>
                    )
                  }
                ]}
                data={change.parameter_changes}
              />
            )}
          </Card>
        ))
      ) : (
        <Text size='sm' c='dimmed'>
          No configuration changes detected during this time range.
        </Text>
      )}
    </Stack>
  )
}

// ── Slow Query - High Resource Usage renderer ───────────────────────

const SlowQueryHugeResourceSection: React.FC<{
  content: SlowQueryContent
  fromTs: number | null
  toTs: number | null
  slowQueryUrlBase: string
}> = ({ content, fromTs, toTs, slowQueryUrlBase }) => {
  const { params, results, summary } = content
  const [page, setPage] = useState(1)
  const totalPages = Math.ceil(results.length / PAGE_SIZE)
  const pagedResults = results.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)

  const [samplesModal, setSamplesModal] = useState<{
    digest: string
    samples: SlowQueryDigestDetail[]
  } | null>(null)

  const columns = useMemo(
    (): MRT_ColumnDef<SlowQueryResult>[] => [
      {
        id: 'digest',
        header: 'Digest',
        size: 220,
        accessorFn: (row) => (
          <DigestLinkWithCopy
            digest={row.digest}
            fromTs={fromTs}
            toTs={toTs}
            slowQueryUrlBase={slowQueryUrlBase}
          />
        )
      },
      {
        id: 'sample_query',
        header: 'Sample Query',
        size: 360,
        maxSize: 360,
        grow: false,
        accessorFn: (row) => (
          <div style={{ maxWidth: 340, overflow: 'hidden' }}>
            <CollapsibleSQL sql={row.sample_query} />
          </div>
        )
      },
      {
        id: 'hit_records',
        header: 'Hit Records',
        size: 110,
        accessorFn: (row) => formatNumber(row.hit_records)
      },
      {
        id: 'avg_query_time_s',
        header: 'Avg Query Time (s)',
        size: 150,
        accessorFn: (row) => row.avg_query_time_s.toFixed(2)
      },
      {
        id: 'max_query_time_s',
        header: 'Max Query Time (s)',
        size: 150,
        accessorFn: (row) => row.max_query_time_s.toFixed(2)
      },
      {
        id: 'hit_query_time_records',
        header: 'Hit Time Records',
        size: 140,
        accessorFn: (row) => formatNumber(row.hit_query_time_records)
      },
      {
        id: 'avg_max_mem_b',
        header: 'Avg Max Mem',
        size: 130,
        accessorFn: (row) => formatMemBytes(row.avg_max_mem_b)
      },
      {
        id: 'max_max_mem_b',
        header: 'Max Max Mem',
        size: 130,
        accessorFn: (row) => formatMemBytes(row.max_max_mem_b)
      },
      {
        id: 'hit_mem_records',
        header: 'Hit Mem Records',
        size: 140,
        accessorFn: (row) => formatNumber(row.hit_mem_records)
      },
      {
        id: 'samples',
        header: 'Samples',
        size: 100,
        accessorFn: (row) => (
          <UnstyledButton
            style={{ color: 'var(--mantine-color-blue-6)', fontSize: 13 }}
            onClick={() =>
              setSamplesModal({
                digest: row.digest,
                samples: row.digest_detail ?? []
              })
            }
          >
            View Samples
          </UnstyledButton>
        )
      }
    ],
    [fromTs, toTs, slowQueryUrlBase]
  )

  const sampleColumns = useMemo(
    (): MRT_ColumnDef<SlowQueryDigestDetail>[] => [
      {
        id: 'time',
        header: 'Time',
        accessorFn: (row) => {
          const seconds = Number(row.time)
          return Number.isFinite(seconds) ? formatDateTime(seconds) : '-'
        }
      },
      {
        id: 'query_time',
        header: 'Query Time (s)',
        accessorFn: (row) => row.query_time.toFixed(3)
      },
      {
        id: 'mem_max',
        header: 'Mem Max',
        accessorFn: (row) => formatMemBytes(row.mem_max)
      },
      {
        id: 'link',
        header: 'Link',
        size: 80,
        accessorFn: (row) =>
          row.link ? (
            <Anchor href={row.link} target='_blank' size='xs'>
              Detail
            </Anchor>
          ) : (
            '-'
          )
      }
    ],
    []
  )

  return (
    <>
      <Stack gap={32}>
        {/* Params */}
        <div style={{ background: '#f8f9fa', borderRadius: 8, padding: 16 }}>
          <Text size='sm' fw={500} mb='xs'>
            Parameters
          </Text>
          <Group gap='xl'>
            <Group gap={6}>
              <Text size='sm'>Memory Threshold:</Text>
              <Text size='sm' fw={500}>
                {formatMemBytes(params.memMaxThreshold)}
              </Text>
              <Tooltip label='Maximum memory consumption allowed per single SQL query'>
                <IconInfoCircle size={14} color='gray' />
              </Tooltip>
            </Group>
            <Group gap={6}>
              <Text size='sm'>Query Time Threshold:</Text>
              <Text size='sm' fw={500}>
                {params.queryTimeThreshold}s
              </Text>
              <Tooltip label='Maximum execution time allowed per single SQL query'>
                <IconInfoCircle size={14} color='gray' />
              </Tooltip>
            </Group>
          </Group>
          <Text size='sm' c='dimmed' mt='xs'>
            The following SQL queries either consumed more memory than{' '}
            {formatMemBytes(params.memMaxThreshold)} or took longer than{' '}
            {params.queryTimeThreshold}s to execute.
          </Text>
        </div>

        {/* Summary */}
        {summary && (
          <div>
            <Text size='sm' fw={500} mb='xs'>
              Summary
            </Text>
            <Group gap='md' wrap='wrap' style={{ rowGap: 4 }}>
              <Group gap={6}>
                <Text size='sm'>Total Records:</Text>
                <Text size='sm' fw={500}>
                  {formatNumber(summary.totalRecords)}
                </Text>
              </Group>
              <Group gap={6}>
                <Text size='sm'>Distinct Digests:</Text>
                <Text size='sm' fw={500}>
                  {formatNumber(summary.distinctDigestHugeResourceUsage)}
                </Text>
              </Group>
              <Group gap={6}>
                <Text size='sm'>Hit Memory Threshold:</Text>
                <Text size='sm' fw={500}>
                  {formatNumber(summary.hitMemMaxThresholdDigest)}
                </Text>
              </Group>
              <Group gap={6}>
                <Text size='sm'>Hit Time Threshold:</Text>
                <Text size='sm' fw={500}>
                  {formatNumber(summary.hitQueryTimeThresholdDigest)}
                </Text>
              </Group>
            </Group>
          </div>
        )}

        {/* Results table */}
        <div>
          <Text size='sm' fw={500} mb='xs'>
            Results ({results.length})
          </Text>
          {results.length > 0 ? (
            <ProTable<SlowQueryResult>
              columns={columns}
              data={pagedResults}
              enableColumnPinning
              initialState={{
                columnPinning: { left: ['digest'], right: ['samples'] }
              }}
              manualPagination
              pagination={
                totalPages > 1
                  ? {
                      total: totalPages,
                      value: page,
                      onChange: setPage,
                      wrapperProps: { justify: 'flex-end' }
                    }
                  : undefined
              }
            />
          ) : (
            <Text size='sm' c='dimmed'>
              No slow queries detected with the given thresholds.
            </Text>
          )}
        </div>
      </Stack>

      {/* Samples modal */}
      <Modal
        opened={!!samplesModal}
        onClose={() => setSamplesModal(null)}
        title={
          <Text fw={500}>Samples — {samplesModal?.digest.slice(0, 16)}...</Text>
        }
        size='70%'
      >
        {samplesModal && samplesModal.samples.length > 0 ? (
          <ProTable<SlowQueryDigestDetail>
            columns={sampleColumns}
            data={samplesModal.samples}
          />
        ) : (
          <Text size='sm' c='dimmed'>
            No samples available.
          </Text>
        )}
      </Modal>
    </>
  )
}

// ── Slow Log Top Digest renderer ────────────────────────────────────
const SlowLogTopDigestSection: React.FC<{
  content: SlowLogTopDigestContent
  fromTs: number | null
  toTs: number | null
  slowQueryUrlBase: string
}> = ({ content, fromTs, toTs, slowQueryUrlBase }) => {
  const results = useMemo(() => content.results ?? {}, [content.results])
  const requestedDimensions = useMemo(
    () =>
      (content.params?.dimensions ?? content.summary?.dimensions ?? []).filter(
        (dimension): dimension is SlowLogTopDigestDimension =>
          typeof dimension === 'string' &&
          isSlowLogTopDigestDimension(dimension)
      ),
    [content.params?.dimensions, content.summary?.dimensions]
  )
  const tabDimensions = useMemo(() => {
    if (requestedDimensions.length > 0) return requestedDimensions

    const dimensionsWithRows = SLOW_LOG_TOP_DIGEST_RESULT_ORDER.filter(
      (dimension) =>
        (results[SLOW_LOG_TOP_DIGEST_DIMENSION_META[dimension].resultKey]
          ?.length ?? 0) > 0
    )
    return dimensionsWithRows.length > 0
      ? dimensionsWithRows
      : SLOW_LOG_TOP_DIGEST_RESULT_ORDER
  }, [requestedDimensions, results])

  const [activeDimension, setActiveDimension] =
    useState<SlowLogTopDigestDimension | null>(null)
  const [page, setPage] = useState(1)

  const activeTab =
    activeDimension && tabDimensions.includes(activeDimension)
      ? activeDimension
      : tabDimensions[0] ?? null
  const activeMeta = activeTab
    ? SLOW_LOG_TOP_DIGEST_DIMENSION_META[activeTab]
    : null
  const activeRows = activeMeta ? results[activeMeta.resultKey] ?? [] : []
  const totalPages = Math.ceil(activeRows.length / PAGE_SIZE)
  const pagedRows = activeRows.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
  const activeMetricColumnId = activeMeta?.metricColumnId
  const countThresholdEntries = useMemo(
    () => getSlowLogTopDigestCountThresholdEntries(content),
    [content]
  )
  const minCount = useMemo(
    () => getSlowLogTopDigestNumberParam(content, 'min_count'),
    [content]
  )
  const minTotalKeys = useMemo(
    () => getSlowLogTopDigestNumberParam(content, 'min_total_keys'),
    [content]
  )

  const columns = useMemo((): MRT_ColumnDef<SlowLogTopDigestRow>[] => {
    const metricColumns: MRT_ColumnDef<SlowLogTopDigestRow>[] = [
      {
        id: 'slow_count',
        header: 'Slow Count',
        size: 120,
        accessorFn: (row) => formatMetricNumber(row.slow_count)
      },
      {
        id: 'total_latency_s',
        header: 'Total Latency (s)',
        size: 150,
        accessorFn: (row) => formatMetricNumber(row.total_latency_s, 2)
      },
      {
        id: 'total_keys',
        header: 'Total Keys',
        size: 130,
        accessorFn: (row) => formatMetricNumber(row.total_keys)
      },
      {
        id: 'process_keys',
        header: 'Process Keys',
        size: 130,
        accessorFn: (row) => formatMetricNumber(row.process_keys)
      },
      {
        id: 'request_count',
        header: 'Request Count',
        size: 130,
        accessorFn: (row) => formatMetricNumber(row.request_count)
      },
      {
        id: 'sum_ru',
        header: 'SUM RU',
        size: 130,
        accessorFn: (row) => formatMetricNumber(row.sum_ru)
      }
    ]
    const prioritizedMetricColumns = activeMetricColumnId
      ? [
          ...metricColumns.filter(
            (column) => column.id === activeMetricColumnId
          ),
          ...metricColumns.filter(
            (column) => column.id !== activeMetricColumnId
          )
        ]
      : metricColumns

    return [
      {
        id: 'rank',
        header: 'Rank',
        Header: () => (
          <Text size='sm' style={{ width: '100%', textAlign: 'center' }}>
            Rank
          </Text>
        ),
        size: 70,
        accessorFn: (row) => (
          <Text size='sm' style={{ width: '100%', textAlign: 'center' }}>
            {row.rank}
          </Text>
        )
      },
      {
        id: 'digest',
        header: 'Digest',
        size: 220,
        accessorFn: (row) => (
          <DigestLinkWithCopy
            digest={row.digest}
            fromTs={fromTs}
            toTs={toTs}
            slowQueryUrlBase={slowQueryUrlBase}
          />
        )
      },
      {
        id: 'query',
        header: 'Sample Query',
        size: 360,
        maxSize: 360,
        grow: false,
        accessorFn: (row) => (
          <div style={{ maxWidth: 340, overflow: 'hidden' }}>
            <CollapsibleSQL sql={row.query || '-'} />
          </div>
        )
      },
      ...prioritizedMetricColumns,
      {
        id: 'max_latency_s',
        header: 'Max Latency (s)',
        size: 140,
        accessorFn: (row) => formatMetricNumber(row.max_latency_s, 3)
      },
      {
        id: 'latest_time',
        header: 'Latest Time',
        size: 180,
        accessorFn: (row) => formatUnixTime(row.latest_time)
      }
    ]
  }, [activeMetricColumnId, fromTs, toTs, slowQueryUrlBase])

  return (
    <Stack gap={32}>
      <div style={{ background: '#f8f9fa', borderRadius: 8, padding: 16 }}>
        <Text size='sm' fw={500} mb='xs'>
          Parameters
        </Text>
        <Group gap='xl' wrap='wrap'>
          <Group gap={6}>
            <Text size='sm'>Top N:</Text>
            <Text size='sm' fw={500}>
              {content.params?.topN ?? content.summary?.topN ?? '-'}
            </Text>
            <Tooltip label='Maximum number of digest entries returned for each selected dimension'>
              <IconInfoCircle size={14} color='gray' />
            </Tooltip>
          </Group>
          <Group gap={6}>
            <Text size='sm'>Top Dimensions:</Text>
            <Text size='sm' fw={500}>
              {(content.params?.dimensions ?? content.summary?.dimensions ?? [])
                .map(formatSlowLogTopDigestDimension)
                .join(', ') || '-'}
            </Text>
            <Tooltip label='Aggregate dimensions used to rank slow log digests'>
              <IconInfoCircle size={14} color='gray' />
            </Tooltip>
          </Group>
          <Group gap={6}>
            <Text size='sm'>Ignore DML:</Text>
            <Text size='sm' fw={500}>
              {content.params?.ignore_dml ?? content.summary?.ignore_dml
                ? 'Yes'
                : 'No'}
            </Text>
            <Tooltip label='When enabled, INSERT, UPDATE, and DELETE slow logs are excluded'>
              <IconInfoCircle size={14} color='gray' />
            </Tooltip>
          </Group>
          <Group gap={6}>
            <Text size='sm'>Min Count:</Text>
            <Text size='sm' fw={500}>
              {minCount > 0 ? formatMetricNumber(minCount) : 'None'}
            </Text>
            <Tooltip label='Digest-level filter applied after grouping. Digests with slow log count below this value are ignored for every selected dimension.'>
              <IconInfoCircle size={14} color='gray' />
            </Tooltip>
          </Group>
          <Group gap={6}>
            <Text size='sm'>Min Total Keys:</Text>
            <Text size='sm' fw={500}>
              {minTotalKeys > 0 ? formatMetricNumber(minTotalKeys) : 'None'}
            </Text>
            <Tooltip label='Row-level filter applied before grouping. Slow logs with total_keys below this value are ignored.'>
              <IconInfoCircle size={14} color='gray' />
            </Tooltip>
          </Group>
          {countThresholdEntries.length > 0 && (
            <Group gap={6}>
              <Text size='sm'>Legacy Count Filters:</Text>
              <Text size='sm' fw={500}>
                {countThresholdEntries
                  .map(
                    ({ dimension, value }) =>
                      `${formatSlowLogTopDigestDimension(
                        dimension
                      )}: Count > ${formatMetricNumber(value)}`
                  )
                  .join(', ')}
              </Text>
              <Tooltip label='Legacy dimension-level filters from older reports. They were applied after grouping by digest.'>
                <IconInfoCircle size={14} color='gray' />
              </Tooltip>
            </Group>
          )}
          <Group gap={6}>
            <Text size='sm'>Exclude Internal:</Text>
            <Text size='sm' fw={500}>
              {content.params?.exclude_internal ??
              content.summary?.exclude_internal
                ? 'Yes'
                : 'No'}
            </Text>
            <Tooltip label='Internal slow logs are excluded by default'>
              <IconInfoCircle size={14} color='gray' />
            </Tooltip>
          </Group>
        </Group>
      </div>

      {content.error && (
        <Text size='sm' c='red'>
          {content.error}
        </Text>
      )}

      <div>
        <Text size='sm' fw={500} mb='xs'>
          Results{activeMeta ? ` - ${activeMeta.label}` : ''} (
          {activeRows.length})
        </Text>
        {tabDimensions.length > 0 ? (
          <Tabs
            value={activeTab}
            onChange={(value) => {
              if (!value || !isSlowLogTopDigestDimension(value)) return
              setActiveDimension(value)
              setPage(1)
            }}
          >
            <Tabs.List>
              {tabDimensions.map((dimension) => {
                const meta = SLOW_LOG_TOP_DIGEST_DIMENSION_META[dimension]
                return (
                  <Tabs.Tab key={dimension} value={dimension}>
                    {meta.label}
                  </Tabs.Tab>
                )
              })}
            </Tabs.List>

            {tabDimensions.map((dimension) => {
              const meta = SLOW_LOG_TOP_DIGEST_DIMENSION_META[dimension]
              const rows = results[meta.resultKey] ?? []
              return (
                <Tabs.Panel key={dimension} value={dimension} pt='md'>
                  {dimension === activeTab && rows.length > 0 ? (
                    <ProTable<SlowLogTopDigestRow>
                      columns={columns}
                      data={pagedRows}
                      enableColumnPinning
                      initialState={{
                        columnPinning: { left: ['rank', 'digest'] }
                      }}
                      manualPagination
                      pagination={
                        totalPages > 1
                          ? {
                              total: totalPages,
                              value: page,
                              onChange: setPage,
                              wrapperProps: { justify: 'flex-end' }
                            }
                          : undefined
                      }
                    />
                  ) : (
                    <Text size='sm' c='dimmed'>
                      No slow log digests for this dimension.
                    </Text>
                  )}
                </Tabs.Panel>
              )
            })}
          </Tabs>
        ) : (
          <Text size='sm' c='dimmed'>
            No slow log digests detected.
          </Text>
        )}
      </div>
    </Stack>
  )
}

// ── SQL TiKV Hotspot renderer ───────────────────────────────────────
const TIKV_HOTSPOT_PARAM_META: Record<
  string,
  { label: string; tooltip: string }
> = {
  metric_step_seconds: {
    label: 'Metric Step (s)',
    tooltip: 'Prometheus range query step'
  },
  metric_rate_window: {
    label: 'Metric Rate Window',
    tooltip: 'PromQL rate window'
  },
  window_min_duration_seconds: {
    label: 'Minimum Window Duration (s)',
    tooltip: 'Minimum anomaly window duration'
  },
  window_merge_gap_seconds: {
    label: 'Window Merge Gap (s)',
    tooltip: 'Maximum gap for merging adjacent anomaly points'
  },
  window_expand_before_seconds: {
    label: 'Expand Window Before (s)',
    tooltip: 'Backward expansion before SQL attribution'
  },
  window_expand_after_seconds: {
    label: 'Expand Window After (s)',
    tooltip: 'Forward expansion before SQL attribution'
  },
  attribution_window_padding_seconds: {
    label: 'Attribution Padding (s)',
    tooltip: 'Extra padding for TopSQL and slow log attribution'
  },
  topsql_top_n: {
    label: 'SQL Candidates per Window',
    tooltip: 'Maximum SQL digests shown for each TiKV anomaly window'
  },
  topsql_top_n_max: {
    label: 'TopSQL Top N Max',
    tooltip: 'Upper bound for TopSQL Top N'
  },
  max_anomaly_windows: {
    label: 'Max Windows to Analyze',
    tooltip: 'Maximum windows attributed in one run'
  },
  max_time_range_hours: {
    label: 'Max Time Range (h)',
    tooltip: 'Maximum allowed inspection time range'
  },
  topsql_cpu_percent_threshold: {
    label: 'Min TopSQL Share (%)',
    tooltip:
      'A SQL digest is reported only when it contributes at least this share of TiKV TopSQL CPU in the anomaly window'
  },
  topsql_dominance_ratio_threshold: {
    label: 'Dominance Ratio',
    tooltip:
      'If one SQL digest uses at least this many times the TopSQL CPU of the next digest, lower-ranked digests are ignored'
  },
  critical_window_duration_seconds: {
    label: 'Critical Window Duration (s)',
    tooltip: 'Duration threshold used to increase anomaly severity'
  },
  include_links: {
    label: 'Include Clinic Links',
    tooltip: 'Whether Clinic review links are included in the result'
  },
  include_promql: {
    label: 'Include PromQL',
    tooltip: 'Whether PromQL expressions are included for debugging'
  },
  tikv_instance_regex: {
    label: 'TiKV Instance Regex',
    tooltip: 'Regex used to select TiKV instances'
  },
  slow_log_match_policy: {
    label: 'Slow Log Enrichment',
    tooltip:
      'Slow logs enrich TopSQL candidates with slow query count and samples; they do not create SQL candidates by themselves'
  },
  duration_threshold_ms: {
    label: 'Duration Alert Threshold (ms)',
    tooltip: 'TiKV coprocessor duration anomaly threshold'
  },
  critical_duration_threshold_ms: {
    label: 'Critical Duration Threshold (ms)',
    tooltip: 'Duration threshold for critical severity'
  },
  grpc_type: {
    label: 'gRPC Type',
    tooltip: 'TiKV gRPC type used for duration detection'
  },
  exclude_grpc_type_regex: {
    label: 'Exclude gRPC Type Regex',
    tooltip: 'gRPC type regex excluded from duration detection'
  },
  absolute_cpu_threshold_percent: {
    label: 'CPU Alert Threshold (%)',
    tooltip: 'Absolute TiKV CPU anomaly threshold'
  },
  relative_cpu_delta_threshold_percent: {
    label: 'Relative CPU Delta (%)',
    tooltip: 'CPU delta above cluster P50 for relative outliers'
  },
  min_cpu_for_relative_outlier_percent: {
    label: 'Min CPU for Relative Outlier (%)',
    tooltip: 'Minimum CPU before applying relative outlier rule'
  },
  critical_cpu_threshold_percent: {
    label: 'Critical CPU Threshold (%)',
    tooltip: 'CPU threshold for critical severity'
  }
}

const HOTSPOT_SEVERITY_COLORS: Record<string, string> = {
  Critical: 'red',
  Major: 'orange',
  Warning: 'yellow'
}
const HOTSPOT_PRIMARY_PARAM_ORDER = [
  'duration_threshold_ms',
  'critical_duration_threshold_ms',
  'absolute_cpu_threshold_percent',
  'relative_cpu_delta_threshold_percent',
  'critical_cpu_threshold_percent',
  'window_min_duration_seconds',
  'critical_window_duration_seconds',
  'topsql_cpu_percent_threshold',
  'topsql_dominance_ratio_threshold'
]
const HOTSPOT_PRIMARY_PARAM_KEYS = new Set(HOTSPOT_PRIMARY_PARAM_ORDER)
const HOTSPOT_HIDDEN_PARAM_KEYS = new Set([
  'metric_step_seconds',
  'metric_rate_window',
  'window_merge_gap_seconds',
  'window_expand_before_seconds',
  'window_expand_after_seconds',
  'attribution_window_padding_seconds',
  'topsql_top_n',
  'topsql_top_n_max',
  'max_anomaly_windows',
  'max_time_range_hours',
  'include_links',
  'include_promql',
  'grpc_type',
  'exclude_grpc_type_regex',
  'counter_evidence_profile',
  'tikv_instance_regex',
  'slow_log_match_policy',
  'min_cpu_for_relative_outlier_percent',
  'digest_process_cpu_share_possible_threshold',
  'digest_process_cpu_share_likely_threshold',
  'topsql_process_cpu_coverage_weak_threshold',
  'topsql_process_cpu_coverage_likely_threshold',
  'apply_topsql_cpu_percent_filter'
])

function getHotspotWindowParts(row: TiKVHotspotWindow) {
  const start = row.node_anomaly?.window_start
  const end = row.node_anomaly?.window_end
  if (start == null || end == null) return null
  return {
    start: formatUnixTime(start),
    end: formatUnixTime(end)
  }
}

function renderHotspotWindowRange(row: TiKVHotspotWindow) {
  const range = getHotspotWindowParts(row)
  if (!range) return '-'
  return (
    <Stack gap={0}>
      <Text size='sm'>{range.start} ~</Text>
      <Text size='sm'>{range.end}</Text>
    </Stack>
  )
}

function renderHotspotAnomalyValue(
  row: TiKVHotspotWindow,
  mode: TiKVHotspotContent['name']
) {
  const anomaly = row.node_anomaly
  if (!anomaly) return '-'
  if (mode === 'sql_tikv_high_duration') {
    return (
      <Stack gap={0}>
        <Text size='sm'>
          max {formatMetricNumber(anomaly.max_duration_ms, 0)} ms
        </Text>
        <Text size='sm' c='dimmed'>
          avg {formatMetricNumber(anomaly.avg_duration_ms, 0)} ms
        </Text>
      </Stack>
    )
  }
  return (
    <Stack gap={0}>
      <Text size='sm'>
        max {formatMetricNumber(anomaly.max_cpu_percent, 1)}%
      </Text>
      <Text size='sm' c='dimmed'>
        avg {formatMetricNumber(anomaly.avg_cpu_percent, 1)}%
      </Text>
    </Stack>
  )
}

function formatTopSqlCpuTime(value?: number) {
  const numeric = Number(value)
  if (!Number.isFinite(numeric)) return '-'
  if (numeric < 1) return `${formatMetricNumber(numeric * 1000, 0)} ms`
  if (numeric < 60) return `${formatMetricNumber(numeric, 1)} s`
  return `${formatMetricNumber(numeric / 60, 1)} min`
}

function getHotspotCandidates(row: TiKVHotspotWindow) {
  return [...(row.sql_candidates ?? [])].sort(
    (a, b) =>
      (a.rank ?? a.topsql_rank ?? 9999) - (b.rank ?? b.topsql_rank ?? 9999)
  )
}

function renderHotspotSlowLogLinks(
  count?: number,
  links?: {
    slow_query_list?: string
    slow_query_detail?: string
  }
) {
  const slowLogText = `Slow Logs: ${formatMetricNumber(count)}`
  return (
    <Stack gap={2}>
      {links?.slow_query_list ? (
        <Anchor href={links.slow_query_list} target='_blank' size='sm'>
          {slowLogText}
        </Anchor>
      ) : (
        <Text size='sm'>{slowLogText}</Text>
      )}
      {links?.slow_query_detail && (
        <Anchor href={links.slow_query_detail} target='_blank' size='xs'>
          Sample Slow Log
        </Anchor>
      )}
    </Stack>
  )
}

const SQLTiKVHotspotSection: React.FC<{
  content: TiKVHotspotContent
}> = ({ content }) => {
  const results = content.results ?? []
  const digestSummaries = content.digest_summary ?? []
  const summary = content.summary ?? {}
  const [page, setPage] = useState(1)
  const totalPages = Math.ceil(results.length / PAGE_SIZE)
  const pagedResults = results.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
  const [showAdvancedParams, setShowAdvancedParams] = useState(false)
  const sqlCandidateWindows =
    summary.sql_candidate_windows ??
    (summary.sql_likely_windows ?? 0) + (summary.sql_possible_windows ?? 0)
  const visibleParamEntries = Object.entries(content.params ?? {}).filter(
    ([key]) => !HOTSPOT_HIDDEN_PARAM_KEYS.has(key)
  )
  const primaryParamEntries = visibleParamEntries
    .filter(([key]) => HOTSPOT_PRIMARY_PARAM_KEYS.has(key))
    .sort(
      ([left], [right]) =>
        HOTSPOT_PRIMARY_PARAM_ORDER.indexOf(left) -
        HOTSPOT_PRIMARY_PARAM_ORDER.indexOf(right)
    )
  const advancedParamEntries = visibleParamEntries.filter(
    ([key]) => !HOTSPOT_PRIMARY_PARAM_KEYS.has(key)
  )

  const digestSummaryColumns = useMemo(
    (): MRT_ColumnDef<TiKVHotspotDigestSummary>[] => [
      {
        id: 'digest',
        header: 'SQL Digest',
        size: 260,
        accessorFn: (row) => (
          <DigestExternalLinkWithCopy
            digest={row.digest}
            url={row.representative_links?.top_sql}
          />
        )
      },
      {
        id: 'affected',
        header: 'Affected',
        size: 150,
        accessorFn: (row) => (
          <Stack gap={0}>
            <Text size='sm'>
              {formatMetricNumber(row.affected_windows)} windows
            </Text>
            <Text size='xs' c='dimmed'>
              {formatMetricNumber(row.affected_instances)} instances
            </Text>
          </Stack>
        )
      },
      {
        id: 'severity',
        header: 'Max Severity',
        size: 120,
        accessorFn: (row) => (
          <Badge
            color={HOTSPOT_SEVERITY_COLORS[row.max_severity ?? ''] ?? 'gray'}
            variant={row.max_severity === 'Critical' ? 'filled' : 'light'}
            size='sm'
          >
            {row.max_severity ?? '-'}
          </Badge>
        )
      },
      {
        id: 'topsql_share',
        header: 'TopSQL Share',
        Header: () => (
          <Tooltip label='For this digest, avg/max share across the anomaly windows where it was selected. The share is digest CPU divided by total TiKV TopSQL CPU in that same instance window.'>
            <Group gap={4} wrap='nowrap'>
              <Text size='sm'>TopSQL Share</Text>
              <IconInfoCircle size={14} color='gray' />
            </Group>
          </Tooltip>
        ),
        size: 160,
        accessorFn: (row) => (
          <Stack gap={0}>
            <Text size='sm'>
              avg {formatMetricPercent(row.avg_topsql_share_percent)}
            </Text>
            <Text size='sm' c='dimmed'>
              max {formatMetricPercent(row.max_topsql_share_percent)}
            </Text>
          </Stack>
        )
      },
      {
        id: 'max_anomaly',
        header:
          content.name === 'sql_tikv_high_duration'
            ? 'Max Duration'
            : 'Max CPU',
        size: 130,
        accessorFn: (row) =>
          content.name === 'sql_tikv_high_duration'
            ? `${formatMetricNumber(row.max_duration_ms, 0)} ms`
            : formatMetricPercent(row.max_cpu_percent)
      },
      {
        id: 'slow_log',
        header: 'Slow Log',
        size: 150,
        accessorFn: (row) =>
          renderHotspotSlowLogLinks(
            row.slow_log_count,
            row.representative_links
          )
      }
    ],
    [content.name]
  )

  const columns = useMemo(
    (): MRT_ColumnDef<TiKVHotspotWindow>[] => [
      {
        id: 'severity',
        header: 'Severity',
        size: 100,
        accessorFn: (row) => (
          <Badge
            color={HOTSPOT_SEVERITY_COLORS[row.node_severity ?? ''] ?? 'gray'}
            variant={row.node_severity === 'Critical' ? 'filled' : 'light'}
            size='sm'
          >
            {row.node_severity ?? '-'}
          </Badge>
        )
      },
      {
        id: 'instance',
        header: 'TiKV Instance',
        size: 160,
        accessorFn: (row) => {
          const instance = row.node_anomaly?.instance ?? '-'
          const url = row.node_anomaly?.links?.metrics
          return url ? (
            <Anchor href={url} target='_blank' size='sm'>
              {instance}
            </Anchor>
          ) : (
            instance
          )
        }
      },
      {
        id: 'window',
        header: 'Window',
        size: 260,
        accessorFn: (row) => renderHotspotWindowRange(row)
      },
      {
        id: 'anomaly',
        header:
          content.name === 'sql_tikv_high_duration' ? 'Duration' : 'CPU Usage',
        size: 190,
        accessorFn: (row) => renderHotspotAnomalyValue(row, content.name)
      },
      {
        id: 'top_candidate',
        header: 'Top SQL',
        size: 460,
        accessorFn: (row) => {
          const candidates = getHotspotCandidates(row)
          if (candidates.length === 0) return '-'
          return (
            <Stack gap='xs'>
              {candidates.map((candidate) => {
                const topSqlLink =
                  candidate.links?.top_sql ?? row.links?.top_sql
                const rank = candidate.rank ?? candidate.topsql_rank
                const cpuText = `Top SQL: CPU Time ${formatTopSqlCpuTime(
                  candidate.cpu_time_seconds
                )} (${formatMetricPercent(
                  candidate.digest_topsql_cpu_share_percent
                )})`

                return (
                  <Stack key={`${candidate.digest}-${rank ?? 'rank'}`} gap={2}>
                    <Group
                      gap={6}
                      wrap='nowrap'
                      style={{ minWidth: 0, width: '100%' }}
                    >
                      {candidates.length > 1 && rank != null && (
                        <Text
                          size='xs'
                          c='dimmed'
                          style={{ flex: '0 0 auto', whiteSpace: 'nowrap' }}
                        >
                          #{rank}
                        </Text>
                      )}
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <DigestExternalLinkWithCopy digest={candidate.digest} />
                      </div>
                    </Group>
                    {topSqlLink ? (
                      <Anchor
                        href={topSqlLink}
                        target='_blank'
                        size='xs'
                        style={DIGEST_TEXT_STYLE}
                      >
                        {cpuText}
                      </Anchor>
                    ) : (
                      <Text size='xs' c='dimmed' style={DIGEST_TEXT_STYLE}>
                        {cpuText}
                      </Text>
                    )}
                  </Stack>
                )
              })}
            </Stack>
          )
        }
      },
      {
        id: 'slow_log',
        header: 'Slow Log',
        size: 170,
        accessorFn: (row) => {
          const candidates = getHotspotCandidates(row)
          if (candidates.length === 0) return '-'
          return (
            <Stack gap='xs'>
              {candidates.map((candidate) => (
                <div
                  key={`${candidate.digest}-${candidate.rank ?? 'slow-log'}`}
                >
                  {renderHotspotSlowLogLinks(
                    candidate.slow_log?.count,
                    candidate.links
                  )}
                </div>
              ))}
            </Stack>
          )
        }
      }
    ],
    [content.name]
  )

  return (
    <Stack gap={32}>
      <div style={{ background: '#f8f9fa', borderRadius: 8, padding: 16 }}>
        <Text size='sm' fw={500} mb='xs'>
          Parameters
        </Text>
        <Group gap='md' wrap='wrap' style={{ rowGap: 4 }}>
          {primaryParamEntries.map(([key, value]) => {
            const meta = TIKV_HOTSPOT_PARAM_META[key]
            return (
              <Group key={key} gap={6}>
                <Text size='sm'>{meta?.label ?? key}:</Text>
                <Text size='sm' fw={500}>
                  {formatHotspotParamValue(key, value)}
                </Text>
                {meta?.tooltip && (
                  <Tooltip label={meta.tooltip}>
                    <IconInfoCircle size={14} color='gray' />
                  </Tooltip>
                )}
              </Group>
            )
          })}
        </Group>
        {advancedParamEntries.length > 0 && (
          <Stack gap='xs' mt='sm'>
            <Button
              variant='subtle'
              size='xs'
              style={{
                alignSelf: 'flex-start',
                color: 'var(--mantine-color-blue-6)',
                paddingLeft: 0
              }}
              onClick={() => setShowAdvancedParams((value) => !value)}
            >
              <Group gap={4} wrap='nowrap'>
                {showAdvancedParams ? (
                  <IconChevronDown size={14} />
                ) : (
                  <IconChevronRight size={14} />
                )}
                <span>
                  {showAdvancedParams
                    ? 'Hide advanced parameters'
                    : `Show advanced parameters (${advancedParamEntries.length})`}
                </span>
              </Group>
            </Button>
            {showAdvancedParams && (
              <Group gap='md' wrap='wrap' style={{ rowGap: 4 }}>
                {advancedParamEntries.map(([key, value]) => {
                  const meta = TIKV_HOTSPOT_PARAM_META[key]
                  return (
                    <Group key={key} gap={6}>
                      <Text size='sm'>{meta?.label ?? key}:</Text>
                      <Text size='sm' fw={500}>
                        {formatHotspotParamValue(key, value)}
                      </Text>
                      {meta?.tooltip && (
                        <Tooltip label={meta.tooltip}>
                          <IconInfoCircle size={14} color='gray' />
                        </Tooltip>
                      )}
                    </Group>
                  )
                })}
              </Group>
            )}
          </Stack>
        )}
      </div>

      {content.error && (
        <Text size='sm' c='red'>
          {content.error}
        </Text>
      )}

      <div>
        <Text size='sm' fw={500} mb='xs'>
          Summary
        </Text>
        <Group gap='md' wrap='wrap' style={{ rowGap: 4 }}>
          <SummaryMetric
            label='Anomaly Windows'
            value={summary.node_anomaly_windows}
          />
          <SummaryMetric
            label='SQL Candidate Windows'
            value={sqlCandidateWindows}
            color='blue'
          />
          <SummaryMetric
            label='No TopSQL Candidate'
            value={summary.not_attributed_windows}
          />
          <SummaryMetric
            label='Data Unavailable'
            value={summary.unknown_windows}
          />
          <SummaryMetric
            label='SQL Candidates'
            value={summary.sql_candidates}
          />
          <SummaryMetric
            label='Distinct SQL Digests'
            value={summary.distinct_sql_digests}
          />
        </Group>
      </div>

      <div>
        <Text size='sm' fw={500} mb='xs'>
          SQL Digest Summary ({digestSummaries.length})
        </Text>
        {digestSummaries.length > 0 ? (
          <ProTable<TiKVHotspotDigestSummary>
            columns={digestSummaryColumns}
            data={digestSummaries}
            enableColumnPinning
            initialState={{
              columnPinning: {
                left: ['digest']
              }
            }}
          />
        ) : (
          <Text size='sm' c='dimmed'>
            No SQL digest reached the TopSQL share threshold.
          </Text>
        )}
      </div>

      <div>
        <Text size='sm' fw={500} mb='xs'>
          Anomaly Windows ({results.length})
        </Text>
        {results.length > 0 ? (
          <ProTable<TiKVHotspotWindow>
            columns={columns}
            data={pagedResults}
            enableColumnPinning
            initialState={{
              columnPinning: {
                left: ['severity', 'instance']
              }
            }}
            manualPagination
            pagination={
              totalPages > 1
                ? {
                    total: totalPages,
                    value: page,
                    onChange: setPage,
                    wrapperProps: { justify: 'flex-end' }
                  }
                : undefined
            }
          />
        ) : (
          <Text size='sm' c='dimmed'>
            No TiKV hotspot windows detected during this time range.
          </Text>
        )}
      </div>
    </Stack>
  )
}

const SummaryMetric: React.FC<{
  label: string
  value?: number
  color?: string
}> = ({ label, value, color }) => (
  <Group gap={6}>
    <Text size='sm'>{label}:</Text>
    <Text size='sm' fw={500} c={color}>
      {formatMetricNumber(value)}
    </Text>
  </Group>
)

// ── Table Full Scan renderer ────────────────────────────────────────
const PARAM_META: Record<string, { label: string; tooltip: string }> = {
  scan_rows_threshold: {
    label: 'Scan Rows Threshold',
    tooltip: 'Minimum number of scanned rows to trigger this check'
  },
  tablefullscan_anomaly_threshold: {
    label: 'Scan Rows Threshold',
    tooltip: 'Minimum number of scanned rows to trigger this check'
  },
  topN: {
    label: 'Top N',
    tooltip:
      'Maximum number of result entries to return, ordered by total scanned rows'
  },
  ignore_dml: {
    label: 'Ignore DML',
    tooltip: 'Whether to ignore DML statements (INSERT/UPDATE/DELETE)'
  },
  ignore_tiflash: {
    label: 'Ignore TiFlash',
    tooltip: 'Whether to ignore queries executed on TiFlash'
  },
  sample_top_size: {
    label: 'Sample Top Size',
    tooltip: 'Number of sample slow logs to include for each result'
  }
}

const TableFullScanSection: React.FC<{
  content: TableFullScanContent
  fromTs: number | null
  toTs: number | null
  slowQueryUrlBase: string
  slowQueryDetailUrlBase: string
}> = ({ content, fromTs, toTs, slowQueryUrlBase, slowQueryDetailUrlBase }) => {
  const { params, results, summary } = content
  const [page, setPage] = useState(1)
  const totalPages = Math.ceil(results.length / PAGE_SIZE)
  const pagedResults = results.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)

  const [samplesModal, setSamplesModal] = useState<{
    digest: string
    samples: TableFullScanSample[]
  } | null>(null)

  const columns = useMemo(
    (): MRT_ColumnDef<TableFullScanResult>[] => [
      {
        id: 'digest',
        header: 'Digest',
        size: 220,
        accessorFn: (row) => (
          <DigestLinkWithCopy
            digest={row.digest}
            fromTs={fromTs}
            toTs={toTs}
            slowQueryUrlBase={slowQueryUrlBase}
          />
        )
      },
      {
        id: 'query',
        header: 'Sample Query',
        size: 360,
        maxSize: 360,
        grow: false,
        accessorFn: (row) => (
          <div style={{ maxWidth: 340, overflow: 'hidden' }}>
            <CollapsibleSQL sql={row.query} />
          </div>
        )
      },
      {
        id: 'total_scanned_rows',
        header: 'Total Scan Rows',
        size: 150,
        accessorFn: (row) => formatNumber(row.total_scanned_rows)
      },
      {
        id: 'slow_count',
        header: 'Slow Count',
        size: 110,
        accessorFn: (row) => formatNumber(row.slow_count)
      },
      {
        id: 'max_scanned_rows',
        header: 'Max Scanned Rows',
        size: 150,
        accessorFn: (row) => formatNumber(row.max_scanned_rows)
      },
      {
        id: 'p50_scanned_rows',
        header: 'P50 Scanned Rows',
        size: 150,
        accessorFn: (row) => formatNumber(row.p50_scanned_rows)
      },
      {
        id: 'p95_scanned_rows',
        header: 'P95 Scanned Rows',
        size: 150,
        accessorFn: (row) => formatNumber(row.p95_scanned_rows)
      },
      {
        id: 'p50_latency_s',
        header: 'P50 Latency (s)',
        size: 130,
        accessorFn: (row) => row.p50_latency_s.toFixed(3)
      },
      {
        id: 'p95_latency_s',
        header: 'P95 Latency (s)',
        size: 130,
        accessorFn: (row) => row.p95_latency_s.toFixed(3)
      },
      {
        id: 'total_latency_s',
        header: 'Total Latency (s)',
        size: 140,
        accessorFn: (row) => row.total_latency_s.toFixed(2)
      },
      {
        id: 'actions',
        header: 'Actions',
        size: 100,
        accessorFn: (row) => (
          <UnstyledButton
            style={{ color: 'var(--mantine-color-blue-6)', fontSize: 13 }}
            onClick={() =>
              setSamplesModal({
                digest: row.digest,
                samples: row.samples ?? []
              })
            }
          >
            View Samples
          </UnstyledButton>
        )
      }
    ],
    [fromTs, toTs, slowQueryUrlBase]
  )

  const sampleColumns = useMemo(
    (): MRT_ColumnDef<TableFullScanSample>[] => [
      {
        id: 'time',
        header: 'Time',
        accessorFn: (row) =>
          row.time_formatted
            ? formatDateTime(new Date(row.time_formatted).getTime() / 1000)
            : '-'
      },
      {
        id: 'db',
        header: 'Database',
        accessorFn: (row) => row.db || '-'
      },
      {
        id: 'act_rows',
        header: 'Actual Rows',
        accessorFn: (row) => formatNumber(row.act_rows)
      },
      {
        id: 'latency_s',
        header: 'Latency (s)',
        accessorFn: (row) => row.latency_s.toFixed(3)
      },
      {
        id: 'detail',
        header: '',
        size: 80,
        accessorFn: (row) => {
          const digest = samplesModal?.digest
          if (!digest) return null
          const connId =
            row.connection_id ?? (row as any).conn_id ?? (row as any).connId
          const params = new URLSearchParams()
          params.set('digest', digest)
          if (connId != null) params.set('connection_id', String(connId))
          if (row.time != null) params.set('timestamp', String(row.time))
          const url = `${slowQueryDetailUrlBase}?${params.toString()}`
          return (
            <Anchor href={url} target='_blank' size='xs'>
              Detail
            </Anchor>
          )
        }
      }
    ],
    [samplesModal?.digest, slowQueryDetailUrlBase]
  )

  return (
    <>
      <Stack gap={32}>
        {/* Params */}
        <div style={{ background: '#f8f9fa', borderRadius: 8, padding: 16 }}>
          <Text size='sm' fw={500} mb='xs'>
            Parameters
          </Text>
          <Group gap='xl' wrap='wrap'>
            {Object.entries(params).map(([key, value]) => {
              const meta = PARAM_META[key]
              return (
                <Group key={key} gap={6}>
                  <Text size='sm'>{meta?.label ?? key}:</Text>
                  <Text size='sm' fw={500}>
                    {typeof value === 'boolean'
                      ? value
                        ? 'Yes'
                        : 'No'
                      : String(value)}
                  </Text>
                  {meta?.tooltip && (
                    <Tooltip label={meta.tooltip}>
                      <IconInfoCircle size={14} color='gray' />
                    </Tooltip>
                  )}
                </Group>
              )
            })}
          </Group>
        </div>

        {/* Summary */}
        {summary && Object.keys(summary).length > 0 && (
          <div>
            <Text size='sm' fw={500} mb='xs'>
              Summary
            </Text>
            <Group gap='md' wrap='wrap' style={{ rowGap: 4 }}>
              {Object.entries(summary).map(([key, value]) => (
                <Group key={key} gap={6}>
                  <Text size='sm'>{key.replace(/_/g, ' ')}:</Text>
                  <Text size='sm' fw={500}>
                    {typeof value === 'number'
                      ? formatNumber(value)
                      : String(value)}
                  </Text>
                </Group>
              ))}
            </Group>
          </div>
        )}

        {/* Results table */}
        <div>
          <Text size='sm' fw={500} mb='xs'>
            Results ({results.length})
          </Text>
          {results.length > 0 ? (
            <ProTable<TableFullScanResult>
              columns={columns}
              data={pagedResults}
              enableColumnPinning
              initialState={{
                columnPinning: { left: ['digest'], right: ['actions'] }
              }}
              manualPagination
              pagination={
                totalPages > 1
                  ? {
                      total: totalPages,
                      value: page,
                      onChange: setPage,
                      wrapperProps: { justify: 'flex-end' }
                    }
                  : undefined
              }
            />
          ) : (
            <Text size='sm' c='dimmed'>
              No queries with table full scan detected.
            </Text>
          )}
        </div>
      </Stack>

      {/* Samples modal */}
      <Modal
        opened={!!samplesModal}
        onClose={() => setSamplesModal(null)}
        title={
          <Text fw={500}>Samples — {samplesModal?.digest.slice(0, 16)}...</Text>
        }
        size='90%'
      >
        {samplesModal && samplesModal.samples.length > 0 ? (
          <ProTable<TableFullScanSample>
            columns={sampleColumns}
            data={samplesModal.samples}
          />
        ) : (
          <Text size='sm' c='dimmed'>
            No samples available.
          </Text>
        )}
      </Modal>
    </>
  )
}

// ── Collapsible SQL display ──────────────────────────────────────────
const COLLAPSED_HEIGHT = 40

const CollapsibleSQL: React.FC<{
  sql: string
}> = ({ sql }) => {
  const [expanded, setExpanded] = useState(false)

  return (
    <div className='p-1'>
      <div
        style={{
          maxHeight: expanded ? undefined : COLLAPSED_HEIGHT,
          overflow: 'hidden'
        }}
      >
        <Prism
          noCopy
          language='sql'
          styles={{
            code: {
              backgroundColor: 'transparent !important',
              padding: 0,
              fontSize: 12,
              whiteSpace: 'pre-wrap',
              wordBreak: 'break-all'
            },
            line: { padding: 0 },
            lineContent: { whiteSpace: 'pre-wrap' }
          }}
        >
          {sql}
        </Prism>
      </div>
      <UnstyledButton
        onClick={() => setExpanded(!expanded)}
        style={{
          fontSize: 12,
          color: 'var(--mantine-color-blue-6)',
          marginTop: 2
        }}
      >
        {expanded ? 'Collapse' : 'Show all'}
      </UnstyledButton>
    </div>
  )
}

export default HealthCheckDetail
