<template>
  <div class="ingest-view">
    <!-- 产品说明横幅 -->
    <section class="banner">
      <div class="banner-text">
        <div class="banner-title-row">
          <h3 class="banner-title">一键导入</h3>
          <span class="banner-tag">AUTOMATED</span>
        </div>
        <p class="banner-desc">
          自动扫描输入目录，完成音频完整性校验、格式转码、指纹去重与元数据检查，
          合格文件整理入库，异常文件归入拒绝目录。全流程状态实时回传。
        </p>
      </div>
      <span class="banner-batch">PIPELINE: INPUT → LIBRARY / REJECTED</span>
    </section>

    <!-- 5/7 分栏 -->
    <div class="split">
      <!-- 左：控制卡 -->
      <div class="split-left">
        <div class="panel control-card">
          <div class="panel-head">
            <app-icon name="folder" :size="16" class="panel-head-icon" />
            <h4 class="panel-head-title">当前挂载路径</h4>
            <button
              class="icon-btn"
              type="button"
              title="刷新配置"
              aria-label="刷新配置"
              :disabled="configLoading"
              @click="reloadConfig"
            >
              <app-icon name="refresh" :size="14" :class="{ spin: configLoading }" />
            </button>
          </div>

          <div class="path-list">
            <div class="path-field">
              <span class="path-label">输入目录 (INPUT)</span>
              <div class="path-value">
                <app-icon name="login" :size="14" class="path-value-icon" />
                <span class="path-text selectable" :title="appState.config.inputDir || '未配置'">
                  {{ appState.config.inputDir || '未配置' }}
                </span>
                <button
                  v-if="appState.config.inputDir"
                  class="copy-btn"
                  type="button"
                  title="复制路径"
                  aria-label="复制输入目录"
                  @click="copyPath(appState.config.inputDir)"
                >
                  <app-icon name="check" :size="12" />
                </button>
              </div>
            </div>

            <div class="path-field">
              <span class="path-label">入库目录 (LIBRARY)</span>
              <div class="path-value">
                <app-icon name="shield" :size="14" class="path-value-icon" />
                <span class="path-text selectable" :title="appState.config.libraryDir || '未配置'">
                  {{ appState.config.libraryDir || '未配置' }}
                </span>
                <button
                  v-if="appState.config.libraryDir"
                  class="copy-btn"
                  type="button"
                  title="复制路径"
                  aria-label="复制入库目录"
                  @click="copyPath(appState.config.libraryDir)"
                >
                  <app-icon name="check" :size="12" />
                </button>
              </div>
            </div>

            <div class="path-field">
              <span class="path-label">拒绝目录 (REJECTED)</span>
              <div class="path-value">
                <app-icon name="reject" :size="14" class="path-value-icon" />
                <span class="path-text selectable" :title="appState.config.rejectedDir || '未配置'">
                  {{ appState.config.rejectedDir || '未配置' }}
                </span>
                <button
                  v-if="appState.config.rejectedDir"
                  class="copy-btn"
                  type="button"
                  title="复制路径"
                  aria-label="复制拒绝目录"
                  @click="copyPath(appState.config.rejectedDir)"
                >
                  <app-icon name="check" :size="12" />
                </button>
              </div>
            </div>
          </div>

          <!-- 未配置提示 -->
          <div v-if="appState.config.attempted && !isConfigured" class="notice notice-warn">
            <app-icon name="warn" :size="16" class="notice-icon" />
            <div class="notice-body">
              <p class="notice-title">尚未配置工作根目录</p>
              <p class="notice-desc">请前往「全局存储配置」设置根目录后再执行导入。</p>
            </div>
          </div>

          <!-- 操作按钮 -->
          <div class="control-actions">
            <button
              v-if="!running"
              class="btn btn-primary"
              type="button"
              :disabled="!canStart"
              @click="startIngest"
            >
              <app-icon name="play" :size="16" :filled="true" />
              <span>执行一键入库</span>
            </button>
            <button
              v-else
              class="btn btn-running"
              type="button"
              disabled
            >
              <app-icon name="refresh" :size="16" class="spin" />
              <span>正在导入分析… ({{ percentage }}%)</span>
            </button>

            <button
              class="btn btn-ghost"
              type="button"
              :disabled="running || activity.length === 0"
              :title="running ? '任务运行中不可清除' : '仅清除本地活动记录，不影响后端统计'"
              @click="clearActivity"
            >
              <app-icon name="refresh" :size="14" />
              <span>清除本地活动记录</span>
            </button>
          </div>

          <p class="control-hint">
            <app-icon name="info" :size="14" class="hint-icon" />
            <span>路径来自「全局存储配置」。此处仅展示只读真值，修改请前往配置页。</span>
          </p>
        </div>
      </div>

      <!-- 右：遥测面板 -->
      <div class="split-right">
        <div class="panel telemetry-card">
          <div class="panel-head">
            <app-icon name="chart" :size="16" class="panel-head-icon" />
            <h4 class="panel-head-title">智能入库监控台</h4>
            <span class="run-badge" :class="runBadgeClass">{{ runBadgeText }}</span>
          </div>

          <!-- 主统计 -->
          <div class="stats-grid stats-primary">
            <div class="stat-cell">
              <span class="stat-name">已处理</span>
              <span class="stat-num">{{ progress.processed ?? 0 }}</span>
            </div>
            <div class="stat-cell tone-ok">
              <span class="stat-name">已入库</span>
              <span class="stat-num">{{ progress.ingestedFiles ?? 0 }}</span>
            </div>
            <div class="stat-cell tone-warn">
              <span class="stat-name">重复跳过</span>
              <span class="stat-num">{{ progress.duplicateFiles ?? 0 }}</span>
            </div>
            <div class="stat-cell tone-purple">
              <span class="stat-name">缺元数据</span>
              <span class="stat-num">{{ progress.missingMetadataFiles ?? 0 }}</span>
            </div>
          </div>

          <!-- 次级错误统计 -->
          <div class="stats-grid stats-secondary">
            <div class="stat-cell-sm tone-error">
              <span class="stat-name-sm">损坏不可读</span>
              <span class="stat-num-sm">{{ progress.unreadableFiles ?? 0 }}</span>
            </div>
            <div class="stat-cell-sm">
              <span class="stat-name-sm">转码失败</span>
              <span class="stat-num-sm">{{ progress.conversionFailedFiles ?? 0 }}</span>
            </div>
            <div class="stat-cell-sm">
              <span class="stat-name-sm">其他拒绝</span>
              <span class="stat-num-sm">{{ progress.otherRejectedFiles ?? 0 }}</span>
            </div>
          </div>

          <!-- 进度条 -->
          <div class="progress-block">
            <div class="progress-meta">
              <span>总体处理进度</span>
              <span class="progress-pct" :class="progressToneClass">{{ percentage }}%</span>
            </div>
            <div class="progress-track">
              <div
                class="progress-fill"
                :class="progressToneClass"
                :style="{ width: percentage + '%' }"
              ></div>
            </div>
          </div>

          <!-- 当前文件 / 最新消息 -->
          <div class="current-line">
            <template v-if="progress.currentFile">
              <span class="current-tag">当前</span>
              <span class="current-file" :title="progress.currentFile">{{ progress.currentFile }}</span>
            </template>
            <template v-else-if="latestMessage">
              <span class="current-tag">消息</span>
              <span class="current-file" :title="latestMessage">{{ latestMessage }}</span>
            </template>
            <template v-else>
              <span class="current-idle">等待任务开始…</span>
            </template>
          </div>

          <!-- 完成状态汇总 -->
          <div v-if="progress.completed" class="result-banner" :class="resultClass">
            <app-icon :name="resultIcon" :size="16" class="result-icon" />
            <span>{{ resultText }}</span>
          </div>

          <!-- 活动记录 -->
          <div class="console-head">
            <div class="console-legend">
              <span class="legend-dot dot-error"></span>
              <span class="legend-dot dot-warn"></span>
              <span class="legend-dot dot-ok"></span>
              <span class="console-legend-label">活动记录</span>
            </div>
            <div class="filter-group" role="group" aria-label="活动记录筛选">
              <button
                v-for="f in filters"
                :key="f.key"
                type="button"
                class="filter-btn"
                :class="{ 'is-active': activeFilter === f.key }"
                @click="activeFilter = f.key"
              >
                {{ f.label }}
              </button>
            </div>
          </div>

          <div
            ref="consoleEl"
            class="console-body c-scroll"
            @scroll="onConsoleScroll"
          >
            <div v-if="filteredActivity.length === 0" class="console-empty">
              <app-icon name="wave" :size="30" class="console-empty-icon" />
              <span>{{ activity.length === 0 ? '暂无活动记录' : '当前筛选无匹配记录' }}</span>
              <span class="console-empty-hint">活动记录仅在本次会话内存中构建，刷新页面后无法恢复历史</span>
            </div>
            <div v-else class="console-lines">
              <div
                v-for="entry in filteredActivity"
                :key="entry.id"
                class="log-line"
              >
                <span class="log-time">{{ entry.time }}</span>
                <span class="log-badge" :class="'badge-' + entry.level">{{ entry.badge }}</span>
                <span class="log-text">
                  <span v-if="entry.file" class="log-file">{{ entry.file }}</span>
                  <span class="log-msg">{{ entry.message }}</span>
                </span>
              </div>
            </div>
          </div>
          <p class="console-note">
            共 {{ activity.length }} 条本地记录（上限 300）。历史记录随页面刷新清空，无法重建。
          </p>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { ElMessage } from 'element-plus';
import AppIcon from './common/AppIcon.vue';
import { startIngest as apiStartIngest, getIngestStatus, getIngestCurrentStatus } from '../api/ingest';
import type { IngestStatusResponse } from '../api/ingest';
import { useWebSocket } from '../composables/useWebSocket';
import type { ProgressMessage } from '../composables/useWebSocket';
import { useAppState } from '../composables/useAppState';

const {
  state: appState,
  isConfigured,
  refreshConfig,
  setIngestRunning,
  setIngestPercentage,
  claimIngestViewOwnership,
  releaseIngestViewOwnership
} = useAppState();

const submitting = ref(false);
const configLoading = ref(false);
const taskId = ref<string | null>(null);

/** 任务运行态：提交中或后端上报 running。 */
const running = computed(() => submitting.value);

const progress = reactive<ProgressMessage>({
  taskId: '',
  type: '',
  total: 0,
  processed: 0,
  success: 0,
  failed: 0,
  currentFile: '',
  message: '',
  completed: false,
  ingestedFiles: 0,
  duplicateFiles: 0,
  missingMetadataFiles: 0,
  unreadableFiles: 0,
  conversionFailedFiles: 0,
  otherRejectedFiles: 0
});

const canStart = computed(() => isConfigured.value && !submitting.value);

const percentage = computed(() => {
  if (progress.completed) return 100;
  if (!progress.total || !progress.processed) return 0;
  return Math.min(100, Math.round((progress.processed / progress.total) * 100));
});

/**
 * 被拒绝类别之和（不含已入库、不含重复跳过）。
 * 重复文件是设计上的跳过，不计入拒绝/部分失败。
 */
const totalRejected = computed(() =>
  (progress.missingMetadataFiles ?? 0)
  + (progress.unreadableFiles ?? 0)
  + (progress.conversionFailedFiles ?? 0)
  + (progress.otherRejectedFiles ?? 0)
);

const isFatalError = computed(() => {
  const msg = progress.message || '';
  return msg.startsWith('导入任务失败') || msg.startsWith('任务内部错误') || msg.includes('异常终止');
});

const latestMessage = computed(() => progress.message || '');

/** 运行状态徽章。 */
const runBadgeText = computed(() => {
  if (submitting.value) return 'INGESTING';
  if (progress.completed) {
    if (isFatalError.value) return 'FAILED';
    if (totalRejected.value > 0) return 'PARTIAL';
    return 'DONE';
  }
  if (progress.taskId) return 'IDLE';
  return 'STANDBY';
});

const runBadgeClass = computed(() => {
  if (submitting.value) return 'badge-run-active';
  if (progress.completed) {
    if (isFatalError.value) return 'badge-run-error';
    if (totalRejected.value > 0) return 'badge-run-warn';
    return 'badge-run-ok';
  }
  return 'badge-run-idle';
});

const progressToneClass = computed(() => {
  if (progress.completed && isFatalError.value) return 'tone-error';
  if (progress.completed && totalRejected.value > 0) return 'tone-warn';
  return 'tone-ok';
});

const resultClass = computed(() => {
  if (isFatalError.value) return 'result-error';
  if (totalRejected.value > 0) return 'result-warn';
  return 'result-ok';
});

const resultIcon = computed(() => {
  if (isFatalError.value) return 'warn';
  if (totalRejected.value > 0) return 'info';
  return 'check';
});

const resultText = computed(() => {
  if (isFatalError.value) {
    return progress.message || '导入任务失败';
  }
  if (totalRejected.value > 0) {
    return `导入完成：成功入库 ${progress.ingestedFiles ?? 0} 首，${totalRejected.value} 首被拒绝`;
  }
  const dups = progress.duplicateFiles ?? 0;
  if (dups > 0) {
    return `导入完成：成功入库 ${progress.ingestedFiles ?? 0} 首，${dups} 首重复跳过`;
  }
  return `导入完成：全部 ${progress.ingestedFiles ?? progress.processed ?? 0} 首成功入库`;
});

/* ---------------- 活动记录（仅由真实状态转移构建） ---------------- */

type ActivityLevel = 'normal' | 'warning' | 'error';

interface ActivityEntry {
  id: number;
  time: string;
  level: ActivityLevel;
  badge: string;
  file: string;
  message: string;
}

const ACTIVITY_CAP = 300;
const activity = reactive<ActivityEntry[]>([]);
let activitySeq = 0;
/** 去重键：连续相同的（file+message）不重复入栈（应对轮询重复）。 */
let lastActivityKey = '';

const filters = [
  { key: 'all', label: '全部' },
  { key: 'normal', label: '正常' },
  { key: 'warning', label: '警告' },
  { key: 'error', label: '错误' }
] as const;

type FilterKey = (typeof filters)[number]['key'];
const activeFilter = ref<FilterKey>('all');

const filteredActivity = computed(() => {
  if (activeFilter.value === 'all') return activity;
  return activity.filter((e) => e.level === activeFilter.value);
});

/** 根据真实进度消息推断记录级别与徽章。 */
function classifyLevel(msg: ProgressMessage): { level: ActivityLevel; badge: string } {
  const text = msg.message || '';
  const fatal =
    text.startsWith('导入任务失败') || text.startsWith('任务内部错误') || text.includes('异常终止');
  if (fatal) return { level: 'error', badge: '[失败]' };
  if (text.includes('损坏') || text.includes('不可读') || text.includes('转码失败')) {
    return { level: 'error', badge: '[错误]' };
  }
  // 重复/跳过按设计是警告级事件记录，但不计入完成态「拒绝」统计。
  if (text.includes('重复') || text.includes('跳过') || text.includes('缺') || text.includes('拒绝')) {
    return { level: 'warning', badge: '[警告]' };
  }
  if (msg.completed) {
    // 完成分类仅看真实拒绝类别，不含 duplicateFiles。
    const rejected =
      (msg.missingMetadataFiles ?? 0)
      + (msg.unreadableFiles ?? 0)
      + (msg.conversionFailedFiles ?? 0)
      + (msg.otherRejectedFiles ?? 0);
    if (rejected > 0) return { level: 'warning', badge: '[部分]' };
    return { level: 'normal', badge: '[完成]' };
  }
  return { level: 'normal', badge: '[进行]' };
}

/**
 * 由真实状态转移追加活动记录。
 * - 仅在 file 或 message 发生变化时记录，避免轮询重复刷屏。
 * - 上限 300，超出丢弃最旧。
 */
function recordActivity(msg: ProgressMessage) {
  const file = msg.currentFile || '';
  const message = msg.message || '';
  if (!file && !message) return;

  const key = `${file}||${message}`;
  if (key === lastActivityKey) return;
  lastActivityKey = key;

  const { level, badge } = classifyLevel(msg);
  activity.push({
    id: ++activitySeq,
    time: new Date().toLocaleTimeString(),
    level,
    badge,
    file,
    message
  });

  if (activity.length > ACTIVITY_CAP) {
    activity.splice(0, activity.length - ACTIVITY_CAP);
  }

  void scrollConsoleIfPinned();
}

function clearActivity() {
  if (submitting.value) return;
  activity.splice(0, activity.length);
  lastActivityKey = '';
  ElMessage.success('已清除本地活动记录');
}

/* ---------------- 控制台自动滚动（用户阅读旧记录时不强制滚动） ---------------- */

const consoleEl = ref<HTMLElement | null>(null);
const pinnedToBottom = ref(true);

function onConsoleScroll() {
  const el = consoleEl.value;
  if (!el) return;
  const distance = el.scrollHeight - el.scrollTop - el.clientHeight;
  pinnedToBottom.value = distance < 24;
}

async function scrollConsoleIfPinned() {
  if (!pinnedToBottom.value) return;
  await nextTick();
  const el = consoleEl.value;
  if (el) el.scrollTop = el.scrollHeight;
}

/* ---------------- 进度消息处理 ---------------- */

function handleProgressMessage(msg: ProgressMessage) {
  Object.assign(progress, msg);
  recordActivity(msg);

  if (typeof msg.running === 'boolean') {
    submitting.value = msg.running;
  } else if (msg.completed) {
    submitting.value = false;
  }

  setIngestRunning(submitting.value);
  setIngestPercentage(percentage.value);
}

const { connect: wsConnect, disconnect: wsDisconnect } = useWebSocket(taskId, handleProgressMessage);

/* ---------------- 轮询兜底 ---------------- */

let pollTimer: ReturnType<typeof setInterval> | null = null;

function statusToProgress(resp: IngestStatusResponse): ProgressMessage {
  return {
    taskId: resp.taskId,
    type: 'ingest',
    total: resp.total,
    processed: resp.processed,
    success: resp.ingestedFiles,
    failed: resp.otherRejectedFiles,
    currentFile: '',
    message: resp.message,
    completed: resp.completed,
    running: resp.running,
    ingestedFiles: resp.ingestedFiles,
    duplicateFiles: resp.duplicateFiles,
    missingMetadataFiles: resp.missingMetadataFiles,
    unreadableFiles: resp.unreadableFiles,
    conversionFailedFiles: resp.conversionFailedFiles,
    otherRejectedFiles: resp.otherRejectedFiles
  };
}

function startPolling() {
  stopPolling();
  pollTimer = setInterval(async () => {
    if (!taskId.value) return;
    try {
      const resp = await getIngestStatus(taskId.value);
      if (resp) {
        handleProgressMessage(statusToProgress(resp));
        if (resp.completed || !resp.running) {
          stopPolling();
          wsDisconnect();
        }
      }
    } catch {
      // 忽略轮询错误，等待下次
    }
  }, 2000);
}

function stopPolling() {
  if (pollTimer !== null) {
    clearInterval(pollTimer);
    pollTimer = null;
  }
}

/* ---------------- 启动导入 ---------------- */

async function startIngest() {
  if (submitting.value) return;
  submitting.value = true;
  setIngestRunning(true);

  try {
    const resp = await apiStartIngest();
    taskId.value = resp.taskId;

    Object.assign(progress, {
      taskId: resp.taskId,
      type: 'ingest',
      total: 0,
      processed: 0,
      success: 0,
      failed: 0,
      currentFile: '',
      message: '开始导入任务...',
      completed: false,
      ingestedFiles: 0,
      duplicateFiles: 0,
      missingMetadataFiles: 0,
      unreadableFiles: 0,
      conversionFailedFiles: 0,
      otherRejectedFiles: 0
    });
    lastActivityKey = '';
    recordActivity(progress);
    setIngestPercentage(0);

    wsConnect();
    startPolling();
  } catch (e: unknown) {
    submitting.value = false;
    setIngestRunning(false);
    ElMessage.error(extractError(e, '启动任务失败'));
  }
}

/* ---------------- 刷新后恢复当前任务 ---------------- */

async function resumeFromCurrentTask() {
  let resp: IngestStatusResponse | null = null;
  try {
    resp = await getIngestCurrentStatus();
  } catch {
    return;
  }

  if (!resp || (!resp.taskId && !resp.running)) return;

  taskId.value = resp.taskId;
  submitting.value = resp.running;
  setIngestRunning(resp.running);

  handleProgressMessage(statusToProgress({
    ...resp,
    message: resp.message || (resp.running ? '恢复任务状态...' : '任务已完成')
  }));

  if (resp.running) {
    wsConnect();
    startPolling();
  }
}

/* ---------------- 配置加载 ---------------- */

async function reloadConfig() {
  configLoading.value = true;
  try {
    await refreshConfig();
  } catch {
    ElMessage.error('加载配置失败');
  } finally {
    configLoading.value = false;
  }
}

/* ---------------- 工具 ---------------- */

function extractError(e: unknown, fallback: string): string {
  if (typeof e === 'object' && e !== null) {
    const maybe = e as { response?: { data?: { message?: unknown } }; message?: unknown };
    const respMsg = maybe.response?.data?.message;
    if (typeof respMsg === 'string' && respMsg) return respMsg;
    if (typeof maybe.message === 'string' && maybe.message) return maybe.message;
  }
  return fallback;
}

async function copyPath(text: string) {
  if (!text) return;
  try {
    if (navigator.clipboard?.writeText) {
      await navigator.clipboard.writeText(text);
      ElMessage.success('路径已复制');
      return;
    }
    throw new Error('clipboard unavailable');
  } catch {
    ElMessage.warning('复制失败，请手动选择路径文本');
  }
}

/* ---------------- 生命周期 ---------------- */

onMounted(async () => {
  // 接管细粒度进度源，暂停外壳后台监视（避免双重 HTTP 轮询）。
  claimIngestViewOwnership();

  // 若外壳尚未加载配置，则此处兜底加载真值。
  if (!appState.config.attempted) {
    await reloadConfig();
  }
  await resumeFromCurrentTask();
});

onUnmounted(() => {
  wsDisconnect();
  stopPolling();
  // 交还所有权：若任务仍在跑，由外壳后台监视继续同步真实完成态。
  releaseIngestViewOwnership(submitting.value);
});
</script>

<style scoped>
.ingest-view {
  max-width: 1280px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

/* ---------- 横幅 ---------- */
.banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 18px 20px;
  border: 1px solid rgba(16, 185, 129, 0.12);
  border-radius: var(--c-radius-lg);
  background: linear-gradient(120deg, rgba(6, 78, 59, 0.18), rgba(15, 23, 42, 0.6));
}

.banner-title-row {
  display: flex;
  align-items: center;
  gap: 10px;
}

.banner-title {
  margin: 0;
  font-size: 18px;
  font-weight: 700;
  color: #fff;
}

.banner-tag {
  padding: 2px 8px;
  border: 1px solid rgba(16, 185, 129, 0.3);
  border-radius: 6px;
  background: rgba(16, 185, 129, 0.16);
  color: var(--c-accent-text);
  font-size: 10px;
  font-weight: 700;
}

.banner-desc {
  margin: 6px 0 0;
  max-width: 640px;
  font-size: 12px;
  line-height: 1.6;
  color: var(--c-text-secondary);
}

.banner-batch {
  flex-shrink: 0;
  padding: 5px 10px;
  border: 1px solid var(--c-border);
  border-radius: 8px;
  background: var(--c-bg-panel);
  color: var(--c-text-muted);
  font-family: var(--c-mono);
  font-size: 10px;
}

/* ---------- 分栏 ---------- */
.split {
  display: grid;
  grid-template-columns: 5fr 7fr;
  gap: 20px;
  align-items: start;
}

/* ---------- 面板通用 ---------- */
.panel {
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-lg);
  background: var(--c-bg-panel-soft);
  padding: 18px;
}

.panel-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-bottom: 12px;
  margin-bottom: 16px;
  border-bottom: 1px solid var(--c-border);
}

.panel-head-icon {
  color: var(--c-accent-text);
}

.panel-head-title {
  margin: 0;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--c-text-regular);
}

.icon-btn {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: 1px solid var(--c-border);
  border-radius: 8px;
  background: var(--c-bg-elevated);
  color: var(--c-text-secondary);
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.icon-btn:hover:not(:disabled) {
  color: var(--c-accent-text);
  border-color: var(--c-border-strong);
}

.icon-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ---------- 路径列表 ---------- */
.path-list {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.path-field {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.path-label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--c-text-muted);
}

.path-value {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  padding: 8px 10px;
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
  background: var(--c-bg-inset);
}

.path-value-icon {
  color: var(--c-text-muted);
  flex-shrink: 0;
}

.path-text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--c-mono);
  font-size: 12px;
  color: var(--c-text-regular);
}

.selectable {
  user-select: all;
}

.copy-btn {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border: 1px solid var(--c-border);
  border-radius: 6px;
  background: var(--c-bg-elevated);
  color: var(--c-text-muted);
  cursor: pointer;
}

.copy-btn:hover {
  color: var(--c-accent-text);
}

/* ---------- 提示 ---------- */
.notice {
  display: flex;
  gap: 10px;
  margin-top: 16px;
  padding: 12px;
  border-radius: var(--c-radius-sm);
}

.notice-warn {
  border: 1px solid rgba(245, 158, 11, 0.25);
  background: rgba(245, 158, 11, 0.08);
}

.notice-icon {
  flex-shrink: 0;
  color: var(--c-warning-text);
}

.notice-title {
  margin: 0;
  font-size: 12px;
  font-weight: 700;
  color: var(--c-warning-text);
}

.notice-desc {
  margin: 2px 0 0;
  font-size: 11px;
  color: var(--c-text-secondary);
}

/* ---------- 操作按钮 ---------- */
.control-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 18px;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  padding: 10px 16px;
  border: 1px solid transparent;
  border-radius: var(--c-radius-sm);
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  transition: opacity 0.15s ease, background-color 0.15s ease;
}

.btn:disabled {
  cursor: not-allowed;
}

.btn-primary {
  color: #fff;
  background: linear-gradient(120deg, var(--c-accent-strong), var(--c-teal));
}

.btn-primary:hover:not(:disabled) {
  opacity: 0.92;
}

.btn-primary:disabled {
  opacity: 0.45;
}

.btn-running {
  color: var(--c-accent-text);
  background: var(--c-bg-elevated);
  border-color: rgba(16, 185, 129, 0.25);
}

.btn-ghost {
  padding: 8px 16px;
  color: var(--c-text-regular);
  background: var(--c-bg-elevated);
  border-color: var(--c-border);
  font-weight: 600;
  font-size: 12px;
}

.btn-ghost:hover:not(:disabled) {
  color: var(--c-text-primary);
  border-color: var(--c-border-strong);
}

.btn-ghost:disabled {
  opacity: 0.5;
}

.control-hint {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 16px 0 0;
  padding: 10px 12px;
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
  background: rgba(2, 6, 23, 0.4);
  font-size: 11px;
  line-height: 1.5;
  color: var(--c-text-secondary);
}

.hint-icon {
  flex-shrink: 0;
  margin-top: 1px;
  color: var(--c-text-muted);
}

/* ---------- 遥测卡 ---------- */
.telemetry-card {
  display: flex;
  flex-direction: column;
}

.run-badge {
  margin-left: auto;
  padding: 3px 8px;
  border-radius: 6px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.05em;
  border: 1px solid var(--c-border);
}

.badge-run-idle {
  background: var(--c-bg-inset);
  color: var(--c-text-muted);
}

.badge-run-active {
  background: rgba(16, 185, 129, 0.1);
  border-color: rgba(16, 185, 129, 0.25);
  color: var(--c-accent-text);
}

.badge-run-ok {
  background: rgba(16, 185, 129, 0.1);
  border-color: rgba(16, 185, 129, 0.25);
  color: var(--c-accent-text);
}

.badge-run-warn {
  background: rgba(245, 158, 11, 0.1);
  border-color: rgba(245, 158, 11, 0.25);
  color: var(--c-warning-text);
}

.badge-run-error {
  background: rgba(244, 63, 94, 0.1);
  border-color: rgba(244, 63, 94, 0.25);
  color: var(--c-error-text);
}

/* ---------- 统计网格 ---------- */
.stats-grid {
  display: grid;
  gap: 8px;
}

.stats-primary {
  grid-template-columns: repeat(4, 1fr);
  margin-bottom: 8px;
}

.stats-secondary {
  grid-template-columns: repeat(3, 1fr);
  margin-bottom: 16px;
}

.stat-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 10px 6px;
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
  background: rgba(2, 6, 23, 0.4);
}

.stat-name {
  font-size: 10px;
  font-weight: 700;
  color: var(--c-text-muted);
}

.stat-num {
  font-family: var(--c-mono);
  font-size: 18px;
  font-weight: 700;
  color: var(--c-text-primary);
}

.stat-cell.tone-ok {
  border-color: rgba(16, 185, 129, 0.16);
  background: rgba(16, 185, 129, 0.06);
}

.stat-cell.tone-ok .stat-num {
  color: var(--c-accent-text);
}

.stat-cell.tone-warn {
  border-color: rgba(245, 158, 11, 0.16);
  background: rgba(245, 158, 11, 0.06);
}

.stat-cell.tone-warn .stat-num {
  color: var(--c-warning-text);
}

.stat-cell.tone-purple {
  border-color: rgba(168, 85, 247, 0.16);
  background: rgba(168, 85, 247, 0.06);
}

.stat-cell.tone-purple .stat-num {
  color: var(--c-purple-text);
}

.stat-cell-sm {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 7px 6px;
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
  background: rgba(2, 6, 23, 0.4);
}

.stat-name-sm {
  font-size: 10px;
  font-weight: 600;
  color: var(--c-text-muted);
}

.stat-num-sm {
  font-family: var(--c-mono);
  font-size: 13px;
  font-weight: 700;
  color: var(--c-text-regular);
}

.stat-cell-sm.tone-error {
  border-color: rgba(244, 63, 94, 0.16);
  background: rgba(244, 63, 94, 0.06);
}

.stat-cell-sm.tone-error .stat-name-sm {
  color: var(--c-error-text);
}

.stat-cell-sm.tone-error .stat-num-sm {
  color: var(--c-error-text);
}

/* ---------- 进度条 ---------- */
.progress-block {
  margin-bottom: 16px;
}

.progress-meta {
  display: flex;
  justify-content: space-between;
  margin-bottom: 6px;
  font-size: 11px;
  font-weight: 700;
  color: var(--c-text-secondary);
}

.progress-pct {
  font-family: var(--c-mono);
}

.progress-pct.tone-ok {
  color: var(--c-accent-text);
}

.progress-pct.tone-warn {
  color: var(--c-warning-text);
}

.progress-pct.tone-error {
  color: var(--c-error-text);
}

.progress-track {
  width: 100%;
  height: 8px;
  border-radius: 999px;
  background: var(--c-bg-inset);
  border: 1px solid var(--c-border);
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  border-radius: 999px;
  transition: width 0.3s ease;
  background: linear-gradient(90deg, var(--c-accent), var(--c-teal));
}

.progress-fill.tone-warn {
  background: linear-gradient(90deg, #d97706, var(--c-warning));
}

.progress-fill.tone-error {
  background: linear-gradient(90deg, #e11d48, var(--c-error));
}

/* ---------- 当前文件行 ---------- */
.current-line {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  padding: 8px 10px;
  margin-bottom: 12px;
  border: 1px solid var(--c-border);
  border-radius: var(--c-radius-sm);
  background: rgba(2, 6, 23, 0.4);
}

.current-tag {
  flex-shrink: 0;
  padding: 1px 6px;
  border-radius: 4px;
  background: var(--c-bg-elevated);
  color: var(--c-text-muted);
  font-size: 10px;
  font-weight: 700;
}

.current-file {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--c-mono);
  font-size: 11px;
  color: var(--c-text-regular);
}

.current-idle {
  font-size: 11px;
  color: var(--c-text-muted);
}

/* ---------- 完成汇总 ---------- */
.result-banner {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  margin-bottom: 14px;
  border-radius: var(--c-radius-sm);
  font-size: 12px;
  font-weight: 600;
}

.result-icon {
  flex-shrink: 0;
}

.result-ok {
  border: 1px solid rgba(16, 185, 129, 0.25);
  background: rgba(16, 185, 129, 0.08);
  color: var(--c-accent-text);
}

.result-warn {
  border: 1px solid rgba(245, 158, 11, 0.25);
  background: rgba(245, 158, 11, 0.08);
  color: var(--c-warning-text);
}

.result-error {
  border: 1px solid rgba(244, 63, 94, 0.25);
  background: rgba(244, 63, 94, 0.08);
  color: var(--c-error-text);
}

/* ---------- 活动记录控制台 ---------- */
.console-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 12px;
  border: 1px solid var(--c-border);
  border-bottom: none;
  border-radius: var(--c-radius-sm) var(--c-radius-sm) 0 0;
  background: var(--c-bg-inset);
}

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

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

.dot-error {
  background: var(--c-error);
}

.dot-warn {
  background: var(--c-warning);
}

.dot-ok {
  background: var(--c-accent);
}

.console-legend-label {
  margin-left: 4px;
  font-family: var(--c-mono);
  font-size: 11px;
  font-weight: 700;
  color: var(--c-text-muted);
}

.filter-group {
  display: flex;
  gap: 4px;
}

.filter-btn {
  padding: 3px 8px;
  border: 1px solid transparent;
  border-radius: 6px;
  background: var(--c-bg-panel);
  color: var(--c-text-muted);
  font-size: 10px;
  font-weight: 700;
  cursor: pointer;
}

.filter-btn:hover {
  color: var(--c-text-regular);
}

.filter-btn.is-active {
  border-color: rgba(16, 185, 129, 0.25);
  background: rgba(16, 185, 129, 0.1);
  color: var(--c-accent-text);
}

.console-body {
  flex: 1;
  min-height: 220px;
  max-height: 360px;
  overflow-y: auto;
  padding: 12px;
  border: 1px solid var(--c-border);
  border-radius: 0 0 var(--c-radius-sm) var(--c-radius-sm);
  background: rgba(2, 6, 23, 0.7);
}

.console-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 100%;
  min-height: 200px;
  color: var(--c-text-faint);
  text-align: center;
  font-size: 12px;
}

.console-empty-icon {
  color: var(--c-border-strong);
}

.console-empty-hint {
  font-size: 10px;
  color: var(--c-text-faint);
}

.console-lines {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.log-line {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding-bottom: 6px;
  border-bottom: 1px solid rgba(30, 41, 59, 0.4);
  font-family: var(--c-mono);
  font-size: 11px;
  line-height: 1.5;
}

.log-line:last-child {
  border-bottom: none;
  padding-bottom: 0;
}

.log-time {
  flex-shrink: 0;
  color: var(--c-text-faint);
  user-select: none;
}

.log-badge {
  flex-shrink: 0;
  font-weight: 700;
}

.badge-normal {
  color: var(--c-accent-text);
}

.badge-warning {
  color: var(--c-warning-text);
}

.badge-error {
  color: var(--c-error-text);
}

.log-text {
  flex: 1;
  min-width: 0;
  word-break: break-word;
}

.log-file {
  margin-right: 6px;
  color: #fff;
  font-weight: 700;
}

.log-msg {
  color: var(--c-text-secondary);
}

.console-note {
  margin: 8px 0 0;
  font-size: 10px;
  color: var(--c-text-faint);
}

/* ---------- 响应式 ---------- */
@media (max-width: 1024px) {
  .split {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 640px) {
  .banner {
    flex-direction: column;
    align-items: flex-start;
  }

  .banner-batch {
    align-self: flex-start;
  }

  .stats-primary {
    grid-template-columns: repeat(2, 1fr);
  }

  .stats-secondary {
    grid-template-columns: 1fr;
  }

  .console-head {
    flex-direction: column;
    align-items: flex-start;
  }

  .filter-group {
    flex-wrap: wrap;
  }
}

@media (max-width: 390px) {
  .ingest-view {
    gap: 14px;
  }

  .panel {
    padding: 14px;
  }

  .stat-num {
    font-size: 16px;
  }

  .log-line {
    flex-wrap: wrap;
  }
}
</style>
