(() => {
  "use strict";

  const $ = (selector, root = document) => root.querySelector(selector);
  const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  const clamp = (n, min, max) => Math.max(min, Math.min(max, Number(n) || 0));
  const escapeHtml = (value) => String(value ?? "").replace(/[&<>'"]/g, (char) => ({
    "&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#39;", '"': "&quot;",
  }[char]));

  const state = {
    health: null,
    devices: [],
    selected: new Set(JSON.parse(localStorage.getItem("deskLanSelected") || "[]")),
    currentPage: "dashboard",
    activeDeviceId: "",
    busy: false,
    monitorTimer: null,
    image: null,
    imageFit: "cover",
    imageZoom: 100,
    imageOffsetX: 0,
    imageOffsetY: 0,
    singleTemplate: "red",
    textLayers: [
      { id: crypto.randomUUID(), content: "会议桌牌", size: 72, color: "#7f1d1d", x: 512, y: 300, stroke: false },
    ],
    activeTextId: "",
    tableHeaders: [],
    tableMatrix: [],
    batchRows: [],
    batchTemplate: "red",
    batchStop: false,
    batchStates: [],
  };
  state.activeTextId = state.textLayers[0].id;

  const templatePalettes = {
    red: { bg1: "#fff7ed", bg2: "#ffffff", bg3: "#fee2e2", accent: "#991b1b", ink: "#7f1d1d", sub: "#78350f" },
    blue: { bg1: "#eaf4ff", bg2: "#ffffff", bg3: "#dbeafe", accent: "#1d4ed8", ink: "#0f172a", sub: "#475569" },
    ink: { bg1: "#08111e", bg2: "#13253a", bg3: "#0c7f86", accent: "#27d8d8", ink: "#ffffff", sub: "#c4f2f2" },
    paper: { bg1: "#f6f4ee", bg2: "#ffffff", bg3: "#e8e4dc", accent: "#1f2937", ink: "#111827", sub: "#596273" },
  };

  async function api(path, options = {}) {
    const response = await fetch(path, {
      cache: "no-store",
      ...options,
      headers: options.body instanceof Blob || options.body instanceof File
        ? { ...(options.headers || {}) }
        : { "Content-Type": "application/json", ...(options.headers || {}) },
    });
    let payload;
    try { payload = await response.json(); } catch { payload = { ok: false, error: await response.text() }; }
    if (!response.ok || payload.ok === false) throw new Error(payload.error || `HTTP ${response.status}`);
    return payload;
  }

  function toast(message, type = "info", timeout = 3600) {
    const node = document.createElement("div");
    node.className = `toast ${type}`;
    node.textContent = message;
    $("#toastStack").append(node);
    setTimeout(() => node.remove(), timeout);
  }

  function setServiceState(mode, text) {
    const node = $("#serviceState");
    node.className = `connection-state ${mode}`;
    $("span", node).textContent = text;
  }

  function saveSelection() {
    localStorage.setItem("deskLanSelected", JSON.stringify([...state.selected]));
    updateStats();
    updateTargetSummaries();
    renderBatchPlan();
  }

  function navigate(page) {
    state.currentPage = page;
    $$(".nav-item").forEach((button) => button.classList.toggle("active", button.dataset.page === page));
    $$(".page").forEach((section) => section.classList.toggle("active", section.id === `page-${page}`));
    const meta = {
      dashboard: ["LYBOM · LOCAL CONTROL", "设备"],
      composer: ["SINGLE DISPLAY", "单张传图"],
      batch: ["BATCH WORKFLOW", "批量名单"],
      settings: ["LOCAL & PRIVATE", "设置"],
    }[page];
    $("#pageEyebrow").textContent = meta[0];
    $("#pageTitle").textContent = meta[1];
    if (page === "composer") renderComposeCanvas();
    if (page === "batch") { renderBatchCanvas(); renderBatchPlan(); }
    if (page === "settings") renderSettingsDevices();
    window.scrollTo({ top: 0, behavior: "smooth" });
  }

  async function loadHealth() {
    try {
      state.health = await api("/api/health");
      setServiceState("online", "本地服务已连接");
      $("#subnetInput").value = state.health.subnet || "192.168.1.0/24";
      const address = (state.health.addresses || [])[0] || "127.0.0.1";
      $("#statAddress").textContent = `${address}:8765`;
      $("#appVersion").textContent = state.health.version || "—";
      $("#localUrl").textContent = `${location.protocol}//127.0.0.1:${location.port || 8765}`;
      $("#mobileUrl").textContent = `${location.protocol}//${address}:${location.port || 8765}`;
    } catch (error) {
      setServiceState("error", "本地服务不可用");
      throw error;
    }
  }

  async function loadDevices(refresh = true) {
    try {
      const data = await api(refresh ? "/api/devices/status" : "/api/devices");
      state.devices = data.devices || [];
      for (const id of [...state.selected]) {
        if (!state.devices.some((device) => device.device_id === id)) state.selected.delete(id);
      }
      renderDevices();
      renderSettingsDevices();
      setServiceState("online", `本地服务已连接 · ${state.devices.filter((d) => d.online).length} 台在线`);
    } catch (error) {
      setServiceState("error", "状态刷新失败");
      if (!state.devices.length) renderDevices();
      console.warn(error);
    }
  }

  async function refreshStatuses(showToast = false) {
    if (state.busy) return;
    await loadDevices(true);
    if (showToast) toast("设备状态已刷新", "success");
  }

  function updateStats() {
    const online = state.devices.filter((device) => device.online).length;
    $("#statTotal").textContent = state.devices.length;
    $("#statOnline").textContent = online;
    $("#statSelected").textContent = state.selected.size;
    $("#statOnlineHint").textContent = state.devices.length ? `离线 ${state.devices.length - online} 台` : "等待发现设备";
  }

  function batteryText(device) {
    if (!device.online || device.battery_visible !== true) return "";
    const pct = Number(device.battery_percent);
    if (!Number.isFinite(pct) || pct < 0 || pct > 100) return "";
    const runtime = Number(device.battery_runtime_min);
    const suffix = Number.isFinite(runtime) && runtime >= 0 && runtime <= 9999
      ? ` · ${Math.floor(runtime / 60)} h ${Math.round(runtime % 60)} min`
      : "";
    return `${Math.round(pct)}%${suffix}`;
  }

  function displayText(device) {
    const mode = String(device.display_mode || "home");
    if (mode === "image") return `图片 ${device.display_slot || device.active_slot || 1}`;
    if (mode === "charging") return "充电页";
    return "主界面";
  }

  function renderDevices() {
    const search = $("#deviceSearch").value.trim().toLowerCase();
    const list = state.devices.filter((device) => {
      const haystack = `${device.name || ""} ${device.device_id || ""} ${device.ip || ""} ${device.group || ""}`.toLowerCase();
      return !search || haystack.includes(search);
    });
    $("#deviceEmpty").classList.toggle("hidden", state.devices.length > 0);
    const grid = $("#deviceGrid");
    grid.classList.toggle("hidden", state.devices.length === 0);
    grid.innerHTML = list.map((device) => {
      const id = device.device_id || device.ip;
      const selected = state.selected.has(id);
      const saved = String(device.saved_slots || "none");
      const battery = batteryText(device);
      return `<article class="device-card ${selected ? "selected" : ""}" data-device-id="${escapeHtml(id)}">
        <div class="device-card-head">
          <label class="device-select"><input type="checkbox" data-select-device="${escapeHtml(id)}" ${selected ? "checked" : ""}>
            <span><span class="device-name">${escapeHtml(device.name || id)}</span><span class="device-id">${escapeHtml(id)} · ${escapeHtml(device.ip)}</span></span>
          </label>
          <span class="status-badge ${device.online ? "online" : ""}">${device.online ? "在线" : "离线"}</span>
        </div>
        <div class="device-metrics">
          ${battery ? `<div class="metric"><span>电量</span><strong>${escapeHtml(battery)}</strong></div>` : ""}
          <div class="metric"><span>当前画面</span><strong>${escapeHtml(displayText(device))}</strong></div>
          <div class="metric"><span>亮度</span><strong>${device.brightness ?? "—"}</strong></div>
          <div class="metric"><span>固件</span><strong>${escapeHtml(device.fw_version || "—")}</strong></div>
          <div class="metric"><span>保存槽位</span><strong>${escapeHtml(saved === "none" ? "无" : saved)}</strong></div>
          <div class="metric"><span>模式</span><strong>${device.lan_only ? "仅局域网" : "云端兼容"}</strong></div>
        </div>
        <div class="device-footer"><span>${device.online ? `延迟 ${device.latency_ms || 0}ms` : escapeHtml(device.error || "未连接")}</span><button data-open-device="${escapeHtml(id)}">单独控制 →</button></div>
      </article>`;
    }).join("");
    updateStats();
    updateTargetSummaries();
    renderBatchPlan();
  }

  function selectedDevices(onlineOnly = false) {
    return state.devices.filter((device) => state.selected.has(device.device_id || device.ip) && (!onlineOnly || device.online));
  }

  function updateTargetSummaries() {
    const selected = selectedDevices(false);
    $("#composeTargets").textContent = selected.length
      ? selected.map((d) => d.name || d.device_id).join("、")
      : "尚未选择设备";
  }

  async function scanDevices() {
    const subnet = $("#subnetInput").value.trim();
    $("#scanBtn").disabled = true;
    $("#scanProgress").classList.remove("hidden");
    try {
      const result = await api("/api/discover", { method: "POST", body: JSON.stringify({ subnet }) });
      toast(result.count ? `发现 ${result.count} 台桌牌` : "扫描完成，没有发现桌牌", result.count ? "success" : "info");
      await loadDevices(true);
    } catch (error) {
      toast(`扫描失败：${error.message}`, "error", 6000);
    } finally {
      $("#scanBtn").disabled = false;
      $("#scanProgress").classList.add("hidden");
    }
  }

  async function addManualIp() {
    const ip = $("#manualIpInput").value.trim();
    if (!ip) return toast("请输入设备 IP", "error");
    $("#addIpBtn").disabled = true;
    try {
      const data = await api("/api/device", { method: "POST", body: JSON.stringify({ ip }) });
      toast(`已添加 ${data.device.device_id}`, "success");
      $("#manualIpInput").value = "";
      await loadDevices(true);
    } catch (error) {
      toast(`添加失败：${error.message}`, "error", 6000);
    } finally { $("#addIpBtn").disabled = false; }
  }

  function openDevice(id) {
    const device = state.devices.find((item) => (item.device_id || item.ip) === id);
    if (!device) return;
    state.activeDeviceId = id;
    $("#drawerName").textContent = device.name || id;
    $("#drawerIdentity").textContent = `${id} · ${device.ip}`;
    $("#drawerBrightness").value = device.brightness ?? 120;
    $("#drawerBrightnessValue").textContent = device.brightness ?? 120;
    $("#drawerSlot").value = device.display_slot || device.active_slot || 1;
    const battery = batteryText(device);
    $("#drawerStatus").innerHTML = `
      <div><span>连接</span><strong>${device.online ? "在线" : "离线"}</strong></div>
      ${battery ? `<div><span>电量</span><strong>${escapeHtml(battery)}</strong></div>` : ""}
      <div><span>当前显示</span><strong>${escapeHtml(displayText(device))}</strong></div>
      <div><span>固件</span><strong>${escapeHtml(device.fw_version || "—")}</strong></div>
      <div><span>模式</span><strong>${device.lan_only ? "仅局域网" : "云端兼容"}</strong></div>
      <div><span>IP</span><strong>${escapeHtml(device.ip)}</strong></div>`;
    $("#deviceDrawer").classList.add("open");
    $("#deviceDrawer").setAttribute("aria-hidden", "false");
    $("#drawerBackdrop").classList.remove("hidden");
  }

  function closeDevice() {
    $("#deviceDrawer").classList.remove("open");
    $("#deviceDrawer").setAttribute("aria-hidden", "true");
    $("#drawerBackdrop").classList.add("hidden");
  }

  async function sendControl(deviceId, action, extra = {}) {
    return api("/api/device/control", {
      method: "POST",
      body: JSON.stringify({ device_id: deviceId, action, ...extra }),
    });
  }

  async function handleDrawerAction(action) {
    if (!state.activeDeviceId) return;
    const extra = {};
    if (["slot", "delete_slot"].includes(action)) extra.slot = Number($("#drawerSlot").value);
    if (action === "brightness") extra.value = Number($("#drawerBrightness").value);
    try {
      await sendControl(state.activeDeviceId, action, extra);
      toast(action === "find" ? "设备将亮暗闪烁三次" : "设备命令已执行", "success");
      await sleep(500);
      await refreshStatuses(false);
      openDevice(state.activeDeviceId);
    } catch (error) { toast(`控制失败：${error.message}`, "error", 6000); }
  }

  function drawTemplate(ctx, template) {
    const p = templatePalettes[template] || templatePalettes.red;
    const gradient = ctx.createLinearGradient(0, 0, 1024, 600);
    gradient.addColorStop(0, p.bg1); gradient.addColorStop(.52, p.bg2); gradient.addColorStop(1, p.bg3);
    ctx.fillStyle = gradient; ctx.fillRect(0, 0, 1024, 600);
    ctx.globalAlpha = template === "ink" ? .2 : .09;
    ctx.fillStyle = p.accent;
    ctx.beginPath(); ctx.arc(930, 40, 230, 0, Math.PI * 2); ctx.fill();
    ctx.beginPath(); ctx.arc(70, 650, 260, 0, Math.PI * 2); ctx.fill();
    ctx.globalAlpha = 1;
    ctx.fillStyle = p.accent; ctx.fillRect(0, 0, 14, 600);
  }

  function drawImage(ctx) {
    if (!state.image) return;
    const canvas = ctx.canvas;
    const img = state.image;
    const base = state.imageFit === "contain"
      ? Math.min(canvas.width / img.width, canvas.height / img.height)
      : Math.max(canvas.width / img.width, canvas.height / img.height);
    const scale = base * state.imageZoom / 100;
    const width = img.width * scale;
    const height = img.height * scale;
    const x = (canvas.width - width) / 2 + state.imageOffsetX * canvas.width / 200;
    const y = (canvas.height - height) / 2 + state.imageOffsetY * canvas.height / 200;
    ctx.drawImage(img, x, y, width, height);
  }

  function drawText(ctx, layer) {
    ctx.save();
    ctx.textAlign = "center"; ctx.textBaseline = "middle";
    ctx.font = `700 ${clamp(layer.size, 16, 180)}px "Microsoft YaHei", "PingFang SC", sans-serif`;
    if (layer.stroke) {
      ctx.strokeStyle = "rgba(255,255,255,.95)";
      ctx.lineWidth = Math.max(3, layer.size / 18);
      ctx.lineJoin = "round";
      ctx.strokeText(layer.content || "", clamp(layer.x, 0, 1024), clamp(layer.y, 0, 600));
    }
    ctx.fillStyle = layer.color || "#111827";
    ctx.fillText(layer.content || "", clamp(layer.x, 0, 1024), clamp(layer.y, 0, 600));
    ctx.restore();
  }

  function renderComposeCanvas() {
    const ctx = $("#composeCanvas").getContext("2d");
    ctx.clearRect(0, 0, 1024, 600);
    drawTemplate(ctx, state.singleTemplate);
    drawImage(ctx);
    state.textLayers.forEach((layer) => drawText(ctx, layer));
    renderTextLayerList();
  }

  function renderTextLayerList() {
    const root = $("#textLayerList");
    root.innerHTML = state.textLayers.map((layer, index) => `<button class="text-layer ${layer.id === state.activeTextId ? "active" : ""}" data-text-layer="${layer.id}">${escapeHtml(layer.content || `文字${index + 1}`)}<span class="remove" data-remove-text="${layer.id}">×</span></button>`).join("");
    const layer = state.textLayers.find((item) => item.id === state.activeTextId);
    $("#textEditor").classList.toggle("hidden", !layer);
    if (!layer) return;
    $("#textContent").value = layer.content;
    $("#textSize").value = layer.size;
    $("#textColor").value = layer.color;
    $("#textX").value = layer.x;
    $("#textY").value = layer.y;
    $("#textStroke").checked = !!layer.stroke;
  }

  function updateActiveText() {
    const layer = state.textLayers.find((item) => item.id === state.activeTextId);
    if (!layer) return;
    layer.content = $("#textContent").value;
    layer.size = clamp($("#textSize").value, 16, 180);
    layer.color = $("#textColor").value;
    layer.x = clamp($("#textX").value, 0, 1024);
    layer.y = clamp($("#textY").value, 0, 600);
    layer.stroke = $("#textStroke").checked;
    renderComposeCanvas();
  }

  function canvasBlob(canvas, quality = .9) {
    return new Promise((resolve, reject) => canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error("JPEG 生成失败")), "image/jpeg", quality));
  }

  async function uploadBlobToDevice(device, blob, save, slot) {
    const id = encodeURIComponent(device.device_id || device.ip);
    const response = await fetch(`/api/device/upload?device_id=${id}&save=${save ? 1 : 0}&slot=${slot}`, {
      method: "POST", headers: { "Content-Type": "image/jpeg" }, body: blob,
    });
    const result = await response.json();
    if (!response.ok || result.ok === false) throw new Error(result.error || `HTTP ${response.status}`);
    return result;
  }

  async function uploadSingle() {
    const devices = selectedDevices(true);
    if (!devices.length) return toast("请先选择至少一台在线设备", "error");
    const status = $("#singleUploadStatus");
    const button = $("#uploadSingleBtn");
    const save = $("#singleSave").value === "1";
    const slot = clamp($("#singleSlot").value, 1, 99);
    state.busy = true; button.disabled = true;
    try {
      const blob = await canvasBlob($("#composeCanvas"));
      let success = 0;
      for (let i = 0; i < devices.length; i++) {
        status.className = "task-message";
        status.textContent = `${i + 1}/${devices.length} 正在上传到 ${devices[i].name || devices[i].device_id}…`;
        await uploadBlobToDevice(devices[i], blob, save, slot);
        success++;
      }
      status.className = "task-message success";
      status.textContent = `完成：${success}/${devices.length} 台设备`;
      toast("传图完成", "success");
    } catch (error) {
      status.className = "task-message error";
      status.textContent = `失败：${error.message}`;
      toast(`传图失败：${error.message}`, "error", 7000);
    } finally {
      state.busy = false; button.disabled = false;
      await refreshStatuses(false);
    }
  }

  function setSelectOptions(select, headers, preferred) {
    select.innerHTML = headers.map((header, index) => `<option value="${index}">${escapeHtml(header || `第${index + 1}列`)}</option>`).join("");
    const match = headers.findIndex((header) => preferred.test(header));
    select.value = String(match >= 0 ? match : Math.min(select.id === "mapCompany" ? 0 : select.id === "mapName" ? 1 : 2, headers.length - 1));
  }

  function remapBatchRows() {
    if (!state.tableMatrix.length) return;
    const ci = Number($("#mapCompany").value || 0);
    const ni = Number($("#mapName").value || 1);
    const ti = Number($("#mapTitle").value || 2);
    state.batchRows = state.tableMatrix.map((row) => ({ company: row[ci] || "", name: row[ni] || "", title: row[ti] || "" }));
    renderBatchRows();
    renderBatchCanvas();
    renderBatchPlan();
  }

  async function parseTableFile(file) {
    if (!file) return;
    try {
      const response = await fetch(`/api/table/parse?filename=${encodeURIComponent(file.name)}`, { method: "POST", body: file });
      const data = await response.json();
      if (!response.ok || data.ok === false) throw new Error(data.error || "解析失败");
      state.tableHeaders = data.headers || [];
      state.tableMatrix = data.rows || [];
      setSelectOptions($("#mapCompany"), state.tableHeaders, /公司|单位|company/i);
      setSelectOptions($("#mapName"), state.tableHeaders, /姓名|名字|name/i);
      setSelectOptions($("#mapTitle"), state.tableHeaders, /职位|职务|title|部门/i);
      $("#tableFileSummary").textContent = `${file.name} · ${data.count} 条记录${data.has_header ? " · 已识别表头" : ""}`;
      remapBatchRows();
      toast(`已导入 ${data.count} 条名单`, "success");
    } catch (error) { toast(`名单解析失败：${error.message}`, "error", 7000); }
  }

  function renderBatchRows() {
    const root = $("#batchTableBody");
    root.innerHTML = state.batchRows.map((row, index) => `<tr data-row="${index}">
      <td>${index + 1}</td>
      <td><input data-row-field="company" value="${escapeHtml(row.company)}"></td>
      <td><input data-row-field="name" value="${escapeHtml(row.name)}"></td>
      <td><input data-row-field="title" value="${escapeHtml(row.title)}"></td>
      <td><button class="row-remove" data-remove-row="${index}">×</button></td>
    </tr>`).join("");
  }

  function batchLayout() {
    return {
      companySize: clamp($("#companySize").value, 24, 90),
      nameSize: clamp($("#nameSize").value, 50, 170),
      titleSize: clamp($("#titleSize").value, 24, 90),
      companyColor: $("#companyColor").value,
      nameColor: $("#nameColor").value,
      titleColor: $("#titleColor").value,
    };
  }

  function drawBatchRow(row, canvas = $("#batchCanvas")) {
    const ctx = canvas.getContext("2d");
    const p = templatePalettes[state.batchTemplate];
    const layout = batchLayout();
    ctx.clearRect(0, 0, 1024, 600);
    drawTemplate(ctx, state.batchTemplate);
    const items = [
      { text: row.company || "示例公司名称", x: 96, y: 120, align: "left", size: layout.companySize, color: layout.companyColor || p.accent },
      { text: row.name || "张三", x: 512, y: 305, align: "center", size: layout.nameSize, color: layout.nameColor || p.ink },
      { text: row.title || "职位 / 部门", x: 512, y: 430, align: "center", size: layout.titleSize, color: layout.titleColor || p.sub },
    ];
    for (const item of items) {
      ctx.save(); ctx.textAlign = item.align; ctx.textBaseline = "middle";
      ctx.font = `700 ${item.size}px "Microsoft YaHei", "PingFang SC", sans-serif`;
      ctx.fillStyle = item.color; ctx.fillText(item.text, item.x, item.y, item.align === "left" ? 820 : 900); ctx.restore();
    }
  }

  function renderBatchCanvas() { drawBatchRow(state.batchRows[0] || {}); }

  function batchPlan() {
    const devices = selectedDevices(false);
    const rows = state.batchRows.filter((row) => row.company || row.name || row.title);
    return Array.from({ length: Math.min(devices.length, rows.length) }, (_, index) => ({ device: devices[index], row: rows[index], index }));
  }

  function renderBatchPlan() {
    const plan = batchPlan();
    $("#batchPlanCount").textContent = plan.length;
    if (!state.batchStates.length || state.batchStates.length !== plan.length) {
      state.batchStates = plan.map(() => ({ state: "pending", text: "等待" }));
    }
    $("#batchApplyList").innerHTML = plan.map((item, index) => {
      const task = state.batchStates[index] || { state: "pending", text: "等待" };
      return `<div class="apply-item ${task.state}" data-apply-index="${index}">
        <div><strong>${escapeHtml(item.device.name || item.device.device_id)}</strong><span>${escapeHtml(item.device.device_id)} · ${escapeHtml(item.device.ip)}</span></div>
        <div><strong>${escapeHtml(item.row.name || "未填写姓名")}</strong><span>${escapeHtml([item.row.company, item.row.title].filter(Boolean).join(" · "))}</span></div>
        <div class="apply-state">${escapeHtml(task.text)}</div>
      </div>`;
    }).join("");
  }

  async function startBatch() {
    const plan = batchPlan();
    if (!plan.length) return toast("请先选择设备并导入名单", "error");
    if (plan.some((item) => !item.device.online)) return toast("选中的设备中有离线设备，请刷新或取消选择", "error");
    state.batchStop = false; state.busy = true;
    state.batchStates = plan.map(() => ({ state: "pending", text: "等待" }));
    $("#startBatchBtn").disabled = true;
    $("#stopBatchBtn").classList.remove("hidden");
    $("#batchProgressShell").classList.remove("hidden");
    renderBatchPlan();
    const slot = clamp($("#batchSlot").value, 1, 99);
    const save = $("#batchSave").checked;
    let success = 0;
    try {
      for (let i = 0; i < plan.length; i++) {
        if (state.batchStop) break;
        state.batchStates[i] = { state: "running", text: "生成图片" };
        setBatchProgress(i, plan.length, `正在生成 ${plan[i].row.name || plan[i].device.name}`);
        renderBatchPlan();
        drawBatchRow(plan[i].row);
        const blob = await canvasBlob($("#batchCanvas"));
        state.batchStates[i] = { state: "running", text: "上传中" };
        setBatchProgress(i, plan.length, `正在上传到 ${plan[i].device.name || plan[i].device.device_id}`);
        renderBatchPlan();
        try {
          await uploadBlobToDevice(plan[i].device, blob, save, slot);
          success++;
          state.batchStates[i] = { state: "success", text: "完成" };
        } catch (error) {
          state.batchStates[i] = { state: "error", text: error.message };
        }
        setBatchProgress(i + 1, plan.length, `${i + 1}/${plan.length} 已处理`);
        renderBatchPlan();
      }
      toast(`批量应用结束：成功 ${success}/${plan.length}`, success === plan.length ? "success" : "info", 6000);
    } finally {
      state.busy = false;
      $("#startBatchBtn").disabled = false;
      $("#stopBatchBtn").classList.add("hidden");
      await refreshStatuses(false);
      renderBatchCanvas();
    }
  }

  function setBatchProgress(done, total, text) {
    const pct = total ? Math.round(done / total * 100) : 0;
    $("#batchProgressText").textContent = text;
    $("#batchProgressPercent").textContent = `${pct}%`;
    $("#batchProgressBar").style.width = `${pct}%`;
  }

  function renderSettingsDevices() {
    const root = $("#settingsDeviceList");
    if (!state.devices.length) { root.innerHTML = '<div class="file-summary">尚未添加设备</div>'; return; }
    root.innerHTML = state.devices.map((device) => `<div class="settings-device-row" data-settings-id="${escapeHtml(device.device_id)}">
      <label>设备编号<input value="${escapeHtml(device.device_id)}" disabled></label>
      <label>显示名称<input data-setting-field="name" value="${escapeHtml(device.name || device.device_id)}"></label>
      <label>分组<input data-setting-field="group" value="${escapeHtml(device.group || "默认分组")}"></label>
      <div><button class="button secondary" data-save-device="${escapeHtml(device.device_id)}">保存</button> <button class="button danger subtle" data-remove-device="${escapeHtml(device.device_id)}">移除</button></div>
    </div>`).join("");
  }

  async function setLanMode(enabled) {
    const devices = selectedDevices(true);
    if (!devices.length) return toast("请先选择在线设备", "error");
    state.busy = true;
    let ok = 0;
    try {
      for (const device of devices) {
        try { await sendControl(device.device_id, "lan_mode", { enabled }); ok++; }
        catch (error) { toast(`${device.name || device.device_id}：${error.message}`, "error", 6000); }
      }
      toast(`${ok}/${devices.length} 台设备已${enabled ? "开启仅局域网" : "恢复云端兼容"}`, ok === devices.length ? "success" : "info");
    } finally { state.busy = false; await refreshStatuses(false); }
  }

  function bindEvents() {
    $$(".nav-item").forEach((button) => button.addEventListener("click", () => navigate(button.dataset.page)));
    $$('[data-page-jump]').forEach((button) => button.addEventListener("click", () => navigate(button.dataset.pageJump)));
    $("#refreshAllBtn").addEventListener("click", () => refreshStatuses(true));
    $("#scanBtn").addEventListener("click", scanDevices);
    $("#addIpBtn").addEventListener("click", addManualIp);
    $("#manualIpInput").addEventListener("keydown", (event) => { if (event.key === "Enter") addManualIp(); });
    $("#deviceSearch").addEventListener("input", renderDevices);
    $("#selectOnlineBtn").addEventListener("click", () => { state.devices.filter((d) => d.online).forEach((d) => state.selected.add(d.device_id)); saveSelection(); renderDevices(); });
    $("#clearSelectionBtn").addEventListener("click", () => { state.selected.clear(); saveSelection(); renderDevices(); });
    $("#deviceGrid").addEventListener("change", (event) => {
      const id = event.target.dataset.selectDevice;
      if (!id) return;
      event.target.checked ? state.selected.add(id) : state.selected.delete(id);
      saveSelection(); renderDevices();
    });
    $("#deviceGrid").addEventListener("click", (event) => { const id = event.target.dataset.openDevice; if (id) openDevice(id); });
    $("#closeDrawerBtn").addEventListener("click", closeDevice);
    $("#drawerBackdrop").addEventListener("click", closeDevice);
    $("#drawerBrightness").addEventListener("input", () => { $("#drawerBrightnessValue").textContent = $("#drawerBrightness").value; });
    $("#deviceDrawer").addEventListener("click", async (event) => {
      const action = event.target.dataset.deviceAction;
      const power = event.target.dataset.powerMode;
      if (action) {
        if (action === "clear_images" && !confirm("确定删除该设备保存的全部图片吗？Wi-Fi 和设备码不会被清除。")) return;
        await handleDrawerAction(action);
      }
      if (power) {
        try { await sendControl(state.activeDeviceId, "power", { mode: power }); toast("供电模式已切换", "success"); }
        catch (error) { toast(error.message, "error"); }
      }
    });

    $("#imageInput").addEventListener("change", (event) => {
      const file = event.target.files[0]; if (!file) return;
      const img = new Image(); const url = URL.createObjectURL(file);
      img.onload = () => { state.image = img; URL.revokeObjectURL(url); $("#canvasHint").textContent = file.name; renderComposeCanvas(); };
      img.onerror = () => { URL.revokeObjectURL(url); toast("图片读取失败", "error"); };
      img.src = url;
    });
    $$('.segmented [data-fit]').forEach((button) => button.addEventListener("click", () => {
      state.imageFit = button.dataset.fit; $$('.segmented [data-fit]').forEach((b) => b.classList.toggle("active", b === button)); renderComposeCanvas();
    }));
    [["imageZoom", "imageZoom"], ["imageOffsetX", "imageOffsetX"], ["imageOffsetY", "imageOffsetY"]].forEach(([id, key]) => $("#" + id).addEventListener("input", () => { state[key] = Number($("#" + id).value); renderComposeCanvas(); }));
    $("#singleTemplateRow").addEventListener("click", (event) => {
      const template = event.target.dataset.template; if (!template) return;
      state.singleTemplate = template; $$(".template-chip", $("#singleTemplateRow")).forEach((b) => b.classList.toggle("active", b === event.target)); renderComposeCanvas();
    });
    $("#addTextBtn").addEventListener("click", () => {
      const layer = { id: crypto.randomUUID(), content: `文字${state.textLayers.length + 1}`, size: 48, color: "#111827", x: 512, y: 380, stroke: false };
      state.textLayers.push(layer); state.activeTextId = layer.id; renderComposeCanvas();
    });
    $("#textLayerList").addEventListener("click", (event) => {
      const removeId = event.target.dataset.removeText;
      if (removeId) { event.stopPropagation(); state.textLayers = state.textLayers.filter((l) => l.id !== removeId); state.activeTextId = state.textLayers[0]?.id || ""; renderComposeCanvas(); return; }
      const button = event.target.closest("[data-text-layer]"); if (!button) return;
      state.activeTextId = button.dataset.textLayer; renderTextLayerList();
    });
    ["textContent", "textSize", "textColor", "textX", "textY", "textStroke"].forEach((id) => $("#" + id).addEventListener("input", updateActiveText));
    $("#uploadSingleBtn").addEventListener("click", uploadSingle);

    $("#tableInput").addEventListener("change", (event) => parseTableFile(event.target.files[0]));
    ["mapCompany", "mapName", "mapTitle"].forEach((id) => $("#" + id).addEventListener("change", remapBatchRows));
    $("#addBatchRowBtn").addEventListener("click", () => { state.batchRows.push({ company: "", name: "", title: "" }); renderBatchRows(); renderBatchPlan(); });
    $("#batchTableBody").addEventListener("input", (event) => {
      const field = event.target.dataset.rowField; if (!field) return;
      const row = Number(event.target.closest("tr").dataset.row); state.batchRows[row][field] = event.target.value; renderBatchCanvas(); renderBatchPlan();
    });
    $("#batchTableBody").addEventListener("click", (event) => {
      if (event.target.dataset.removeRow === undefined) return;
      state.batchRows.splice(Number(event.target.dataset.removeRow), 1); renderBatchRows(); renderBatchCanvas(); renderBatchPlan();
    });
    $("#batchTemplateRow").addEventListener("click", (event) => {
      const template = event.target.dataset.template; if (!template) return;
      state.batchTemplate = template;
      const p = templatePalettes[template]; $("#companyColor").value = p.accent; $("#nameColor").value = p.ink; $("#titleColor").value = p.sub;
      $$(".template-chip", $("#batchTemplateRow")).forEach((b) => b.classList.toggle("active", b === event.target)); renderBatchCanvas();
    });
    ["companySize", "nameSize", "titleSize", "companyColor", "nameColor", "titleColor"].forEach((id) => $("#" + id).addEventListener("input", renderBatchCanvas));
    $("#startBatchBtn").addEventListener("click", startBatch);
    $("#stopBatchBtn").addEventListener("click", () => { state.batchStop = true; $("#batchProgressText").textContent = "将在当前设备完成后停止"; });
    $("#enableLanOnlyBtn").addEventListener("click", () => setLanMode(true));
    $("#disableLanOnlyBtn").addEventListener("click", () => setLanMode(false));
    $("#settingsDeviceList").addEventListener("click", async (event) => {
      const saveId = event.target.dataset.saveDevice;
      const removeId = event.target.dataset.removeDevice;
      if (saveId) {
        const row = event.target.closest(".settings-device-row");
        try {
          await api("/api/device/update", { method: "POST", body: JSON.stringify({ device_id: saveId, ip: state.devices.find((d) => d.device_id === saveId).ip, name: $('[data-setting-field="name"]', row).value, group: $('[data-setting-field="group"]', row).value }) });
          toast("设备信息已保存", "success"); await loadDevices(false);
        } catch (error) { toast(error.message, "error"); }
      }
      if (removeId && confirm("只从本机列表移除此设备？不会清除设备本身。")) {
        await api("/api/device/remove", { method: "POST", body: JSON.stringify({ device_id: removeId }) });
        state.selected.delete(removeId); saveSelection(); await loadDevices(false);
      }
    });
  }

  async function init() {
    bindEvents();
    renderComposeCanvas();
    renderBatchRows();
    renderBatchCanvas();
    try { await loadHealth(); await loadDevices(true); }
    catch (error) { toast(`本地服务连接失败：${error.message}`, "error", 8000); }
    state.monitorTimer = setInterval(() => refreshStatuses(false), 8000);
  }

  document.addEventListener("DOMContentLoaded", init);
})();
