/* global React, I, UI */
// =============== กระทบยอดธนาคาร · Bank Reconciliation ===============
// อัพโหลด statement (PDF / Excel / CSV / วางข้อความ) → ระบบอ่านรายการเดินบัญชี
// → จับคู่กับเอกสารขาย-เอกสารจ่ายในระบบ (อัตโนมัติ + ผู้ใช้ยืนยัน/แก้เอง) → สรุปผลต่างรายเดือน
const { useState: useStateBR, useEffect: useEffectBR, useMemo: useMemoBR } = React;

// ─────────── ที่เก็บข้อมูล (sync ขึ้น Supabase ผ่าน LocalStore) ───────────
// โครงสร้าง: { "<accountId>|<YYYY-MM>": { lines:[], matches:{}, ignored:{}, closed, ... } }
const BR_KEY = "sss-bank-recon";
function brLoadAll() {
  const v = window.lsSyncGet ? window.lsSyncGet(BR_KEY) : null;
  if (v && typeof v === "object") return v;
  try { return window.lsRead(BR_KEY, {}); } catch { return {}; }
}
function brSaveAll(all) {
  if (window.lsSyncSet) window.lsSyncSet(BR_KEY, all);
  else { try { localStorage.setItem(BR_KEY, JSON.stringify(all)); } catch {} }
  window.dispatchEvent(new Event("sss-bank-recon-changed"));
}
function brPid(accountId, ym) { return `${accountId}|${ym}`; }
function brGet(pid) { return brLoadAll()[pid] || null; }
function brSet(pid, patch) {
  const all = brLoadAll();
  all[pid] = { ...(all[pid] || {}), ...patch, updatedAt: new Date().toISOString() };
  brSaveAll(all);
  return all[pid];
}

// ─────────── วันที่ / ตัวเลข ───────────
const BR_TH_MON = ["ม.ค.", "ก.พ.", "มี.ค.", "เม.ย.", "พ.ค.", "มิ.ย.", "ก.ค.", "ส.ค.", "ก.ย.", "ต.ค.", "พ.ย.", "ธ.ค."];
const BR_TH_MON_ALT = { "มกราคม": 1, "กุมภาพันธ์": 2, "มีนาคม": 3, "เมษายน": 4, "พฤษภาคม": 5, "มิถุนายน": 6, "กรกฎาคม": 7, "สิงหาคม": 8, "กันยายน": 9, "ตุลาคม": 10, "พฤศจิกายน": 11, "ธันวาคม": 12 };
const BR_EN_MON = { jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6, jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12 };

// ปี 2 หลัก: ≥50 = พ.ศ.ย่อ (68 → 2568 → ค.ศ.2025) · <50 = ค.ศ.ย่อ (26 → 2026)
function brYear(y) {
  y = parseInt(y, 10);
  if (isNaN(y)) return null;
  if (y < 100) y = y >= 50 ? 2500 + y : 2000 + y;
  return y > 2400 ? y - 543 : y;
}
function brISO(y, m, d) {
  if (!y || !m || !d || m < 1 || m > 12 || d < 1 || d > 31) return null;
  return `${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
}
// หาวันที่ตัวแรกในข้อความ → {iso, end}
function brFindDate(text) {
  let m;
  if ((m = /^\s*(\d{4})-(\d{2})-(\d{2})/.exec(text))) return { iso: brISO(brYear(m[1]), +m[2], +m[3]), end: m[0].length };
  if ((m = /^\s*(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{2,4})/.exec(text))) return { iso: brISO(brYear(m[3]), +m[2], +m[1]), end: m[0].length };
  if ((m = /^\s*(\d{1,2})\s*(ม\.ค\.|ก\.พ\.|มี\.ค\.|เม\.ย\.|พ\.ค\.|มิ\.ย\.|ก\.ค\.|ส\.ค\.|ก\.ย\.|ต\.ค\.|พ\.ย\.|ธ\.ค\.)\s*(\d{2,4})/.exec(text)))
    return { iso: brISO(brYear(m[3]), BR_TH_MON.indexOf(m[2]) + 1, +m[1]), end: m[0].length };
  if ((m = /^\s*(\d{1,2})\s*(มกราคม|กุมภาพันธ์|มีนาคม|เมษายน|พฤษภาคม|มิถุนายน|กรกฎาคม|สิงหาคม|กันยายน|ตุลาคม|พฤศจิกายน|ธันวาคม)\s*(\d{2,4})/.exec(text)))
    return { iso: brISO(brYear(m[3]), BR_TH_MON_ALT[m[2]], +m[1]), end: m[0].length };
  if ((m = /^\s*(\d{1,2})[\s\-]([A-Za-z]{3})[a-z]*[\s\-,]*(\d{2,4})/.exec(text))) {
    const mon = BR_EN_MON[m[2].toLowerCase()];
    if (mon) return { iso: brISO(brYear(m[3]), mon, +m[1]), end: m[0].length };
  }
  return null;
}
function brThaiDate(iso) {
  if (!iso) return "—";
  const [y, m, d] = String(iso).split("-").map(Number);
  return `${d} ${BR_TH_MON[m - 1]} ${y + 543}`;
}
function brMonthLabel(ym) {
  const [y, m] = String(ym).split("-").map(Number);
  return `${BR_TH_MON[m - 1]} ${y + 543}`;
}
function brYm(iso) { return String(iso || "").slice(0, 7); }
function brNum(s) { const n = parseFloat(String(s == null ? "" : s).replace(/[^\d.\-]/g, "")); return isNaN(n) ? 0 : n; }
function brDays(a, b) {
  if (!a || !b) return 999;
  return Math.abs((new Date(a + "T00:00:00") - new Date(b + "T00:00:00")) / 86400000);
}

// ─────────── ตัวอ่าน statement (ข้อความ) ───────────
// รหัสรายการของ SCB: X1/FI = เงินเข้า · X2/FE = เงินออก (FE = ค่าธรรมเนียม)
const BR_OUT_WORDS = ["ถอน", "โอนออก", "จ่าย", "ชำระ", "ค่าธรรมเนียม", "ค่าบริการ", "หักบัญชี", "ตัดบัญชี", "เช็คจ่าย", "atm", "debit", "withdraw", "payment", "transfer out", "fee", "x2", "fe "];
const BR_IN_WORDS = ["ฝาก", "รับโอน", "เงินเข้า", "โอนเข้า", "ดอกเบี้ย", "คืนเงิน", "deposit", "credit", "interest", "transfer in", "x1"];
const BR_AMT_RE = /-?\d{1,3}(?:,\d{3})+\.\d{2}|-?\d+\.\d{2}/g;

// แยก "รายละเอียด (DESC)" กับ "บันทึกช่วยจำ (NOTE)" ออกจากข้อความก้อนเดียว
function brSplitDescNote(s) {
  let desc = String(s || "").replace(/\s+/g, " ").trim(), note = "";
  const i = desc.search(/(NOTE|หมายเหตุ|บันทึกช่วยจำ)\s*[:：]/i);
  if (i >= 0) {
    note = desc.slice(i).replace(/^(NOTE|หมายเหตุ|บันทึกช่วยจำ)\s*[:：]\s*/i, "").trim();
    desc = desc.slice(0, i).trim();
  }
  desc = desc.replace(/^(DESC|DESCRIPTION|รายละเอียด|รายการ)\s*[:：]\s*/i, "").trim();
  if (note === "-" || note === "—" || note === ".") note = "";
  return { desc, note };
}

// อ่านข้อความ statement → รายการเดินบัญชี
// หลักการ: ทุกบรรทัดที่ขึ้นต้นด้วยวันที่ + มีจำนวนเงินอย่างน้อย 1 ตัว
// คำบรรยาย: ธนาคารส่วนใหญ่ (เช่น SCB) วางคอลัมน์ DESC/NOTE ไว้ "หลัง" ยอดคงเหลือ
//   → ใช้ข้อความหลังตัวเลขตัวสุดท้ายเป็นรายละเอียดหลัก ส่วนข้อความหน้าตัวเลข (เช่น X2 ENET)
//     เก็บเป็นรหัส/ช่องทาง · บรรทัด NOTE/DESC ที่ตกลงมาบรรทัดถัดไปจะถูกเติมให้รายการล่าสุด
// ทิศทาง (เข้า/ออก) ตัดสินจาก "ยอดคงเหลือเปลี่ยนไปเท่าไร" ก่อน (แม่นสุด ใช้ได้ทุกธนาคาร)
// ถ้าไม่มีคอลัมน์คงเหลือ → เดาจากคำในรายการ แล้วให้ผู้ใช้แก้ได้ในหน้าตรวจทาน
function brParseStatementText(text) {
  const rows = String(text || "").split(/\r?\n/);
  const out = [];
  let prevBal = null;
  rows.forEach(raw => {
    const t = String(raw).replace(/ /g, " ").trim();
    if (!t) return;
    const dt = brFindDate(t);
    if (!dt || !dt.iso) {
      // บรรทัดต่อเนื่องของรายการล่าสุด (NOTE : … หรือ DESC ที่ตกบรรทัด)
      const last = out[out.length - 1];
      if (!last) return;
      const mn = /^(NOTE|หมายเหตุ|บันทึกช่วยจำ)\s*[:：]\s*(.*)$/i.exec(t);
      if (mn) { const v = mn[2].trim(); if (v && v !== "-" && v !== "—") last.note = last.note ? last.note + " " + v : v; return; }
      const md = /^(DESC|DESCRIPTION|รายละเอียด)\s*[:：]\s*(.*)$/i.exec(t);
      if (md) {
        const dn2 = brSplitDescNote(md[2]);
        if (dn2.desc) last.desc = (last.desc && last.desc !== last.code) ? last.desc + " " + dn2.desc : dn2.desc;
        if (dn2.note) last.note = last.note ? last.note + " " + dn2.note : dn2.note;
      }
      return;
    }
    const rest = t.slice(dt.end);
    // เก็บตำแหน่งของตัวเลขทุกตัว เพื่อตัดข้อความทั้งหน้าและหลังคอลัมน์ตัวเลข
    const re = new RegExp(BR_AMT_RE.source, "g");
    const hits = [];
    let mm;
    while ((mm = re.exec(rest))) hits.push({ v: brNum(mm[0]), start: mm.index, end: mm.index + mm[0].length });
    if (!hits.length) return;
    const nums = hits.map(h => h.v);
    const headRaw = rest.slice(0, hits[0].start).replace(/\s+/g, " ").trim();
    const tailRaw = rest.slice(hits[hits.length - 1].end).replace(/\s+/g, " ").trim();
    const code = headRaw.replace(/^\d{1,2}[:.]\d{2}(?::\d{2})?\s*/, "").trim();   // ตัดเวลา 12:02 ออก เหลือ X2 ENET
    const dn = brSplitDescNote(tailRaw);
    let amount = 0, bal = null, dir = "";
    if (nums.length >= 2) {
      bal = nums[nums.length - 1];
      amount = Math.abs(nums[nums.length - 2]);
      if (prevBal != null && amount > 0) {
        const delta = bal - prevBal;
        if (Math.abs(delta - amount) < 0.05) dir = "in";
        else if (Math.abs(delta + amount) < 0.05) dir = "out";
      }
    } else {
      amount = Math.abs(nums[0]);
    }
    if (!amount) return;
    if (!dir) {
      const low = (code + " " + dn.desc + " " + t).toLowerCase();
      const hitOut = BR_OUT_WORDS.some(w => low.includes(w));
      const hitIn = BR_IN_WORDS.some(w => low.includes(w));
      dir = hitIn && !hitOut ? "in" : hitOut && !hitIn ? "out" : (nums[0] < 0 ? "out" : "out");
    }
    if (bal != null) prevBal = bal;
    out.push({ date: dt.iso, desc: dn.desc || code || "(ไม่มีคำบรรยาย)", note: dn.note || "", code, amount, dir, bal, raw: t });
  });
  return out;
}

// อ่านตาราง (Excel/CSV) → รายการเดินบัญชี · เดาคอลัมน์จากหัวตาราง
function brParseSheetRows(rows) {
  if (!rows || !rows.length) return [];
  const norm = (s) => String(s == null ? "" : s).replace(/\s+/g, "").toLowerCase();
  // หาแถวหัวตาราง (แถวที่มีคำว่า วันที่/date)
  let hIdx = rows.findIndex(r => (r || []).some(c => /วันที่|date|วัน\/เดือน/i.test(String(c || ""))));
  if (hIdx < 0) hIdx = 0;
  const head = (rows[hIdx] || []).map(norm);
  const find = (...keys) => head.findIndex(h => keys.some(k => h.includes(k)));
  const cDate = find("วันที่", "date", "วันเดือนปี");
  // คอลัมน์รายละเอียดจริง (DESC) มาก่อนคอลัมน์รหัส/ช่องทาง — SCB มีทั้งคู่
  const cDesc = find("รายละเอียด", "คำอธิบาย", "description", "detail", "narrative");
  const cCode = find("รายการ", "code", "channel", "ช่องทาง");
  const cNote = find("บันทึกช่วยจำ", "หมายเหตุ", "note", "memo");
  const cOut = find("ถอน", "จ่าย", "เดบิต", "debit", "withdraw");
  const cIn = find("ฝาก", "รับ", "เครดิต", "credit", "deposit");
  const cAmt = find("จำนวนเงิน", "amount", "จำนวน");
  const cBal = find("คงเหลือ", "balance", "ยอดคงเหลือ");
  const out = [];
  let prevBal = null;
  for (let i = hIdx + 1; i < rows.length; i++) {
    const r = rows[i] || [];
    const dRaw = cDate >= 0 ? r[cDate] : r[0];
    if (dRaw == null || dRaw === "") continue;
    let iso = null;
    if (dRaw instanceof Date) iso = brISO(dRaw.getFullYear(), dRaw.getMonth() + 1, dRaw.getDate());
    else { const d = brFindDate(String(dRaw).trim()); iso = d && d.iso; }
    if (!iso) continue;
    const vOut = cOut >= 0 ? brNum(r[cOut]) : 0;
    const vIn = cIn >= 0 ? brNum(r[cIn]) : 0;
    const bal = cBal >= 0 ? brNum(r[cBal]) : null;
    let amount = 0, dir = "";
    if (vOut > 0) { amount = vOut; dir = "out"; }
    else if (vIn > 0) { amount = vIn; dir = "in"; }
    else if (cAmt >= 0) {
      const v = brNum(r[cAmt]);
      amount = Math.abs(v);
      if (bal != null && prevBal != null) dir = (bal - prevBal) >= 0 ? "in" : "out";
      else dir = v < 0 ? "out" : "in";
    }
    if (!amount) continue;
    if (bal != null) prevBal = bal;
    const rawDesc = (cDesc >= 0 ? String(r[cDesc] || "") : r.filter(c => typeof c === "string").join(" "));
    const dn = brSplitDescNote(rawDesc);
    const code = cCode >= 0 ? String(r[cCode] || "").replace(/\s+/g, " ").trim() : "";
    let note = dn.note;
    if (cNote >= 0) {
      const v = brSplitDescNote(String(r[cNote] || "")).note || String(r[cNote] || "").trim();
      if (v && v !== "-" && v !== "—") note = note ? note + " " + v : v;
    }
    out.push({ date: iso, desc: dn.desc || code || "(ไม่มีคำบรรยาย)", note, code, amount, dir, bal, raw: r.join(" | ") });
  }
  return out;
}

// ─────────── อ่าน PDF (โหลด pdf.js เมื่อใช้งานจริงเท่านั้น) ───────────
let _brPdfLib = null;
async function brLoadPdfJs() {
  if (_brPdfLib) return _brPdfLib;
  if (!window.pdfjsLib) {
    await new Promise((res, rej) => {
      const s = document.createElement("script");
      s.src = "https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.min.js";
      s.onload = res; s.onerror = () => rej(new Error("โหลดตัวอ่าน PDF ไม่สำเร็จ — ตรวจอินเทอร์เน็ต"));
      document.head.appendChild(s);
    });
  }
  const lib = window.pdfjsLib;
  // worker จาก CDN โดยตรงติด CSP (worker-src 'self' blob:) → ดึงเป็นข้อความแล้วทำเป็น blob URL
  if (!lib.GlobalWorkerOptions.workerSrc) {
    try {
      const r = await fetch("https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.worker.min.js");
      const blob = new Blob([await r.text()], { type: "application/javascript" });
      lib.GlobalWorkerOptions.workerSrc = URL.createObjectURL(blob);
    } catch { lib.GlobalWorkerOptions.workerSrc = ""; }
  }
  _brPdfLib = lib;
  return lib;
}
// รหัสผ่านไฟล์ statement — เก็บเฉพาะเครื่องนี้ (คีย์นี้ไม่อยู่ในรายการซิงค์ จึงไม่ถูกส่งขึ้นฐานข้อมูล)
const BR_PW_KEY = "sss-br-pdfpw";
function brPwGet(accId) { try { return (JSON.parse(localStorage.getItem(BR_PW_KEY) || "{}"))[accId] || ""; } catch { return ""; } }
function brPwSet(accId, pw) {
  try {
    const m = JSON.parse(localStorage.getItem(BR_PW_KEY) || "{}");
    if (pw) m[accId] = pw; else delete m[accId];
    localStorage.setItem(BR_PW_KEY, JSON.stringify(m));
  } catch {}
}

// ดึงข้อความจาก PDF โดยจัดกลุ่มตามตำแหน่งแนวตั้ง → ได้บรรทัดเหมือนที่ตาเห็น
// รองรับไฟล์ที่ล็อกรหัส — โยน error ที่มี needPassword ให้หน้าจอขึ้นช่องกรอกรหัส
async function brPdfToText(file, onProgress, password) {
  const lib = await brLoadPdfJs();
  const buf = await file.arrayBuffer();   // อ่านใหม่ทุกครั้ง — pdf.js ย้าย buffer ไป worker ใช้ซ้ำไม่ได้
  let pdf;
  try {
    pdf = await lib.getDocument(password ? { data: buf, password } : { data: buf }).promise;
  } catch (e) {
    const isPw = e && (e.name === "PasswordException" || /password/i.test(e.message || ""));
    if (isPw) {
      const wrong = e.code === 2 || /incorrect|invalid/i.test(e.message || "");
      const err = new Error(wrong
        ? "รหัสผ่านไม่ถูกต้อง — ตรวจแล้วลองใหม่อีกครั้ง (มักเป็นเลขบัตรประชาชน / เบอร์โทร / วันเดือนปีเกิด ตามที่ธนาคารกำหนด)"
        : "ไฟล์ PDF นี้ล็อกด้วยรหัสผ่าน — ใส่รหัสด้านล่างแล้วกด \"อ่านไฟล์อีกครั้ง\"");
      err.needPassword = true; err.wrongPassword = wrong;
      throw err;
    }
    throw e;
  }
  const pages = [];
  for (let p = 1; p <= pdf.numPages; p++) {
    onProgress && onProgress(`กำลังอ่านหน้า ${p}/${pdf.numPages}…`);
    const page = await pdf.getPage(p);
    const content = await page.getTextContent();
    const rows = {};
    content.items.forEach(it => {
      if (!it.str || !it.str.trim()) return;
      const y = Math.round(it.transform[5] / 3) * 3;           // รวมบรรทัดที่ y ใกล้กัน
      (rows[y] = rows[y] || []).push({ x: it.transform[4], s: it.str });
    });
    const lines = Object.keys(rows).sort((a, b) => b - a)
      .map(k => rows[k].sort((a, b) => a.x - b.x).map(i => i.s).join(" ").replace(/\s+/g, " ").trim());
    pages.push(lines.join("\n"));
  }
  return pages.join("\n");
}

// ─────────── เอกสารในระบบที่ใช้จับคู่ ───────────
// side: in = เงินเข้า (เอกสารขาย) · out = เงินออก (เอกสารจ่าย)
// cash: เอกสารที่ "เงินเคลื่อนไหวจริง" (ใบเสร็จ/ใบสำคัญจ่าย) — ได้แต้มพิเศษตอนจับคู่
// เพราะบางครั้งคำบรรยายในบัญชีอ้างเลขใบแจ้งหนี้ แต่คู่ที่ถูกต้องทางบัญชีคือใบเสร็จของใบแจ้งหนี้นั้น
const BR_SOURCES = [
  { list: "Receipts", label: "ใบเสร็จรับเงิน", side: "in", go: "receipts", party: r => r.customer, docType: null, cash: true },
  { list: "TaxInvoices", label: "ใบกำกับภาษี", side: "in", go: "tax-invoices", party: r => r.customer, docType: "tax-invoice", paidStatus: "ชำระแล้ว" },
  { list: "Invoices", label: "ใบแจ้งหนี้", side: "in", go: "invoices", party: r => r.customer, docType: "invoice", paidStatus: "ชำระแล้ว" },
  { list: "BillingNotes", label: "ใบวางบิล", side: "in", go: "billing-notes", party: r => r.customer, docType: "billing-note", paidStatus: "เก็บเงินแล้ว" },
  { list: "PaymentVouchers", label: "ใบสำคัญจ่าย", side: "out", go: "payment-vouchers", party: r => r.payee, docType: "payment-voucher", paidStatus: "จ่ายแล้ว", cash: true },
  { list: "Expenses", label: "ค่าใช้จ่าย", side: "out", go: "expenses", party: r => r.vendor, docType: null, cash: true },
  { list: "ReceiptVouchers", label: "ใบสำคัญรับเงิน", side: "out", go: "receipt-vouchers", party: r => r.payee, docType: null, cash: true },
  { list: "PurchaseOrders", label: "ใบสั่งซื้อ", side: "out", go: "purchase-orders", party: r => r.vendor, docType: "purchase-order", paidStatus: null },
];
const BR_SRC = {};
BR_SOURCES.forEach(s => { BR_SRC[s.list] = s; });

function brDocISO(r) {
  return r.iso || (window.DocDates && window.DocDates.toISO ? window.DocDates.toISO(r.date) : null) || (brFindDate(String(r.date || "")) || {}).iso || null;
}
// เอกสารทั้งหมดที่ใช้จับคู่ได้ (กรองช่วงเดือน ±1 เดือน เพื่อให้เอกสารคาบเกี่ยวเดือนติดมาด้วย)
function brBuildDocs(ym) {
  const D = window.SSSData || {};
  const out = [];
  const inRange = (iso) => {
    if (!iso) return false;
    const [y, m] = ym.split("-").map(Number);
    const lo = new Date(y, m - 2, 1), hi = new Date(y, m + 1, 0);
    const d = new Date(iso + "T00:00:00");
    return d >= lo && d <= hi;
  };
  BR_SOURCES.forEach(src => {
    (D[src.list] || []).forEach(r => {
      const iso = brDocISO(r);
      if (!inRange(iso)) return;
      const amount = Math.abs(Number(r.netPay != null ? r.netPay : (r.amount != null ? r.amount : r.grand)) || 0);
      if (!amount) return;
      out.push({
        key: src.list + "::" + r.no, list: src.list, no: r.no, label: src.label, side: src.side, go: src.go, cash: !!src.cash,
        date: iso, party: (src.party(r) || "").trim(), amount, status: r.status || "", ref: r.ref || "",
      });
    });
  });
  return out;
}

// ─────────── เครื่องจับคู่ ───────────
function brNormTxt(s) { return String(s || "").toLowerCase().replace(/[\s\-_.()]/g, ""); }
// คะแนนความเข้ากันของรายการ statement กับเอกสาร 1 ใบ
function brScore(line, doc) {
  if (line.dir !== doc.side) return 0;
  const diff = Math.abs(line.amount - doc.amount);
  let s = 0;
  if (diff < 0.5) s += 100;
  else if (diff <= doc.amount * 0.01) s += 55;          // ต่างไม่เกิน 1% (ค่าธรรมเนียมโอน)
  else return 0;                                        // ยอดไม่ใกล้เลย → ไม่ใช่คู่
  const dd = brDays(line.date, doc.date);
  if (dd <= 1) s += 35; else if (dd <= 3) s += 28; else if (dd <= 7) s += 20; else if (dd <= 15) s += 10; else if (dd <= 45) s += 3; else s -= 10;
  const nd = brNormTxt(line.desc + " " + (line.note || "") + " " + (line.code || ""));
  if (doc.no && nd.includes(brNormTxt(doc.no))) s += 90;                    // เลขที่เอกสารอยู่ในคำบรรยาย = ชัวร์
  if (doc.ref && brNormTxt(doc.ref) && nd.includes(brNormTxt(doc.ref))) s += 45;
  const party = brNormTxt(doc.party);
  let nameHit = false;
  if (party.length >= 4 && nd.includes(party.slice(0, 8))) { s += 40; nameHit = true; }
  else if (doc.party) {
    const tok = doc.party.split(/\s+/).filter(w => w.length >= 3).map(brNormTxt);
    if (tok.some(w => nd.includes(w))) { s += 22; nameHit = true; }
  }
  // หลักฐานแย้ง: statement บอกชื่อคู่ค้ามาชัดเจน แต่ไม่ใช่ชื่อในเอกสารใบนี้
  // → ยอดตรงเฉย ๆ ไม่พอที่จะจับคู่ให้อัตโนมัติ (ยังขึ้นเป็นข้อเสนอให้ผู้ใช้เลือกเองได้)
  const bodyTxt = brNormTxt(line.desc + " " + (line.note || "")).replace(/[0-9]/g, "");
  const hasRealDesc = bodyTxt.length >= 8 && brNormTxt(line.desc) !== brNormTxt(line.code || "");
  if (!nameHit && hasRealDesc && party.length >= 4 && !(doc.no && nd.includes(brNormTxt(doc.no)))) s -= 35;
  if (doc.cash) s += 30;   // ใบเสร็จ/ใบสำคัญจ่าย = เงินเคลื่อนไหวจริง — ชนะใบแจ้งหนี้/ใบกำกับที่ยอดเท่ากัน
  return s;
}
// จับคู่อัตโนมัติ: เลือกคู่ที่คะแนนสูงและไม่กำกวม · เอกสาร 1 ใบใช้ได้ครั้งเดียว
function brAutoMatch(lines, docs, existing) {
  const used = new Set();
  Object.values(existing || {}).forEach(m => (m.docs || []).forEach(d => used.add(d.key)));
  const pairs = [];
  lines.forEach(line => {
    if ((existing || {})[line.id]) return;                 // จับคู่ไว้แล้ว/ข้ามแล้ว — ไม่ยุ่ง
    docs.forEach(doc => {
      const sc = brScore(line, doc);
      if (sc >= 120) pairs.push({ lineId: line.id, doc, sc });
    });
  });
  pairs.sort((a, b) => b.sc - a.sc);
  const result = {};
  pairs.forEach(p => {
    if (result[p.lineId] || used.has(p.doc.key)) return;
    result[p.lineId] = { docs: [{ key: p.doc.key, list: p.doc.list, no: p.doc.no, amount: p.doc.amount, label: p.doc.label }], auto: true, score: p.sc };
    used.add(p.doc.key);
  });
  return result;
}
// ── คะแนนแบบ "หลวม" สำหรับเสนอให้ผู้ใช้เลือกเอง ──
// ต่างจาก brScore (ใช้จับคู่อัตโนมัติ ต้องมั่นใจ) — อันนี้ไม่ตัดเอกสารทิ้งเพราะยอดไม่ตรง
// เสนอทุกใบที่ "ชื่อตรง" หรือ "ยอดตรง" อย่างใดอย่างหนึ่ง พร้อมบอกเหตุผลให้ตรวจก่อนกด
function brSuggestScore(line, doc) {
  const nd = brNormTxt(line.desc + " " + (line.note || "") + " " + (line.code || ""));
  const why = [];
  let s = 0;
  // 1) ยอดเงิน
  const diff = Math.abs(line.amount - doc.amount);
  if (diff < 0.5) { s += 100; why.push("ยอดตรงพอดี"); }
  else if (diff <= doc.amount * 0.01 + 0.5) { s += 62; why.push("ยอดต่าง " + Math.round(diff) + " บ."); }
  else if (diff <= Math.max(200, doc.amount * 0.1)) { s += 26; why.push("ยอดใกล้เคียง"); }
  // 2) ชื่อคู่ค้า
  const party = brNormTxt(doc.party);
  if (party.length >= 4 && nd.includes(party.slice(0, 8))) { s += 78; why.push("ชื่อตรง"); }
  else if (doc.party) {
    const tok = String(doc.party).split(/\s+/).map(brNormTxt).filter(w => w.length >= 3);
    const hits = tok.filter(w => nd.includes(w));
    if (hits.length >= 2) { s += 50; why.push("ชื่อตรงหลายคำ"); }
    else if (hits.length === 1) { s += 30; why.push("ชื่อคล้าย"); }
  }
  // 3) เลขที่เอกสาร / อ้างอิง ที่ปรากฏในคำบรรยายหรือบันทึกช่วยจำ
  if (doc.no && nd.includes(brNormTxt(doc.no))) { s += 85; why.push("เลขเอกสารอยู่ในรายการ"); }
  if (doc.ref && brNormTxt(doc.ref).length >= 5 && nd.includes(brNormTxt(doc.ref))) { s += 45; why.push("เลขอ้างอิงตรง"); }
  // 4) ความใกล้ของวันที่
  const dd = brDays(line.date, doc.date);
  if (dd <= 1) { s += 22; why.push("วันเดียวกัน"); }
  else if (dd <= 7) s += 14;
  else if (dd <= 31) s += 6;
  else s -= 6;
  // 5) คนละทิศทาง (เงินเข้า vs เอกสารจ่าย) — ยังเสนอให้เห็น เผื่อทิศทางที่อ่านมาผิด แต่ลำดับท้าย ๆ
  if (line.dir !== doc.side) { s -= 55; why.push("⚠ คนละทิศทาง"); }
  if (doc.cash) s += 8;
  return { s, why };
}
// ข้อเสนอสำหรับ 1 รายการ — เรียงคะแนน พร้อมเหตุผลว่าทำไมถึงเสนอใบนี้
function brSuggest(line, docs, usedKeys, limit) {
  return docs.map(d => { const r = brSuggestScore(line, d); return { doc: d, sc: r.s, why: r.why }; })
    .filter(x => x.sc >= 25 && !usedKeys.has(x.doc.key))
    .sort((a, b) => b.sc - a.sc).slice(0, limit || 15);
}

// ─────────── พิมพ์รายงาน ───────────
function brPrint(barTitle, html) {
  document.getElementById("__print_modal")?.remove();
  document.body.classList.remove("printing-modal");
  const modal = document.createElement("div");
  modal.id = "__print_modal";
  modal.className = "print-modal";
  modal.innerHTML = `
    <div class="print-modal-bar no-print">
      <div style="font-weight:600;font-size:14px;flex:1">${barTitle}</div>
      <button id="__print_close" class="pm-btn pm-btn-ghost">ปิด (Esc)</button>
      <button id="__print_btn" class="pm-btn pm-btn-primary">🖨 พิมพ์ / บันทึก PDF</button>
    </div>
    <div class="print-modal-paper">${html}</div>`;
  document.body.appendChild(modal);
  document.body.classList.add("printing-modal");
  const cleanup = () => { modal.remove(); document.body.classList.remove("printing-modal"); document.removeEventListener("keydown", onKey); };
  const onKey = (e) => { if (e.key === "Escape") cleanup(); };
  document.addEventListener("keydown", onKey);
  modal.addEventListener("click", e => { if (e.target === modal) cleanup(); });
  document.getElementById("__print_btn").addEventListener("click", () => window.print());
  document.getElementById("__print_close").addEventListener("click", cleanup);
}

function brPrintReport({ acc, ym, lines, matches, ignored, docs, sum }) {
  const C = (window.SSSData || {}).Company || {};
  const esc = (t) => String(t == null ? "" : t).replace(/</g, "&lt;");
  const money = (n) => (Math.round(n * 100) / 100).toLocaleString("th-TH", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
  const accent = "#0f766e";
  const row = (l) => {
    const m = matches[l.id];
    const ig = ignored[l.id];
    return `<tr>
      <td style="border:1px solid #d6d3d1;padding:3px 6px;white-space:nowrap">${esc(brThaiDate(l.date))}</td>
      <td style="border:1px solid #d6d3d1;padding:3px 6px">${esc(l.desc)}${l.note ? `<div style="font-size:9px;color:#78716c">📝 ${esc(l.note)}</div>` : ""}</td>
      <td style="border:1px solid #d6d3d1;padding:3px 6px;text-align:right;color:#15803d">${l.dir === "in" ? money(l.amount) : ""}</td>
      <td style="border:1px solid #d6d3d1;padding:3px 6px;text-align:right;color:#b91c1c">${l.dir === "out" ? money(l.amount) : ""}</td>
      <td style="border:1px solid #d6d3d1;padding:3px 6px;font-size:9.5px">${m ? (m.docs || []).map(d => esc(d.no)).join(", ") : ig ? `<i style="color:#78716c">ไม่จับคู่ · ${esc(ig.reason || "")}</i>` : `<b style="color:#b45309">ยังไม่จับคู่</b>`}</td>
    </tr>`;
  };
  const unmatchedDocs = docs.filter(d => !sum.usedKeys.has(d.key) && brYm(d.date) === ym);
  const html = `
    <div style="background:white;color:#0c0a09;width:100%;min-height:270mm;padding:12mm 14mm;box-sizing:border-box;position:relative">
      <div style="display:flex;justify-content:space-between;align-items:flex-start;border-bottom:2px solid ${accent};padding-bottom:12px;margin-bottom:14px">
        <div style="display:flex;gap:12px;align-items:center">
          <div style="width:44px;height:44px;background:#0c0a09;color:white;display:grid;place-items:center;font-weight:700;font-size:14px;border-radius:6px">SSS</div>
          <div>
            <div style="font-weight:700;font-size:14px">${esc(C.name || "")}</div>
            <div style="font-size:10.5px;color:#57534e">เลขผู้เสียภาษี ${esc(C.taxId || "")}</div>
          </div>
        </div>
        <div style="text-align:right">
          <h1 style="margin:0;font-size:20px;color:${accent};font-weight:700">รายงานกระทบยอดธนาคาร</h1>
          <div style="font-size:10.5px;color:#78716c;letter-spacing:0.06em">BANK RECONCILIATION</div>
          <div style="font-size:11.5px;margin-top:4px"><b>${esc(acc.emoji || "")} ${esc(acc.name || "")}</b> · งวด <b>${esc(brMonthLabel(ym))}</b></div>
        </div>
      </div>
      <table style="width:100%;border-collapse:collapse;font-size:11.5px;margin-bottom:12px">
        <tr>
          <td style="border:1px solid #d6d3d1;padding:6px 10px;background:#f7f6f4;width:50%">เงินเข้าตาม statement (${sum.inCount} รายการ)</td>
          <td style="border:1px solid #d6d3d1;padding:6px 10px;text-align:right;font-weight:700;color:#15803d">฿${money(sum.inTotal)}</td>
        </tr>
        <tr>
          <td style="border:1px solid #d6d3d1;padding:6px 10px;background:#f7f6f4">เงินออกตาม statement (${sum.outCount} รายการ)</td>
          <td style="border:1px solid #d6d3d1;padding:6px 10px;text-align:right;font-weight:700;color:#b91c1c">฿${money(sum.outTotal)}</td>
        </tr>
        <tr>
          <td style="border:1px solid #d6d3d1;padding:6px 10px;background:#f7f6f4"><b>ยอดเคลื่อนไหวสุทธิ</b></td>
          <td style="border:1px solid #d6d3d1;padding:6px 10px;text-align:right;font-weight:700">฿${money(sum.inTotal - sum.outTotal)}</td>
        </tr>
        <tr>
          <td style="border:1px solid #d6d3d1;padding:6px 10px;background:#f7f6f4">จับคู่เอกสารแล้ว</td>
          <td style="border:1px solid #d6d3d1;padding:6px 10px;text-align:right">${sum.matchedCount} / ${lines.length} รายการ · ฿${money(sum.matchedAmount)}</td>
        </tr>
        <tr>
          <td style="border:1px solid #d6d3d1;padding:6px 10px;background:#fffbeb"><b>ยังไม่จับคู่ (ต้องตรวจสอบ)</b></td>
          <td style="border:1px solid #d6d3d1;padding:6px 10px;text-align:right;font-weight:700;color:#b45309">${sum.pendingCount} รายการ · ฿${money(sum.pendingAmount)}</td>
        </tr>
      </table>
      <div style="font-size:12px;font-weight:700;margin-bottom:5px">รายการเดินบัญชีทั้งหมด (${lines.length} รายการ)</div>
      <table style="width:100%;border-collapse:collapse;font-size:10.5px;margin-bottom:14px">
        <thead><tr style="background:${accent};color:white">
          <th style="padding:4px 6px;text-align:left;width:22mm">วันที่</th><th style="padding:4px 6px;text-align:left">รายการ</th>
          <th style="padding:4px 6px;text-align:right;width:24mm">เงินเข้า</th><th style="padding:4px 6px;text-align:right;width:24mm">เงินออก</th>
          <th style="padding:4px 6px;text-align:left;width:38mm">เอกสารที่จับคู่</th>
        </tr></thead>
        <tbody>${lines.map(row).join("")}</tbody>
      </table>
      ${unmatchedDocs.length ? `
        <div style="font-size:12px;font-weight:700;margin-bottom:5px">เอกสารในระบบเดือนนี้ที่ยังไม่พบใน statement (${unmatchedDocs.length} ใบ)</div>
        <table style="width:100%;border-collapse:collapse;font-size:10.5px">
          <thead><tr style="background:#f7f6f4"><th style="border:1px solid #d6d3d1;padding:4px 6px;text-align:left">ประเภท</th><th style="border:1px solid #d6d3d1;padding:4px 6px;text-align:left">เลขที่</th><th style="border:1px solid #d6d3d1;padding:4px 6px;text-align:left">วันที่</th><th style="border:1px solid #d6d3d1;padding:4px 6px;text-align:left">คู่ค้า</th><th style="border:1px solid #d6d3d1;padding:4px 6px;text-align:right">จำนวนเงิน</th></tr></thead>
          <tbody>${unmatchedDocs.map(d => `<tr>
            <td style="border:1px solid #d6d3d1;padding:3px 6px">${esc(d.label)}</td>
            <td style="border:1px solid #d6d3d1;padding:3px 6px;font-family:monospace">${esc(d.no)}</td>
            <td style="border:1px solid #d6d3d1;padding:3px 6px;white-space:nowrap">${esc(brThaiDate(d.date))}</td>
            <td style="border:1px solid #d6d3d1;padding:3px 6px">${esc(d.party)}</td>
            <td style="border:1px solid #d6d3d1;padding:3px 6px;text-align:right">${money(d.amount)}</td>
          </tr>`).join("")}</tbody>
        </table>` : ""}
      <div style="display:grid;grid-template-columns:1fr 1fr;gap:26px;margin-top:26px;font-size:11px">
        <div style="text-align:center"><div style="border-top:1px dashed #57534e;padding-top:5px;margin-top:34px"><b>ผู้จัดทำ</b><div style="color:#78716c">( ..................................... ) · วันที่ ..........</div></div></div>
        <div style="text-align:center"><div style="border-top:1px dashed #57534e;padding-top:5px;margin-top:34px"><b>ผู้ตรวจสอบ</b><div style="color:#78716c">( ..................................... ) · วันที่ ..........</div></div></div>
      </div>
      <div style="position:absolute;left:14mm;right:14mm;bottom:7mm;border-top:2px solid ${accent};padding-top:5px;font-size:9.5px;color:#78716c;display:flex;justify-content:space-between">
        <span>${esc(C.name || "")}</span><span>กระทบยอด ${esc(acc.name || "")} · ${esc(brMonthLabel(ym))} · ออกโดยระบบ SSS</span>
      </div>
    </div>`;
  brPrint(`รายงานกระทบยอด ${acc.name} · ${brMonthLabel(ym)}`, html);
}

// ─────────── หน้าจอนำเข้า statement ───────────
function BRImportDrawer({ acc, ym, existingLines, onClose, onDone }) {
  const { Btn, Drawer } = window.UI;
  const [busy, setBusy] = useStateBR("");
  const [rows, setRows] = useStateBR([]);
  const [pasted, setPasted] = useStateBR("");
  const [fileName, setFileName] = useStateBR("");
  const [mode, setMode] = useStateBR("");     // pdf | sheet | text
  const [err, setErr] = useStateBR("");
  // รหัสผ่านไฟล์ PDF (statement ธนาคารมักล็อกด้วยเลขบัตร/เบอร์โทร)
  const [pw, setPw] = useStateBR(() => brPwGet(acc.id));
  const [showPw, setShowPw] = useStateBR(false);
  const [needPw, setNeedPw] = useStateBR(false);
  const [remember, setRemember] = useStateBR(() => !!brPwGet(acc.id));
  const [pending, setPending] = useStateBR(null);   // ไฟล์ที่รออ่านซ้ำหลังใส่รหัส

  const takeLines = (parsed, name, m) => {
    if (!parsed.length) { setErr("อ่านไม่พบรายการเดินบัญชีในไฟล์นี้ — ถ้าเป็น PDF แบบสแกน (รูปภาพ) ให้ใช้ไฟล์ Excel/CSV จากธนาคาร หรือคัดลอกข้อความมาวางแทน"); setRows([]); return; }
    setErr("");
    setFileName(name || ""); setMode(m);
    setRows(parsed.map((r, i) => ({ ...r, _i: i, use: true })));
  };

  const runFile = async (file, password) => {
    if (!file) return;
    setErr(""); setBusy("กำลังอ่านไฟล์…");
    try {
      if (/\.pdf$/i.test(file.name)) {
        const text = await brPdfToText(file, setBusy, password);
        setNeedPw(false);
        if (password) brPwSet(acc.id, remember ? password : "");   // จำรหัสไว้เฉพาะเครื่องนี้ (ถ้าติ๊กไว้)
        takeLines(brParseStatementText(text), file.name, "pdf");
      } else if (/\.(xlsx|xls|csv)$/i.test(file.name)) {
        if (!window.XLSX) throw new Error("ตัวอ่าน Excel ยังไม่พร้อม — ลองรีเฟรชหน้า");
        const wb = window.XLSX.read(await file.arrayBuffer(), { type: "array", cellDates: true, password: password || undefined });
        const ws = wb.Sheets[wb.SheetNames[0]];
        const arr = window.XLSX.utils.sheet_to_json(ws, { header: 1, raw: true, defval: "" });
        takeLines(brParseSheetRows(arr), file.name, "sheet");
      } else throw new Error("รองรับไฟล์ PDF · Excel (.xlsx/.xls) · CSV เท่านั้น");
    } catch (e2) {
      setRows([]);
      const msg = e2.message || String(e2);
      // ไฟล์ Excel ที่ล็อกรหัสแบบใหม่ ตัวอ่านถอดรหัสไม่ได้ → บอกทางออกให้ชัด
      if (!e2.needPassword && /password|encrypt/i.test(msg) && !/\.pdf$/i.test(file.name)) {
        setErr("ไฟล์ Excel นี้ล็อกรหัสไว้ — ตัวอ่านในเว็บถอดรหัส Excel ไม่ได้ ให้เปิดไฟล์ด้วย Excel ใส่รหัส แล้ว Save As เป็นไฟล์ใหม่แบบไม่ใส่รหัส (หรือใช้ไฟล์ PDF แทน ซึ่งใส่รหัสในช่อง 🔒 ได้)");
      } else {
        setErr(msg);
        if (e2.needPassword) { setNeedPw(true); setShowPw(true); }
      }
    }
    setBusy("");
  };

  const onFile = async (e) => {
    const file = (e.target.files || [])[0]; e.target.value = "";
    if (!file) return;
    setPending(file);
    await runFile(file, pw);
  };

  const usePasted = () => {
    const parsed = brParseStatementText(pasted);
    takeLines(parsed, "วางข้อความเอง", "text");
  };

  const updRow = (i, k, v) => setRows(list => list.map(r => r._i === i ? { ...r, [k]: v } : r));
  const chosen = rows.filter(r => r.use);
  const inTotal = chosen.filter(r => r.dir === "in").reduce((s, r) => s + r.amount, 0);
  const outTotal = chosen.filter(r => r.dir === "out").reduce((s, r) => s + r.amount, 0);
  const offMonth = chosen.filter(r => brYm(r.date) !== ym).length;

  const confirm = () => {
    if (!chosen.length) { window.toast && window.toast("ยังไม่มีรายการที่เลือก"); return; }
    // กันซ้ำ: รายการเดิมที่วัน+ยอด+ทิศทาง+คำบรรยายเหมือนกัน ถือว่าซ้ำ ไม่เพิ่มใหม่
    const sig = (l) => `${l.date}|${l.dir}|${Math.round(l.amount * 100)}|${brNormTxt(l.desc).slice(0, 30)}`;
    const seen = new Set((existingLines || []).map(sig));
    let dup = 0;
    const add = [];
    chosen.forEach((r, i) => {
      if (seen.has(sig(r))) { dup++; return; }
      seen.add(sig(r));
      add.push({ id: "SL-" + Date.now().toString(36) + "-" + i, date: r.date, desc: r.desc, note: r.note || "", code: r.code || "",
        amount: r.amount, dir: r.dir, bal: r.bal == null ? null : r.bal, raw: r.raw, src: fileName });
    });
    onDone(add, { fileName, dup, total: chosen.length });
  };

  return (
    <Drawer open={true} onClose={onClose} wide title={`นำเข้า statement · ${acc.name} · ${brMonthLabel(ym)}`}
      footer={<>
        <Btn kind="ghost" onClick={onClose}>ยกเลิก</Btn>
        <Btn kind="primary" icon={I.check} onClick={confirm} disabled={!chosen.length}>เพิ่ม {chosen.length} รายการเข้างวดนี้</Btn>
      </>}>
      <div style={{display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginBottom: 12}}>
        <div>
          <window.UI.DropZone>
            <label className="btn" style={{cursor: "pointer", justifyContent: "center", padding: "14px 10px", flexDirection: "column", gap: 4, height: "auto", width: "100%"}}>
              <input type="file" accept=".pdf,.xlsx,.xls,.csv" style={{display: "none"}} onChange={onFile}/>
              <span style={{fontSize: 20}}>📄</span>
              <span style={{fontWeight: 700, fontSize: 13}}>{pending ? `เลือกไฟล์อื่น (ตอนนี้: ${pending.name.slice(0, 22)})` : "เลือกไฟล์ statement"}</span>
              <span style={{fontSize: 11, color: "var(--ink-3)"}}>PDF · Excel · CSV</span>
            </label>
          </window.UI.DropZone>
          {/* รหัสผ่านไฟล์ — statement ธนาคารมักล็อกด้วยเลขบัตร/เบอร์โทร */}
          <div style={{marginTop: 8, border: "1px solid " + (needPw ? "var(--warning)" : "var(--line)"), borderRadius: 9, padding: "8px 10px", background: needPw ? "rgba(180,83,9,0.07)" : "var(--surface-2)"}}>
            <div style={{fontSize: 11.5, fontWeight: 700, marginBottom: 5}}>🔒 รหัสผ่านไฟล์ {needPw && <span style={{color: "var(--warning)"}}>— ไฟล์นี้ต้องใส่รหัส</span>}</div>
            <div style={{display: "flex", gap: 6}}>
              <input className="input" style={{flex: 1, height: 30, fontSize: 12.5}} type={showPw ? "text" : "password"} value={pw} autoComplete="off"
                onChange={e => setPw(e.target.value)}
                onKeyDown={e => { if (e.key === "Enter" && pending) runFile(pending, pw); }}
                placeholder="เช่น เลขบัตรประชาชน / เบอร์โทร"/>
              <button className="icon-btn" title={showPw ? "ซ่อนรหัส" : "แสดงรหัส"} onClick={() => setShowPw(v => !v)} style={{fontSize: 13}}>{showPw ? "🙈" : "👁"}</button>
            </div>
            <label style={{display: "flex", alignItems: "center", gap: 6, fontSize: 11, color: "var(--ink-3)", marginTop: 6, cursor: "pointer"}}>
              <input type="checkbox" checked={remember} onChange={e => { setRemember(e.target.checked); if (!e.target.checked) brPwSet(acc.id, ""); }}/>
              จำรหัสของบัญชีนี้ไว้ (เฉพาะเครื่องนี้ ไม่ส่งขึ้นฐานข้อมูล)
            </label>
            {pending && /\.pdf$/i.test(pending.name) && (
              <Btn size="sm" style={{marginTop: 7, width: "100%"}} kind={needPw ? "primary" : "ghost"} onClick={() => runFile(pending, pw)} disabled={!!busy}>อ่านไฟล์อีกครั้งด้วยรหัสนี้</Btn>
            )}
          </div>
        </div>
        <div style={{border: "1px dashed var(--line-strong)", borderRadius: 10, padding: 10}}>
          <div style={{fontSize: 12, fontWeight: 700, marginBottom: 5}}>หรือคัดลอกข้อความจาก statement มาวาง</div>
          <textarea className="textarea" style={{minHeight: 60, fontSize: 11.5}} value={pasted} onChange={e => setPasted(e.target.value)}
            placeholder={"01/08/68  โอนเงินเข้า บจก.ตัวอย่าง   128,400.00   350,120.55"}/>
          <Btn size="sm" style={{marginTop: 6}} onClick={usePasted} disabled={!pasted.trim()}>อ่านข้อความนี้</Btn>
        </div>
      </div>

      {busy && <div style={{padding: 10, background: "var(--surface-2)", borderRadius: 8, fontSize: 12.5, marginBottom: 10}}>⏳ {busy}</div>}
      {err && <div style={{padding: 10, background: "var(--danger-bg, #fef2f2)", border: "1px solid var(--danger)", color: "var(--danger)", borderRadius: 8, fontSize: 12.5, marginBottom: 10, lineHeight: 1.6}}>⚠️ {err}</div>}

      {rows.length > 0 && (
        <>
          <div style={{display: "flex", gap: 14, alignItems: "center", flexWrap: "wrap", padding: "8px 12px", background: "var(--surface-2)", borderRadius: 8, marginBottom: 8, fontSize: 12.5}}>
            <b>ตรวจทานก่อนบันทึก — {chosen.length}/{rows.length} รายการ</b>
            <span style={{color: "var(--success)"}}>เข้า ฿{window.UI.fmt0(inTotal)}</span>
            <span style={{color: "var(--danger)"}}>ออก ฿{window.UI.fmt0(outTotal)}</span>
            {offMonth > 0 && <span style={{color: "var(--warning)"}}>⚠️ {offMonth} รายการอยู่นอกเดือน {brMonthLabel(ym)}</span>}
            <span style={{marginLeft: "auto", color: "var(--ink-3)", fontSize: 11.5}}>ทิศทางผิด? กดปุ่ม เข้า/ออก เพื่อสลับได้</span>
          </div>
          <div className="table-wrap" style={{maxHeight: 420, overflow: "auto"}}>
            <table className="t">
              <thead><tr><th style={{width: 34}}></th><th>วันที่</th><th>รายการ</th><th style={{width: 96}}>ทิศทาง</th><th className="right" style={{width: 120}}>จำนวนเงิน</th></tr></thead>
              <tbody>
                {rows.map(r => (
                  <tr key={r._i} style={{opacity: r.use ? 1 : 0.45}}>
                    <td><input type="checkbox" checked={r.use} onChange={e => updRow(r._i, "use", e.target.checked)}/></td>
                    <td style={{whiteSpace: "nowrap", fontSize: 12}}>{brThaiDate(r.date)}{brYm(r.date) !== ym && <span title="อยู่นอกเดือนที่เลือก" style={{color: "var(--warning)"}}> ⚠</span>}</td>
                    <td style={{fontSize: 11.5, maxWidth: 340}} title={r.raw}>
                      <div style={{overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap"}}>{r.desc}</div>
                      {(r.note || r.code) && <div style={{fontSize: 10, color: "var(--ink-4)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap"}}>
                        {r.code ? <span className="mono">{r.code}</span> : null}{r.code && r.note ? " · " : ""}{r.note ? `📝 ${r.note}` : ""}
                      </div>}
                    </td>
                    <td>
                      <button className="btn" style={{padding: "2px 8px", fontSize: 11, height: 24, background: r.dir === "in" ? "var(--success)" : "var(--danger)", color: "#fff", borderColor: "transparent"}}
                        onClick={() => updRow(r._i, "dir", r.dir === "in" ? "out" : "in")}>{r.dir === "in" ? "↓ เข้า" : "↑ ออก"}</button>
                    </td>
                    <td className="right amount" style={{fontSize: 12.5}}>
                      <input className="input" type="number" step="0.01" value={r.amount} onChange={e => updRow(r._i, "amount", Math.abs(Number(e.target.value) || 0))}
                        style={{height: 26, textAlign: "right", fontSize: 12}}/>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </>
      )}

      <div style={{marginTop: 12, padding: 11, background: "var(--surface-2)", borderRadius: 9, fontSize: 11.5, color: "var(--ink-3)", lineHeight: 1.75}}>
        <b style={{color: "var(--ink-1)"}}>เคล็ดลับ:</b> ถ้าธนาคารมีให้ดาวน์โหลดเป็น <b>Excel/CSV</b> ให้ใช้ไฟล์นั้นดีที่สุด — อ่านได้แม่นกว่า PDF มาก<br/>
        <b style={{color: "var(--ink-1)"}}>ไฟล์มีรหัส:</b> statement ที่ธนาคารส่งทางอีเมลมักล็อกรหัส (เลขบัตรประชาชน · เบอร์โทร · วันเดือนปีเกิด) ใส่ในช่อง 🔒 แล้วกดอ่านอีกครั้ง — ติ๊ก "จำรหัส" ไว้ครั้งเดียว เดือนหน้าไม่ต้องพิมพ์ใหม่<br/>
        PDF ที่เป็นภาพสแกน (ถ่ายรูป/สแกนกระดาษ) อ่านอัตโนมัติไม่ได้ ต้องใช้ Excel/CSV หรือพิมพ์รายการเอง<br/>
        ระบบไม่เก็บไฟล์ statement ไว้ในฐานข้อมูล เก็บเฉพาะรายการที่อ่านได้ (ประหยัดพื้นที่) — เก็บไฟล์ต้นฉบับไว้ที่เซิร์ฟเวอร์ไฟล์ของบริษัท
      </div>
    </Drawer>
  );
}

// ─────────── หน้าจอจับคู่เอกสาร (ต่อ 1 รายการ) ───────────
// เปิดจากรายการที่ยังไม่จับคู่ = จับคู่ใหม่ · เปิดจากรายการที่จับคู่แล้ว = แก้ไข (ติ๊กออก/เพิ่มใบอื่นได้)
function BRMatchDrawer({ line, docs, usedKeys, current, onClose, onSave }) {
  const { Btn, Drawer, Badge, fmt0 } = window.UI;
  const isEdit = !!(current && current.docs && current.docs.length);
  const [sel, setSel] = useStateBR(() => (current && current.docs) ? current.docs.map(d => d.key) : []);
  const [q, setQ] = useStateBR("");
  const [showAll, setShowAll] = useStateBR(false);
  const byKey = {}; docs.forEach(d => { byKey[d.key] = d; });
  // เอกสารที่ถูกใช้กับรายการอื่นไปแล้ว — ซ่อนจากข้อเสนอ ยกเว้นใบที่รายการนี้เลือกอยู่
  const used = new Set([...usedKeys].filter(k => !sel.includes(k)));
  const sugg = useMemoBR(() => brSuggest(line, docs, used, showAll ? 60 : 15), [line.id, docs.length, sel.length, showAll]);
  const searched = useMemoBR(() => {
    const sq = q.trim().toLowerCase();
    if (!sq) return [];
    // ค้นได้ทุกใบ ไม่จำกัดทิศทาง — เผื่อทิศทางที่อ่านจาก statement มาผิด
    return docs.filter(d => String(d.no).toLowerCase().includes(sq) || String(d.party).toLowerCase().includes(sq)
      || String(Math.round(d.amount)).includes(sq.replace(/[,\s]/g, ""))).slice(0, 25);
  }, [q, docs.length]);
  const selDocs = sel.map(k => byKey[k]).filter(Boolean);
  // ใบที่จับคู่ไว้เดิมแต่หาไม่เจอในรายการเอกสารของเดือนนี้ (เช่น เอกสารถูกลบ/ย้ายเดือน) — ยังให้ติ๊กออกได้
  const orphan = (current && current.docs ? current.docs : []).filter(d => sel.includes(d.key) && !byKey[d.key]);
  const selTotal = selDocs.reduce((s, d) => s + d.amount, 0) + orphan.reduce((s, d) => s + (d.amount || 0), 0);
  const diff = line.amount - selTotal;
  const toggle = (k) => setSel(p => p.includes(k) ? p.filter(x => x !== k) : [...p, k]);

  const WHY_TONE = { "ยอดตรงพอดี": "success", "ชื่อตรง": "success", "ชื่อตรงหลายคำ": "success", "เลขเอกสารอยู่ในรายการ": "success", "เลขอ้างอิงตรง": "success", "วันเดียวกัน": "info" };
  const DocRow = ({ d, sc, why }) => {
    const on = sel.includes(d.key);
    const amtGap = Math.abs(d.amount - line.amount);
    return (
      <div onClick={() => toggle(d.key)} style={{display: "flex", gap: 10, alignItems: "flex-start", padding: "8px 10px", borderRadius: 8, cursor: "pointer",
        border: "1px solid " + (on ? "var(--primary, #0f766e)" : "var(--line)"), background: on ? "rgba(15,118,110,0.09)" : "var(--surface)", marginBottom: 5}}>
        <input type="checkbox" readOnly checked={on} style={{marginTop: 3}}/>
        <div style={{flex: 1, minWidth: 0}}>
          <div style={{fontSize: 12.5, fontWeight: 600}}>
            {d.label} <span className="mono" style={{color: "var(--ink-3)"}}>{d.no}</span>
            {d.side !== line.dir && <span style={{color: "var(--warning)", fontSize: 11, marginLeft: 6}}>({d.side === "in" ? "เอกสารขาย" : "เอกสารจ่าย"})</span>}
          </div>
          <div style={{fontSize: 11.5, color: "var(--ink-2, var(--ink-3))", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap"}}>{d.party || "—"}</div>
          <div style={{fontSize: 11, color: "var(--ink-3)"}}>{brThaiDate(d.date)}{d.status ? ` · ${d.status}` : ""}</div>
          {why && why.length > 0 && (
            <div style={{display: "flex", gap: 4, flexWrap: "wrap", marginTop: 4}}>
              {why.map((w, i) => <Badge key={i} tone={WHY_TONE[w] || "outline"}>{w}</Badge>)}
            </div>
          )}
        </div>
        <div style={{textAlign: "right", flexShrink: 0}}>
          <div className="amount" style={{fontSize: 13.5, fontWeight: 700, color: amtGap < 0.5 ? "var(--success)" : undefined}}>฿{fmt0(d.amount)}</div>
          {amtGap >= 0.5 && <div style={{fontSize: 10, color: "var(--ink-4)"}}>ต่าง ฿{fmt0(amtGap)}</div>}
        </div>
      </div>
    );
  };

  return (
    <Drawer open={true} onClose={onClose} wide title={isEdit ? "แก้ไขการจับคู่เอกสาร" : "จับคู่รายการกับเอกสาร"}
      footer={<>
        <Btn kind="ghost" onClick={onClose}>ยกเลิก</Btn>
        {isEdit && <Btn kind="ghost" onClick={() => setSel([])} disabled={!sel.length}>ล้างที่เลือกทั้งหมด</Btn>}
        <Btn kind="primary" icon={I.check} onClick={() => onSave(selDocs)} disabled={!selDocs.length}>
          {isEdit ? "บันทึกการแก้ไข" : "บันทึกการจับคู่"} ({selDocs.length})
        </Btn>
      </>}>
      <div style={{padding: "10px 12px", borderRadius: 10, background: line.dir === "in" ? "rgba(21,128,61,0.08)" : "rgba(185,28,28,0.08)", border: "1px solid var(--line)", marginBottom: 12}}>
        <div style={{fontSize: 11.5, color: "var(--ink-3)"}}>รายการจาก statement</div>
        <div style={{display: "flex", alignItems: "center", gap: 10}}>
          <div style={{flex: 1, minWidth: 0}}>
            <div style={{fontSize: 13.5, fontWeight: 700}}>{line.desc}</div>
            {line.note && <div style={{fontSize: 12, color: "var(--ink-2, var(--ink-3))"}}>📝 {line.note}</div>}
            <div style={{fontSize: 11.5, color: "var(--ink-3)"}}>{brThaiDate(line.date)}{line.code ? ` · ${line.code}` : ""}</div>
          </div>
          <div className="amount" style={{fontSize: 18, fontWeight: 800, color: line.dir === "in" ? "var(--success)" : "var(--danger)"}}>
            {line.dir === "in" ? "+" : "−"}฿{fmt0(line.amount)}
          </div>
        </div>
      </div>

      {/* เอกสารที่เลือกไว้ตอนนี้ — ติ๊กออกได้ทันทีถ้าจับคู่ผิด */}
      {(selDocs.length > 0 || orphan.length > 0) && (
        <div style={{marginBottom: 12, border: "1px solid var(--line-strong)", borderRadius: 10, padding: "9px 11px", background: "var(--surface-2)"}}>
          <div style={{fontSize: 12, fontWeight: 700, marginBottom: 6}}>เอกสารที่จับคู่อยู่ ({selDocs.length + orphan.length} ใบ) — กด ✕ เพื่อเอาออก</div>
          <div style={{display: "flex", flexWrap: "wrap", gap: 6}}>
            {selDocs.map(d => (
              <span key={d.key} style={{display: "inline-flex", alignItems: "center", gap: 5, padding: "3px 6px 3px 9px", borderRadius: 999, border: "1px solid var(--line-strong)", background: "var(--surface)", fontSize: 11.5}}>
                <b className="mono">{d.no}</b> <span style={{color: "var(--ink-3)"}}>฿{fmt0(d.amount)}</span>
                <button className="icon-btn" title="เอาเอกสารใบนี้ออก" onClick={() => toggle(d.key)} style={{width: 17, height: 17, padding: 0}}><I.close size={9}/></button>
              </span>
            ))}
            {orphan.map(d => (
              <span key={d.key} style={{display: "inline-flex", alignItems: "center", gap: 5, padding: "3px 6px 3px 9px", borderRadius: 999, border: "1px dashed var(--warning)", fontSize: 11.5}}
                title="ไม่พบเอกสารใบนี้ในเดือนนี้แล้ว (อาจถูกลบหรือแก้วันที่)">
                <b className="mono">{d.no}</b> <span style={{color: "var(--warning)"}}>⚠ ไม่พบเอกสาร</span>
                <button className="icon-btn" title="เอาออก" onClick={() => toggle(d.key)} style={{width: 17, height: 17, padding: 0}}><I.close size={9}/></button>
              </span>
            ))}
          </div>
          <div style={{marginTop: 7, fontSize: 12.5, paddingTop: 7, borderTop: "1px dashed var(--line)"}}>
            รวม <b>฿{fmt0(selTotal)}</b> ·
            {Math.abs(diff) < 0.5 ? <b style={{color: "var(--success)"}}> ตรงกับยอดในบัญชีพอดี ✓</b>
              : <b style={{color: "var(--warning)"}}> ต่างจากยอดในบัญชี ฿{fmt0(Math.abs(diff))} {diff > 0 ? "(เอกสารน้อยกว่า — อาจมีอีกใบ/มีค่าธรรมเนียม)" : "(เอกสารมากกว่า — อาจรับ/จ่ายเพียงบางส่วน)"}</b>}
          </div>
        </div>
      )}

      <div className="label mb" style={{display: "flex", alignItems: "center", justifyContent: "space-between"}}>
        <span>เอกสารที่ระบบเสนอ — ตรวจก่อนเลือก (ป้ายบอกเหตุผลว่าตรงเรื่องอะไร)</span>
        <button className="badge outline" style={{cursor: "pointer"}} onClick={() => setShowAll(v => !v)}>{showAll ? "แสดงเฉพาะที่ใกล้เคียง" : "แสดงเพิ่มเติม"}</button>
      </div>
      {sugg.length === 0
        ? <div style={{fontSize: 12.5, color: "var(--ink-3)", padding: "10px 12px", background: "var(--surface-2)", borderRadius: 8, marginBottom: 12, lineHeight: 1.7}}>
            ไม่พบเอกสารที่ชื่อหรือยอดใกล้เคียงในเดือนนี้และเดือนข้างเคียง<br/>
            ลองค้นหาเองด้านล่าง (ค้นได้ทั้งเลขที่ · ชื่อคู่ค้า · ยอดเงิน) หรือกด "ไม่ต้องจับคู่" ถ้าเป็นค่าธรรมเนียม/ดอกเบี้ย/โอนระหว่างบัญชีบริษัทเอง
          </div>
        : <div style={{marginBottom: 12}}>{sugg.map(s => <DocRow key={s.doc.key} d={s.doc} sc={s.sc} why={s.why}/>)}</div>}

      <div className="label mb">ค้นหาเอกสารเอง (เลขที่ · ชื่อคู่ค้า · ยอดเงิน — ค้นได้ทุกประเภท ไม่จำกัดทิศทาง)</div>
      <input className="input mb" value={q} onChange={e => setQ(e.target.value)} placeholder="เช่น INV-2569 · ชื่อลูกค้า/ผู้ขาย · 35000"/>
      {searched.map(d => <DocRow key={d.key} d={d}/>)}
      {q.trim() && searched.length === 0 && <div style={{fontSize: 12.5, color: "var(--ink-3)"}}>ไม่พบเอกสารที่ตรงกับคำค้น</div>}
    </Drawer>
  );
}

// ─────────── หน้าหลัก ───────────
function BankReconPage({ navigate }) {
  const { Btn, Badge, Stat, Card, PageHead, Tabs, fmt0 } = window.UI;
  const accounts = (window.CompanyCashStore && window.CompanyCashStore.ACCOUNTS) || [{ id: "CASH", name: "เงินสดบริษัท", short: "เงินสด", emoji: "💵" }];
  const nowYm = new Date().toISOString().slice(0, 7);
  const [accId, setAccId] = useStateBR(() => (accounts.find(a => a.kind !== "เงินสด") || accounts[0] || {}).id || "CASH");
  const [ym, setYm] = useStateBR(nowYm);
  const [, force] = useStateBR(0);
  const [tab, setTab] = useStateBR("pending");
  const [importing, setImporting] = useStateBR(false);
  const [matching, setMatching] = useStateBR(null);
  const [sel, setSel] = useStateBR(() => new Set());        // รายการที่ติ๊กเลือกไว้ (ลบ/จัดการทีละหลายรายการ)
  const [autoStatus, setAutoStatus] = useStateBR(() => { try { return localStorage.getItem("sss-br-autostatus") !== "0"; } catch { return true; } });

  useEffectBR(() => {
    const h = () => force(n => n + 1);
    window.addEventListener("sss-bank-recon-changed", h);
    window.addEventListener("sss-db-loaded", h);
    window.addEventListener("sss-localstore-polled", h);
    return () => { window.removeEventListener("sss-bank-recon-changed", h); window.removeEventListener("sss-db-loaded", h); window.removeEventListener("sss-localstore-polled", h); };
  }, []);
  // เปลี่ยนบัญชี/งวด/แท็บ → ล้างรายการที่ติ๊กไว้ กันเผลอลบรายการที่มองไม่เห็นบนจอ
  useEffectBR(() => { setSel(new Set()); }, [accId, ym, tab]);

  const acc = accounts.find(a => a.id === accId) || accounts[0] || { id: "CASH", name: "เงินสด" };
  const pid = brPid(accId, ym);
  const period = brGet(pid) || {};
  const lines = (period.lines || []).slice().sort((a, b) => String(a.date).localeCompare(String(b.date)));
  const matches = period.matches || {};
  const ignored = period.ignored || {};
  const closed = !!period.closed;

  const docs = useMemoBR(() => brBuildDocs(ym), [ym, (window.SSSData.Receipts || []).length, (window.SSSData.PaymentVouchers || []).length, (window.SSSData.Invoices || []).length]);
  const usedKeys = useMemoBR(() => {
    const s = new Set();
    Object.values(matches).forEach(m => (m.docs || []).forEach(d => s.add(d.key)));
    return s;
  }, [JSON.stringify(matches)]);

  // ── สรุปยอด ──
  const sum = useMemoBR(() => {
    const inLines = lines.filter(l => l.dir === "in"), outLines = lines.filter(l => l.dir === "out");
    const matchedIds = Object.keys(matches);
    const pending = lines.filter(l => !matches[l.id] && !ignored[l.id]);
    return {
      inCount: inLines.length, inTotal: inLines.reduce((s, l) => s + l.amount, 0),
      outCount: outLines.length, outTotal: outLines.reduce((s, l) => s + l.amount, 0),
      matchedCount: matchedIds.length,
      matchedAmount: matchedIds.reduce((s, id) => s + ((lines.find(l => l.id === id) || {}).amount || 0), 0),
      pendingCount: pending.length, pendingAmount: pending.reduce((s, l) => s + l.amount, 0),
      ignoredCount: Object.keys(ignored).length,
      usedKeys,
    };
  }, [JSON.stringify(lines.map(l => l.id + l.amount)), JSON.stringify(matches), JSON.stringify(ignored)]);

  const docsUnmatched = docs.filter(d => !usedKeys.has(d.key) && brYm(d.date) === ym);

  // ── การกระทำ ──
  const guardClosed = () => {
    if (closed) { window.toast && window.toast("งวดนี้ปิดแล้ว — กด 'เปิดงวดอีกครั้ง' ก่อนแก้ไข"); return true; }
    return false;
  };
  const setStatusOf = (doc) => {
    if (!autoStatus) return;
    const src = BR_SRC[doc.list];
    if (!src || !src.docType || !src.paidStatus) return;
    try { window.DocStatusStore && window.DocStatusStore.set(src.docType, doc.no, src.paidStatus); } catch {}
  };

  const onImported = (add, info) => {
    const next = [...(period.lines || []), ...add];
    // จับคู่อัตโนมัติทันทีหลังนำเข้า
    const auto = brAutoMatch(next.map(l => l), docs, matches);
    const merged = { ...matches, ...auto };
    Object.values(auto).forEach(m => (m.docs || []).forEach(d => setStatusOf(d)));
    brSet(pid, { accountId: accId, ym, lines: next, matches: merged, ignored, fileName: info.fileName, importedAt: new Date().toISOString(),
      importedBy: (window.Session && window.Session.get() && window.Session.get().name) || "" });
    setImporting(false);
    window.logActivity && window.logActivity("นำเข้า statement", `${acc.name} · ${brMonthLabel(ym)} · ${add.length} รายการ`, "create");
    window.toast && window.toast(`เพิ่ม ${add.length} รายการ${info.dup ? ` (ข้ามซ้ำ ${info.dup})` : ""} · จับคู่อัตโนมัติได้ ${Object.keys(auto).length} รายการ`);
  };

  const runAuto = () => {
    if (guardClosed()) return;
    const auto = brAutoMatch(lines, docs, matches);
    const n = Object.keys(auto).length;
    if (!n) { window.toast && window.toast("ไม่พบคู่ใหม่ที่มั่นใจพอ — จับคู่เองได้จากปุ่มในแต่ละรายการ"); return; }
    Object.values(auto).forEach(m => (m.docs || []).forEach(d => setStatusOf(d)));
    brSet(pid, { matches: { ...matches, ...auto } });
    window.toast && window.toast(`จับคู่อัตโนมัติเพิ่ม ${n} รายการ`);
  };

  const saveMatch = (line, selDocs) => {
    const m = { ...matches, [line.id]: { docs: selDocs.map(d => ({ key: d.key, list: d.list, no: d.no, amount: d.amount, label: d.label })), auto: false, at: new Date().toISOString() } };
    const ig = { ...ignored }; delete ig[line.id];
    selDocs.forEach(setStatusOf);
    brSet(pid, { matches: m, ignored: ig });
    setMatching(null);
    window.toast && window.toast(`จับคู่ ${selDocs.length} เอกสารแล้ว`);
  };
  const unmatch = (line) => {
    if (guardClosed()) return;
    const m = { ...matches }; delete m[line.id];
    brSet(pid, { matches: m });
  };
  const markIgnore = (line, reason) => {
    if (guardClosed()) return;
    const ig = { ...ignored, [line.id]: { reason, at: new Date().toISOString() } };
    const m = { ...matches }; delete m[line.id];
    brSet(pid, { ignored: ig, matches: m });
  };
  const unIgnore = (line) => {
    if (guardClosed()) return;
    const ig = { ...ignored }; delete ig[line.id];
    brSet(pid, { ignored: ig });
  };
  const delLine = async (line) => {
    if (guardClosed()) return;
    const ok = window.confirmDelete ? await window.confirmDelete(`รายการ ${brThaiDate(line.date)}`, `${line.desc} · ฿${fmt0(line.amount)}`) : window.confirm("ลบรายการนี้?");
    if (!ok) return;
    const m = { ...matches }; delete m[line.id];
    const ig = { ...ignored }; delete ig[line.id];
    brSet(pid, { lines: (period.lines || []).filter(l => l.id !== line.id), matches: m, ignored: ig });
    setSel(s => { const n = new Set(s); n.delete(line.id); return n; });
  };

  // ── เลือกหลายรายการ: ลบ / ทำเครื่องหมายไม่จับคู่ / ล้างสถานะ ──
  const toggleSel = (id) => setSel(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
  const toggleSelAll = (ids, on) => setSel(s => { const n = new Set(s); ids.forEach(id => on ? n.add(id) : n.delete(id)); return n; });
  const bulkDelete = async () => {
    if (guardClosed()) return;
    const ids = [...sel];
    if (!ids.length) return;
    const amt = ids.reduce((s, id) => s + ((lines.find(l => l.id === id) || {}).amount || 0), 0);
    const ok = window.confirmDelete
      ? await window.confirmDelete(`${ids.length} รายการที่เลือก`, `รวม ฿${fmt0(amt)} · การจับคู่ของรายการเหล่านี้จะถูกลบไปด้วย`)
      : window.confirm(`ลบ ${ids.length} รายการที่เลือก?`);
    if (!ok) return;
    const m = { ...matches }, ig = { ...ignored };
    ids.forEach(id => { delete m[id]; delete ig[id]; });
    brSet(pid, { lines: (period.lines || []).filter(l => !sel.has(l.id)), matches: m, ignored: ig });
    setSel(new Set());
    window.toast && window.toast(`ลบ ${ids.length} รายการแล้ว`);
  };
  const bulkIgnore = () => {
    if (guardClosed()) return;
    const ids = [...sel];
    if (!ids.length) return;
    const m = { ...matches }, ig = { ...ignored };
    ids.forEach(id => { delete m[id]; ig[id] = { reason: "ไม่ต้องจับคู่ (เลือกหลายรายการ)", at: new Date().toISOString() }; });
    brSet(pid, { matches: m, ignored: ig });
    setSel(new Set());
    window.toast && window.toast(`ทำเครื่องหมายไม่ต้องจับคู่ ${ids.length} รายการ`);
  };
  const bulkReset = () => {
    if (guardClosed()) return;
    const ids = [...sel];
    if (!ids.length) return;
    const m = { ...matches }, ig = { ...ignored };
    ids.forEach(id => { delete m[id]; delete ig[id]; });
    brSet(pid, { matches: m, ignored: ig });
    setSel(new Set());
    window.toast && window.toast(`ล้างสถานะ ${ids.length} รายการ — กลับไปรอจับคู่`);
  };
  const deleteAllLines = async () => {
    if (guardClosed()) return;
    if (!lines.length) return;
    const ok = window.confirmDelete
      ? await window.confirmDelete(`ทุกรายการในงวด ${brMonthLabel(ym)}`, `${acc.name} · ${lines.length} รายการ — ลบแล้วนำเข้า statement ใหม่ได้`)
      : window.confirm(`ลบทุกรายการในงวดนี้ (${lines.length} รายการ)?`);
    if (!ok) return;
    brSet(pid, { lines: [], matches: {}, ignored: {} });
    setSel(new Set());
    window.logActivity && window.logActivity("ล้างรายการกระทบยอด", `${acc.name} · ${brMonthLabel(ym)} · ${lines.length} รายการ`, "delete");
    window.toast && window.toast("ลบรายการทั้งงวดแล้ว");
  };
  // สร้างรายการในสมุดรายรับ-รายจ่าย จากรายการ statement ที่ไม่มีเอกสาร
  const toCashbook = (line) => {
    if (guardClosed()) return;
    let list = [];
    try { list = window.lsRead("sss-cashbook", []); } catch {}
    const entry = {
      id: "CB-" + Date.now(), kind: line.dir === "in" ? "รับ" : "จ่าย", date: line.date,
      category: line.dir === "in" ? "รายรับอื่น ๆ" : "รายจ่ายอื่น ๆ",
      desc: `${line.desc} (จาก statement ${acc.name})`, amount: line.amount,
      accountId: accId, method: acc.name, attachments: [],
    };
    list.unshift(entry);
    try { localStorage.setItem("sss-cashbook", JSON.stringify(list)); } catch {}
    window.dispatchEvent(new Event("sss-cashbook-changed"));
    markIgnore(line, "บันทึกเข้ารายรับรายจ่ายแล้ว");
    window.toast && window.toast("เพิ่มเข้ารายรับ-รายจ่ายแล้ว");
  };
  const toggleClose = () => {
    if (!closed && sum.pendingCount > 0) {
      if (!window.confirm(`ยังมี ${sum.pendingCount} รายการที่ยังไม่จับคู่ — ปิดงวดเลยหรือไม่?`)) return;
    }
    brSet(pid, { closed: !closed, closedAt: !closed ? new Date().toISOString() : null,
      closedBy: !closed ? ((window.Session && window.Session.get() && window.Session.get().name) || "") : null });
    window.toast && window.toast(!closed ? "ปิดงวดแล้ว — ล็อกไม่ให้แก้ไข" : "เปิดงวดอีกครั้ง");
  };

  // ── ตัวเลือกเดือน (ย้อนหลัง 18 เดือน) ──
  const monthOpts = useMemoBR(() => {
    const out = [];
    const d = new Date();
    for (let i = 0; i < 18; i++) { out.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`); d.setMonth(d.getMonth() - 1); }
    return out;
  }, []);
  const all = brLoadAll();
  const doneMonths = Object.keys(all).filter(k => k.startsWith(accId + "|")).map(k => k.split("|")[1]);

  const shown = lines.filter(l => tab === "all" ? true : tab === "pending" ? (!matches[l.id] && !ignored[l.id]) : tab === "matched" ? !!matches[l.id] : !!ignored[l.id]);
  // ข้อเสนออันดับ 1 ของแต่ละรายการที่ยังไม่จับคู่ — แสดงในตารางให้ตรวจแล้วกดยืนยันได้เลย
  const topSuggest = useMemoBR(() => {
    const map = {};
    lines.forEach(l => {
      if (matches[l.id] || ignored[l.id]) return;
      const s = brSuggest(l, docs, usedKeys, 1)[0];
      if (s && s.sc >= 55) map[l.id] = s;
    });
    return map;
  }, [lines.length, docs.length, JSON.stringify(matches), JSON.stringify(ignored)]);

  const shownIds = shown.map(l => l.id);
  const allShownSel = shownIds.length > 0 && shownIds.every(id => sel.has(id));
  const selCount = shownIds.filter(id => sel.has(id)).length;
  const selAmount = shownIds.filter(id => sel.has(id)).reduce((s, id) => s + ((lines.find(l => l.id === id) || {}).amount || 0), 0);

  return (
    <>
      <PageHead
        title="กระทบยอดธนาคาร"
        sub={`นำเข้า statement แล้วจับคู่กับเอกสารขาย-เอกสารจ่ายในระบบ · แยกตามบัญชีและเดือน`}
        right={<>
          <Btn icon={I.download} kind="ghost" onClick={() => brPrintReport({ acc, ym, lines, matches, ignored, docs, sum })} disabled={!lines.length}>พิมพ์รายงาน</Btn>
          <Btn icon={I.plus} kind="primary" onClick={() => { if (!guardClosed()) setImporting(true); }}>นำเข้า statement</Btn>
        </>}
      />

      {/* เลือกบัญชี + เดือน */}
      <div style={{display: "flex", gap: 10, flexWrap: "wrap", alignItems: "flex-end", marginBottom: 16}}>
        <div className="field" style={{minWidth: 240}}><label>บัญชีธนาคาร</label>
          <select className="input" value={accId} onChange={e => setAccId(e.target.value)}>
            {accounts.map(a => <option key={a.id} value={a.id}>{a.emoji} {a.name}{a.detail ? ` · ${a.detail}` : ""}</option>)}
          </select>
        </div>
        <div className="field" style={{minWidth: 170}}><label>งวด (เดือน)</label>
          <select className="input" value={ym} onChange={e => setYm(e.target.value)}>
            {monthOpts.map(m => <option key={m} value={m}>{brMonthLabel(m)}{doneMonths.includes(m) ? " ✓" : ""}</option>)}
          </select>
        </div>
        <label style={{display: "flex", alignItems: "center", gap: 7, fontSize: 12.5, paddingBottom: 8, cursor: "pointer"}}>
          <input type="checkbox" checked={autoStatus} onChange={e => { setAutoStatus(e.target.checked); try { localStorage.setItem("sss-br-autostatus", e.target.checked ? "1" : "0"); } catch {} }}/>
          อัพเดตสถานะเอกสารเป็น "ชำระแล้ว/จ่ายแล้ว" เมื่อจับคู่
        </label>
        <div style={{marginLeft: "auto", display: "flex", gap: 8, alignItems: "center", paddingBottom: 4}}>
          {closed && <Badge tone="success">🔒 ปิดงวดแล้ว {period.closedBy ? `· ${period.closedBy}` : ""}</Badge>}
          {lines.length > 0 && <Btn size="sm" kind="ghost" icon={I.sparkle} onClick={runAuto}>จับคู่อัตโนมัติอีกครั้ง</Btn>}
          {lines.length > 0 && <Btn size="sm" kind={closed ? "ghost" : "primary"} onClick={toggleClose}>{closed ? "เปิดงวดอีกครั้ง" : "ปิดงวด"}</Btn>}
        </div>
      </div>

      {lines.length === 0 ? (
        <Card title={`ยังไม่มี statement ของ ${acc.name} · ${brMonthLabel(ym)}`}>
          <div style={{padding: "22px 6px", textAlign: "center"}}>
            <div style={{fontSize: 40, marginBottom: 10}}>🏦</div>
            <div style={{fontSize: 14, fontWeight: 600, marginBottom: 6}}>เริ่มกระทบยอดโดยนำเข้า statement ของเดือนนี้</div>
            <div style={{fontSize: 12.5, color: "var(--ink-3)", lineHeight: 1.8, maxWidth: 640, margin: "0 auto 16px"}}>
              รองรับไฟล์ <b>PDF</b> (e-Statement ที่เป็นข้อความ) · <b>Excel / CSV</b> จากธนาคาร (แม่นที่สุด) · หรือคัดลอกข้อความมาวาง<br/>
              ระบบจะอ่านวันที่ · รายการ · จำนวนเงิน · ยอดคงเหลือ แล้วจับคู่กับใบเสร็จ/ใบแจ้งหนี้/ใบสำคัญจ่าย/ค่าใช้จ่ายในระบบให้อัตโนมัติ
            </div>
            <Btn kind="primary" icon={I.plus} onClick={() => setImporting(true)}>นำเข้า statement</Btn>
          </div>
        </Card>
      ) : (
        <>
          <div className="grid cols-4 mb-lg">
            <Stat icon={I.arrowDown} label="เงินเข้าตาม statement" value={`฿${fmt0(sum.inTotal)}`} sub={`${sum.inCount} รายการ`}/>
            <Stat icon={I.arrowUp} label="เงินออกตาม statement" value={`฿${fmt0(sum.outTotal)}`} sub={`${sum.outCount} รายการ`}/>
            <Stat icon={I.check} label="จับคู่เอกสารแล้ว" value={`${sum.matchedCount}/${lines.length}`} sub={`฿${fmt0(sum.matchedAmount)}${sum.ignoredCount ? ` · ข้าม ${sum.ignoredCount}` : ""}`}/>
            <Stat icon={I.calc} label="ยังไม่จับคู่" value={`${sum.pendingCount} รายการ`} sub={`฿${fmt0(sum.pendingAmount)} · ${lines.length ? Math.round((sum.matchedCount + sum.ignoredCount) / lines.length * 100) : 0}% เสร็จ`}/>
          </div>

          <Card flush title={`รายการเดินบัญชี · ${acc.emoji || ""} ${acc.name} · ${brMonthLabel(ym)}`}
            sub={period.fileName ? `นำเข้าจาก ${period.fileName}${period.importedBy ? ` โดย ${period.importedBy}` : ""}` : "รายการที่นำเข้าไว้"}
            right={<Tabs active={tab} onChange={setTab} items={[
              { key: "pending", label: `ยังไม่จับคู่ (${sum.pendingCount})` },
              { key: "matched", label: `จับคู่แล้ว (${sum.matchedCount})` },
              { key: "ignored", label: `ไม่จับคู่ (${sum.ignoredCount})` },
              { key: "all", label: `ทั้งหมด (${lines.length})` },
            ]}/>}>
            {/* แถบจัดการรายการที่เลือก */}
            {!closed && (
              <div style={{display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", padding: "8px 14px", borderBottom: "1px solid var(--line)",
                background: selCount ? "rgba(180,83,9,0.08)" : "var(--surface-2)"}}>
                {selCount > 0 ? (
                  <>
                    <b style={{fontSize: 12.5}}>เลือกไว้ {selCount} รายการ · ฿{fmt0(selAmount)}</b>
                    <Btn size="sm" kind="ghost" onClick={() => setSel(new Set())}>ยกเลิกการเลือก</Btn>
                    <span style={{marginLeft: "auto", display: "flex", gap: 6, flexWrap: "wrap"}}>
                      <Btn size="sm" kind="ghost" onClick={bulkIgnore}>⊘ ไม่ต้องจับคู่</Btn>
                      <Btn size="sm" kind="ghost" onClick={bulkReset}>↺ ล้างสถานะ</Btn>
                      <Btn size="sm" kind="danger" icon={I.trash} onClick={bulkDelete}>ลบที่เลือก ({selCount})</Btn>
                    </span>
                  </>
                ) : (
                  <>
                    <span style={{fontSize: 12, color: "var(--ink-3)"}}>ติ๊กช่องหน้ารายการเพื่อลบ/จัดการหลายรายการพร้อมกัน</span>
                    <span style={{marginLeft: "auto"}}>
                      <Btn size="sm" kind="ghost" icon={I.trash} onClick={deleteAllLines}>ลบทุกรายการในงวดนี้ ({lines.length})</Btn>
                    </span>
                  </>
                )}
              </div>
            )}
            <div className="table-wrap">
              <table className="t">
                <thead><tr>
                  <th style={{width: 34}}>
                    <input type="checkbox" title={allShownSel ? "ไม่เลือกทั้งหมด" : "เลือกทั้งหมดในหน้านี้"} checked={allShownSel}
                      onChange={e => toggleSelAll(shownIds, e.target.checked)} disabled={closed || !shownIds.length}/>
                  </th>
                  <th style={{width: 100}}>วันที่</th><th>รายการใน statement</th>
                  <th className="right" style={{width: 110}}>เงินเข้า</th><th className="right" style={{width: 110}}>เงินออก</th>
                  <th style={{width: 330}}>เอกสารที่จับคู่ / ระบบเสนอ</th><th style={{width: 130}}></th>
                </tr></thead>
                <tbody>
                  {shown.map(l => {
                    const m = matches[l.id], ig = ignored[l.id];
                    const mDocs = m ? (m.docs || []) : [];
                    const mTotal = mDocs.reduce((s, d) => s + (d.amount || 0), 0);
                    const gap = Math.abs(l.amount - mTotal) >= 0.5;
                    return (
                      <tr key={l.id} style={{background: sel.has(l.id) ? "rgba(180,83,9,0.10)" : ig ? "var(--surface-2)" : undefined}}>
                        <td><input type="checkbox" checked={sel.has(l.id)} onChange={() => toggleSel(l.id)} disabled={closed}/></td>
                        <td style={{fontSize: 12, whiteSpace: "nowrap"}}>{brThaiDate(l.date)}</td>
                        <td>
                          <div style={{fontSize: 12.5, fontWeight: 500}}>{l.desc}</div>
                          {l.note && <div style={{fontSize: 11, color: "var(--ink-3)"}}>📝 {l.note}</div>}
                          <div style={{fontSize: 10.5, color: "var(--ink-4)"}}>
                            {l.code ? <span className="mono">{l.code}</span> : null}
                            {l.code && l.bal != null ? " · " : ""}
                            {l.bal != null ? `คงเหลือ ฿${fmt0(l.bal)}` : ""}
                          </div>
                        </td>
                        <td className="right amount" style={{color: "var(--success)", fontWeight: 600}}>{l.dir === "in" ? `฿${fmt0(l.amount)}` : ""}</td>
                        <td className="right amount" style={{color: "var(--danger)", fontWeight: 600}}>{l.dir === "out" ? `฿${fmt0(l.amount)}` : ""}</td>
                        <td>
                          {m ? (
                            <div style={{display: "flex", flexWrap: "wrap", gap: 4, alignItems: "center"}}>
                              {mDocs.map(d => (
                                <button key={d.key} className="badge outline mono" style={{cursor: "pointer", fontSize: 10.5}}
                                  title={`${d.label} ฿${fmt0(d.amount)} — เปิดเอกสาร`}
                                  onClick={() => navigate((BR_SRC[d.list] || {}).go || "dashboard")}>{d.no}</button>
                              ))}
                              {m.auto && <Badge tone="info">อัตโนมัติ</Badge>}
                              {gap && <Badge tone="warning">ต่าง ฿{fmt0(Math.abs(l.amount - mTotal))}</Badge>}
                            </div>
                          ) : ig ? <span style={{fontSize: 11.5, color: "var(--ink-3)"}}>⊘ {ig.reason || "ไม่ต้องจับคู่"}</span>
                            : (() => {
                              // เสนอเอกสารที่ชื่อ/ยอดตรง ให้ตรวจแล้วกดยืนยันได้ทันทีจากตาราง
                              const ts = topSuggest[l.id];
                              if (!ts) return <span style={{fontSize: 11.5, color: "var(--warning)"}}>● รอจับคู่ — กด 📎 เพื่อค้นเอกสาร</span>;
                              const amtOk = Math.abs(ts.doc.amount - l.amount) < 0.5;
                              return (
                                <div style={{display: "flex", flexWrap: "wrap", gap: 4, alignItems: "center"}}>
                                  <span style={{fontSize: 10.5, color: "var(--ink-4)"}}>เสนอ</span>
                                  <button className="badge outline mono" style={{cursor: "pointer", fontSize: 10.5}} title={`${ts.doc.label} · ${ts.doc.party} · ${brThaiDate(ts.doc.date)} — เปิดดู/เลือกใบอื่น`}
                                    onClick={() => setMatching(l)}>{ts.doc.no}</button>
                                  <span style={{fontSize: 10.5, color: amtOk ? "var(--success)" : "var(--warning)"}}>฿{fmt0(ts.doc.amount)}</span>
                                  {ts.why.slice(0, 2).map((w, i) => <Badge key={i} tone={w === "ชื่อตรง" || w === "ยอดตรงพอดี" ? "success" : "outline"}>{w}</Badge>)}
                                  {!closed && <button className="badge" style={{cursor: "pointer", fontSize: 10.5, background: "var(--success)", color: "#fff", border: "none"}}
                                    title="ยืนยันจับคู่กับใบนี้" onClick={() => saveMatch(l, [ts.doc])}>✓ ใช้ใบนี้</button>}
                                </div>
                              );
                            })()}
                        </td>
                        <td>
                          <div style={{display: "flex", gap: 3, justifyContent: "flex-end"}}>
                            {!closed && !ig && <button className="icon-btn" title={m ? "แก้ไขการจับคู่ (เลือกใหม่/เอาออก)" : "จับคู่เอกสาร"} onClick={() => setMatching(l)}
                              style={m ? {color: "var(--primary, #0f766e)"} : undefined}>{m ? <I.edit size={13}/> : <I.paperclip size={13}/>}</button>}
                            {!closed && m && <button className="icon-btn" title="ยกเลิกการจับคู่" onClick={() => unmatch(l)}><I.close size={13}/></button>}
                            {!closed && !m && !ig && <button className="icon-btn" title="ไม่ต้องจับคู่ (ค่าธรรมเนียม/ดอกเบี้ย/โอนภายใน)" onClick={() => markIgnore(l, "ค่าธรรมเนียม/ดอกเบี้ย/โอนภายใน")}>⊘</button>}
                            {!closed && !m && !ig && <button className="icon-btn" title="บันทึกเข้ารายรับ-รายจ่าย" onClick={() => toCashbook(l)}><I.money size={13}/></button>}
                            {!closed && ig && <button className="icon-btn" title="กลับมาจับคู่" onClick={() => unIgnore(l)} style={{fontSize: 13}}>↺</button>}
                            {!closed && <button className="icon-btn" title="ลบรายการนี้" onClick={() => delLine(l)}><I.trash size={13}/></button>}
                          </div>
                        </td>
                      </tr>
                    );
                  })}
                  {shown.length === 0 && <tr><td colSpan={7} style={{textAlign: "center", padding: 22, color: "var(--ink-3)", fontSize: 12.5}}>ไม่มีรายการในหมวดนี้</td></tr>}
                </tbody>
              </table>
            </div>
          </Card>

          {/* เอกสารในระบบที่ยังไม่พบใน statement */}
          <Card className="mt-lg" flush title={`เอกสารเดือนนี้ที่ยังไม่พบใน statement (${docsUnmatched.length} ใบ)`}
            sub="เอกสารที่ออกแล้วแต่ยังไม่เห็นเงินเข้า-ออกในบัญชี — ใช้ตามเก็บเงิน/ตรวจสอบการจ่าย">
            <div className="table-wrap" style={{maxHeight: 380, overflow: "auto"}}>
              <table className="t">
                <thead><tr><th>ประเภท</th><th>เลขที่</th><th>วันที่</th><th>คู่ค้า</th><th>สถานะ</th><th className="right">จำนวนเงิน</th></tr></thead>
                <tbody>
                  {docsUnmatched.sort((a, b) => String(a.date).localeCompare(String(b.date))).map(d => (
                    <tr key={d.key}>
                      <td><Badge tone={d.side === "in" ? "success" : "warning"}>{d.label}</Badge></td>
                      <td className="mono" style={{fontSize: 11.5}}>
                        <button className="badge outline mono" style={{cursor: "pointer"}} onClick={() => navigate(d.go)}>{d.no}</button>
                      </td>
                      <td style={{fontSize: 12, whiteSpace: "nowrap"}}>{brThaiDate(d.date)}</td>
                      <td style={{fontSize: 12}}>{d.party || "—"}</td>
                      <td style={{fontSize: 11.5, color: "var(--ink-3)"}}>{d.status || "—"}</td>
                      <td className="right amount" style={{fontWeight: 600}}>฿{fmt0(d.amount)}</td>
                    </tr>
                  ))}
                  {docsUnmatched.length === 0 && <tr><td colSpan={6} style={{textAlign: "center", padding: 20, color: "var(--success)", fontSize: 12.5}}>✓ เอกสารทุกใบของเดือนนี้จับคู่กับ statement ครบแล้ว</td></tr>}
                </tbody>
              </table>
            </div>
          </Card>
        </>
      )}

      {importing && <BRImportDrawer acc={acc} ym={ym} existingLines={period.lines || []} onClose={() => setImporting(false)} onDone={onImported}/>}
      {matching && <BRMatchDrawer line={matching} docs={docs} usedKeys={usedKeys} current={matches[matching.id]}
        onClose={() => setMatching(null)} onSave={(sel) => saveMatch(matching, sel)}/>}
    </>
  );
}

window.BankReconPage = BankReconPage;
window.BankReconStore = { loadAll: brLoadAll, get: brGet, set: brSet, pid: brPid, parseText: brParseStatementText };
