// === API clients ===
// SECURITY: credentials live in localStorage only. Never logged. Never sent anywhere except the configured endpoints.

const ISVM_BASE = "https://web-production-9e9d.up.railway.app";
const NAVER_API_BASE = "https://api.commerce.naver.com";

// --- Credential storage (localStorage) ---
const CRED_KEY = "smartfarm.creds.v1";
function loadCreds() {
  try { return JSON.parse(localStorage.getItem(CRED_KEY) || "{}"); }
  catch { return {}; }
}
function saveCreds(c) {
  localStorage.setItem(CRED_KEY, JSON.stringify(c));
}
function clearCreds() {
  localStorage.removeItem(CRED_KEY);
}

// --- ISVM client ---
async function isvmHealth() {
  try {
    const res = await fetch(`${ISVM_BASE}/health`, { method: "GET" });
    return res.ok;
  } catch { return false; }
}

async function isvmSearch({ keyword, limit = 20, sort = "sales" }) {
  const res = await fetch(`${ISVM_BASE}/api/search`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ keyword, limit, sort }),
  });
  if (!res.ok) throw new Error(`ISVM search failed: HTTP ${res.status}`);
  const json = await res.json();
  if (!json.success) throw new Error(json.error || "ISVM 검색 실패");
  return json.data || [];
}

// ISVM 상품의 모든 이미지 URL 조회 (메인 + 상세 + 추가)
// 우선 우리 프록시의 직접 스크래이프 라우트 사용 (Railway crawler 회귀 대응).
// 실패 시 Railway crawler API로 fallback.
async function isvmFetchProductImages(productCode) {
  const proxyBase = _proxyBase();
  // 1차: 프록시 직접 스크래이프 (모든 productsNew/ 이미지 포함)
  if (proxyBase) {
    try {
      const r = await fetch(`${proxyBase}/isvm/extract-images/${encodeURIComponent(productCode)}`);
      if (r.ok) {
        const json = await r.json();
        if (json.success && Array.isArray(json.data) && json.data.length > 0) {
          return json.data;
        }
      }
    } catch (e) {
      console.warn("[isvm proxy scrape 실패, Railway crawler로 fallback]", e.message);
    }
  }
  // 2차 fallback: Railway crawler (상품코드 파일명 매칭만 — 누락 발생 가능)
  const res = await fetch(`${ISVM_BASE}/api/product-images`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ product_code: productCode }),
  });
  if (!res.ok) throw new Error(`ISVM 다중 이미지 조회 실패: HTTP ${res.status}`);
  const json = await res.json();
  if (!json.success) throw new Error(json.error || "ISVM 다중 이미지 조회 실패");
  return json.data || [];
}

// Add products with batching (10 at a time per spec)
async function isvmAddProducts(codes, onProgress) {
  const BATCH = 10;
  const merged = [];
  let totalSuccess = 0;
  for (let i = 0; i < codes.length; i += BATCH) {
    const batch = codes.slice(i, i + BATCH);
    const res = await fetch(`${ISVM_BASE}/api/add-products`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ product_codes: batch, start_order: i + 1 }),
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const json = await res.json();
    const results = json.data?.results || [];
    merged.push(...results);
    totalSuccess += json.data?.success_count || 0;
    onProgress && onProgress({ done: Math.min(i + BATCH, codes.length), total: codes.length, results: merged });
  }
  return { success_count: totalSuccess, total_count: codes.length, results: merged };
}

// --- Naver Commerce API payload builder ---
// We CANNOT call Naver from the browser directly:
//  1) bcrypt signing requires the client_secret in plaintext (must stay on server)
//  2) Naver whitelists server IPs
//  3) CORS not allowed for browser origins
// So we build the payload here and the user posts via their own backend proxy.

function buildNaverTokenPayload(clientId, timestamp = Date.now()) {
  return {
    method: "POST",
    url: `${NAVER_API_BASE}/external/v1/oauth2/token`,
    contentType: "application/x-www-form-urlencoded",
    formData: {
      client_id: clientId,
      timestamp: String(timestamp),
      grant_type: "client_credentials",
      type: "SELF",
      // client_secret_sign generated server-side:
      // base64( bcrypt( client_id + "_" + timestamp, client_secret ) )
      client_secret_sign: "<server_side_bcrypt_signature>",
    },
    note: "client_secret_sign은 서버에서 bcrypt(`${client_id}_${timestamp}`, client_secret)을 base64 인코딩하여 생성해야 합니다.",
  };
}

// Map ISVM/local product → Naver Commerce product registration payload
function buildNaverProductPayload(product, options = {}) {
  const {
    leafCategoryId = "50000000", // user must override
    deliveryCompany = "CJGLS",
    baseFee = 3000,
    afterServicePhone = "1588-0000",
    afterServiceGuide = "구매 후 7일 이내 미사용 제품 교환/환불 가능",
    sellerOriginCode = "0200037",
  } = options;

  return {
    originProduct: {
      statusType: "SALE",
      saleType: "NEW",
      leafCategoryId,
      name: product.name,
      detailContent: `<div>${(product.desc || product.name).replace(/[<>]/g, "")}</div>`,
      images: {
        representativeImage: { url: product.image_url || "" },
        optionalImages: product.images || [],
      },
      salePrice: product.salePrice ?? product.suggestedPrice ?? product.price,
      stockQuantity: product.stock || 0,
      deliveryInfo: {
        deliveryType: "DELIVERY",
        deliveryAttributeType: "NORMAL",
        deliveryCompany,
        deliveryBundleGroupUsable: false,
        deliveryFee: {
          deliveryFeeType: "PAID",
          baseFee,
          deliveryFeePayType: "COLLECT_OR_PREPAID",
          deliveryAreaType: "AREA_2",
          area2extraFee: 5000,
          area3extraFee: 10000,
        },
      },
      detailAttribute: {
        afterServiceInfo: {
          afterServiceTelephoneNumber: afterServicePhone,
          afterServiceGuideContent: afterServiceGuide,
        },
        originAreaInfo: { originAreaCode: sellerOriginCode, importer: "", content: "", plural: false },
        sellerCodeInfo: {
          sellerManagementCode: product.product_code || product.sku || "",
          sellerBarcode: "",
        },
        purchaseQuantityInfo: {
          minPurchaseQuantity: 1,
          maxPurchaseQuantityPerId: 0,
          maxPurchaseQuantityPerOrder: 0,
        },
        naverShoppingSearchInfo: {
          // modelName 자동 입력 안 함 — 항상 틀린 값이 들어가서 차라리 빈 값
          modelName: "",
          brandName: product.brand || "",
          manufacturerName: product.brand || "",
        },
      },
      customerBenefit: {},
    },
    smartstoreChannelProduct: {
      channelProductName: product.name,
      naverShoppingRegistration: true,
      channelProductDisplayStatusType: "ON",
    },
  };
}

// --- Naver Commerce client (백엔드 프록시 경유) ---
function _proxyBase() {
  const c = loadCreds();
  return (c.proxy_url || "").replace(/\/+$/, "");
}
function _proxyHeaders() {
  const c = loadCreds();
  return {
    "X-Client-Id": c.naver_client_id || "",
    "X-Client-Secret": c.naver_client_secret || "",
  };
}
function naverConfigured() {
  const c = loadCreds();
  return Boolean(c.proxy_url && c.naver_client_id && c.naver_client_secret);
}

// 네이버 channelProduct 1개 → 내부 product 모델
function naverToInternalProduct(cp) {
  const isOutOfStock = cp.statusType === "OUTOFSTOCK" || (cp.stockQuantity || 0) === 0;
  const rootCategory = (cp.wholeCategoryName || "").split(">")[0] || "기타";
  return {
    id: String(cp.originProductNo),
    originProductNo: cp.originProductNo,
    channelProductNo: cp.channelProductNo,
    name: cp.name || "",
    category: rootCategory,
    categoryFull: cp.wholeCategoryName || "",
    naverCategoryId: cp.categoryId || "",
    price: cp.salePrice || 0,
    discountedPrice: cp.discountedPrice || 0,
    stock: cp.stockQuantity || 0,
    status: isOutOfStock ? "outofstock" : "active",
    imageUrl: cp.representativeImage?.url || "",
    regDate: cp.regDate || "",
    modifiedDate: cp.modifiedDate || "",
    // 네이버 search 응답엔 없는 필드 (호환 위해 빈 값)
    costPrice: 0,
    sales30d: 0,
    views30d: 0,
    rating: 0,
    reviews: 0,
    sku: cp.channelProductNo ? String(cp.channelProductNo) : "",
    brand: "",
    desc: "",
    tone: (cp.originProductNo || 0) % 9,
  };
}

function _flattenNaverProducts(naverResponse) {
  const out = [];
  for (const item of naverResponse.contents || []) {
    for (const cp of item.channelProducts || []) {
      out.push(naverToInternalProduct(cp));
    }
  }
  return out;
}

async function naverFetchProducts({ page = 1, size = 100, searchKeyword, productStatusTypes } = {}) {
  if (!naverConfigured()) throw new Error("API 자격증명이 설정되지 않았습니다 (API 설정 메뉴에서 입력)");
  const params = new URLSearchParams({ page: String(page), size: String(size) });
  if (searchKeyword) params.set("searchKeyword", searchKeyword);
  if (productStatusTypes && productStatusTypes.length) params.set("productStatusTypes", productStatusTypes.join(","));

  // 캐시 무효화: 브라우저가 동일 URL을 캐시하지 않도록 timestamp 추가
  params.set("_ts", String(Date.now()));
  const url = `${_proxyBase()}/naver/products?${params.toString()}`;
  const res = await fetch(url, { headers: _proxyHeaders(), cache: "no-store" });
  if (!res.ok) {
    const errText = await res.text().catch(() => "");
    throw new Error(`네이버 상품 조회 실패 (HTTP ${res.status}) ${errText.slice(0, 200)}`);
  }
  const json = await res.json();
  return {
    products: _flattenNaverProducts(json),
    page: json.page,
    size: json.size,
    totalElements: json.totalElements,
    totalPages: json.totalPages,
    raw: json,
  };
}

// 원상품 전체 조회 (가격 수정 전 GET-then-PUT 패턴)
async function naverFetchOriginProduct(originProductNo) {
  if (!naverConfigured()) throw new Error("API 자격증명이 설정되지 않았습니다");
  const url = `${_proxyBase()}/naver/origin-products/${originProductNo}?_ts=${Date.now()}`;
  const res = await fetch(url, { headers: _proxyHeaders(), cache: "no-store" });
  if (!res.ok) {
    const errText = await res.text().catch(() => "");
    throw new Error(`원상품 조회 실패 (HTTP ${res.status}) ${errText.slice(0, 200)}`);
  }
  return res.json();
}

// 원상품 전체 수정 (GET 결과를 받아 salePrice 등만 수정한 객체 그대로 전달)
async function naverUpdateOriginProduct(originProductNo, fullPayload) {
  if (!naverConfigured()) throw new Error("API 자격증명이 설정되지 않았습니다");
  const c = loadCreds();
  const url = `${_proxyBase()}/naver/origin-products/${originProductNo}`;
  const res = await fetch(url, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      clientId: c.naver_client_id,
      clientSecret: c.naver_client_secret,
      payload: fullPayload,
    }),
  });
  const json = await res.json();
  if (!res.ok) {
    throw new Error(`원상품 수정 실패 (HTTP ${res.status}) ${JSON.stringify(json).slice(0, 300)}`);
  }
  return json;
}

// 가격만 변경하는 패턴: GET → salePrice 교체 → PUT
async function naverChangePrice(originProductNo, newSalePrice) {
  return naverChangeProductPricing(originProductNo, { salePrice: newSalePrice });
}

// 통합 상품 수정. changes의 키가 undefined면 변경 안 함.
//   changes = { salePrice?, immediateDiscount? (0=제거), stockQuantity?, name?, statusType? }
async function naverUpdateProduct(originProductNo, changes) {
  const current = await naverFetchOriginProduct(originProductNo);
  if (!current.originProduct) throw new Error("응답에 originProduct가 없음");
  const updated = JSON.parse(JSON.stringify(current));
  if (changes.salePrice !== undefined) updated.originProduct.salePrice = changes.salePrice;
  if (changes.stockQuantity !== undefined) updated.originProduct.stockQuantity = changes.stockQuantity;
  if (changes.name !== undefined) {
    updated.originProduct.name = changes.name;
    // 전용상품명 비활성화 — 채널 전용 이름이 채워져 있으면 originProduct.name 바꿔도 채널 표시는 그대로.
    // 비워두면 originProduct.name이 모든 채널에 sync됨. (사용자 발견 — 전용상품명 Y가 sync 안 되는 원인)
    if (updated.smartstoreChannelProduct) {
      updated.smartstoreChannelProduct.channelProductName = "";
    }
    if (updated.windowChannelProduct) {
      updated.windowChannelProduct.channelProductName = "";
    }
  }
  if (changes.statusType !== undefined) updated.originProduct.statusType = changes.statusType;
  if (changes.detailContent !== undefined) updated.originProduct.detailContent = changes.detailContent;
  if (changes.immediateDiscount !== undefined) {
    if (!updated.originProduct.customerBenefit) updated.originProduct.customerBenefit = {};
    if (changes.immediateDiscount > 0) {
      updated.originProduct.customerBenefit.immediateDiscountPolicy = {
        ...(updated.originProduct.customerBenefit.immediateDiscountPolicy || {}),
        discountMethod: { value: changes.immediateDiscount, unitType: "WON" },
      };
    } else {
      delete updated.originProduct.customerBenefit.immediateDiscountPolicy;
    }
  }
  const result = await naverUpdateOriginProduct(originProductNo, updated);
  return { changes, result };
}

// 가격/즉시할인만 변경하는 래퍼 (기존 호환)
async function naverChangeProductPricing(originProductNo, changes) {
  return naverUpdateProduct(originProductNo, changes);
}

// --- Naver Orders client ---
function _fmtKST(d) {
  const kst = new Date(d.getTime() + 9 * 3600 * 1000);
  return kst.toISOString().replace("Z", "+09:00").replace(/\.\d{3}/, ".000");
}

// 주문 상태 매핑 (네이버 → 내부)
function naverOrderStatusToInternal(productOrderStatus, claimStatus) {
  if (claimStatus === "CANCEL_DONE" || claimStatus === "CANCEL_REQUEST") return "canceled";
  if (claimStatus === "RETURN_DONE") return "returned";
  if (claimStatus === "EXCHANGE_DONE") return "exchanged";
  switch (productOrderStatus) {
    case "PAYMENT_WAITING": return "pending";
    case "PAYED": return "paid";
    case "DELIVERING": return "shipped";
    case "DELIVERED": return "delivered";
    case "PURCHASE_DECIDED": return "delivered";
    case "CANCELED": return "canceled";
    case "RETURNED": return "returned";
    case "EXCHANGED": return "exchanged";
    default: return (productOrderStatus || "unknown").toLowerCase();
  }
}

// 네이버 주문 entry 1개 → 내부 order 모델
function naverToInternalOrder(entry) {
  const c = entry.content || {};
  const order = c.order || {};
  const po = c.productOrder || {};
  const sa = po.shippingAddress || {};
  return {
    id: po.productOrderId || entry.productOrderId,
    productOrderId: po.productOrderId,
    orderId: order.orderId,
    date: (order.paymentDate || order.orderDate || "").replace("T", " ").slice(0, 16),
    buyer: order.ordererName || "",
    phone: order.ordererTel || "",
    address: ((sa.baseAddress || "") + " " + (sa.detailedAddress || "")).trim(),
    zipcode: sa.zipCode || "",
    items: [{
      pid: po.productId || "",
      qty: po.quantity || 0,
      name: po.productName || "",
      price: po.unitPrice || 0,
      option: po.productOption || po.optionName || "",
      optionCode: po.optionCode || "",
      optionPrice: po.optionPrice || 0,
      productClass: po.productClass || "",
    }],
    status: naverOrderStatusToInternal(po.productOrderStatus, po.claimStatus),
    memo: po.shippingMemo || "",
    rawProductOrderStatus: po.productOrderStatus || "",
    rawClaimStatus: po.claimStatus || "",
    deliveryCompany: po.expectedDeliveryCompany || "",
    shippingDueDate: po.shippingDueDate || "",
    totalPaymentAmount: po.totalPaymentAmount || 0,
    paymentMeans: order.paymentMeans || "",
    // 보내는분(판매자 발송지/반품교환지) 정보 — 로젠 송장 출력에 사용
    sender: po.takingAddress ? {
      name: po.takingAddress.name || "",
      tel: po.takingAddress.tel1 || "",
      zipcode: po.takingAddress.zipCode || "",
      address: ((po.takingAddress.baseAddress || "") + " " + (po.takingAddress.detailedAddress || "")).trim(),
    } : null,
  };
}

const _sleep = (ms) => new Promise(r => setTimeout(r, ms));

// 단일 24시간 윈도우 호출 (429 시 자동 재시도 backoff)
async function _fetchOrders24h(fromDate, toDate, attempt = 0) {
  const headers = _proxyHeaders();
  const path = "/external/v1/pay-order/seller/product-orders?from=" + encodeURIComponent(_fmtKST(fromDate)) + "&to=" + encodeURIComponent(_fmtKST(toDate));
  // 주의: _raw_get은 _path 안에 쿼리(from/to)를 인코딩해 담음. URL 레벨에 _ts 같은
  // 추가 쿼리 붙이면 서버가 그걸 네이버로 forward해서 "?from=...&to=...?_ts=..." 망가짐.
  // 캐시는 서버의 Cache-Control: no-store 헤더로 차단되므로 _ts 불필요.
  const url = _proxyBase() + "/naver/_raw_get?_path=" + encodeURIComponent(path);
  const res = await fetch(url, { headers, cache: "no-store" });
  if (res.status === 429 && attempt < 3) {
    await _sleep(1000 * Math.pow(2, attempt));
    return _fetchOrders24h(fromDate, toDate, attempt + 1);
  }
  if (!res.ok) {
    const t = await res.text().catch(() => "");
    throw new Error("주문 조회 실패 (HTTP " + res.status + ") " + t.slice(0, 200));
  }
  const json = await res.json();
  return json.data?.contents || [];
}

// 슬라이딩 윈도우로 N일 데이터 수집 (24h x N 호출). 호출 간 250ms sleep으로 rate limit 회피.
async function naverFetchOrders({ days = 7, onProgress } = {}) {
  if (!naverConfigured()) throw new Error("API 자격증명이 설정되지 않았습니다");
  const all = [];
  const now = new Date();
  for (let dayOffset = 0; dayOffset < days; dayOffset++) {
    if (dayOffset > 0) await _sleep(250);
    const to = new Date(now.getTime() - dayOffset * 24 * 3600 * 1000);
    const from = new Date(now.getTime() - (dayOffset + 1) * 24 * 3600 * 1000);
    try {
      const entries = await _fetchOrders24h(from, to);
      all.push(...entries.map(naverToInternalOrder));
    } catch (err) {
      console.warn("[fetchOrders day " + dayOffset + "]", err.message);
    }
    onProgress && onProgress({ done: dayOffset + 1, total: days });
  }
  all.sort((a, b) => (b.date || "").localeCompare(a.date || ""));
  return all;
}

// PC 파일 직접 업로드 (File 객체) → 네이버 URL 반환
async function naverUploadImageFile(file) {
  if (!naverConfigured()) throw new Error("API 자격증명이 설정되지 않았습니다");
  const c = loadCreds();
  const form = new FormData();
  form.append("image", file);
  const res = await fetch(_proxyBase() + "/naver/upload-image-file", {
    method: "POST",
    headers: {
      "X-Client-Id": c.naver_client_id,
      "X-Client-Secret": c.naver_client_secret,
    },
    body: form,
  });
  const json = await res.json();
  if (!res.ok) throw new Error("파일 업로드 실패 (HTTP " + res.status + ") " + JSON.stringify(json).slice(0, 300));
  const url = json.images?.[0]?.url || json.imageUrl || json.url;
  if (!url) throw new Error("이미지 응답에서 URL을 찾을 수 없음: " + JSON.stringify(json).slice(0, 200));
  return url;
}

// 네이버 SmartEditor 형식 HTML 생성 (이미지 컴포넌트들)
function buildSmartEditorImagesHtml(naverImageUrls) {
  if (!naverImageUrls || naverImageUrls.length === 0) return "";
  const blocks = naverImageUrls.map(url => ({ type: "image", url }));
  return buildSmartEditorHtml(blocks);
}

// HTML escape (텍스트 블록 직렬화용)
function _escHtml(s) {
  return String(s == null ? "" : s)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

// 블록 배열 → SmartEditor HTML.
//   blocks = [{ type:'image', url }, { type:'text', text }, { type:'html', html }]
//   이미지 URL은 호출 전에 모두 네이버 URL로 업로드 완료된 상태여야 함.
function buildSmartEditorHtml(blocks) {
  if (!blocks || blocks.length === 0) return "";
  const ts = Date.now();
  const parts = blocks.map((b, i) => {
    const id = "SE-" + ts + "-" + i;
    if (b.type === "image" && b.url) {
      return '<div class="se-component se-image se-l-default" id="' + id + '">' +
        '<div class="se-component-content se-component-content-fit">' +
        '<div class="se-section se-section-image se-l-default se-section-align-">' +
        '<div class="se-module se-module-image">' +
        '<a class="se-module-image-link __se_image_link __se_link">' +
        '<img src="' + _escHtml(b.url) + '" alt="" class="se-image-resource">' +
        '</a></div></div></div></div>';
    }
    if (b.type === "text") {
      const raw = b.text == null ? "" : String(b.text);
      // 줄바꿈을 <p> 단락으로 분리. 빈 줄은 &nbsp;로 보존.
      const paragraphs = raw.split(/\r?\n/).map(line => {
        const t = _escHtml(line);
        const safe = t.length === 0 ? "&nbsp;" : t;
        return '<p class="se-text-paragraph se-text-paragraph-align-"><span class="se-fs- se-ff-">' + safe + '</span></p>';
      }).join("");
      return '<div class="se-component se-text se-l-default" id="' + id + '">' +
        '<div class="se-component-content">' +
        '<div class="se-section se-section-text se-l-default">' +
        '<div class="se-module se-module-text">' + paragraphs + '</div></div></div></div>';
    }
    if (b.type === "html" && b.html) {
      // 원본 HTML 보존 (래퍼 없이 그대로 삽입)
      return b.html;
    }
    return "";
  }).filter(Boolean);
  return '<div class="se-viewer se-theme-default" lang="ko-KR"><div class="se-main-container">' + parts.join("") + '</div></div>';
}

// SmartEditor HTML → 블록 배열.
// 알 수 없는 컴포넌트나 비-SE HTML은 html 블록으로 보존하여 손실 없이 round-trip.
function parseSmartEditorHtml(html) {
  if (!html || !String(html).trim()) return [];
  let doc;
  try {
    doc = new DOMParser().parseFromString(html, "text/html");
  } catch {
    return [{ type: "html", html }];
  }
  const root = doc.querySelector(".se-main-container") || doc.body;
  if (!root) return [{ type: "html", html }];

  // se-main-container가 없으면 일반 HTML로 간주 — html 블록 하나로 보존
  if (!doc.querySelector(".se-main-container")) {
    const stripped = (doc.body.innerHTML || "").trim();
    if (!stripped) return [];
    return [{ type: "html", html: stripped }];
  }

  const out = [];
  let idSeed = 1;
  const nextId = () => "blk-" + Date.now() + "-" + (idSeed++);
  for (const node of Array.from(root.children)) {
    if (!(node instanceof Element)) continue;
    const cls = node.className || "";
    if (cls.indexOf("se-image") !== -1) {
      const img = node.querySelector("img");
      const url = img?.getAttribute("src") || "";
      if (url) {
        const isNaver = /(shop-phinf\.pstatic\.net|phinf\.pstatic\.net|naver\.com)/.test(url);
        out.push({ id: nextId(), type: "image", url, source: isNaver ? "naver" : "external" });
      }
      continue;
    }
    if (cls.indexOf("se-text") !== -1) {
      // 각 <p> 단락의 텍스트만 추출, &nbsp; → 공백
      const ps = node.querySelectorAll("p, div.se-text-paragraph");
      const seen = ps.length > 0 ? Array.from(ps) : [node];
      const lines = seen.map(p => {
        const raw = (p.textContent || "").replace(/ /g, " ");
        return raw.trim() === "" ? "" : raw.replace(/\s+$/g, "");
      });
      const text = lines.join("\n").replace(/\n+$/g, "");
      out.push({ id: nextId(), type: "text", text });
      continue;
    }
    // 알 수 없는 SE 컴포넌트(동영상, 표 등) → 원본 HTML 보존
    out.push({ id: nextId(), type: "html", html: node.outerHTML });
  }
  return out;
}

// 외부 이미지 URL → 네이버 자체 서버에 업로드 → 네이버 URL 반환
async function naverUploadImageFromUrl(externalUrl) {
  if (!naverConfigured()) throw new Error("API 자격증명이 설정되지 않았습니다");
  const c = loadCreds();
  const res = await fetch(_proxyBase() + "/naver/upload-image-from-url", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ clientId: c.naver_client_id, clientSecret: c.naver_client_secret, imageUrl: externalUrl }),
  });
  const json = await res.json();
  if (!res.ok) throw new Error("이미지 업로드 실패 (HTTP " + res.status + ") " + JSON.stringify(json).slice(0, 300));
  // 네이버 응답 구조 추측: { images: [{ url: "..." }] }
  const url = json.images?.[0]?.url || json.imageUrl || json.url;
  if (!url) throw new Error("이미지 응답에서 URL을 찾을 수 없음: " + JSON.stringify(json).slice(0, 200));
  return url;
}

// 신규 상품 등록 — payload는 GET origin-product와 동일 구조
async function naverRegisterProduct(payload) {
  if (!naverConfigured()) throw new Error("API 자격증명이 설정되지 않았습니다");
  const c = loadCreds();
  const res = await fetch(_proxyBase() + "/naver/products", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ clientId: c.naver_client_id, clientSecret: c.naver_client_secret, payload }),
  });
  const json = await res.json();
  if (!res.ok) throw new Error("등록 실패 (HTTP " + res.status + ") " + JSON.stringify(json).slice(0, 400));
  return json;
}

// Template 기반 등록 — 기준상품에서는 카테고리 코드 등 최소한만 복사.
// 배송/A&S/상세설명/SEO/콘텐츠 게시글 번호는 template에서 가져오지 않음 (사용자 요구).
//   templateOriginProductNo: 사용자 스토어의 기존 상품 번호 (카테고리 구조 참조용)
//   overrides: { name, salePrice, stockQuantity, imageUrl?, detailContent?, sellerCode?, leafCategoryId? }
async function naverRegisterFromTemplate(templateOriginProductNo, overrides) {
  const tpl = await naverFetchOriginProduct(templateOriginProductNo);
  if (!tpl.originProduct) throw new Error("template 응답에 originProduct가 없음");
  const payload = JSON.parse(JSON.stringify(tpl));
  delete payload.originProductNo;
  delete payload.smartstoreChannelProductNo;

  if (overrides.name !== undefined) payload.originProduct.name = overrides.name;
  if (overrides.salePrice !== undefined) payload.originProduct.salePrice = overrides.salePrice;
  if (overrides.stockQuantity !== undefined) payload.originProduct.stockQuantity = overrides.stockQuantity;
  if (overrides.leafCategoryId !== undefined) payload.originProduct.leafCategoryId = overrides.leafCategoryId;
  // modelName 자동 등록 제거 — 항상 틀린 값이 들어가서 차라리 빈 값으로
  if (payload.originProduct.detailAttribute?.naverShoppingSearchInfo) {
    payload.originProduct.detailAttribute.naverShoppingSearchInfo.modelName = "";
  }

  // === 배송 정보: 정책 필드만 우리 설정값으로 덮어쓰기. ===
  // 택배사 코드는 template의 값 유지 (사용자 매장에서 이미 검증된 코드 — 네이버 enum 추측 안 함).
  // 50,000원 이상 무료 + 도서/산간 추가요금만 강제.
  const c = loadCreds();
  const baseFee = (c.naver_base_fee != null && c.naver_base_fee !== "") ? parseInt(c.naver_base_fee) : 3000;
  const freeAmount = (c.naver_free_amount != null && c.naver_free_amount !== "") ? parseInt(c.naver_free_amount) : 50000;
  const remoteFee = (c.naver_remote_fee != null && c.naver_remote_fee !== "") ? parseInt(c.naver_remote_fee) : 5000;
  payload.originProduct.deliveryInfo = payload.originProduct.deliveryInfo || {};
  // 배송비 정책만 우리 값으로 교체 (deliveryCompany 등 코드 필드는 template 유지)
  payload.originProduct.deliveryInfo.deliveryFee = {
    ...(payload.originProduct.deliveryInfo.deliveryFee || {}),
    deliveryFeeType: "CONDITIONAL_FREE",      // 조건부 무료
    baseFee,
    freeConditionalAmount: freeAmount,         // 이 금액 이상 무료
    deliveryFeePayType: "PREPAID",
    deliveryAreaType: "AREA_3",
    area2extraFee: remoteFee,
    area3extraFee: remoteFee,
  };

  // === A&S 정보: template에서 안 가져옴. 설정 기본값으로 강제 ===
  const asPhone = c.naver_as_phone || "1588-0000";
  payload.originProduct.detailAttribute = payload.originProduct.detailAttribute || {};
  payload.originProduct.detailAttribute.afterServiceInfo = {
    afterServiceTelephoneNumber: asPhone,
    afterServiceGuideContent: "구매 후 7일 이내 미사용 제품 교환/환불 가능합니다. 자세한 사항은 고객센터로 문의해 주세요.",
  };

  // === 상세설명/SEO/게시글: template 누설 차단 ===
  // SEO 정보 전체를 minimal로 재생성 — template의 pageTitle/sellerTags 절대 안 남게.
  // (taxType은 보존 — 필수 필드일 가능성)
  const newName = overrides.name !== undefined ? overrides.name : (payload.originProduct.name || "");
  const tplTaxType = payload.originProduct.seoInfo?.taxType;
  payload.originProduct.seoInfo = {
    pageTitle: newName.slice(0, 100),
    metaDescription: "",
    sellerTags: [],
  };
  if (tplTaxType) payload.originProduct.seoInfo.taxType = tplTaxType;
  if (overrides.imageUrl !== undefined || overrides.optionalImageUrls !== undefined) {
    // 메인 이미지 처리
    let repUrl = overrides.imageUrl;
    if (repUrl && !/(shop-phinf\.pstatic\.net|phinf\.pstatic\.net|naver\.com)/.test(repUrl)) {
      repUrl = await naverUploadImageFromUrl(repUrl);
    }
    // 추가 이미지들 순차 업로드 (실패한 건 건너뛰고 진행)
    const optionalImages = [];
    if (overrides.optionalImageUrls && Array.isArray(overrides.optionalImageUrls)) {
      // 네이버 optionalImages 한도(보통 9개)
      const MAX_OPTIONAL = 9;
      const toUpload = overrides.optionalImageUrls.slice(0, MAX_OPTIONAL);
      for (const ext of toUpload) {
        try {
          let u = ext;
          if (u && !/(shop-phinf\.pstatic\.net|phinf\.pstatic\.net|naver\.com)/.test(u)) {
            u = await naverUploadImageFromUrl(u);
          }
          optionalImages.push({ url: u });
        } catch (err) {
          console.warn("[추가 이미지 업로드 실패]", ext, err.message);
        }
      }
    }
    payload.originProduct.images = {
      representativeImage: repUrl ? { url: repUrl } : payload.originProduct.images?.representativeImage,
      optionalImages,
    };
  }
  // 상세설명: override 있으면 그걸로. 없거나 빈 string이어도 template 게 안 남도록 명시적 빈 SmartEditor wrapper.
  if (overrides.detailContent !== undefined && overrides.detailContent.trim()) {
    payload.originProduct.detailContent = overrides.detailContent;
  } else {
    payload.originProduct.detailContent = '<div class="se-viewer se-theme-default" lang="ko-KR"><div class="se-main-container"></div></div>';
  }
  if (overrides.sellerCode !== undefined) {
    payload.originProduct.detailAttribute = payload.originProduct.detailAttribute || {};
    payload.originProduct.detailAttribute.sellerCodeInfo = payload.originProduct.detailAttribute.sellerCodeInfo || {};
    payload.originProduct.detailAttribute.sellerCodeInfo.sellerManagementCode = overrides.sellerCode;
  }
  // 신규 등록 시 즉시할인 제거 (사용자가 별도로 설정해야)
  if (payload.originProduct.customerBenefit?.immediateDiscountPolicy) {
    delete payload.originProduct.customerBenefit.immediateDiscountPolicy;
  }
  // Discussion #3025 공식 답변: 공지사항 연동 안 하면 bbsSeq 키 자체를 빼야 함.
  // (bbsSeq: 0은 "일련번호 0인 공지사항 등록"이라는 invalid 의미)
  if (payload.smartstoreChannelProduct) {
    payload.smartstoreChannelProduct = {
      naverShoppingRegistration: true,
      channelProductDisplayStatusType: "ON",
      channelProductName: "",
      storeKeepExclusiveProduct: false,
      // bbsSeq 의도적으로 빠짐 — 공지사항 연동 안 함
    };
  }
  // 윈도쇼핑 안 씀 → 통째 삭제
  delete payload.windowChannelProduct;
  // smartstoreGroupChannel(매장 그룹)이 있으면 빈 객체로 (공지 그룹 미연결)
  if (payload.smartstoreGroupChannel) {
    payload.smartstoreGroupChannel = {};
  }

  // 방어적: payload 어디든지 contentsBBS / channelNo / bbsSeq 필드 재귀 제거.
  // (template에 남아있는 공지사항/채널 참조 누설 차단)
  const stripFields = (obj, fields) => {
    if (!obj || typeof obj !== "object") return;
    if (Array.isArray(obj)) { obj.forEach(o => stripFields(o, fields)); return; }
    for (const f of fields) if (f in obj) delete obj[f];
    for (const k of Object.keys(obj)) stripFields(obj[k], fields);
  };
  stripFields(payload, ["contentsBBS", "channelNo", "bbsSeq"]);

  return naverRegisterProduct(payload);
}

// === 로젠택배 Open API ============================================
// 작동 흐름: 주문 등록 → 송장번호 채번 → 출력 팝업 열기
// 자격증명: localStorage의 logen_user_id (8자), logen_cust_cd (8자), logen_env ("prod"|"dev")

function logenConfigured() {
  const c = loadCreds();
  return Boolean(c.logen_user_id && c.logen_cust_cd && c.proxy_url);
}

function _logenCreds() {
  const c = loadCreds();
  return {
    userId: c.logen_user_id || "",
    custCd: c.logen_cust_cd || "",
    env: c.logen_env || "prod",
  };
}

// 네이버 주문 → 로젠 주문 등록 payload row 변환.
// fareTy: 010(선불)/020(착불)/030(신용)/040(기타). 기본 010 선불.
function _orderToLogenRow(order, { custCd, takeDt, fareTy = "010" }) {
  const sender = order.sender || {};
  const items = order.items || [];
  const firstItem = items[0] || {};
  const itemName = items.map(it => it.name + (it.option ? `(${it.option})` : "") + `×${it.qty}`).join(" / ").slice(0, 100);
  const totalQty = items.reduce((s, it) => s + (it.qty || 1), 0);
  // 보내는사람 정보가 없으면 빈 값. takingAddress 없는 mock 주문 대비.
  return {
    custCd,
    takeDt,
    fareTy,
    sndCustNm: (sender.name || "").slice(0, 50),
    sndCustAddr: (sender.address || "").slice(0, 1000),
    sndTelNo: (sender.tel || "").slice(0, 20),
    sndCellNo: (sender.tel || "").slice(0, 20),
    rcvCustNm: (order.buyer || "").slice(0, 50),
    rcvCustAddr: (order.address || "").slice(0, 1000),
    rcvTelNo: (order.phone || "").slice(0, 20),
    rcvCellNo: (order.phone || "").slice(0, 20),
    qty: String(totalQty || 1),
    dlvFare: "0",  // 운임 (선불일 때 0이면 무료배송 또는 기등록)
    goodsNm: itemName || (firstItem.name || "").slice(0, 100),
    remark: (order.memo || "").slice(0, 200),
    // 사용자 주문번호를 우리 시스템 ID로 추적
    custOrderNo: (order.productOrderId || order.id || "").slice(0, 100),
  };
}

// 주문 등록. orders는 우리 내부 모델 배열.
async function logenRegisterOrders(orders, opts = {}) {
  if (!logenConfigured()) throw new Error("로젠 자격증명이 설정되지 않았습니다");
  const { userId, custCd, env } = _logenCreds();
  const today = new Date();
  const takeDt = opts.takeDt || (today.getFullYear() + String(today.getMonth() + 1).padStart(2, "0") + String(today.getDate()).padStart(2, "0"));
  const fareTy = opts.fareTy || "010";

  const data = orders.map(o => _orderToLogenRow(o, { custCd, takeDt, fareTy }));
  const res = await fetch(_proxyBase() + "/logen/register-orders", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ env, payload: { userId, data } }),
  });
  const json = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error("로젠 주문 등록 실패 (HTTP " + res.status + ") " + JSON.stringify(json).slice(0, 300));
  return { takeDt, response: json, perOrder: json.data || [] };
}

// 송장번호 채번. qty 개수만큼 발급.
async function logenGetSlipNo(qty) {
  if (!logenConfigured()) throw new Error("로젠 자격증명이 설정되지 않았습니다");
  const { userId, env } = _logenCreds();
  const res = await fetch(_proxyBase() + "/logen/get-slip-no", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ env, payload: { userId, slipQty: qty } }),
  });
  const json = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error("로젠 채번 실패 (HTTP " + res.status + ") " + JSON.stringify(json).slice(0, 300));
  return json;
}

// 출력 팝업 URL 받기. 응답 JSON에 url이 있을 가능성 (실제 응답 구조 검증 필요).
async function logenGetPrintPopupUrl(takeDt) {
  if (!logenConfigured()) throw new Error("로젠 자격증명이 설정되지 않았습니다");
  const { userId, custCd, env } = _logenCreds();
  const params = new URLSearchParams({ env, userId, custCd, takeDt });
  const res = await fetch(_proxyBase() + "/logen/print-popup-url?" + params.toString());
  const json = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error("로젠 출력 팝업 URL 실패 (HTTP " + res.status + ") " + JSON.stringify(json).slice(0, 300));
  return json;
}

// 등록된 주문의 송장번호 일괄 조회.
// items: [{ fixTakeNo }, ...]
async function logenInquirySlipNo(items) {
  if (!logenConfigured()) throw new Error("로젠 자격증명이 설정되지 않았습니다");
  const { userId, custCd, env } = _logenCreds();
  const data = items.map(it => ({ custCd, fixTakeNo: it.fixTakeNo }));
  const res = await fetch(_proxyBase() + "/logen/inquiry-slip-no", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ env, payload: { userId, data } }),
  });
  const json = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error("로젠 송장번호 조회 실패 (HTTP " + res.status + ") " + JSON.stringify(json).slice(0, 300));
  return json;
}

// === 문의 (Q&A) 및 리뷰 ============================================
// 서버 라우트가 path 후보 여러 개를 자동 시도함. 응답 헤더 X-Naver-Path-Used로 어느 path가 먹는지 확인 가능.

async function naverFetchQA({ page = 1, size = 20, answered, from, to } = {}) {
  if (!naverConfigured()) throw new Error("API 자격증명이 설정되지 않았습니다");
  const params = new URLSearchParams({ page: String(page), size: String(size) });
  if (answered !== undefined) params.set("answered", String(answered));
  if (from) params.set("from", from);
  if (to) params.set("to", to);
  params.set("_ts", String(Date.now()));
  const res = await fetch(_proxyBase() + "/naver/qa?" + params.toString(), {
    headers: _proxyHeaders(), cache: "no-store",
  });
  if (!res.ok) {
    const t = await res.text().catch(() => "");
    throw new Error("문의 조회 실패 (HTTP " + res.status + ") " + t.slice(0, 300));
  }
  const json = await res.json();
  // 네이버 응답 구조 추측: { contents: [...], page, size, totalElements, totalPages }
  return {
    items: json.contents || json.items || [],
    page: json.page || page,
    size: json.size || size,
    totalElements: json.totalElements || 0,
    totalPages: json.totalPages || 1,
    pathUsed: res.headers.get("X-Naver-Path-Used"),
    raw: json,
  };
}

async function naverAnswerQA(qaId, content) {
  if (!naverConfigured()) throw new Error("API 자격증명이 설정되지 않았습니다");
  if (!content || !content.trim()) throw new Error("답변 내용을 입력하세요");
  const c = loadCreds();
  const res = await fetch(_proxyBase() + "/naver/qa/" + encodeURIComponent(qaId) + "/answer", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ clientId: c.naver_client_id, clientSecret: c.naver_client_secret, content }),
  });
  const json = await res.json().catch(() => ({}));
  if (!res.ok) {
    throw new Error("문의 답변 실패 (HTTP " + res.status + ") " + JSON.stringify(json).slice(0, 300));
  }
  return { ...json, pathUsed: res.headers.get("X-Naver-Path-Used") };
}

async function naverFetchReviews({ page = 1, size = 20, hasComment, from, to } = {}) {
  if (!naverConfigured()) throw new Error("API 자격증명이 설정되지 않았습니다");
  const params = new URLSearchParams({ page: String(page), size: String(size) });
  if (hasComment !== undefined) params.set("hasComment", String(hasComment));
  if (from) params.set("from", from);
  if (to) params.set("to", to);
  params.set("_ts", String(Date.now()));
  const res = await fetch(_proxyBase() + "/naver/reviews?" + params.toString(), {
    headers: _proxyHeaders(), cache: "no-store",
  });
  if (!res.ok) {
    const t = await res.text().catch(() => "");
    throw new Error("리뷰 조회 실패 (HTTP " + res.status + ") " + t.slice(0, 300));
  }
  const json = await res.json();
  return {
    items: json.contents || json.items || [],
    page: json.page || page,
    size: json.size || size,
    totalElements: json.totalElements || 0,
    totalPages: json.totalPages || 1,
    pathUsed: res.headers.get("X-Naver-Path-Used"),
    raw: json,
  };
}

async function naverCommentReview(reviewId, content) {
  if (!naverConfigured()) throw new Error("API 자격증명이 설정되지 않았습니다");
  if (!content || !content.trim()) throw new Error("답글 내용을 입력하세요");
  const c = loadCreds();
  const res = await fetch(_proxyBase() + "/naver/reviews/" + encodeURIComponent(reviewId) + "/comment", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ clientId: c.naver_client_id, clientSecret: c.naver_client_secret, content }),
  });
  const json = await res.json().catch(() => ({}));
  if (!res.ok) {
    throw new Error("리뷰 답글 실패 (HTTP " + res.status + ") " + JSON.stringify(json).slice(0, 300));
  }
  return { ...json, pathUsed: res.headers.get("X-Naver-Path-Used") };
}

// 발송 처리 — 송장번호 등록
//   list = [{ productOrderId, trackingNumber, deliveryCompanyCode?, deliveryMethod?, dispatchDate? }]
async function naverDispatchOrders(list) {
  if (!naverConfigured()) throw new Error("API 자격증명이 설정되지 않았습니다");
  if (!list || list.length === 0) throw new Error("발송 처리할 주문이 없습니다");
  const c = loadCreds();
  const today = _fmtKST(new Date());
  const payload = {
    dispatchProductOrders: list.map(d => ({
      productOrderId: d.productOrderId,
      deliveryMethod: d.deliveryMethod || "DELIVERY",
      deliveryCompanyCode: d.deliveryCompanyCode || "LOGEN",
      trackingNumber: d.trackingNumber,
      dispatchDate: d.dispatchDate || today,
    })),
  };
  const res = await fetch(_proxyBase() + "/naver/orders/dispatch", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ clientId: c.naver_client_id, clientSecret: c.naver_client_secret, payload }),
  });
  const json = await res.json();
  if (!res.ok) {
    throw new Error("발송 처리 실패 (HTTP " + res.status + ") " + JSON.stringify(json).slice(0, 300));
  }
  return json;
}

window.API = {
  ISVM_BASE, NAVER_API_BASE,
  loadCreds, saveCreds, clearCreds,
  isvmHealth, isvmSearch, isvmAddProducts, isvmFetchProductImages,
  buildNaverTokenPayload, buildNaverProductPayload,
  naverConfigured, naverFetchProducts, naverToInternalProduct,
  naverFetchOriginProduct, naverUpdateOriginProduct, naverChangePrice, naverChangeProductPricing,
  naverUpdateProduct,
  naverFetchOrders, naverToInternalOrder, naverOrderStatusToInternal,
  naverDispatchOrders,
  naverRegisterProduct, naverRegisterFromTemplate, naverUploadImageFromUrl,
  naverUploadImageFile, buildSmartEditorImagesHtml, buildSmartEditorHtml, parseSmartEditorHtml,
  naverFetchQA, naverAnswerQA, naverFetchReviews, naverCommentReview,
  logenConfigured, logenRegisterOrders, logenGetSlipNo, logenGetPrintPopupUrl, logenInquirySlipNo,
};
