"use client";

import React, { useEffect, useMemo, useRef, useState } from "react";
import {
  Alert,
  Box,
  Button,
  CircularProgress,
  Paper,
  Stack,
  Tab,
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableRow,
  Tabs,
  TextField,
  Typography,
} from "@mui/material";
import { FileDownload } from "@mui/icons-material";
import dayjs from "dayjs";
import { formatHongKongDateTime } from "@/utils/formatHongKongDateTime";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
import LotLabelPrintModal from "@/components/InventorySearch/LotLabelPrintModal";
import {
  buildOnPackJobOrdersPayload,
  downloadOnPackTextQrZip,
  fetchJobOrders,
  pushOnPackTextQrZipToNgpcl,
  type JobOrderListItem,
} from "@/app/api/bagPrint/actions";
import {
  fetchLaserBag2Settings,
  runLaserBag2AutoSend,
  type LaserBag2AutoSendReport,
  type LaserLastReceiveSuccess,
} from "@/app/api/laserPrint/actions";
import * as XLSX from "xlsx";

interface TabPanelProps {
  children?: React.ReactNode;
  index: number;
  value: number;
}

function TabPanel(props: TabPanelProps) {
  const { children, value, index, ...other } = props;
  return (
    <div
      role="tabpanel"
      hidden={value !== index}
      id={`simple-tabpanel-${index}`}
      aria-labelledby={`simple-tab-${index}`}
      {...other}
    >
      {value === index && <Box sx={{ p: 3 }}>{children}</Box>}
    </div>
  );
}

export default function TestingPage() {
  const [tabValue, setTabValue] = useState(0);
  const [lotLabelModalOpen, setLotLabelModalOpen] = useState(false);

  const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
    setTabValue(newValue);
  };

  // --- 1. GRN Preview (M18) ---
  const [grnPreviewReceiptDate, setGrnPreviewReceiptDate] =
    useState("2026-03-16");
  // --- 2. OnPack NGPCL (same job-order → ZIP logic as /bagPrint) ---
  const [onpackPlanDate, setOnpackPlanDate] = useState(() =>
    dayjs().format("YYYY-MM-DD"),
  );
  const [onpackJobOrders, setOnpackJobOrders] = useState<JobOrderListItem[]>(
    [],
  );
  const [onpackLoading, setOnpackLoading] = useState(false);
  const [onpackLoadError, setOnpackLoadError] = useState<string | null>(null);
  const [onpackLemonDownloading, setOnpackLemonDownloading] = useState(false);
  const [onpackPushLoading, setOnpackPushLoading] = useState(false);
  const [onpackPushResult, setOnpackPushResult] = useState<string | null>(null);
  // --- 3. Laser Bag2 auto-send (same as /laserPrint + DB LASER_PRINT.*) ---
  const [laserAutoPlanDate, setLaserAutoPlanDate] = useState(() =>
    dayjs().format("YYYY-MM-DD"),
  );
  const [laserAutoLimit, setLaserAutoLimit] = useState("1");
  const [laserAutoLoading, setLaserAutoLoading] = useState(false);
  const [laserAutoReport, setLaserAutoReport] =
    useState<LaserBag2AutoSendReport | null>(null);
  const [laserAutoError, setLaserAutoError] = useState<string | null>(null);
  const [laserLastReceive, setLaserLastReceive] =
    useState<LaserLastReceiveSuccess | null>(null);
  const bomShopSyncInFlightRef = useRef(false);
  const bomShopSyncAllInFlightRef = useRef(false);
  const [bomShopSyncBomId, setBomShopSyncBomId] = useState("78");
  const [bomShopM18HeaderId, setBomShopM18HeaderId] = useState("");
  const [bomShopSyncLoading, setBomShopSyncLoading] = useState(false);
  const [bomShopSyncResult, setBomShopSyncResult] = useState<string | null>(
    null,
  );
  const [bomShopSyncAllLoading, setBomShopSyncAllLoading] = useState(false);
  const [bomShopSyncAllResult, setBomShopSyncAllResult] = useState<
    string | null
  >(null);
  const bomByItemCodeInFlightRef = useRef(false);
  const [bomByItemCodeInput, setBomByItemCodeInput] = useState("");
  const [bomByItemCodeLoading, setBomByItemCodeLoading] = useState(false);
  const [bomByItemCodeResult, setBomByItemCodeResult] = useState<string | null>(
    null,
  );
  const whatsAppTestInFlightRef = useRef(false);
  const [whatsAppVar1, setWhatsAppVar1] = useState("12/1");
  const [whatsAppVar2, setWhatsAppVar2] = useState("3pm");
  const [whatsAppTestLoading, setWhatsAppTestLoading] = useState(false);
  const [whatsAppTestResult, setWhatsAppTestResult] = useState<string | null>(
    null,
  );
  const emailTestInFlightRef = useRef(false);
  const [emailTestSubject, setEmailTestSubject] = useState(
    "FPSMS M18 sync alert [TEST]",
  );
  const [emailTestMessage, setEmailTestMessage] = useState(
    "FPSMS sync alert test message from /testing page.",
  );
  const [emailTestLoading, setEmailTestLoading] = useState(false);
  const [emailTestResult, setEmailTestResult] = useState<string | null>(null);

  const onpackPayload = useMemo(
    () => buildOnPackJobOrdersPayload(onpackJobOrders),
    [onpackJobOrders],
  );

  useEffect(() => {
    if (tabValue !== 1) return;
    let cancelled = false;
    (async () => {
      setOnpackLoading(true);
      setOnpackLoadError(null);
      try {
        const data = await fetchJobOrders(onpackPlanDate);
        if (!cancelled) setOnpackJobOrders(data);
      } catch (e) {
        if (!cancelled) {
          setOnpackLoadError(
            e instanceof Error ? e.message : "Failed to load job orders",
          );
          setOnpackJobOrders([]);
        }
      } finally {
        if (!cancelled) setOnpackLoading(false);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [tabValue, onpackPlanDate]);

  useEffect(() => {
    if (tabValue !== 2) return;
    let cancelled = false;
    (async () => {
      try {
        const s = await fetchLaserBag2Settings();
        if (!cancelled) setLaserLastReceive(s.lastReceiveSuccess ?? null);
      } catch {
        if (!cancelled) setLaserLastReceive(null);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [tabValue]);

  const handleDownloadGrnPreviewXlsx = async () => {
    try {
      const response = await clientAuthFetch(
        `${NEXT_PUBLIC_API_URL}/report/grn-preview-m18?receiptDate=${encodeURIComponent(
          grnPreviewReceiptDate,
        )}`,
        { method: "GET" },
      );
      if (response.status === 401 || response.status === 403) return;
      if (!response.ok) throw new Error(`Download failed: ${response.status}`);

      const data = await response.json();
      const rows = Array.isArray(data?.rows) ? data.rows : [];

      const ws = XLSX.utils.json_to_sheet(rows);
      const wb = XLSX.utils.book_new();
      XLSX.utils.book_append_sheet(wb, ws, "GRN Preview");

      const xlsxArrayBuffer = XLSX.write(wb, {
        bookType: "xlsx",
        type: "array",
      });
      const blob = new Blob([xlsxArrayBuffer], {
        type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
      });

      const url = window.URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = url;
      link.setAttribute(
        "download",
        `grn-preview-m18-${grnPreviewReceiptDate}.xlsx`,
      );
      document.body.appendChild(link);
      link.click();
      link.remove();
      window.URL.revokeObjectURL(url);
    } catch (e) {
      console.error("GRN Preview XLSX Download Error:", e);
      alert("GRN Preview XLSX download failed. Check console/network.");
    }
  };

  const downloadBlob = (blob: Blob, filename: string) => {
    const url = window.URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.setAttribute("download", filename);
    document.body.appendChild(link);
    link.click();
    link.remove();
    window.URL.revokeObjectURL(url);
  };

  const handleOnpackDownloadLemonZip = async () => {
    if (onpackPayload.length === 0) {
      alert(
        "No job orders with item code for this plan date (same rule as Bag Print).",
      );
      return;
    }
    setOnpackLemonDownloading(true);
    try {
      const blob = await downloadOnPackTextQrZip({ jobOrders: onpackPayload });
      downloadBlob(blob, `onpack2023_lemon_qr_${onpackPlanDate}.zip`);
    } catch (e) {
      console.error("Lemon OnPack ZIP download error:", e);
      alert(e instanceof Error ? e.message : "Lemon OnPack ZIP failed");
    } finally {
      setOnpackLemonDownloading(false);
    }
  };

  const handleLaserBag2AutoSend = async () => {
    setLaserAutoLoading(true);
    setLaserAutoError(null);
    setLaserAutoReport(null);
    try {
      const lim = parseInt(laserAutoLimit.trim(), 10);
      const report = await runLaserBag2AutoSend({
        planStart: laserAutoPlanDate,
        limitPerRun: Number.isFinite(lim) ? lim : 1,
      });
      setLaserAutoReport(report);
      try {
        const s = await fetchLaserBag2Settings();
        setLaserLastReceive(s.lastReceiveSuccess ?? null);
      } catch {
        /* ignore */
      }
    } catch (e) {
      setLaserAutoError(e instanceof Error ? e.message : String(e));
    } finally {
      setLaserAutoLoading(false);
    }
  };

  const handleOnpackPushNgpcl = async () => {
    if (onpackPayload.length === 0) {
      alert("No job orders with item code for this plan date.");
      return;
    }
    setOnpackPushLoading(true);
    setOnpackPushResult(null);
    try {
      const r = await pushOnPackTextQrZipToNgpcl({ jobOrders: onpackPayload });
      setOnpackPushResult(
        `${r.pushed ? "Pushed" : "Not pushed"}: ${r.message}`,
      );
    } catch (e) {
      const msg = e instanceof Error ? e.message : String(e);
      setOnpackPushResult(`Error: ${msg}`);
      alert(msg);
    } finally {
      setOnpackPushLoading(false);
    }
  };

  const handleBomShopSyncM18 = async () => {
    if (bomShopSyncInFlightRef.current) return;
    const id = parseInt(bomShopSyncBomId.trim(), 10);
    if (!Number.isFinite(id) || id <= 0) {
      alert("Enter a valid BOM id (positive integer).");
      return;
    }
    bomShopSyncInFlightRef.current = true;
    setBomShopSyncLoading(true);
    setBomShopSyncResult(null);
    try {
      const m18H = bomShopM18HeaderId.trim();
      const qs =
        m18H && /^\d+$/.test(m18H)
          ? `?m18HeaderId=${encodeURIComponent(m18H)}`
          : "";
      const response = await clientAuthFetch(
        `${NEXT_PUBLIC_API_URL}/m18/test/bom-shop-sync/${id}${qs}`,
        { method: "POST" },
      );
      if (response.status === 401 || response.status === 403) return;
      const text = await response.text();
      let display = text;
      try {
        const parsed: unknown = JSON.parse(text);
        display = JSON.stringify(parsed, null, 2);
      } catch {
        /* keep raw */
      }
      if (!response.ok) {
        setBomShopSyncResult(`HTTP ${response.status}\n\n${display}`);
        return;
      }
      setBomShopSyncResult(display);
    } catch (e) {
      const msg = e instanceof Error ? e.message : String(e);
      setBomShopSyncResult(`Error: ${msg}`);
    } finally {
      setBomShopSyncLoading(false);
      bomShopSyncInFlightRef.current = false;
    }
  };

  const handleBomShopSyncAllM18 = async () => {
    if (bomShopSyncAllInFlightRef.current) return;
    bomShopSyncAllInFlightRef.current = true;
    setBomShopSyncAllLoading(true);
    setBomShopSyncAllResult(null);
    try {
      const response = await clientAuthFetch(
        `${NEXT_PUBLIC_API_URL}/scheduler/trigger/bom-shop-sync-all`,
        { method: "GET" },
      );
      if (response.status === 401 || response.status === 403) return;
      const text = await response.text();
      let display = text;
      try {
        const parsed: unknown = JSON.parse(text);
        display = JSON.stringify(parsed, null, 2);
      } catch {
        /* plain string from backend is fine */
      }
      if (!response.ok) {
        setBomShopSyncAllResult(`HTTP ${response.status}\n\n${display}`);
        return;
      }
      setBomShopSyncAllResult(display);
    } catch (e) {
      const msg = e instanceof Error ? e.message : String(e);
      setBomShopSyncAllResult(`Error: ${msg}`);
    } finally {
      setBomShopSyncAllLoading(false);
      bomShopSyncAllInFlightRef.current = false;
    }
  };

  const handleWhatsAppSyncAlertTest = async () => {
    if (whatsAppTestInFlightRef.current) return;
    whatsAppTestInFlightRef.current = true;
    setWhatsAppTestLoading(true);
    setWhatsAppTestResult(null);
    try {
      const params = new URLSearchParams();
      const v1 = whatsAppVar1.trim();
      const v2 = whatsAppVar2.trim();
      if (v1) params.set("var1", v1);
      if (v2) params.set("var2", v2);
      const qs = params.toString();
      const response = await clientAuthFetch(
        `${NEXT_PUBLIC_API_URL}/scheduler/trigger/sync-alert-test-whatsapp${qs ? `?${qs}` : ""}`,
        { method: "GET" },
      );
      if (response.status === 401 || response.status === 403) return;
      const text = await response.text();
      if (!response.ok) {
        setWhatsAppTestResult(`HTTP ${response.status}\n\n${text}`);
        return;
      }
      setWhatsAppTestResult(text);
    } catch (e) {
      const msg = e instanceof Error ? e.message : String(e);
      setWhatsAppTestResult(`Error: ${msg}`);
    } finally {
      setWhatsAppTestLoading(false);
      whatsAppTestInFlightRef.current = false;
    }
  };

  const handleSyncAlertTestEmail = async () => {
    if (emailTestInFlightRef.current) return;
    emailTestInFlightRef.current = true;
    setEmailTestLoading(true);
    setEmailTestResult(null);
    try {
      const params = new URLSearchParams();
      const subj = emailTestSubject.trim();
      const msg = emailTestMessage.trim();
      if (subj) params.set("subject", subj);
      if (msg) params.set("message", msg);
      const qs = params.toString();
      const response = await clientAuthFetch(
        `${NEXT_PUBLIC_API_URL}/scheduler/trigger/sync-alert-test-email${qs ? `?${qs}` : ""}`,
        { method: "GET" },
      );
      if (response.status === 401 || response.status === 403) return;
      const text = await response.text();
      if (!response.ok) {
        setEmailTestResult(`HTTP ${response.status}\n\n${text}`);
        return;
      }
      setEmailTestResult(text);
    } catch (e) {
      const msg = e instanceof Error ? e.message : String(e);
      setEmailTestResult(`Error: ${msg}`);
    } finally {
      setEmailTestLoading(false);
      emailTestInFlightRef.current = false;
    }
  };

  const handleBomLookupByItemCode = async () => {
    if (bomByItemCodeInFlightRef.current) return;
    const code = bomByItemCodeInput.trim();
    if (!code) {
      alert("Enter an item code.");
      return;
    }
    bomByItemCodeInFlightRef.current = true;
    setBomByItemCodeLoading(true);
    setBomByItemCodeResult(null);
    try {
      const response = await clientAuthFetch(
        `${NEXT_PUBLIC_API_URL}/bom/by-item-code?code=${encodeURIComponent(code)}`,
        { method: "GET" },
      );
      if (response.status === 401 || response.status === 403) return;
      const text = await response.text();
      let display = text;
      try {
        const parsed: unknown = JSON.parse(text);
        display = JSON.stringify(parsed, null, 2);
      } catch {
        /* keep raw */
      }
      setBomByItemCodeResult(display);
      if (!response.ok) {
        alert(`Lookup failed: HTTP ${response.status}`);
      }
    } catch (e) {
      const msg = e instanceof Error ? e.message : String(e);
      setBomByItemCodeResult(`Error: ${msg}`);
      alert(msg);
    } finally {
      setBomByItemCodeLoading(false);
      bomByItemCodeInFlightRef.current = false;
    }
  };

  const Section = ({
    title,
    children,
  }: {
    title: string;
    children?: React.ReactNode;
  }) => (
    <Paper
      sx={{
        p: 3,
        minHeight: "450px",
        display: "flex",
        flexDirection: "column",
      }}
    >
      <Typography
        variant="h5"
        gutterBottom
        color="primary"
        sx={{ borderBottom: "2px solid #f0f0f0", pb: 1, mb: 2 }}
      >
        {title}
      </Typography>
      {children || (
        <Typography color="textSecondary" sx={{ m: "auto" }}>
          Waiting for implementation...
        </Typography>
      )}
    </Paper>
  );

  return (
    <Box sx={{ p: 4 }}>
      <Typography variant="h4" sx={{ mb: 4, fontWeight: "bold" }}>
        Testing
      </Typography>

      <Tabs
        value={tabValue}
        onChange={handleTabChange}
        aria-label="testing sections tabs"
        centered
        variant="fullWidth"
      >
        <Tab label="1. GRN Preview" />
        <Tab label="2. OnPack NGPCL" />
        <Tab label="3. Laser Bag2 自動送" />
        <Tab label="4. 批號標籤列印" />
        <Tab label="5. M18 BOM shop" />
        <Tab label="6. Sync alert email" />
      </Tabs>

      <TabPanel value={tabValue} index={0}>
        <Section title="1. GRN Preview (M18)">
          <Stack
            direction="row"
            spacing={2}
            sx={{ mb: 2, alignItems: "center" }}
          >
            <TextField
              size="small"
              label="Receipt Date"
              type="date"
              value={grnPreviewReceiptDate}
              onChange={(e) => setGrnPreviewReceiptDate(e.target.value)}
              InputLabelProps={{ shrink: true }}
            />
            <Button
              variant="contained"
              color="success"
              size="medium"
              startIcon={<FileDownload />}
              onClick={handleDownloadGrnPreviewXlsx}
            >
              Download GRN Preview XLSX
            </Button>
          </Stack>
          <Typography variant="body2" color="textSecondary">
            Backend endpoint:{" "}
            <code>/report/grn-preview-m18?receiptDate=YYYY-MM-DD</code>
          </Typography>
        </Section>
      </TabPanel>

      <TabPanel value={tabValue} index={1}>
        <Section title="2. OnPack NGPCL (same logic as /bagPrint)">
          <Alert severity="info" sx={{ mb: 2 }}>
            Uses <strong>GET /py/job-orders?planStart=</strong> for the day,
            then the same <code>jobOrders</code> payload as{" "}
            <strong>Bag Print → 下載 OnPack2023檸檬機</strong>. The ZIP contains
            loose <code>.job</code> / <code>.image</code> / BMPs — extract
            before sending to NGE; the ZIP itself is only a transport bundle.
          </Alert>
          <Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
            Distinct item codes in the list produce one label set each (backend
            groups by code). Configure <code>ngpcl.push-url</code> on the server
            to POST the same lemon ZIP bytes to your NGPCL HTTP gateway;
            otherwise use download only.
          </Typography>

          <Stack
            direction={{ xs: "column", sm: "row" }}
            spacing={2}
            sx={{ mb: 2, alignItems: "center", flexWrap: "wrap" }}
          >
            <TextField
              size="small"
              label="Plan date (planStart)"
              type="date"
              value={onpackPlanDate}
              onChange={(e) => setOnpackPlanDate(e.target.value)}
              InputLabelProps={{ shrink: true }}
            />
            <Typography variant="body2" color="textSecondary">
              {onpackLoading ? (
                <>
                  <CircularProgress
                    size={16}
                    sx={{ mr: 1, verticalAlign: "middle" }}
                  />
                  Loading job orders…
                </>
              ) : (
                `${onpackJobOrders.length} job order(s), ${onpackPayload.length} row(s) with item code → ZIP`
              )}
            </Typography>
          </Stack>
          {onpackLoadError ? (
            <Alert severity="error" sx={{ mb: 2 }}>
              {onpackLoadError}
            </Alert>
          ) : null}

          <Table size="small" sx={{ mb: 2, maxWidth: 900 }}>
            <TableHead>
              <TableRow>
                <TableCell>JO id</TableCell>
                <TableCell>Code</TableCell>
                <TableCell>Item code</TableCell>
                <TableCell>Lot</TableCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {onpackJobOrders.length === 0 && !onpackLoading ? (
                <TableRow>
                  <TableCell colSpan={4}>
                    <Typography variant="body2" color="textSecondary">
                      No rows for this date (or still loading).
                    </Typography>
                  </TableCell>
                </TableRow>
              ) : (
                onpackJobOrders.map((jo) => (
                  <TableRow key={jo.id}>
                    <TableCell>{jo.id}</TableCell>
                    <TableCell>{jo.code ?? "—"}</TableCell>
                    <TableCell>{jo.itemCode ?? "—"}</TableCell>
                    <TableCell>{jo.lotNo ?? "—"}</TableCell>
                  </TableRow>
                ))
              )}
            </TableBody>
          </Table>

          <TextField
            fullWidth
            multiline
            minRows={3}
            label="Resolved POST body (download-onpack-qr-text / NGPCL push)"
            value={JSON.stringify({ jobOrders: onpackPayload }, null, 2)}
            InputProps={{ readOnly: true }}
            sx={{ mb: 2, fontFamily: "monospace" }}
          />

          <Stack
            direction={{ xs: "column", sm: "row" }}
            spacing={2}
            sx={{ mb: 2, flexWrap: "wrap" }}
          >
            <Button
              variant="contained"
              color="success"
              onClick={handleOnpackDownloadLemonZip}
              disabled={onpackLemonDownloading || onpackLoading}
            >
              {onpackLemonDownloading
                ? "Downloading…"
                : "Download lemon OnPack ZIP"}
            </Button>
            <Button
              variant="outlined"
              onClick={handleOnpackPushNgpcl}
              disabled={onpackPushLoading || onpackLoading}
            >
              {onpackPushLoading
                ? "Pushing…"
                : "Push to NGPCL (server → ngpcl.push-url)"}
            </Button>
          </Stack>
          {onpackPushResult ? (
            <TextField
              fullWidth
              multiline
              minRows={2}
              label="Last NGPCL push result"
              value={onpackPushResult}
              InputProps={{ readOnly: true }}
            />
          ) : null}
          <Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
            <code>POST /plastic/download-onpack-qr-text</code> ·{" "}
            <code>POST /plastic/ngpcl/push-onpack-qr-text</code> (same body)
          </Typography>
        </Section>
      </TabPanel>

      <TabPanel value={tabValue} index={2}>
        <Section title="3. Laser Bag2 自動送（與 /laserPrint 相同邏輯）">
          {laserLastReceive ? (
            <Alert severity="info" sx={{ mb: 2 }}>
              <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
                上次印表機已確認（receive）的工單（資料庫）
              </Typography>
              <Typography variant="body2" sx={{ mt: 0.5 }}>
                工單號：{laserLastReceive.jobOrderNo ?? "—"}　Lot：
                {laserLastReceive.lotNo ?? "—"}
              </Typography>
              <Typography
                variant="body2"
                sx={{ mt: 0.5, fontFamily: "monospace" }}
              >
                JSON：{" "}
                {laserLastReceive.itemId != null &&
                laserLastReceive.stockInLineId != null
                  ? JSON.stringify({
                      itemId: laserLastReceive.itemId,
                      stockInLineId: laserLastReceive.stockInLineId,
                    })
                  : "—"}
              </Typography>
              <Typography
                variant="caption"
                color="textSecondary"
                display="block"
                sx={{ mt: 0.5 }}
              >
                {formatHongKongDateTime(laserLastReceive.sentAt)}　{laserLastReceive.source ?? ""}
              </Typography>
            </Alert>
          ) : null}
          <Alert severity="warning" sx={{ mb: 2 }}>
            依資料庫 <strong>LASER_PRINT.host</strong>、
            <strong>LASER_PRINT.port</strong>、
            <strong>LASER_PRINT.itemCodes</strong> 查當日包裝工單並送
            TCP（每筆工單預設 3 次、間隔 3 秒，與前端點列相同）。
            排程預設關閉；啟用請設{" "}
            <code>laser.bag2.auto-send.enabled=true</code>（後端
            application.yml）。
          </Alert>
          <Stack
            direction={{ xs: "column", sm: "row" }}
            spacing={2}
            sx={{ mb: 2, alignItems: "center", flexWrap: "wrap" }}
          >
            <TextField
              size="small"
              label="Plan date (planStart)"
              type="date"
              value={laserAutoPlanDate}
              onChange={(e) => setLaserAutoPlanDate(e.target.value)}
              InputLabelProps={{ shrink: true }}
            />
            <TextField
              size="small"
              label="limitPerRun（目前固定只送第一筆）"
              value={laserAutoLimit}
              onChange={(e) => setLaserAutoLimit(e.target.value)}
              sx={{ width: 200 }}
              helperText="目前後端會限制為第一筆；此欄位保留給未來調整"
            />
            <Button
              variant="contained"
              color="primary"
              onClick={() => void handleLaserBag2AutoSend()}
              disabled={laserAutoLoading}
            >
              {laserAutoLoading
                ? "送出中…"
                : "執行 POST /plastic/laser-bag2-auto-send"}
            </Button>
          </Stack>
          {laserAutoError ? (
            <Alert severity="error" sx={{ mb: 2 }}>
              {laserAutoError}
            </Alert>
          ) : null}
          {laserAutoReport ? (
            <TextField
              fullWidth
              multiline
              minRows={8}
              label="回應（LaserBag2AutoSendReport）"
              value={JSON.stringify(laserAutoReport, null, 2)}
              InputProps={{ readOnly: true }}
              sx={{ fontFamily: "monospace" }}
            />
          ) : null}
          <Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
            <code>
              POST
              /api/plastic/laser-bag2-auto-send?planStart=YYYY-MM-DD&amp;limitPerRun=N
            </code>
          </Typography>
        </Section>
      </TabPanel>

      <TabPanel value={tabValue} index={3}>
        <Section title="4. 批號標籤列印（掃碼 → 查同品批號 → 選印表機 → 列印）">
          <Alert severity="info" sx={{ mb: 2 }}>
            此工具會呼叫後端 <code>/inventoryLotLine/analyze-qr-code</code>{" "}
            找同品可用批號，再用 <code>/inventoryLotLine/print-label</code>（需
            printerId）送出列印。
          </Alert>
          <Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
            <Button
              variant="contained"
              onClick={() => setLotLabelModalOpen(true)}
            >
              開啟列印視窗
            </Button>
            <Typography
              variant="body2"
              color="text.secondary"
              sx={{ alignSelf: "center" }}
            >
              掃碼格式：<code>{'{"itemId":16431,"stockInLineId":10381'}</code>
            </Typography>
          </Stack>
          <LotLabelPrintModal
            open={lotLabelModalOpen}
            onClose={() => setLotLabelModalOpen(false)}
          />
        </Section>
      </TabPanel>

      <TabPanel value={tabValue} index={4}>
        <Section title="5. M18 BOM shop sync (udfBomForShop)">
          <Alert severity="info" sx={{ mb: 2 }}>
            Requires setting <code>M18.bom.shop.sync.enabled=true</code>. Use{" "}
            <code>m18HeaderId</code> query param (or the field below) so M18{" "}
            <strong>updates</strong> the existing udfBomForShop header instead of creating a duplicate.
          </Alert>
          <Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600 }}>
            Lookup BOM id by item code (like PO-by-code)
          </Typography>
          <Typography variant="body2" color="textSecondary" sx={{ mb: 1 }}>
            Uses <code>GET /bom/by-item-code?code=…</code> — item is the BOM
            header product (<code>bom.item</code>), same as FPSMS finished-good
            <code> items.code</code>.
          </Typography>
          <Stack
            direction={{ xs: "column", sm: "row" }}
            spacing={2}
            sx={{ mb: 2, alignItems: "center", flexWrap: "wrap" }}
          >
            <TextField
              size="small"
              label="Item code"
              value={bomByItemCodeInput}
              onChange={(e) => setBomByItemCodeInput(e.target.value)}
              sx={{ width: 220 }}
            />
            <Button
              variant="outlined"
              onClick={() => void handleBomLookupByItemCode()}
              disabled={bomByItemCodeLoading}
            >
              {bomByItemCodeLoading ? "Looking up…" : "Lookup BOM id"}
            </Button>
          </Stack>
          {bomByItemCodeResult ? (
            <Stack spacing={1} sx={{ mb: 3 }}>
              <TextField
                fullWidth
                multiline
                minRows={4}
                label="Lookup response (BomIdByItemCodeResponse)"
                value={bomByItemCodeResult}
                InputProps={{ readOnly: true }}
                sx={{ fontFamily: "monospace" }}
              />
              <Button
                size="small"
                variant="text"
                onClick={() => {
                  try {
                    const o = JSON.parse(bomByItemCodeResult) as {
                      bomId?: number;
                      bomM18Id?: number;
                    };
                    if (o?.bomId != null)
                      setBomShopSyncBomId(String(o.bomId));
                    if (o?.bomM18Id != null)
                      setBomShopM18HeaderId(String(o.bomM18Id));
                  } catch {
                    /* ignore */
                  }
                }}
              >
                Copy bomId + bomM18Id to sync fields below
              </Button>
            </Stack>
          ) : null}
          <Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600 }}>
            M18 udfBomForShop sync
          </Typography>
          <Typography variant="body2" color="textSecondary" sx={{ mb: 1 }}>
            GET <code>/scheduler/trigger/bom-shop-sync-all</code>: push{" "}
            <strong>all</strong> non-deleted BOMs (same nightly job path;
            respects <code>M18.bom.shop.sync.enabled</code>). Check{" "}
            <code>scheduler_sync_log</code> (<code>M18_BOM_SHOP</code>) for row
            counts.
          </Typography>
          <Stack direction="row" spacing={2} sx={{ mb: 2, flexWrap: "wrap" }}>
            <Button
              variant="outlined"
              color="secondary"
              onClick={() => void handleBomShopSyncAllM18()}
              disabled={bomShopSyncAllLoading}
            >
              {bomShopSyncAllLoading
                ? "Syncing all BOMs…"
                : "Sync all BOMs to M18"}
            </Button>
          </Stack>
          {bomShopSyncAllResult ? (
            <TextField
              fullWidth
              multiline
              minRows={3}
              label="Bulk trigger response"
              value={bomShopSyncAllResult}
              InputProps={{ readOnly: true }}
              sx={{
                mb: 2,
                fontFamily: "monospace",
              }}
            />
          ) : null}
          <Typography variant="body2" color="textSecondary" sx={{ mb: 1 }}>
            POST <code>/m18/test/bom-shop-sync/:bomId</code> with optional{" "}
            <code>?m18HeaderId=</code> (M18 udfBomForShop header id for{" "}
            <strong>update</strong>; from lookup <code>bomM18Id</code> or{" "}
            <code>Bom.m18Id</code> in DB). If omitted, backend uses{" "}
            <code>bom.m18Id</code> when set; otherwise creates a new M18 row.
          </Typography>
          <Stack
            direction={{ xs: "column", sm: "row" }}
            spacing={2}
            sx={{ mb: 2, alignItems: "center", flexWrap: "wrap" }}
          >
            <TextField
              size="small"
              label="BOM id"
              value={bomShopSyncBomId}
              onChange={(e) => setBomShopSyncBomId(e.target.value)}
              sx={{ width: 160 }}
            />
            <TextField
              size="small"
              label="M18 header id (optional, update)"
              value={bomShopM18HeaderId}
              onChange={(e) => setBomShopM18HeaderId(e.target.value)}
              sx={{ width: 220 }}
              helperText="e.g. 255 from bomM18Id — forces main.id in payload"
            />
            <Button
              variant="contained"
              color="primary"
              onClick={() => void handleBomShopSyncM18()}
              disabled={bomShopSyncLoading}
            >
              {bomShopSyncLoading ? "Syncing…" : "Sync BOM to M18"}
            </Button>
          </Stack>
          {bomShopSyncResult ? (
            <TextField
              fullWidth
              multiline
              minRows={10}
              label="Response (M18BomShopSyncTriggerResult)"
              value={bomShopSyncResult}
              InputProps={{ readOnly: true }}
              sx={{ fontFamily: "monospace" }}
            />
          ) : null}
        </Section>
      </TabPanel>

      <TabPanel value={tabValue} index={5}>
        <Section title="6. M18 sync alert email">
          <Alert severity="info" sx={{ mb: 2 }}>
            Production sync errors email{" "}
            <strong>vluk@2fi-solutions.com.hk</strong> and{" "}
            <strong>kelvin.yau@2fi-solutions.com.hk</strong> (see{" "}
            <code>scheduler.sync-alert.email.to-addresses</code>). WhatsApp/Twilio
            is disabled. SMTP from DB <code>MAIL.smtp.*</code> (e.g. Gmail{" "}
            <code>vinluk95@gmail.com</code> + app password).
          </Alert>
          <Stack spacing={2} sx={{ mb: 2, maxWidth: 720 }}>
            <TextField
              size="small"
              label="Email subject"
              value={emailTestSubject}
              onChange={(e) => setEmailTestSubject(e.target.value)}
              fullWidth
            />
            <TextField
              label="Test message"
              value={emailTestMessage}
              onChange={(e) => setEmailTestMessage(e.target.value)}
              multiline
              minRows={3}
              fullWidth
            />
            <Stack direction="row" spacing={2} sx={{ flexWrap: "wrap" }}>
              <Button
                variant="contained"
                color="primary"
                onClick={() => void handleSyncAlertTestEmail()}
                disabled={emailTestLoading}
              >
                {emailTestLoading ? "Sending…" : "Send test email message"}
              </Button>
            </Stack>
          </Stack>
          {emailTestResult ? (
            <TextField
              fullWidth
              multiline
              minRows={2}
              label="Email test response"
              value={emailTestResult}
              InputProps={{ readOnly: true }}
              sx={{ fontFamily: "monospace", mb: 3 }}
            />
          ) : null}
          <Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
            <code>
              GET /scheduler/trigger/sync-alert-test-email?subject=…&amp;message=…
            </code>
            <br />
            <code>GET /scheduler/trigger/sync-alert-check</code> — run alert rules
            now (empty = OK)
          </Typography>
        </Section>
      </TabPanel>
    </Box>
  );
}
