import { useEffect, useState, useDeferredValue, startTransition } from "react";
import { useRef } from "react";

import { fetchJson, fetchJsonCached, invalidateJsonCache, primeJsonCache, uploadFile } from "./api/client.js";
import LogsPanel from "./components/LogsPanel.jsx";
import OverviewPanel from "./components/OverviewPanel.jsx";
import SettingsPanel from "./components/SettingsPanel.jsx";
import TaskTable from "./components/TaskTable.jsx";
import TaskDetailCard from "./components/TaskDetailCard.jsx";
import {
  attentionLabel,
  currentStepLabel,
  summarizeAttention,
  summarizeDelivery,
  taskDisplayStatus,
  taskPrimaryActionLabel,
} from "./lib/format.js";

const NAV_ITEMS = ["Overview", "Tasks", "Settings", "Logs"];

function buildTasksUrl(query) {
  const params = new URLSearchParams();
  params.set("limit", String(query.limit || 24));
  params.set("offset", String(query.offset || 0));
  params.set("sort", String(query.sort || "updated_desc"));
  if (query.status) params.set("status", query.status);
  if (query.search) params.set("search", query.search);
  if (query.attention) params.set("attention", query.attention);
  if (query.delivery) params.set("delivery", query.delivery);
  return `/tasks?${params.toString()}`;
}

function parseHashState() {
  const raw = window.location.hash.replace(/^#/, "");
  const [viewPart, queryPart = ""] = raw.split("?");
  const params = new URLSearchParams(queryPart);
  return {
    view: NAV_ITEMS.includes(viewPart) ? viewPart : "Tasks",
    taskId: params.get("task") || "",
  };
}

function syncHashState(view, taskId) {
  const params = new URLSearchParams();
  if (taskId && view === "Tasks") params.set("task", taskId);
  const suffix = params.toString() ? `?${params.toString()}` : "";
  window.history.replaceState(null, "", `#${view}${suffix}`);
}

function FocusQueue({ tasks, selectedTaskId, onSelectTask, onRunTask }) {
  const focusItems = tasks
    .filter((task) => ["manual_now", "retry_now", "waiting_retry"].includes(summarizeAttention(task)))
    .sort((a, b) => {
      const score = { manual_now: 0, retry_now: 1, waiting_retry: 2 };
      const diff = score[summarizeAttention(a)] - score[summarizeAttention(b)];
      if (diff !== 0) return diff;
      return String(b.updated_at).localeCompare(String(a.updated_at));
    })
    .slice(0, 6);

  if (!focusItems.length) return null;

  return (
    <article className="panel">
      <div className="panel-head">
        <div>
          <p className="eyebrow">Priority Queue</p>
          <h2>需要优先处理的任务</h2>
        </div>
        <div className="panel-meta">{focusItems.length} tasks</div>
      </div>
      <div className="focus-grid">
        {focusItems.map((task) => (
          <button
            key={task.id}
            type="button"
            className={selectedTaskId === task.id ? "focus-card active" : "focus-card"}
            onClick={() => onSelectTask(task.id)}
            onMouseEnter={() => onSelectTask(task.id, { prefetch: true })}
          >
            <div className="focus-card-head">
              <span className="status-badge">{attentionLabel(summarizeAttention(task))}</span>
              <span className="status-badge">{taskDisplayStatus(task)}</span>
            </div>
            <strong>{task.title}</strong>
            <p>{currentStepLabel(task)}</p>
            <div className="row-actions">
              <button
                className="nav-btn compact-btn"
                onClick={(event) => {
                  event.stopPropagation();
                  onSelectTask(task.id);
                }}
              >
                打开详情
              </button>
              <button
                className="nav-btn compact-btn strong-btn"
                onClick={(event) => {
                  event.stopPropagation();
                  onRunTask?.(task.id);
                }}
              >
                {taskPrimaryActionLabel(task)}
              </button>
            </div>
          </button>
        ))}
      </div>
    </article>
  );
}

function TasksView({
  tasks,
  taskTotal,
  taskQuery,
  selectedTaskId,
  onSelectTask,
  onRunTask,
  taskDetail,
  session,
  loading,
  detailLoading,
  actionBusy,
  selectedStepName,
  onSelectStep,
  onRetryStep,
  onResetStep,
  onBindFullVideo,
  onOpenSessionTask,
  onSessionMerge,
  onSessionRebind,
  onTaskQueryChange,
}) {
  const deferredSearch = useDeferredValue(taskQuery.search);

  const filtered = tasks.filter((task) => {
    const haystack = `${task.id} ${task.title}`.toLowerCase();
    if (deferredSearch && !haystack.includes(deferredSearch.toLowerCase())) return false;
    return true;
  });

  const pageStart = taskTotal ? taskQuery.offset + 1 : 0;
  const pageEnd = taskQuery.offset + tasks.length;
  const canPrev = taskQuery.offset > 0;
  const canNext = taskQuery.offset + taskQuery.limit < taskTotal;

  return (
    <section className="tasks-layout-react">
      <div className="tasks-main-stack">
        <FocusQueue tasks={tasks} selectedTaskId={selectedTaskId} onSelectTask={onSelectTask} onRunTask={onRunTask} />
        <article className="panel">
          <div className="panel-head">
            <div>
              <p className="eyebrow">Tasks Workspace</p>
              <h2>Task Table</h2>
            </div>
            <div className="panel-meta">{loading ? "syncing..." : `${pageStart}-${pageEnd} / ${taskTotal}`}</div>
          </div>
          <div className="toolbar-grid">
            <input
              value={taskQuery.search}
              onChange={(event) => onTaskQueryChange({ search: event.target.value, offset: 0 })}
              placeholder="搜索任务标题或 task id"
            />
            <select value={taskQuery.status} onChange={(event) => onTaskQueryChange({ status: event.target.value, offset: 0 })}>
              <option value="">全部状态</option>
              <option value="running">处理中</option>
              <option value="failed_retryable">待重试</option>
              <option value="failed_manual">待人工</option>
              <option value="published">待收尾</option>
              <option value="collection_synced">已完成</option>
            </select>
            <select value={taskQuery.attention} onChange={(event) => onTaskQueryChange({ attention: event.target.value, offset: 0 })}>
              <option value="">全部关注状态</option>
              <option value="manual_now">仅看需人工</option>
              <option value="retry_now">仅看到点重试</option>
              <option value="waiting_retry">仅看等待重试</option>
            </select>
            <select value={taskQuery.delivery} onChange={(event) => onTaskQueryChange({ delivery: event.target.value, offset: 0 })}>
              <option value="">全部交付状态</option>
              <option value="pending_comment">评论待完成</option>
              <option value="cleanup_removed">已清理视频</option>
            </select>
            <select value={taskQuery.sort} onChange={(event) => onTaskQueryChange({ sort: event.target.value, offset: 0 })}>
              <option value="updated_desc">最近更新</option>
              <option value="updated_asc">最早更新</option>
              <option value="title_asc">标题 A-Z</option>
              <option value="title_desc">标题 Z-A</option>
              <option value="status_asc">按状态</option>
            </select>
            <select value={String(taskQuery.limit)} onChange={(event) => onTaskQueryChange({ limit: Number(event.target.value), offset: 0 })}>
              <option value="12">12 / 页</option>
              <option value="24">24 / 页</option>
              <option value="48">48 / 页</option>
            </select>
          </div>
          <div className="row-actions" style={{ marginBottom: 12 }}>
            <button className="nav-btn compact-btn" onClick={() => onTaskQueryChange({ offset: Math.max(0, taskQuery.offset - taskQuery.limit) })} disabled={!canPrev || loading}>
              上一页
            </button>
            <button className="nav-btn compact-btn" onClick={() => onTaskQueryChange({ offset: taskQuery.offset + taskQuery.limit })} disabled={!canNext || loading}>
              下一页
            </button>
          </div>
          <TaskTable tasks={filtered} selectedTaskId={selectedTaskId} onSelectTask={onSelectTask} onRunTask={onRunTask} />
        </article>
      </div>
      <TaskDetailCard
        payload={taskDetail}
        session={session}
        loading={detailLoading}
        actionBusy={actionBusy}
        selectedStepName={selectedStepName}
        onSelectStep={onSelectStep}
        onRetryStep={onRetryStep}
        onResetStep={onResetStep}
        onBindFullVideo={onBindFullVideo}
        onOpenSessionTask={onOpenSessionTask}
        onSessionMerge={onSessionMerge}
        onSessionRebind={onSessionRebind}
      />
    </section>
  );
}

export default function App() {
  const initialLocation = parseHashState();
  const [view, setView] = useState(initialLocation.view);
  const [health, setHealth] = useState(false);
  const [doctorOk, setDoctorOk] = useState(false);
  const [tasks, setTasks] = useState([]);
  const [taskTotal, setTaskTotal] = useState(0);
  const [taskQuery, setTaskQuery] = useState({
    search: "",
    status: "",
    attention: "",
    delivery: "",
    sort: "updated_desc",
    limit: 24,
    offset: 0,
  });
  const [services, setServices] = useState({ items: [] });
  const [scheduler, setScheduler] = useState(null);
  const [history, setHistory] = useState({ items: [] });
  const [logs, setLogs] = useState({ items: [] });
  const [selectedLogName, setSelectedLogName] = useState("");
  const [logContent, setLogContent] = useState(null);
  const [filterCurrentTaskLogs, setFilterCurrentTaskLogs] = useState(false);
  const [autoRefreshLogs, setAutoRefreshLogs] = useState(false);
  const [settings, setSettings] = useState({});
  const [settingsSchema, setSettingsSchema] = useState(null);
  const [selectedTaskId, setSelectedTaskId] = useState(initialLocation.taskId);
  const [selectedStepName, setSelectedStepName] = useState("");
  const [taskDetail, setTaskDetail] = useState(null);
  const [currentSession, setCurrentSession] = useState(null);
  const [loading, setLoading] = useState(true);
  const [detailLoading, setDetailLoading] = useState(false);
  const [overviewLoading, setOverviewLoading] = useState(false);
  const [logLoading, setLogLoading] = useState(false);
  const [settingsLoading, setSettingsLoading] = useState(false);
  const [actionBusy, setActionBusy] = useState("");
  const [panelBusy, setPanelBusy] = useState("");
  const [toasts, setToasts] = useState([]);
  const detailCacheRef = useRef(new Map());

  function pushToast(kind, text) {
    const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    setToasts((current) => [...current, { id, kind, text }]);
  }

  function removeToast(id) {
    setToasts((current) => current.filter((item) => item.id !== id));
  }

  async function loadOverviewPanels() {
    const [servicesPayload, schedulerPayload, historyPayload] = await Promise.all([
      fetchJsonCached("/runtime/services"),
      fetchJsonCached("/scheduler/preview"),
      fetchJsonCached("/history?limit=20"),
    ]);
    setServices(servicesPayload);
    setScheduler(schedulerPayload);
    setHistory(historyPayload);
  }

  async function loadShell() {
    setLoading(true);
    try {
      const [healthPayload, doctorPayload, taskPayload] = await Promise.all([
        fetchJsonCached("/health"),
        fetchJsonCached("/doctor"),
        fetchJson(buildTasksUrl(taskQuery)),
      ]);
      setHealth(Boolean(healthPayload.ok));
      setDoctorOk(Boolean(doctorPayload.ok));
      setTasks(taskPayload.items || []);
      setTaskTotal(taskPayload.total || 0);
      startTransition(() => {
        if (!selectedTaskId && taskPayload.items?.length) {
          setSelectedTaskId(taskPayload.items[0].id);
        }
      });
    } finally {
      setLoading(false);
    }
  }

  async function loadTasksOnly(query = taskQuery) {
    const url = buildTasksUrl(query);
    const taskPayload = await fetchJson(url);
    primeJsonCache(url, taskPayload);
    setTasks(taskPayload.items || []);
    setTaskTotal(taskPayload.total || 0);
    return taskPayload.items || [];
  }

  async function loadSessionDetail(sessionKey) {
    if (!sessionKey) {
      setCurrentSession(null);
      return null;
    }
    const payload = await fetchJson(`/sessions/${encodeURIComponent(sessionKey)}`);
    primeJsonCache(`/sessions/${encodeURIComponent(sessionKey)}`, payload);
    setCurrentSession(payload);
    return payload;
  }

  async function loadTaskDetail(taskId) {
    const cached = detailCacheRef.current.get(taskId);
    if (cached) {
      setTaskDetail(cached);
      void loadSessionDetail(cached.context?.session_key);
      setDetailLoading(false);
    }
    setDetailLoading(true);
    try {
      const [task, steps, artifacts, history, timeline, context] = await Promise.all([
        fetchJsonCached(`/tasks/${encodeURIComponent(taskId)}`),
        fetchJsonCached(`/tasks/${encodeURIComponent(taskId)}/steps`),
        fetchJsonCached(`/tasks/${encodeURIComponent(taskId)}/artifacts`),
        fetchJsonCached(`/tasks/${encodeURIComponent(taskId)}/history`),
        fetchJsonCached(`/tasks/${encodeURIComponent(taskId)}/timeline`),
        fetchJsonCached(`/tasks/${encodeURIComponent(taskId)}/context`),
      ]);
      const payload = { task, steps, artifacts, history, timeline, context };
      detailCacheRef.current.set(taskId, payload);
      primeJsonCache(`/tasks/${encodeURIComponent(taskId)}`, task);
      primeJsonCache(`/tasks/${encodeURIComponent(taskId)}/steps`, steps);
      primeJsonCache(`/tasks/${encodeURIComponent(taskId)}/artifacts`, artifacts);
      primeJsonCache(`/tasks/${encodeURIComponent(taskId)}/history`, history);
      primeJsonCache(`/tasks/${encodeURIComponent(taskId)}/timeline`, timeline);
      primeJsonCache(`/tasks/${encodeURIComponent(taskId)}/context`, context);
      setTaskDetail(payload);
      await loadSessionDetail(context?.session_key);
      if (!selectedStepName) {
        const suggested = steps.items?.find((step) => ["failed_retryable", "failed_manual", "running"].includes(step.status))?.step_name
          || steps.items?.find((step) => step.status !== "succeeded")?.step_name
          || "";
        setSelectedStepName(suggested);
      }
    } finally {
      setDetailLoading(false);
    }
  }

  async function refreshSelectedTask(taskId = selectedTaskId, { refreshTasks = true } = {}) {
    if (refreshTasks) {
      const refreshedTasks = await loadTasksOnly();
      if (!taskId && refreshedTasks.length) {
        taskId = refreshedTasks[0].id;
      }
    }
    if (!taskId) {
      setTaskDetail(null);
      setCurrentSession(null);
      return;
    }
    await loadTaskDetail(taskId);
  }

  function invalidateTaskCaches(taskId) {
    invalidateJsonCache("/tasks?");
    if (taskId) {
      detailCacheRef.current.delete(taskId);
      invalidateJsonCache(`/tasks/${encodeURIComponent(taskId)}`);
    }
  }

  function invalidateSessionCaches(sessionKey) {
    if (!sessionKey) return;
    invalidateJsonCache(`/sessions/${encodeURIComponent(sessionKey)}`);
  }

  async function prefetchTaskDetail(taskId) {
    if (!taskId || detailCacheRef.current.has(taskId)) return;
    try {
      const [task, steps, artifacts, history, timeline, context] = await Promise.all([
        fetchJsonCached(`/tasks/${encodeURIComponent(taskId)}`),
        fetchJsonCached(`/tasks/${encodeURIComponent(taskId)}/steps`),
        fetchJsonCached(`/tasks/${encodeURIComponent(taskId)}/artifacts`),
        fetchJsonCached(`/tasks/${encodeURIComponent(taskId)}/history`),
        fetchJsonCached(`/tasks/${encodeURIComponent(taskId)}/timeline`),
        fetchJsonCached(`/tasks/${encodeURIComponent(taskId)}/context`),
      ]);
      detailCacheRef.current.set(taskId, { task, steps, artifacts, history, timeline, context });
    } catch {
      // Ignore prefetch failures; normal navigation will surface the actual error.
    }
  }

  useEffect(() => {
    let cancelled = false;
    loadShell().catch((error) => {
      if (!cancelled) pushToast("hot", `初始化失败: ${error}`);
    });
    return () => {
      cancelled = true;
    };
  }, []);

  useEffect(() => {
    syncHashState(view, selectedTaskId);
  }, [view, selectedTaskId]);

  useEffect(() => {
    if (view !== "Tasks") return;
    loadTasksOnly(taskQuery).catch((error) => pushToast("hot", `任务列表加载失败: ${error}`));
  }, [taskQuery, view]);

  useEffect(() => {
    if (!toasts.length) return undefined;
    const timer = window.setTimeout(() => setToasts((current) => current.slice(1)), 3200);
    return () => window.clearTimeout(timer);
  }, [toasts]);

  useEffect(() => {
    function handleHashChange() {
      const next = parseHashState();
      setView(next.view);
      if (next.taskId) {
        setSelectedTaskId(next.taskId);
      }
    }
    window.addEventListener("hashchange", handleHashChange);
    return () => window.removeEventListener("hashchange", handleHashChange);
  }, []);

  useEffect(() => {
    if (view !== "Overview") return;
    let cancelled = false;
    async function loadOverviewView() {
      setOverviewLoading(true);
      try {
        const [servicesPayload, schedulerPayload, historyPayload] = await Promise.all([
          fetchJsonCached("/runtime/services"),
          fetchJsonCached("/scheduler/preview"),
          fetchJsonCached("/history?limit=20"),
        ]);
        if (cancelled) return;
        setServices(servicesPayload);
        setScheduler(schedulerPayload);
        setHistory(historyPayload);
      } finally {
        if (!cancelled) setOverviewLoading(false);
      }
    }
    loadOverviewView();
    return () => {
      cancelled = true;
    };
  }, [view]);

  useEffect(() => {
    if (!selectedTaskId) return;
    let cancelled = false;
    loadTaskDetail(selectedTaskId).catch((error) => {
      if (!cancelled) pushToast("hot", `任务详情加载失败: ${error}`);
    });
    return () => {
      cancelled = true;
    };
  }, [selectedTaskId]);

  async function loadCurrentLogContent(logName = selectedLogName) {
    if (!logName) return;
    setLogLoading(true);
    try {
      const currentTask = tasks.find((item) => item.id === selectedTaskId);
      let url = `/logs?name=${encodeURIComponent(logName)}&lines=200`;
      if (filterCurrentTaskLogs && currentTask?.title) {
        url += `&contains=${encodeURIComponent(currentTask.title)}`;
      }
      const payload = await fetchJson(url);
      setLogContent(payload);
    } finally {
      setLogLoading(false);
    }
  }

  useEffect(() => {
    if (view !== "Logs") return;
    let cancelled = false;
    async function loadLogsIndex() {
      setLogLoading(true);
      try {
        const logsPayload = await fetchJson("/logs");
        if (cancelled) return;
        setLogs(logsPayload);
        if (!selectedLogName && logsPayload.items?.length) {
          startTransition(() => setSelectedLogName(logsPayload.items[0].name));
        }
      } finally {
        if (!cancelled) setLogLoading(false);
      }
    }
    loadLogsIndex();
    return () => {
      cancelled = true;
    };
  }, [view]);

  useEffect(() => {
    if (view !== "Logs" || !selectedLogName) return;
    let cancelled = false;
    loadCurrentLogContent(selectedLogName).catch((error) => {
      if (!cancelled) pushToast("hot", `日志加载失败: ${error}`);
    });
    return () => {
      cancelled = true;
    };
  }, [view, selectedLogName, filterCurrentTaskLogs, selectedTaskId, tasks]);

  useEffect(() => {
    if (view !== "Logs" || !selectedLogName || !autoRefreshLogs) return;
    const timer = window.setInterval(() => {
      loadCurrentLogContent(selectedLogName).catch(() => {});
    }, 5000);
    return () => window.clearInterval(timer);
  }, [view, selectedLogName, autoRefreshLogs, filterCurrentTaskLogs, selectedTaskId, tasks]);

  useEffect(() => {
    if (view !== "Settings") return;
    let cancelled = false;
    async function loadSettingsView() {
      setSettingsLoading(true);
      try {
        const [settingsPayload, schemaPayload] = await Promise.all([
          fetchJsonCached("/settings"),
          fetchJsonCached("/settings/schema"),
        ]);
        if (cancelled) return;
        setSettings(settingsPayload);
        setSettingsSchema(schemaPayload);
      } finally {
        if (!cancelled) setSettingsLoading(false);
      }
    }
    loadSettingsView();
    return () => {
      cancelled = true;
    };
  }, [view]);

  const currentView = (() => {
    if (view === "Overview") {
      return (
        <OverviewPanel
          health={health}
          doctorOk={doctorOk}
          tasks={{ items: tasks }}
          services={services}
          scheduler={scheduler}
          history={history}
          loading={overviewLoading}
          onRefreshScheduler={async () => {
            setPanelBusy("refresh_scheduler");
            try {
              const payload = await fetchJson("/scheduler/preview");
              setScheduler(payload);
              pushToast("good", "Scheduler 已刷新");
            } finally {
              setPanelBusy("");
            }
          }}
          onRefreshHistory={async () => {
            setPanelBusy("refresh_history");
            try {
              const payload = await fetchJson("/history?limit=20");
              setHistory(payload);
              pushToast("good", "Recent Actions 已刷新");
            } finally {
              setPanelBusy("");
            }
          }}
          onStageImport={async (sourcePath) => {
            setPanelBusy("stage_import");
            try {
              const result = await fetchJson("/stage/import", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ source_path: sourcePath }),
              });
              await loadTasksOnly();
              pushToast("good", `已导入到 stage: ${result.target_path}`);
            } finally {
              setPanelBusy("");
            }
          }}
          onStageUpload={async (file) => {
            setPanelBusy("stage_upload");
            try {
              const result = await uploadFile("/stage/upload", file);
              await loadTasksOnly();
              pushToast("good", `已上传到 stage: ${result.target_path}`);
            } finally {
              setPanelBusy("");
            }
          }}
          onRunOnce={async () => {
            setPanelBusy("run_once");
            try {
              await fetchJson("/worker/run-once", { method: "POST" });
              invalidateJsonCache("/tasks?");
              await loadTasksOnly();
              if (selectedTaskId) await refreshSelectedTask(selectedTaskId, { refreshTasks: false });
              pushToast("good", "Worker 已执行一轮");
            } finally {
              setPanelBusy("");
            }
          }}
          onServiceAction={async (serviceId, action) => {
            const busyKey = `service:${serviceId}:${action}`;
            setPanelBusy(busyKey);
            try {
              await fetchJson(`/runtime/services/${serviceId}/${action}`, { method: "POST" });
              invalidateJsonCache("/runtime/services");
              await loadShell();
              if (view === "Overview") {
                await loadOverviewPanels();
              }
              pushToast("good", `${serviceId} ${action} 完成`);
            } finally {
              setPanelBusy("");
            }
          }}
          busy={panelBusy}
        />
      );
    }
    if (view === "Tasks") {
      return (
        <TasksView
          tasks={tasks}
          taskTotal={taskTotal}
          taskQuery={taskQuery}
          selectedTaskId={selectedTaskId}
          onSelectTask={(taskId, options = {}) => {
            if (options.prefetch) {
              prefetchTaskDetail(taskId);
              return;
            }
            startTransition(() => {
              setSelectedTaskId(taskId);
              setSelectedStepName("");
            });
          }}
          onRunTask={async (taskId) => {
            setActionBusy("run");
            try {
              const result = await fetchJson(`/tasks/${encodeURIComponent(taskId)}/actions/run`, { method: "POST" });
              invalidateTaskCaches(taskId);
              invalidateSessionCaches(taskDetail?.context?.session_key);
              await refreshSelectedTask(taskId);
              pushToast("good", `任务已推进: ${taskId} / processed=${result.processed.length}`);
            } finally {
              setActionBusy("");
            }
          }}
          taskDetail={taskDetail}
          session={currentSession}
          loading={loading}
          detailLoading={detailLoading}
          actionBusy={actionBusy}
          selectedStepName={selectedStepName}
          onSelectStep={setSelectedStepName}
          onRetryStep={async (stepName) => {
            if (!selectedTaskId || !stepName) return;
            setActionBusy("retry");
            try {
              const result = await fetchJson(`/tasks/${encodeURIComponent(selectedTaskId)}/actions/retry-step`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ step_name: stepName }),
              });
              invalidateTaskCaches(selectedTaskId);
              invalidateSessionCaches(taskDetail?.context?.session_key);
              await refreshSelectedTask(selectedTaskId);
              pushToast("good", `已重试 ${stepName} / processed=${result.processed.length}`);
            } finally {
              setActionBusy("");
            }
          }}
          onResetStep={async (stepName) => {
            if (!selectedTaskId || !stepName) return;
            if (!window.confirm(`确认重置到 step=${stepName} 并清理其后的产物吗？`)) return;
            setActionBusy("reset");
            try {
              const result = await fetchJson(`/tasks/${encodeURIComponent(selectedTaskId)}/actions/reset-to-step`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ step_name: stepName }),
              });
              invalidateTaskCaches(selectedTaskId);
              invalidateSessionCaches(taskDetail?.context?.session_key);
              await refreshSelectedTask(selectedTaskId);
              pushToast("good", `已重置到 ${stepName} / processed=${result.run.processed.length}`);
            } finally {
              setActionBusy("");
            }
          }}
          onBindFullVideo={async (fullVideoBvid) => {
            if (!selectedTaskId || !fullVideoBvid) return;
            setActionBusy("bind_full_video");
            try {
              await fetchJson(`/tasks/${encodeURIComponent(selectedTaskId)}/bind-full-video`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ full_video_bvid: fullVideoBvid }),
              });
              invalidateTaskCaches(selectedTaskId);
              invalidateSessionCaches(taskDetail?.context?.session_key);
              await refreshSelectedTask(selectedTaskId);
              pushToast("good", `已绑定完整版 BV: ${fullVideoBvid}`);
            } finally {
              setActionBusy("");
            }
          }}
          onOpenSessionTask={(taskId) => {
            startTransition(() => {
              setSelectedTaskId(taskId);
              setSelectedStepName("");
            });
          }}
          onSessionMerge={async (rawTaskIds) => {
            const sessionKey = currentSession?.session_key || taskDetail?.context?.session_key;
            const taskIds = String(rawTaskIds)
              .split(",")
              .map((item) => item.trim())
              .filter(Boolean);
            if (!sessionKey || !taskIds.length) return;
            setActionBusy("session_merge");
            try {
              await fetchJson(`/sessions/${encodeURIComponent(sessionKey)}/merge`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ task_ids: taskIds }),
              });
              invalidateJsonCache("/tasks?");
              invalidateSessionCaches(sessionKey);
              taskIds.forEach((taskId) => invalidateTaskCaches(taskId));
              await refreshSelectedTask(selectedTaskId);
              pushToast("good", `已合并 ${taskIds.length} 个任务到 session ${sessionKey}`);
            } finally {
              setActionBusy("");
            }
          }}
          onSessionRebind={async (fullVideoBvid) => {
            const sessionKey = currentSession?.session_key || taskDetail?.context?.session_key;
            if (!sessionKey || !fullVideoBvid) return;
            setActionBusy("session_rebind");
            try {
              await fetchJson(`/sessions/${encodeURIComponent(sessionKey)}/rebind`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ full_video_bvid: fullVideoBvid }),
              });
              invalidateSessionCaches(sessionKey);
              if (selectedTaskId) invalidateTaskCaches(selectedTaskId);
              await refreshSelectedTask(selectedTaskId);
              pushToast("good", `已为 session ${sessionKey} 绑定完整版 BV`);
            } finally {
              setActionBusy("");
            }
          }}
          onTaskQueryChange={(patch) => {
            setTaskQuery((current) => ({ ...current, ...patch }));
          }}
        />
      );
    }
    if (view === "Settings") {
      return (
        <SettingsPanel
          settings={settings}
          schema={settingsSchema}
          loading={settingsLoading}
          onSave={async (payload) => {
            await fetchJson("/settings", {
              method: "PUT",
              headers: { "Content-Type": "application/json" },
              body: JSON.stringify(payload),
            });
            invalidateJsonCache("/settings");
            invalidateJsonCache("/settings/schema");
            const refreshed = await fetchJson("/settings");
            setSettings(refreshed);
            pushToast("good", "Settings 已保存并刷新");
            return refreshed;
          }}
        />
      );
    }
    return (
      <LogsPanel
        logs={logs.items || []}
        selectedLogName={selectedLogName}
        onSelectLog={(name) => startTransition(() => setSelectedLogName(name))}
        logContent={logContent}
        loading={logLoading}
        currentTaskTitle={tasks.find((item) => item.id === selectedTaskId)?.title || ""}
        filterCurrentTask={filterCurrentTaskLogs}
        onToggleFilterCurrentTask={setFilterCurrentTaskLogs}
        autoRefresh={autoRefreshLogs}
        onToggleAutoRefresh={setAutoRefreshLogs}
        onRefreshLog={async () => {
          if (!selectedLogName) return;
          setPanelBusy("refresh_log");
          try {
            await loadCurrentLogContent(selectedLogName);
            pushToast("good", "日志已刷新");
          } finally {
            setPanelBusy("");
          }
        }}
        busy={panelBusy === "refresh_log"}
      />
    );
  })();

  return (
    <div className="react-shell">
      <aside className="react-sidebar">
        <p className="eyebrow">Biliup Next</p>
        <h1>Frontend</h1>
        <p className="sidebar-copy">React + Vite 控制台迁移骨架，先接管任务工作台。</p>
        <nav className="sidebar-nav-react">
          {NAV_ITEMS.map((item) => (
            <button
              key={item}
              className={view === item ? "nav-btn active" : "nav-btn"}
              onClick={() => setView(item)}
            >
              {item}
            </button>
          ))}
        </nav>
      </aside>
      <main className="react-main">
        <header className="react-topbar">
          <div>
            <p className="eyebrow">Migration Workspace</p>
            <h2>{view}</h2>
          </div>
          <div className="status-row">
            <span className={`status-badge ${health ? "good" : "hot"}`}>API {health ? "ok" : "down"}</span>
            <span className={`status-badge ${doctorOk ? "good" : "warn"}`}>Doctor {doctorOk ? "ready" : "warn"}</span>
            <span className="status-badge">{taskTotal} tasks</span>
          </div>
        </header>
        {toasts.length ? (
          <div className="toast-stack">
            {toasts.map((toast) => (
              <div key={toast.id} className={`status-banner ${toast.kind}`}>
                <span>{toast.text}</span>
                <button className="toast-close" onClick={() => removeToast(toast.id)}>关闭</button>
              </div>
            ))}
          </div>
        ) : null}
        {currentView}
      </main>
    </div>
  );
}
