/**
 * STONK CRM GAS v2 — batch snapshot + optimistic revision. (v5.6.74)
 * Dùng với "Sheet mẫu" (bound script): KHÔNG cần Script Properties.
 *  - Mở Sheet → tự tạo tab STONK_CONFIG + sinh token vào ô B1.
 *  - Menu "STONK CRM → Xem token" để copy token dán vào extension.
 *  - MỖI SHOP 1 TAB riêng theo TÊN SHOP (giống Sheet chung cũ) → 1 file GAS dùng được nhiều shop.
 *  - Cột tiếng Việt dễ đọc (Mã KOC, Handle, Tên, Ngày xin, Follower, GMV, Sản phẩm, Trạng thái...);
 *    các cột kỹ thuật (recordKey, reviewJson...) nằm CUỐI và được ẨN sẵn.
 * (Nâng cao) Script rời: đặt Script Property STONK_SPREADSHEET_ID và STONK_TOKEN.
 */
var STONK_GAS_SCHEMA_VERSION = 3;
var CONFIG_SHEET = "STONK_CONFIG";
// Cột hiển thị (tiếng Việt) + cột kỹ thuật ở cuối (ẨN). CHỈ THÊM cột mới vào CUỐI, không chen giữa.
var RECORD_HEADERS = [
  "Mã KOC",        // 0
  "Handle",        // 1
  "Tên KOC",       // 2
  "Ngày xin mẫu",  // 3
  "Follower",      // 4
  "GMV",           // 5
  "Sản phẩm",      // 6
  "SKU",           // 7
  "Hình thức",     // 8
  "Trạng thái",    // 9
  "Người duyệt",   // 10
  "Ghi chú",       // 11
  "Cập nhật",      // 12 (lần bản ghi thay đổi gần nhất — duyệt/sửa)
  "Đồng bộ lúc",   // 13 (lần extension đồng bộ snapshot gần nhất)
  "recordKey",     // 14 (kỹ thuật — ẩn)
  "applyId",       // 15
  "productId",     // 16
  "skuId",         // 17
  "snapshotId",    // 18
  "revision",      // 19
  "reviewJson",    // 20
  "capturedAt"     // 21
];
var COL = { KOC_ID: 0, HANDLE: 1, NAME: 2, NGAY_XIN: 3, FOLLOWER: 4, GMV: 5, SAN_PHAM: 6, SKU: 7, HINH_THUC: 8, TRANG_THAI: 9, NGUOI_DUYET: 10, GHI_CHU: 11, CAP_NHAT: 12, DONG_BO: 13, RECORD_KEY: 14, APPLY_ID: 15, PRODUCT_ID: 16, SKU_ID: 17, SNAPSHOT_ID: 18, REVISION: 19, REVIEW_JSON: 20, CAPTURED_AT: 21 };
var HIDDEN_FROM_COL = COL.RECORD_KEY + 1; // cột 14 (1-based) trở đi là kỹ thuật → ẩn
var STATUS_VI = {
  pending_review: "Chờ duyệt", approved: "Đã duyệt", ready_to_ship: "Sẵn sàng gửi",
  shipped: "Đã gửi hàng", in_progress: "Đang tiến hành", completed: "Hoàn tất",
  rejected: "Từ chối", cancelled: "Đã hủy", expired: "Hết hạn"
};

function jsonResponse_(value) {
  return ContentService.createTextOutput(JSON.stringify(value)).setMimeType(ContentService.MimeType.JSON);
}

function requestBody_(e) {
  try { return JSON.parse((e && e.postData && e.postData.contents) || "{}"); }
  catch (error) { throw new Error("Body JSON không hợp lệ"); }
}

function spreadsheet_() {
  var id = PropertiesService.getScriptProperties().getProperty("STONK_SPREADSHEET_ID");
  if (id) return SpreadsheetApp.openById(id);
  var active = SpreadsheetApp.getActiveSpreadsheet();
  if (active) return active; // Script GẮN LIỀN Sheet (bound) → tự nhận, khỏi STONK_SPREADSHEET_ID.
  throw new Error("Script chưa gắn Sheet: hãy dùng bản 'Tạo bản sao' Sheet mẫu, hoặc đặt STONK_SPREADSHEET_ID.");
}

function tokens_() {
  try { return JSON.parse(PropertiesService.getScriptProperties().getProperty("STONK_SHOP_TOKENS_JSON") || "{}"); }
  catch (error) { throw new Error("STONK_SHOP_TOKENS_JSON không hợp lệ"); }
}

// ── TOKEN TRONG SHEET (đơn giản nhất, khỏi Script Properties) ────────────────
function genToken_() {
  return (Utilities.getUuid() + Utilities.getUuid()).replace(/-/g, "").slice(0, 40);
}
function ensureConfig_() {
  var book = SpreadsheetApp.getActiveSpreadsheet();
  if (!book) return null;
  var sh = book.getSheetByName(CONFIG_SHEET) || book.insertSheet(CONFIG_SHEET);
  if (String(sh.getRange("A1").getValue()) !== "STONK_TOKEN") {
    sh.getRange("A1").setValue("STONK_TOKEN");
    sh.getRange("A2").setValue("Token bí mật của Sheet này — copy ô B1 dán vào extension, ĐỪNG chia sẻ.");
  }
  if (!String(sh.getRange("B1").getValue())) sh.getRange("B1").setValue(genToken_());
  return sh;
}
function configToken_() {
  var book = SpreadsheetApp.getActiveSpreadsheet();
  if (!book) return "";
  var sh = book.getSheetByName(CONFIG_SHEET);
  return sh ? String(sh.getRange("B1").getValue() || "") : "";
}
function onOpen() {
  try { ensureConfig_(); } catch (error) {}
  try {
    SpreadsheetApp.getUi().createMenu("STONK CRM")
      .addItem("Xem token (copy vào extension)", "showToken_")
      .addItem("Tạo lại token mới", "resetToken_")
      .addToUi();
  } catch (error) {}
}
function showToken_() {
  ensureConfig_();
  SpreadsheetApp.getUi().alert("Token của Sheet này (copy dán vào extension → ô Token riêng của shop):\n\n" + configToken_());
}
function resetToken_() {
  var sh = ensureConfig_();
  if (sh) sh.getRange("B1").setValue(genToken_());
  SpreadsheetApp.getUi().alert("Đã tạo token mới:\n\n" + configToken_() + "\n\nNhớ dán lại vào extension rồi Kiểm tra & lưu.");
}

function authorize_(body) {
  var shopId = String(body.shopId || "");
  var supplied = String(body.token || "");
  // Ưu tiên TOKEN trong Sheet (STONK_CONFIG!B1). Fallback Script Property / map nhiều shop (nâng cao).
  var expected = configToken_()
    || String(PropertiesService.getScriptProperties().getProperty("STONK_TOKEN") || "")
    || String(tokens_()[shopId] || "");
  if (!supplied || !expected || supplied !== expected) throw new Error("Token không đúng. Mở Sheet → menu STONK CRM → Xem token để lấy đúng giá trị.");
  return shopId || "default";
}

// ── TÊN TAB THEO SHOP (giống Sheet chung cũ) ─────────────────────────────────
function sanitizeSheetName_(name) {
  return String(name || "").replace(/[\[\]\*\?:\/\\']/g, " ").replace(/\s+/g, " ").trim().slice(0, 80);
}
function shopSheetKey_(shopId) { return "STONK_SHEET_NAME_" + shopId; }
function shopSheetName_(shopId, shopName) {
  var props = PropertiesService.getScriptProperties();
  var clean = sanitizeSheetName_(shopName);
  if (clean) { props.setProperty(shopSheetKey_(shopId), clean); return clean; }
  return String(props.getProperty(shopSheetKey_(shopId)) || "") || ("SHOP " + shopId);
}

function sheet_(name, headers) {
  var book = spreadsheet_();
  var sheet = book.getSheetByName(name) || book.insertSheet(name);
  if (sheet.getLastRow() === 0) {
    sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
    try {
      sheet.setFrozenRows(1);
      sheet.getRange(1, 1, 1, headers.length).setFontWeight("bold");
      sheet.hideColumns(HIDDEN_FROM_COL, headers.length - HIDDEN_FROM_COL + 1); // ẩn cột kỹ thuật
    } catch (error) {}
  }
  return sheet;
}
function dataSheet_(shopId, shopName) { return sheet_(shopSheetName_(shopId, shopName), RECORD_HEADERS); }
function stagingSheet_() { return sheet_("_STONK_CRM_STAGING", ["shopId", "snapshotId"].concat(RECORD_HEADERS)); }

function stableKey_(shopId, record) {
  var applyId = String(record.applyId || "");
  var productId = String(record.productId || "");
  var skuId = String(record.skuId || "");
  if (!applyId || !productId) throw new Error("Thiếu applyId hoặc productId");
  return [shopId, applyId, productId, skuId].join("|");
}

function statusVi_(status) { return STATUS_VI[String(status || "")] || String(status || ""); }
function viDate_(iso) {
  if (!iso) return "";
  var d = new Date(iso);
  if (isNaN(d.getTime())) return String(iso);
  return Utilities.formatDate(d, "GMT+7", "dd/MM/yyyy HH:mm");
}

// Dựng 1 hàng dữ liệu từ record extension gửi lên (record.readable = field dễ đọc).
function recordRow_(shopId, snapshotId, record, now) {
  var r = record.readable || {};
  var review = record.review || {};
  return [
    String(r.kocId || ""),
    r.handle ? "@" + String(r.handle).replace(/^@/, "") : "",
    String(r.kocName || ""),
    viDate_(r.ngayXin || record.capturedAt),
    Number(r.follower || 0),
    // GMV có thể là SỐ hoặc CHUỖI ("₫1,2M") — chỉ ép số khi hợp lệ, không để NaN lọt vào Sheet
    (function(v) { var n = Number(v); return v === "" || v == null ? "" : (isFinite(n) ? n : String(v)); })(r.gmv),
    String(r.sanPham || ""),
    String(r.sku || ""),
    String(r.hinhThuc || ""),
    statusVi_(review.status || r.trangThai),
    String(review.reviewerName || r.nguoiDuyet || ""),
    String(review.note || r.ghiChu || ""),
    viDate_(now),
    viDate_(now),
    stableKey_(shopId, record),
    String(record.applyId || ""),
    String(record.productId || ""),
    String(record.skuId || ""),
    snapshotId,
    Number(record.revision || 0),
    JSON.stringify(record.review || {}),
    String(record.capturedAt || "")
  ];
}

function revisionKey_(shopId) { return "STONK_CRM_REVISION_" + shopId; }
function currentRevision_(shopId) { return Number(PropertiesService.getScriptProperties().getProperty(revisionKey_(shopId)) || 0); }
function sessionKey_(shopId, snapshotId) { return "STONK_CRM_SESSION_" + shopId + "_" + snapshotId; }

function capabilities_() {
  return {
    ok: true,
    capabilities: {
      crmSync: "batch-v2", schemaVersion: STONK_GAS_SCHEMA_VERSION,
      actions: ["begin", "upsertBatch", "commit", "getRecord", "patchReview"],
      stableKey: "shopId+applyId+productId+skuId", optimisticRevision: true,
      perShopSheet: true, maxBatchSize: 500
    }
  };
}

function doGet(e) {
  try {
    if (String((e && e.parameter && e.parameter.action) || "capabilities") === "capabilities") return jsonResponse_(capabilities_());
    return jsonResponse_({ ok: false, error: "Action GET không hỗ trợ" });
  } catch (error) { return jsonResponse_({ ok: false, error: String(error.message || error) }); }
}

function doPost(e) {
  try {
    var body = requestBody_(e);
    if (body.action === "capabilities") return jsonResponse_(capabilities_());
    var shopId = authorize_(body);
    var lock = LockService.getScriptLock();
    lock.waitLock(25000);
    try {
      if (body.action === "begin") return jsonResponse_(begin_(shopId, body));
      if (body.action === "upsertBatch") return jsonResponse_(upsertBatch_(shopId, body));
      if (body.action === "commit") return jsonResponse_(commit_(shopId, body));
      if (body.action === "getRecord") return jsonResponse_(getRecord_(shopId, body));
      if (body.action === "patchReview") return jsonResponse_(patchReview_(shopId, body));
      return jsonResponse_({ ok: false, error: "Action không hỗ trợ" });
    } finally { lock.releaseLock(); }
  } catch (error) { return jsonResponse_({ ok: false, error: String(error.message || error) }); }
}

function begin_(shopId, body) {
  var snapshotId = String(body.snapshotId || "");
  if (!snapshotId) throw new Error("Thiếu snapshotId");
  if (body.shopName) shopSheetName_(shopId, body.shopName); // nhớ tên tab của shop
  var revision = currentRevision_(shopId);
  var baseRevision = Number(body.baseRevision || 0);
  if (baseRevision !== revision) return { ok: false, conflict: true, currentRevision: revision, error: "Dữ liệu GAS đã thay đổi trên máy khác" };
  var session = { shopId: shopId, snapshotId: snapshotId, expectedTotal: Number(body.expectedTotal || 0), baseRevision: revision, batches: {}, startedAt: new Date().toISOString() };
  PropertiesService.getScriptProperties().setProperty(sessionKey_(shopId, snapshotId), JSON.stringify(session));
  return { ok: true, snapshotId: snapshotId, revision: revision, maxBatchSize: 500 };
}

function upsertBatch_(shopId, body) {
  var snapshotId = String(body.snapshotId || "");
  var propertyKey = sessionKey_(shopId, snapshotId);
  var rawSession = PropertiesService.getScriptProperties().getProperty(propertyKey);
  if (!rawSession) throw new Error("Phiên đồng bộ không tồn tại hoặc đã hết hạn");
  var session = JSON.parse(rawSession);
  if (currentRevision_(shopId) !== Number(session.baseRevision || 0)) return { ok: false, conflict: true, currentRevision: currentRevision_(shopId), error: "Revision đã thay đổi" };
  var records = Array.isArray(body.records) ? body.records.slice(0, 500) : [];
  var batchId = String(body.batchId || "");
  if (!batchId) throw new Error("Thiếu batchId");
  if (session.batches[batchId] !== undefined) return { ok: true, duplicate: true, accepted: session.batches[batchId] };
  var staging = stagingSheet_();
  var now = new Date().toISOString();
  var rows = records.map(function(record) {
    return [shopId, snapshotId].concat(recordRow_(shopId, snapshotId, record, now));
  });
  if (rows.length) staging.getRange(staging.getLastRow() + 1, 1, rows.length, rows[0].length).setValues(rows);
  session.batches[batchId] = rows.length;
  PropertiesService.getScriptProperties().setProperty(propertyKey, JSON.stringify(session));
  return { ok: true, accepted: rows.length, batchId: batchId };
}

function commit_(shopId, body) {
  var snapshotId = String(body.snapshotId || "");
  var propertyKey = sessionKey_(shopId, snapshotId);
  var rawSession = PropertiesService.getScriptProperties().getProperty(propertyKey);
  if (!rawSession) throw new Error("Phiên đồng bộ không tồn tại");
  var session = JSON.parse(rawSession);
  var stagedCount = Object.keys(session.batches || {}).reduce(function(sum, key) { return sum + Number(session.batches[key] || 0); }, 0);
  if (Number(session.expectedTotal || 0) && stagedCount !== Number(session.expectedTotal)) throw new Error("Snapshot chưa đủ: " + stagedCount + "/" + session.expectedTotal);
  if (currentRevision_(shopId) !== Number(session.baseRevision || 0)) return { ok: false, conflict: true, currentRevision: currentRevision_(shopId), error: "Revision đã thay đổi" };
  var staging = stagingSheet_();
  var main = dataSheet_(shopId, body.shopName);
  var stagingWidth = RECORD_HEADERS.length + 2; // shopId + snapshotId + dữ liệu
  var stagingRows = staging.getLastRow() > 1 ? staging.getRange(2, 1, staging.getLastRow() - 1, stagingWidth).getValues() : [];
  var staged = stagingRows
    .filter(function(row) { return String(row[0]) === shopId && String(row[1]) === snapshotId; })
    .map(function(row) { return row.slice(2); });
  var current = main.getLastRow() > 1 ? main.getRange(2, 1, main.getLastRow() - 1, RECORD_HEADERS.length).getValues() : [];
  var previousByKey = {};
  current.forEach(function(row) { previousByKey[String(row[COL.RECORD_KEY])] = row; });
  var byKey = {};
  staged.forEach(function(row) {
    var previous = previousByKey[String(row[COL.RECORD_KEY])];
    // Giữ phần DUYỆT cũ (người duyệt/ghi chú/reviewJson/revision) nếu snapshot mới chưa mang review
    if (previous && String(row[COL.REVIEW_JSON] || "{}") === "{}" && String(previous[COL.REVIEW_JSON] || "{}") !== "{}") {
      row[COL.REVIEW_JSON] = previous[COL.REVIEW_JSON];
      row[COL.NGUOI_DUYET] = previous[COL.NGUOI_DUYET];
      row[COL.GHI_CHU] = previous[COL.GHI_CHU];
      row[COL.REVISION] = previous[COL.REVISION];
      try {
        var prevReview = JSON.parse(String(previous[COL.REVIEW_JSON] || "{}"));
        if (prevReview.status) row[COL.TRANG_THAI] = statusVi_(prevReview.status);
      } catch (error) {}
    }
    byKey[String(row[COL.RECORD_KEY])] = row;
  });
  var merged = Object.keys(byKey).map(function(key) { return byKey[key]; });
  if (main.getLastRow() > 1) main.getRange(2, 1, main.getLastRow() - 1, RECORD_HEADERS.length).clearContent();
  if (merged.length) main.getRange(2, 1, merged.length, RECORD_HEADERS.length).setValues(merged);
  var keepStaging = stagingRows.filter(function(row) { return String(row[0]) !== shopId || String(row[1]) !== snapshotId; });
  if (staging.getLastRow() > 1) staging.getRange(2, 1, staging.getLastRow() - 1, stagingWidth).clearContent();
  if (keepStaging.length) staging.getRange(2, 1, keepStaging.length, stagingWidth).setValues(keepStaging);
  var nextRevision = Number(session.baseRevision || 0) + 1;
  PropertiesService.getScriptProperties().setProperty(revisionKey_(shopId), String(nextRevision));
  PropertiesService.getScriptProperties().deleteProperty(propertyKey);
  return { ok: true, committed: staged.length, revision: nextRevision, snapshotId: snapshotId };
}

function findRecordRow_(shopId, recordKey, shopName) {
  var sheet = dataSheet_(shopId, shopName);
  if (sheet.getLastRow() <= 1) return { sheet: sheet, row: 0, values: null };
  var values = sheet.getRange(2, 1, sheet.getLastRow() - 1, RECORD_HEADERS.length).getValues();
  for (var index = 0; index < values.length; index += 1) if (String(values[index][COL.RECORD_KEY]) === recordKey) return { sheet: sheet, row: index + 2, values: values[index] };
  return { sheet: sheet, row: 0, values: null };
}

function getRecord_(shopId, body) {
  var recordKey = stableKey_(shopId, body);
  var found = findRecordRow_(shopId, recordKey, body.shopName);
  return { ok: true, found: !!found.values, recordKey: recordKey, revision: found.values ? Number(found.values[COL.REVISION] || 0) : 0, review: found.values ? JSON.parse(String(found.values[COL.REVIEW_JSON] || "{}")) : {} };
}

function patchReview_(shopId, body) {
  var recordKey = stableKey_(shopId, body);
  var found = findRecordRow_(shopId, recordKey, body.shopName);
  if (!found.values) throw new Error("Không tìm thấy bản ghi cần duyệt");
  var currentRevision = Number(found.values[COL.REVISION] || 0);
  if (Number(body.revision || 0) !== currentRevision) return { ok: false, conflict: true, currentRevision: currentRevision, review: JSON.parse(String(found.values[COL.REVIEW_JSON] || "{}")), error: "Bản ghi đã được máy khác cập nhật" };
  var nextRevision = currentRevision + 1;
  var review = body.review || {};
  var now = new Date().toISOString();
  // Cập nhật cả cột dễ đọc lẫn cột kỹ thuật cho hàng này
  if (review.status) found.sheet.getRange(found.row, COL.TRANG_THAI + 1).setValue(statusVi_(review.status));
  found.sheet.getRange(found.row, COL.NGUOI_DUYET + 1, 1, 2).setValues([[String(review.reviewerName || ""), String(review.note || "")]]);
  found.sheet.getRange(found.row, COL.CAP_NHAT + 1).setValue(viDate_(now));
  found.sheet.getRange(found.row, COL.REVISION + 1, 1, 2).setValues([[nextRevision, JSON.stringify(review)]]);
  return { ok: true, recordKey: recordKey, revision: nextRevision, review: review };
}
