<template>
  <CommonPage title="公司清洗控制台">
    <div class="monitor-shell">
      <section class="monitor-hero">
        <div class="hero-copy">
          <span class="hero-badge">MySQL Company Pipeline</span>
          <h1>公司清洗现在只写 MySQL，ClickHouse 只保留职位数据。</h1>
          <p>
            在这里统一完成候选提取、批量执行、失败重试和结果查看。队列、详情、状态统计都已经切到新的
            MySQL 流程。
          </p>
          <div class="hero-pills">
            <span class="hero-pill">运行中 {{ totalProcessing }}</span>
            <span class="hero-pill">活跃来源 {{ activeSources }}</span>
          </div>
        </div>
        <div class="hero-actions">
          <n-button type="primary" size="large" :loading="refreshing" @click="refreshAll">
            <template #icon>
              <TheIcon icon="mdi:refresh" />
            </template>
            刷新面板
          </n-button>
          <div class="hero-note">
            <span>最近刷新</span>
            <strong>{{ lastRefreshLabel }}</strong>
            <small>自动刷新 30s</small>
          </div>
        </div>
      </section>

      <!-- 爬虫入库统计区域 -->
      <section class="ingest-section">
        <div class="ingest-title">
          <TheIcon icon="mdi:spider-web" />
          <span>爬虫职位入库统计</span>
          <small>近 7 天 · 数据来自 ClickHouse</small>
        </div>
        <div class="ingest-platform-grid">
          <div
            v-for="p in ingestPlatformCards"
            :key="p.platform"
            class="ingest-platform-card"
          >
            <div class="ingest-platform-label">
              <TheIcon :icon="platformIconMap[p.platform]" />
              {{ p.label }}
            </div>
            <div class="ingest-platform-total">{{ p.total.toLocaleString() }}</div>
            <div class="ingest-platform-meta">
              今日 +{{ p.today }}
            </div>
            <div class="ingest-platform-time">
              最近: {{ p.last_ingest_at || '--' }}
            </div>
          </div>
        </div>
        <div class="ingest-trend" v-if="trendRows.length > 0">
          <div class="ingest-trend-label">近 7 天每日入库趋势</div>
          <n-data-table
            size="small"
            :columns="trendColumns"
            :data="trendRows"
            :pagination="false"
            :bordered="false"
            class="ingest-trend-table"
          />
        </div>
      </section>

      <section class="metric-grid">
        <n-card :bordered="false" class="metric-card metric-card--pending">
          <div class="metric-label">待处理队列</div>
          <div class="metric-value">{{ stats.total_pending || 0 }}</div>
          <div class="metric-meta">待抓取公司详情的记录数</div>
        </n-card>
        <n-card :bordered="false" class="metric-card metric-card--done">
          <div class="metric-label">今日已完成</div>
          <div class="metric-value">{{ stats.today_processed || 0 }}</div>
          <div class="metric-meta">今日成功写入 MySQL 的公司数</div>
        </n-card>
        <n-card :bordered="false" class="metric-card metric-card--failed">
          <div class="metric-label">职位写入</div>
          <div class="metric-value">{{ totalJobsStored }}</div>
          <div class="metric-meta">公司清洗后同步到 ClickHouse 的职位数</div>
        </n-card>
        <n-card :bordered="false" class="metric-card metric-card--rate">
          <div class="metric-label">整体完成率</div>
          <div class="metric-value">{{ completionRate }}%</div>
          <div class="metric-meta">当前 {{ totalProcessing }} 条记录正在执行</div>
        </n-card>
      </section>

      <section class="pipeline-strip">
        <div class="pipeline-step">
          <span>01</span>
          <strong>职位候选抽取</strong>
          <p>从 ClickHouse 职位表提取公司候选，不再依赖公司表。</p>
        </div>
        <div class="pipeline-step">
          <span>02</span>
          <strong>MySQL 队列去重</strong>
          <p>统一按来源和公司 ID 去重，进入待处理队列。</p>
        </div>
        <div class="pipeline-step">
          <span>03</span>
          <strong>详情抓取入库</strong>
          <p>结构化字段和 `raw_json` 同步写入 MySQL 公司表。</p>
        </div>
        <div class="pipeline-step">
          <span>04</span>
          <strong>结果监控重试</strong>
          <p>失败记录保留队列状态，可直接重试并查看最终 JSON。</p>
        </div>
      </section>

      <section class="control-grid">
        <n-card :bordered="false" class="control-card">
          <template #header>
            <div class="section-title">
              <TheIcon icon="mdi:magnify-scan" />
              <span>候选提取</span>
            </div>
          </template>
          <div class="control-body">
            <n-select
              v-model:value="selectedSource"
              :options="sourceOptions"
              placeholder="选择提取来源"
              class="control-field"
            />
            <n-input-number
              v-model:value="analyzeLimit"
              :min="1"
              :max="5000"
              class="control-field"
              placeholder="提取数量"
            >
              <template #suffix>条</template>
            </n-input-number>
            <n-button type="info" secondary :loading="analyzing" @click="handleAnalyze">
              开始提取
            </n-button>
          </div>
          <n-alert type="info" :show-icon="false">
            从 ClickHouse 的职位表抽取未清洗过的公司候选，写入 MySQL 队列表。
          </n-alert>
        </n-card>

        <n-card :bordered="false" class="control-card">
          <template #header>
            <div class="section-title">
              <TheIcon icon="mdi:database-sync" />
              <span>批量执行</span>
            </div>
          </template>
          <div class="control-body control-body--wide">
            <n-select
              v-model:value="globalProxy"
              :options="batchProxyOptions"
              placeholder="全局代理，可选"
              clearable
              class="control-field control-field--grow"
            />
            <n-input-number
              v-model:value="runLimit"
              :min="1"
              :max="5000"
              class="control-field"
              placeholder="执行数量"
            >
              <template #suffix>条</template>
            </n-input-number>
            <n-input-number
              v-model:value="batchMaxDelay"
              :min="1"
              :max="60"
              class="control-field"
              placeholder="最大延时"
            >
              <template #suffix>秒</template>
            </n-input-number>
          </div>
          <n-alert type="warning" :show-icon="false">
            这些配置会应用到批量执行和单条重试。
          </n-alert>
        </n-card>
      </section>

      <section class="platform-grid">
        <n-card
          v-for="platform in platformCards"
          :key="platform.source"
          :bordered="false"
          class="platform-card"
        >
          <div class="platform-header">
            <div>
              <div class="platform-title">
                <TheIcon :icon="platform.icon" />
                <span>{{ platform.label }}</span>
              </div>
              <div class="platform-subtitle">今日队列状态</div>
            </div>
            <n-tag size="small" :bordered="false" :type="platform.failed > 0 ? 'error' : 'success'">
              {{ platform.total }} 总量
            </n-tag>
          </div>

          <div class="platform-stats">
            <div class="platform-stat">
              <span>Pending</span>
              <strong>{{ platform.pending }}</strong>
            </div>
            <div class="platform-stat">
              <span>Done</span>
              <strong>{{ platform.done }}</strong>
            </div>
            <div class="platform-stat">
              <span>Processing</span>
              <strong>{{ platform.processing }}</strong>
            </div>
            <div class="platform-stat">
              <span>Failed</span>
              <strong>{{ platform.failed }}</strong>
            </div>
          </div>

          <div class="platform-job-strip">
            <span>职位同步</span>
            <strong>{{ platform.jobs_stored }}</strong>
            <small
              >抓取 {{ platform.jobs_fetched }} / 重复 {{ platform.jobs_duplicate }} / 失败
              {{ platform.jobs_failed }}</small
            >
          </div>

          <n-progress
            type="line"
            :percentage="platform.rate"
            :height="10"
            :show-indicator="false"
            processing
            class="platform-progress"
          />

          <div class="platform-footer">
            <span class="platform-rate">完成率 {{ platform.rate.toFixed(1) }}%</span>
            <div class="platform-actions">
              <n-button size="small" tertiary @click="focusQueue(platform.source)"> 队列 </n-button>
              <n-button
                size="small"
                type="primary"
                secondary
                :disabled="platform.pending === 0"
                :loading="!!platformRunningMap[platform.source]"
                @click="handleExecuteBySource(platform.source)"
              >
                执行
              </n-button>
            </div>
          </div>
        </n-card>
      </section>

      <n-card :bordered="false" class="queue-card">
        <template #header>
          <div class="section-title">
            <TheIcon icon="mdi:format-list-bulleted-square" />
            <span>实时队列</span>
          </div>
        </template>
        <template #header-extra>
          <n-button text type="primary" @click="fetchDetail">
            <template #icon>
              <TheIcon icon="mdi:refresh" />
            </template>
            刷新队列
          </n-button>
        </template>

        <div class="queue-toolbar">
          <div class="queue-filters">
            <n-select
              v-model:value="drawerSource"
              :options="sourceOptions"
              clearable
              placeholder="筛选来源"
              class="queue-filter"
              @update:value="handleDrawerFilter"
            />
            <n-select
              v-model:value="drawerStatus"
              :options="statusOptions"
              clearable
              placeholder="筛选状态"
              class="queue-filter"
              @update:value="handleDrawerFilter"
            />
          </div>
          <div class="queue-summary">
            <span>当前记录</span>
            <strong>{{ pagination.itemCount }}</strong>
          </div>
        </div>

        <n-data-table
          remote
          :columns="detailColumns"
          :data="detailData"
          :loading="detailLoading"
          :pagination="pagination"
          :scroll-x="1200"
          @update:page="handlePageChange"
        />
      </n-card>

      <n-card :bordered="false" class="guide-card">
        <template #header>
          <div class="section-title">
            <TheIcon icon="mdi:information-outline" />
            <span>运行说明</span>
          </div>
        </template>
        <div class="guide-grid">
          <div class="guide-item">
            <strong>候选提取</strong>
            <p>从 ClickHouse 职位表提取公司 ID，去重后写入 MySQL 队列。</p>
          </div>
          <div class="guide-item">
            <strong>批量执行</strong>
            <p>调用各站公司详情接口，把结构化字段和 `raw_json` 一起写入 MySQL。</p>
          </div>
          <div class="guide-item">
            <strong>失败重试</strong>
            <p>队列中 `failed` 记录可以单条重试，不再回写 ClickHouse 公司表。</p>
          </div>
        </div>
      </n-card>

      <n-modal
        v-model:show="showResultModal"
        preset="card"
        title="公司详情 JSON"
        style="width: 900px"
      >
        <n-code :code="resultJson" language="json" word-wrap />
      </n-modal>
      <n-modal
        v-model:show="showSingleResultModal"
        preset="card"
        title="任务执行结果"
        style="width: 900px"
      >
        <n-code :code="singleResultJson" language="json" word-wrap />
      </n-modal>
    </div>
  </CommonPage>
</template>

<script setup>
import { computed, h, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import {
  NAlert,
  NButton,
  NCard,
  NCode,
  NDataTable,
  NInputNumber,
  NModal,
  NProgress,
  NSelect,
  NTag,
  useMessage,
} from 'naive-ui'

import CommonPage from '@/components/page/CommonPage.vue'
import TheIcon from '@/components/icon/TheIcon.vue'
import api from '@/api'
import proxyApi from '@/api/proxy'

const message = useMessage()

const stats = ref({
  total_pending: 0,
  today_processed: 0,
  details: [],
})

// ─── 爬虫入库统计 ─────────────────────────────────────
const ingestStatsRaw = ref({})
const platformIconMap = {
  boss: 'mdi:alpha-b-box-outline',
  qcwy: 'mdi:alpha-q-box-outline',
  zhilian: 'mdi:alpha-z-box-outline',
}
const platformLabelMap = {
  boss: 'Boss直聘',
  qcwy: '前程无忧',
  zhilian: '智联招聘',
}

const ingestPlatformCards = computed(() =>
  ['boss', 'qcwy', 'zhilian'].map((p) => ({
    platform: p,
    label: platformLabelMap[p],
    total: ingestStatsRaw.value[p]?.total || 0,
    today: ingestStatsRaw.value[p]?.today || 0,
    last_ingest_at: ingestStatsRaw.value[p]?.last_ingest_at || null,
  }))
)

const trendColumns = computed(() => [
  { title: '日期', key: 'date', width: 110 },
  { title: 'Boss', key: 'boss', width: 80 },
  { title: '前程无忧', key: 'qcwy', width: 90 },
  { title: '智联招聘', key: 'zhilian', width: 90 },
])

const trendRows = computed(() => {
  const allDates = new Set()
  const dateMap = { boss: {}, qcwy: {}, zhilian: {} }
  ;['boss', 'qcwy', 'zhilian'].forEach((p) => {
    const daily = ingestStatsRaw.value[p]?.daily_counts || []
    daily.forEach((item) => {
      allDates.add(item.date)
      dateMap[p][item.date] = item.count
    })
  })
  return [...allDates].sort().reverse().map((date) => ({
    date,
    boss: dateMap.boss[date] ?? 0,
    qcwy: dateMap.qcwy[date] ?? 0,
    zhilian: dateMap.zhilian[date] ?? 0,
  }))
})

const fetchIngestStats = async () => {
  try {
    const res = await api.getIngestStats({ days: 7 })
    if (res.code === 200) {
      ingestStatsRaw.value = res.data || {}
    }
  } catch (e) {
    // 静默失败，不影响主监控面板
    console.warn('fetchIngestStats failed:', e)
  }
}
const refreshing = ref(false)
const analyzing = ref(false)
const detailLoading = ref(false)
const lastRefreshedAt = ref(null)

const detailData = ref([])
const showResultModal = ref(false)
const resultJson = ref('')
const showSingleResultModal = ref(false)
const singleResultJson = ref('')

const allProxies = ref([])
const executingMap = ref({})
const platformRunningMap = ref({})

const runLimit = ref(100)
const analyzeLimit = ref(100)
const selectedSource = ref(null)
const globalProxy = ref(null)
const batchMaxDelay = ref(5)
const drawerSource = ref(null)
const drawerStatus = ref(null)

const pagination = reactive({
  page: 1,
  pageSize: 20,
  itemCount: 0,
  prefix: ({ itemCount }) => `共 ${itemCount} 条`,
})

const sourceOptions = [
  { label: '全部数据源', value: null },
  { label: 'Boss直聘', value: 'boss' },
  { label: '前程无忧', value: 'qcwy' },
  { label: '智联招聘', value: 'zhilian' },
]

const statusOptions = [
  { label: '全部状态', value: null },
  { label: '待处理', value: 'pending' },
  { label: '处理中', value: 'processing' },
  { label: '已完成', value: 'done' },
  { label: '失败', value: 'failed' },
]

const platformMeta = {
  boss: { label: 'Boss直聘', icon: 'mdi:alpha-b-box-outline' },
  qcwy: { label: '前程无忧', icon: 'mdi:alpha-q-box-outline' },
  zhilian: { label: '智联招聘', icon: 'mdi:alpha-z-box-outline' },
}
const platformSources = ['boss', 'qcwy', 'zhilian']

const batchProxyOptions = computed(() => {
  if (!allProxies.value.length) return []
  const source = selectedSource.value
  return allProxies.value
    .filter((item) => {
      if (!item.is_active) return false
      if (!source) return true
      return item.platform === source || item.platform === 'all'
    })
    .map((item) => ({
      label: `${item.name} (${item.proxy_type.toUpperCase()})`,
      value: item.proxy_url,
    }))
})

const normalizedDetails = computed(() => {
  const detailMap = new Map((stats.value.details || []).map((item) => [item.source, item]))
  return platformSources.map((source) => {
    const item = detailMap.get(source) || {}
    const pending = Number(item.pending || 0)
    const processing = Number(item.processing || 0)
    const done = Number(item.done || 0)
    const failed = Number(item.failed || 0)
    const jobsFetched = Number(item.jobs_fetched || 0)
    const jobsStored = Number(item.jobs_stored || 0)
    const jobsDuplicate = Number(item.jobs_duplicate || 0)
    const jobsFailed = Number(item.jobs_failed || 0)
    const total = Number(item.total || pending + processing + done + failed)
    return {
      source,
      pending,
      processing,
      done,
      failed,
      jobs_fetched: jobsFetched,
      jobs_stored: jobsStored,
      jobs_duplicate: jobsDuplicate,
      jobs_failed: jobsFailed,
      total,
      rate: total ? (done / total) * 100 : 0,
    }
  })
})

const totalFailed = computed(() =>
  normalizedDetails.value.reduce((sum, item) => sum + (item.failed || 0), 0)
)

const totalJobsStored = computed(() =>
  normalizedDetails.value.reduce((sum, item) => sum + (item.jobs_stored || 0), 0)
)

const totalProcessing = computed(() =>
  normalizedDetails.value.reduce((sum, item) => sum + (item.processing || 0), 0)
)

const activeSources = computed(
  () => normalizedDetails.value.filter((item) => item.total > 0).length
)

const lastRefreshLabel = computed(() => {
  if (!lastRefreshedAt.value) return '--:--:--'
  return new Date(lastRefreshedAt.value).toLocaleTimeString('zh-CN', { hour12: false })
})

const completionRate = computed(() => {
  const total = normalizedDetails.value.reduce((sum, item) => sum + item.total, 0)
  const done = normalizedDetails.value.reduce((sum, item) => sum + item.done, 0)
  return total ? ((done / total) * 100).toFixed(1) : '0.0'
})

const platformCards = computed(() =>
  normalizedDetails.value.map((item) => ({
    ...item,
    ...platformMeta[item.source],
  }))
)

const detailColumns = [
  {
    title: '平台',
    key: 'source',
    width: 100,
    render(row) {
      return platformMeta[row.source]?.label || row.source
    },
  },
  {
    title: '公司名称',
    key: 'company_name',
    width: 220,
    ellipsis: { tooltip: true },
  },
  {
    title: '公司ID',
    key: 'company_id',
    width: 160,
    ellipsis: { tooltip: true },
  },
  {
    title: '状态',
    key: 'status',
    width: 100,
    render(row) {
      const typeMap = { pending: 'warning', processing: 'info', done: 'success', failed: 'error' }
      return h(
        NTag,
        { type: typeMap[row.status] || 'default', size: 'small', bordered: false },
        { default: () => row.status }
      )
    },
  },
  {
    title: '错误信息',
    key: 'error_msg',
    minWidth: 220,
    ellipsis: { tooltip: true },
    render(row) {
      return row.error_msg || '-'
    },
  },
  {
    title: '职位同步',
    key: 'jobs_summary',
    minWidth: 220,
    render(row) {
      return `${row.jobs_stored || 0} 入库 / ${row.jobs_duplicate || 0} 重复 / ${
        row.jobs_failed || 0
      } 失败`
    },
  },
  {
    title: '更新时间',
    key: 'updated_at',
    width: 180,
    render(row) {
      return row.updated_at ? row.updated_at.replace('T', ' ').split('.')[0] : '-'
    },
  },
  {
    title: '操作',
    key: 'actions',
    width: 170,
    fixed: 'right',
    render(row) {
      if (row.status === 'pending' || row.status === 'failed') {
        return h(
          NButton,
          {
            size: 'small',
            type: 'primary',
            loading: !!executingMap.value[row.company_id],
            onClick: () => handleExecuteSingle(row),
          },
          { default: () => (row.status === 'failed' ? '重试' : '执行') }
        )
      }
      if (row.status === 'processing') {
        return h(
          NButton,
          {
            size: 'small',
            disabled: true,
          },
          { default: () => '处理中' }
        )
      }
      return h(
        NButton,
        {
          size: 'small',
          tertiary: true,
          onClick: () => viewCompanyResult(row),
        },
        { default: () => '查看结果' }
      )
    },
  },
]

const fetchStats = async () => {
  try {
    const res = await api.getCompanyCleaningStats()
    if (res.code === 200) {
      stats.value = res.data || { total_pending: 0, today_processed: 0, details: [] }
    }
  } catch (e) {
    message.error(`刷新统计失败: ${e.message}`)
  }
}

const fetchDetail = async () => {
  detailLoading.value = true
  try {
    const res = await api.getCompanyCleaningList({
      page: pagination.page,
      page_size: pagination.pageSize,
      source: drawerSource.value || undefined,
      status: drawerStatus.value || undefined,
    })
    if (res.code === 200) {
      detailData.value = res.data || []
      pagination.itemCount = res.total || 0
    }
  } finally {
    detailLoading.value = false
  }
}

const refreshAll = async () => {
  refreshing.value = true
  try {
    await Promise.all([fetchStats(), fetchDetail(), fetchIngestStats()])
    lastRefreshedAt.value = Date.now()
  } finally {
    refreshing.value = false
  }
}

const handleDrawerFilter = () => {
  pagination.page = 1
  fetchDetail()
}

const handlePageChange = (page) => {
  pagination.page = page
  fetchDetail()
}

const focusQueue = (source) => {
  drawerSource.value = source
  drawerStatus.value = null
  pagination.page = 1
  fetchDetail()
}

const viewCompanyResult = async (row) => {
  if (row.status !== 'done') {
    message.warning('当前记录未完成清洗')
    return
  }
  try {
    const res = await api.getCompanyCleaningDetail({
      source: row.source,
      company_id: row.company_id,
      company_name: row.company_name,
    })
    if (res.code === 200 && res.data) {
      const content = res.data.data || res.data
      resultJson.value = JSON.stringify(content, null, 2)
      showResultModal.value = true
    } else {
      message.warning(res.msg || '未找到公司清洗结果')
    }
  } catch (e) {
    message.error('获取公司详情失败')
  }
}

const handleAnalyze = async () => {
  if (!analyzeLimit.value || analyzeLimit.value <= 0) {
    message.warning('请先填写提取数量')
    return
  }
  try {
    analyzing.value = true
    const res = await api.collectPendingCompanies({
      limit: analyzeLimit.value,
      source: selectedSource.value || null,
    })
    const totalCreated = Number(res?.data?.total_created || 0)
    const sourceSummaries = Object.values(res?.data?.sources || {})
    const sourceText = sourceSummaries
      .map(
        (item) => `${platformMeta[item.source]?.label || item.source} +${item.created_count || 0}`
      )
      .join('，')
    if (totalCreated > 0) {
      message.success(sourceText || `已完成候选提取，新增 ${totalCreated} 条`)
    } else {
      message.warning(
        sourceText || '本次没有新增候选，通常是该来源最近窗口内没有可提取数据，或已全部入库/入队'
      )
    }
    await refreshAll()
  } catch (e) {
    message.error(`数据提取失败: ${e.message}`)
  } finally {
    analyzing.value = false
  }
}

const handleExecuteSingle = async (row) => {
  try {
    executingMap.value[row.company_id] = true
    const payload = {
      source: row.source,
      company_id: row.company_id,
      max_delay_seconds: batchMaxDelay.value || 5,
    }
    if (globalProxy.value) {
      payload.proxy = globalProxy.value
    }
    const res = await api.runSingleCompanyCleaning(payload)
    const success = !!(res && res.code === 200 && res.data && res.data.success)
    if (success) {
      message.success('任务执行成功')
      await refreshAll()
      return
    }
    singleResultJson.value = JSON.stringify({ request: payload, response: res }, null, 2)
    showSingleResultModal.value = true
    message.error((res && res.msg) || '任务执行失败')
  } catch (e) {
    message.error(`执行异常: ${e.message}`)
  } finally {
    executingMap.value[row.company_id] = false
  }
}

const handleExecuteBySource = async (source) => {
  if (!runLimit.value || runLimit.value <= 0) {
    message.warning('请先填写执行数量')
    return
  }
  try {
    platformRunningMap.value[source] = true
    const payload = {
      limit: runLimit.value,
      source,
      max_delay_seconds: batchMaxDelay.value || 5,
    }
    if (globalProxy.value) {
      payload.proxy = globalProxy.value
    }
    const res = await api.runPendingCompanies(payload)
    if (res.code === 200) {
      message.success(res.msg || '已触发批量任务')
      await refreshAll()
      return
    }
    message.error(res.msg || '触发失败')
  } catch (e) {
    message.error(`触发失败: ${e.message}`)
  } finally {
    platformRunningMap.value[source] = false
  }
}

const loadProxies = async () => {
  try {
    const res = await proxyApi.list({ page: 1, page_size: 100, is_active: true })
    allProxies.value = res.data || res.data?.data || []
  } catch (e) {
    allProxies.value = []
  }
}

let refreshTimer = null

onMounted(() => {
  refreshAll()
  loadProxies()
  refreshTimer = window.setInterval(() => {
    refreshAll()
  }, 30000)
})

onBeforeUnmount(() => {
  if (refreshTimer) {
    window.clearInterval(refreshTimer)
  }
})
</script>

<style scoped>
.monitor-shell {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

/* ─── 爬虫统计区域 ─────────────────────────────────── */
.ingest-section {
  background: var(--n-card-color, #fff);
  border-radius: 16px;
  padding: 20px 24px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  box-shadow: 0 1px 3px rgba(0,0,0,.06);
}

.ingest-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  font-weight: 600;
  color: var(--n-text-color);
}

.ingest-title small {
  font-weight: 400;
  font-size: 12px;
  opacity: .6;
  margin-left: 4px;
}

.ingest-platform-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
}

.ingest-platform-card {
  background: var(--n-card-color, #f8fafc);
  border: 1px solid var(--n-border-color, #e5e7eb);
  border-radius: 12px;
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.ingest-platform-label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  font-weight: 600;
  color: var(--n-text-color-disabled);
  text-transform: uppercase;
  letter-spacing: .04em;
}

.ingest-platform-total {
  font-size: 28px;
  font-weight: 700;
  color: var(--n-text-color);
  line-height: 1.2;
  margin-top: 4px;
}

.ingest-platform-meta {
  font-size: 12px;
  color: #10b981;
  font-weight: 500;
}

.ingest-platform-time {
  font-size: 11px;
  opacity: .5;
  margin-top: 2px;
}

.ingest-trend-label {
  font-size: 12px;
  font-weight: 600;
  opacity: .6;
  margin-bottom: 6px;
}

.ingest-trend-table {
  border-radius: 8px;
  overflow: hidden;
}

.monitor-hero {
  display: flex;
  justify-content: space-between;
  gap: 20px;
  padding: 28px;
  border-radius: 24px;
  color: #f8fafc;
  background: radial-gradient(circle at top right, rgba(255, 255, 255, 0.16), transparent 32%),
    linear-gradient(135deg, #0f172a, #153e75 52%, #0f766e);
}

.hero-copy {
  max-width: 720px;
}

.hero-badge {
  display: inline-flex;
  padding: 6px 12px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  background: rgba(255, 255, 255, 0.14);
}

.hero-copy h1 {
  margin: 14px 0 10px;
  font-size: 32px;
  line-height: 1.15;
  font-weight: 800;
}

.hero-copy p {
  margin: 0;
  max-width: 640px;
  color: rgba(248, 250, 252, 0.8);
  line-height: 1.65;
}

.hero-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 18px;
}

.hero-pill {
  display: inline-flex;
  align-items: center;
  padding: 8px 12px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 700;
  color: #e2e8f0;
  background: rgba(15, 23, 42, 0.26);
}

.hero-actions {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
}

.hero-note {
  display: grid;
  gap: 4px;
  min-width: 120px;
  padding: 14px 16px;
  border-radius: 16px;
  text-align: right;
  background: rgba(15, 23, 42, 0.26);
}

.hero-note span {
  font-size: 12px;
  color: rgba(248, 250, 252, 0.72);
}

.hero-note strong {
  font-size: 22px;
  line-height: 1;
}

.hero-note small {
  font-size: 12px;
  color: rgba(248, 250, 252, 0.72);
}

.metric-grid,
.control-grid,
.platform-grid,
.guide-grid,
.pipeline-strip {
  display: grid;
  gap: 16px;
}

.metric-grid {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

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

.platform-grid {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.pipeline-strip {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.metric-card,
.control-card,
.platform-card,
.queue-card,
.guide-card {
  border-radius: 22px;
  overflow: hidden;
  box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
}

.metric-card {
  color: #0f172a;
}

.metric-card :deep(.n-card__content),
.control-card :deep(.n-card__content),
.platform-card :deep(.n-card__content),
.queue-card :deep(.n-card__content),
.guide-card :deep(.n-card__content) {
  display: grid;
  gap: 12px;
}

.metric-card--pending {
  background: linear-gradient(160deg, #fff7ed, #ffedd5);
}

.metric-card--done {
  background: linear-gradient(160deg, #ecfdf5, #d1fae5);
}

.metric-card--failed {
  background: linear-gradient(160deg, #fef2f2, #fee2e2);
}

.metric-card--rate {
  background: linear-gradient(160deg, #eff6ff, #dbeafe);
}

.metric-label {
  font-size: 13px;
  font-weight: 700;
  color: rgba(15, 23, 42, 0.7);
}

.metric-value {
  font-size: 36px;
  line-height: 1;
  font-weight: 800;
}

.metric-meta {
  font-size: 13px;
  color: rgba(15, 23, 42, 0.62);
}

.section-title {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-size: 16px;
  font-weight: 700;
}

.control-body {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
}

.control-body--wide {
  align-items: stretch;
}

.control-field {
  width: 160px;
}

.control-field--grow {
  flex: 1 1 240px;
  min-width: 220px;
}

.platform-card {
  background: radial-gradient(circle at top right, rgba(59, 130, 246, 0.1), transparent 28%),
    linear-gradient(180deg, #ffffff, #f8fafc);
}

.platform-header,
.platform-footer,
.queue-toolbar {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  align-items: center;
}

.platform-title {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 18px;
  font-weight: 700;
}

.platform-subtitle,
.platform-rate,
.queue-summary span,
.guide-item p {
  color: #64748b;
}

.platform-stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
}

.platform-stat {
  padding: 12px 14px;
  border-radius: 16px;
  background: #f8fafc;
}

.platform-stat span {
  display: block;
  font-size: 12px;
  color: #64748b;
}

.platform-stat strong {
  display: block;
  margin-top: 6px;
  font-size: 24px;
  line-height: 1;
}

.platform-progress {
  margin-top: -2px;
}

.platform-job-strip {
  display: grid;
  gap: 4px;
  padding: 12px 14px;
  border-radius: 16px;
  background: linear-gradient(135deg, #eff6ff, #ecfeff);
}

.platform-job-strip span {
  font-size: 12px;
  color: #64748b;
}

.platform-job-strip strong {
  font-size: 24px;
  line-height: 1;
}

.platform-job-strip small {
  color: #475569;
}

.platform-actions,
.queue-filters {
  display: flex;
  gap: 10px;
  align-items: center;
}

.queue-card :deep(.n-card-header),
.guide-card :deep(.n-card-header),
.control-card :deep(.n-card-header) {
  padding-bottom: 8px;
}

.queue-filter {
  width: 170px;
}

.queue-summary {
  display: inline-flex;
  gap: 10px;
  align-items: baseline;
}

.queue-summary strong {
  font-size: 22px;
  line-height: 1;
}

.pipeline-step {
  padding: 18px;
  border-radius: 20px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(248, 250, 252, 0.98)),
    linear-gradient(135deg, #dbeafe, #f0fdf4);
  box-shadow: 0 14px 30px rgba(15, 23, 42, 0.05);
}

.pipeline-step span {
  display: inline-flex;
  margin-bottom: 10px;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.08em;
  color: #0f766e;
}

.pipeline-step strong {
  display: block;
  margin-bottom: 8px;
  font-size: 16px;
}

.pipeline-step p {
  margin: 0;
  line-height: 1.65;
  color: #64748b;
}

.guide-grid {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.guide-item {
  padding: 16px;
  border-radius: 18px;
  background: #f8fafc;
}

.guide-item strong {
  display: block;
  margin-bottom: 6px;
}

.guide-item p {
  margin: 0;
  line-height: 1.6;
}

@media (max-width: 1200px) {
  .metric-grid,
  .platform-grid,
  .guide-grid,
  .pipeline-strip {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 900px) {
  .monitor-hero,
  .control-grid,
  .metric-grid,
  .platform-grid,
  .guide-grid,
  .pipeline-strip {
    grid-template-columns: 1fr;
  }

  .monitor-hero,
  .hero-actions,
  .queue-toolbar {
    align-items: stretch;
    text-align: left;
  }

  .hero-actions {
    align-items: flex-start;
  }

  .hero-note {
    text-align: left;
  }

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

  .queue-filters {
    width: 100%;
    flex-wrap: wrap;
  }

  .queue-filter,
  .control-field,
  .control-field--grow {
    width: 100%;
  }
}
</style>
