const fs = require('fs');
const path = require('path');
const https = require('node:https');

let cachedXlsx = null;

function getXlsx() {
  if (cachedXlsx) {
    return cachedXlsx;
  }

  try {
    cachedXlsx = require('xlsx');
    return cachedXlsx;
  } catch (error) {
    error.message = `缺少依赖 xlsx。请在 extensions/FrameworkExcelGenerator 目录执行 npm ci 后重试。原始错误: ${error.message}`;
    throw error;
  }
}

// --- helpers (module scope, do NOT rely on `this`) ---

function safeKeyName(key) {
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
    return `'${key}'`;
  }
  return key;
}

function findCommonKeys(langData) {
  const keysPerLang = Object.values(langData).map(data => new Set(Object.keys(data)));
  if (keysPerLang.length === 0) return [];
  return [...keysPerLang[0]].filter(key => keysPerLang.every(set => set.has(key)));
}

// 读上一次生成的 proxy 文件，从中抽出 XxxLanguageEnum 的 key 顺序。
// 该顺序是 LanguageManager.getEnumKeys() 在运行时返回的顺序，也是
// LanguageLabelLocalize._key 数字索引所依赖的"稳定基准"。
// 任何旧 prefab 只存了 _key 而没有 _persistKey 的，都靠这条顺序在重导后仍能命中同一个 key 名。
function readPriorKeyOrder(outputBase, folderName) {
  const proxyFileName = `${folderName.charAt(0).toUpperCase()}${folderName.slice(1)}Language`;
  const proxyFilePath = path.join(outputBase, `${proxyFileName}.ts`);
  if (!fs.existsSync(proxyFilePath)) return [];
  let content;
  try {
    content = fs.readFileSync(proxyFilePath, 'utf8');
  } catch (e) {
    return [];
  }
  // 优先解析新格式 `XxxKeyOrder = [ ... ] as const;`，这是 V8 整数 key 重排问题的修复点。
  const arrRe = new RegExp(
    proxyFileName + 'KeyOrder\\s*[:\\w\\s<>,]*=\\s*\\[([\\s\\S]*?)\\]\\s*as\\s+const',
  );
  const arrMatch = content.match(arrRe);
  if (arrMatch) {
    const body = arrMatch[1];
    const itemRe = /(?:'([^']+)'|"([^"]+)")\s*,?/g;
    const keys = [];
    let m;
    while ((m = itemRe.exec(body)) !== null) {
      keys.push(m[1] != null ? m[1] : m[2]);
    }
    if (keys.length > 0) return keys;
  }
  // 老 proxy（没有 KeyOrder 数组）回落到 Enum 块：源文件里 enum 行序就是当时的 key 顺序。
  const blockRe = new RegExp(
    'const\\s+' + proxyFileName + 'Enum\\s*=\\s*\\{([\\s\\S]*?)\\}\\s*as\\s+const;',
  );
  const blockMatch = content.match(blockRe);
  if (!blockMatch) return [];
  const body = blockMatch[1];
  const lineRe = /^\s*(?:'([^']+)'|"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))\s*:/gm;
  const keys = [];
  let m;
  while ((m = lineRe.exec(body)) !== null) {
    keys.push(m[1] != null ? m[1] : (m[2] != null ? m[2] : m[3]));
  }
  return keys;
}

// 按 priorOrder 重建对象：已存在 key 保持原序，新 key 追加末尾，priorOrder 里
// 已被移除的 key 自然丢弃。priorOrder 为空（首次生成）时原样返回。
function reorderByPrior(obj, priorOrder) {
  if (!priorOrder || priorOrder.length === 0) return obj;
  const out = {};
  for (const k of priorOrder) {
    if (Object.prototype.hasOwnProperty.call(obj, k)) out[k] = obj[k];
  }
  for (const k of Object.keys(obj)) {
    if (!Object.prototype.hasOwnProperty.call(out, k)) out[k] = obj[k];
  }
  return out;
}

function toDbUrl(absPath) {
  const rel = path.relative(Editor.Project.path, absPath).replace(/\\/g, '/');
  if (!rel || rel.startsWith('..')) return null;
  return 'db://' + rel;
}

async function refreshAssetDB(outputBase, extraDirAbs) {
  // Cocos Creator 3.x AssetDB is message-driven (Editor.AssetDB doesn't exist).
  const targets = [];
  const outUrl = toDbUrl(outputBase);
  if (outUrl) targets.push(outUrl);
  if (extraDirAbs) {
    const extraUrl = toDbUrl(extraDirAbs);
    if (extraUrl) targets.push(extraUrl);
  }
  for (const url of targets) {
    try {
      await Editor.Message.request('asset-db', 'refresh-asset', url);
      console.log(`[excel-to-lang] 已刷新: ${url}`);
    } catch (err) {
      // Fallback for very old CC 2.x just in case.
      if (Editor.AssetDB && Editor.AssetDB.refresh) {
        try { await Editor.AssetDB.refresh(url); } catch (e) { /* ignore */ }
      } else {
        console.warn(`[excel-to-lang] 刷新失败: ${url}`, err && err.message);
      }
    }
  }
}

function formatObject(obj) {
  const entries = Object.entries(obj);
  if (entries.length === 0) return '{}';
  const items = entries.map(([key, value]) => {
    const escapedValue = value
      .replace(/\\/g, '\\\\')
      .replace(/"/g, '\\"')
      .replace(/\n/g, '\\n')
      .replace(/\r/g, '\\r');
    return `  "${key}": "${escapedValue}"`;
  }).join(',\n');
  return `{\n${items}\n}`;
}

function createProxyLanguageFile(outputBase, folderName, langFileNames, langData) {
  const proxyFileName = `${folderName.charAt(0).toUpperCase()}${folderName.slice(1)}Language`;
  const proxyFilePath = path.join(outputBase, `${proxyFileName}.ts`);

  const commonKeys = findCommonKeys(langData);

  const importStatements = Object.entries(langFileNames).map(([lang, name]) => {
    return `import { ${name} } from './${lang}/${name}';`;
  }).join('\n');

  const langPackMapping = Object.entries(langFileNames).map(([lang, name]) => {
    return `  '${lang}': ${name},`;
  }).join('\n');

  // Types are inlined here via `declare global` declaration merging — no separate
  // .d.ts file is maintained.
  const proxyContent = `
${importStatements}

export interface I${proxyFileName} {
${commonKeys.map(key => `  readonly ${safeKeyName(key)}: string;`).join('\n')}
  readonly [key: string]: string;
}

declare global {
  interface LanguageBundleAccessor {
    /** ${folderName} 多语言包 */
    readonly ${folderName}: I${proxyFileName};
  }
}

const ${proxyFileName}Enum = {
${commonKeys.map((key) => `  ${safeKeyName(key)}: ${JSON.stringify(key)},`).join('\n')}
} as const;

// 显式数组保留 key 的稳定顺序：V8 对整数式字符串 key（如 "100018"）会按
// 数字升序在 Object.keys() 中重排，使得 LanguageLabelLocalize 上序列化的
// _key 数字索引在表格插入/删除时漂移。把顺序固化成数组、运行时直接读它，
// 配合导表工具的"保持上次顺序、新 key 追加末尾"策略，旧 prefab 永远稳。
export const ${proxyFileName}KeyOrder: ReadonlyArray<string> = [
${commonKeys.map((key) => `  ${JSON.stringify(key)},`).join('\n')}
] as const;

export type ${proxyFileName}Type = typeof ${proxyFileName}Enum;

const languagePacks = {
${langPackMapping}
};

const proxy = new Proxy({}, {
  get(_, key: string) {
    const currentLang =
      typeof Entry !== 'undefined' && Entry.languageMgr?.getLanguage
        ? Entry.languageMgr.getLanguage()
        : Object.keys(languagePacks)[0];

    const langPack = languagePacks[currentLang];

    if (langPack && langPack[key]) {
      return langPack[key];
    }

    const defaultLang = Object.keys(languagePacks)[0];
    const defaultPack = languagePacks[defaultLang];
    if (defaultPack && defaultPack[key]) {
      return defaultPack[key];
    }

    const commonKeys = getCommonKeys();
    if (commonKeys.has(key)) {
      for (const lang in languagePacks) {
        if (languagePacks[lang][key]) {
          return languagePacks[lang][key];
        }
      }
    }

    return '';
  },

  has(_, key) {
    return getCommonKeys().has(key);
  },

  getOwnPropertyDescriptor() {
    return {
      enumerable: true,
      configurable: true
    };
  }
});

function getCommonKeys() {
  const allKeys = Object.values(languagePacks).map(pack => Object.keys(pack));
  if (allKeys.length === 0) return new Set();

  return new Set(
    allKeys.reduce((commonKeys, keys) =>
      commonKeys.filter(key => keys.includes(key))
    , allKeys[0])
  );
}

/**
 * 获取当前语言的所有值
 */
export function getCurrentLanguageValues(): Record<string, string> {
  const currentLang =
    typeof Entry !== 'undefined' && Entry.languageMgr?.getLanguage
      ? Entry.languageMgr.getLanguage()
      : Object.keys(languagePacks)[0];

  return {...languagePacks[currentLang]};
}

export const ${proxyFileName} = proxy as I${proxyFileName};

/**
 * 真正干注册的函数。挂到 Entry 上：使 Entry.${folderName}.<key> 直接可用。
 * 类型扩展通过本文件顶部的 declare global LanguageBundleAccessor 完成。
 *
 * 说明：本模板既覆盖 cc Bundle 包（assetManager.loadBundle 在运行时异步触发，
 * 那时 Entry 早就 new 过），也覆盖非 bundle 包（通过 framework/language/Extension.ts
 * 的静态 import 链触发，那时 Framework.ts 模块 body 还没跑完，Entry 尚未挂载）。
 * 同步尝试一次；失败就用 queueMicrotask + setTimeout 兜底，等 entry.start() 完成
 * 之后再注册。bundle 场景里同步直接成功，对其性能无影响。
 */
function register${proxyFileName}Pack() {
  if (typeof Entry === 'undefined' || !Entry.languageMgr) return false;
  Entry.languageMgr.registerLanguagePackage('${folderName}', ${proxyFileName}Enum, (key: string, language?: string) => {
    const currentLang = language || Entry.languageMgr.getLanguage();
    const langPack = languagePacks[currentLang];
    if (langPack && langPack[key]) return langPack[key];
    const defaultLang = Object.keys(languagePacks)[0];
    const defaultPack = languagePacks[defaultLang];
    if (defaultPack && defaultPack[key]) return defaultPack[key];
    return '';
  }, ${proxyFileName}KeyOrder);
  // 仅当尚未设置时再设置，避免后注册的包覆盖前面包的支持语言列表
  const existingLangs = Entry.languageMgr.getSupportedLanguages();
  if (!existingLangs || existingLangs.length === 0) {
    Entry.languageMgr.setSupportedLanguages(Object.keys(languagePacks));
  }
  (Entry as unknown as Record<string, unknown>).${folderName} = ${proxyFileName};
  return true;
}

if (!register${proxyFileName}Pack()) {
  const retry = () => {
    if (!register${proxyFileName}Pack()) {
      setTimeout(register${proxyFileName}Pack, 0);
    }
  };
  if (typeof queueMicrotask === 'function') {
    queueMicrotask(retry);
  } else {
    Promise.resolve().then(retry);
  }
}
`;

  fs.writeFileSync(proxyFilePath, proxyContent, { flag: 'w' });
  return proxyFilePath;
}

/**
 * Upsert an entry for this bundle in assets/framework/types/Language.d.ts.
 * The file uses marker comments so we can safely replace just the generated
 * sections without touching any manual edits outside the markers.
 *
 * @param {string} folderName   bundle 名称（例如 common）
 * @param {string} bundleName   sheetName 转换后的 bundle 路径（例如 common/main）
 * @param {string} interfaceName  export 的 interface 名（例如 ICommonLanguage）
 * @param {string} proxyFileName 生成的 proxy 文件名（例如 CommonLanguage）
 */
function normalizeBundleRoot(bundleRoot) {
  let r = String(bundleRoot || '').trim().replace(/^[\\/]+|[\\/]+$/g, '');
  // Tolerate users typing the leading "assets/".
  if (r === 'assets' || r.startsWith('assets/')) {
    r = r === 'assets' ? '' : r.slice('assets/'.length);
  }
  return r;
}

function getBundleRootSegments(bundleRoot) {
  // Empty bundleRoot is allowed (files go directly under assets/<sheet>/src/language).
  const r = normalizeBundleRoot(bundleRoot);
  return r ? r.split('/') : [];
}

/**
 * If sheet name is "hall_indonesia" → bundleName is "hall/indonesia", and the
 * user also typed bundleRoot="hall", the natural join would yield
 * assets/hall/hall/indonesia/. Strip the leading bundleRoot from bundleName so
 * the path collapses to assets/hall/indonesia/.
 */
function stripBundleRootFromBundle(bundleName, bundleRoot) {
  const r = normalizeBundleRoot(bundleRoot);
  if (!r) return bundleName;
  if (bundleName === r) return '';
  if (bundleName.startsWith(r + '/')) return bundleName.slice(r.length + 1);
  return bundleName;
}

function escapeRegex(s) {
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

/**
 * 判断 outputBase 所在的 bundle 目录是否为 cc Bundle。
 * outputBase 形如 `<project>/assets/<X>/src/language`，bundle 候选目录是它的祖父
 * （即 `<project>/assets/<X>`，也是 .meta 上挂 userData.isBundle 的那一级）。
 * 同步读 meta；任何异常（meta 不存在 / 解析失败）按"非 bundle"处理。
 */
function isOutputUnderBundle(outputBase) {
  const candidate = path.dirname(path.dirname(outputBase));
  const metaPath = `${candidate}.meta`;
  try {
    if (!fs.existsSync(metaPath)) return false;
    const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
    return !!(meta && meta.userData && (meta.userData.isBundle === true || meta.userData.bundle === true));
  } catch (e) {
    return false;
  }
}

/**
 * 当生成的代理位于非 bundle 目录时，把它的 import 行 upsert 到
 * `assets/framework/language/Extension.ts` 内的 auto marker 区块。
 * marker 之外的内容不会被动；区块内按字典序排序、去重，便于 diff review。
 */
function upsertExtensionImportIfNonBundle(projectPath, outputBase, proxyFilePath) {
  if (isOutputUnderBundle(outputBase)) {
    // cc Bundle 走 assetManager.loadBundle 路径，不需要静态 import
    return;
  }

  const extPath = path.join(projectPath, 'assets', 'framework', 'language', 'Extension.ts');
  if (!fs.existsSync(extPath)) {
    console.warn('[excel-to-lang] Extension.ts 不存在，跳过 import upsert:', extPath);
    return;
  }

  // 从 Extension.ts 角度看 proxy 的相对路径（去掉 .ts 后缀），跨平台用 posix 分隔符
  const extDir = path.dirname(extPath);
  const rel = path.relative(extDir, proxyFilePath).replace(/\\/g, '/').replace(/\.ts$/, '');
  const importLine = `import '${rel.startsWith('.') ? rel : './' + rel}';`;

  const START = '// === auto-language-imports:start (managed by FrameworkExcelGenerator, do not edit by hand) ===';
  const END = '// === auto-language-imports:end ===';

  const raw = fs.readFileSync(extPath, 'utf8');
  const startIdx = raw.indexOf(START);
  const endIdx = raw.indexOf(END);
  if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {
    console.warn('[excel-to-lang] Extension.ts 缺少 marker 区块，跳过 import upsert（请确认文件顶部 marker 未被改名）');
    return;
  }

  const before = raw.slice(0, startIdx + START.length);
  const after = raw.slice(endIdx);
  const middle = raw.slice(startIdx + START.length, endIdx);

  // 解析区块内已有 import；与新行合并 → 去重 → 排序 → 重新拼回
  const existing = new Set(
    middle
      .split('\n')
      .map((l) => l.trim())
      .filter((l) => /^import\s+['"][^'"]+['"];?$/.test(l)),
  );
  existing.add(importLine.endsWith(';') ? importLine : `${importLine};`);
  const sorted = Array.from(existing).sort();

  const newBlock = `\n${sorted.join('\n')}\n`;
  const next = `${before}${newBlock}${after}`;
  if (next !== raw) {
    fs.writeFileSync(extPath, next, { flag: 'w' });
    console.log(`[excel-to-lang] Extension.ts upsert: ${importLine}`);
  }
}

// Process one sheet's worth of rows (already parsed into a 2D array, exactly the
// shape XLSX.utils.sheet_to_json({header:1}) returns). Shared by both the local
// Excel pipeline and the Lark Open Sheets pipeline.
function processSheetRows(sheetName, jsonData, bundleRoot, callback) {
  if (!Array.isArray(jsonData) || jsonData.length < 2) {
    callback({ success: false, message: `[${sheetName}] 至少需要两行数据` });
    return;
  }

  const headers = (jsonData[0] || []).map(h => String(h == null ? '' : h).trim().toLowerCase());
  const seqIndex = headers.indexOf('seq');
  const describeIndex = headers.indexOf('describe');
  const keyIndex = headers.indexOf('key');

  if (keyIndex === -1) {
    callback({ success: false, message: `[${sheetName}] 缺少必要的列:key` });
    return;
  }

  const langColumns = [];
  for (let j = 0; j < headers.length; j++) {
    if (j !== keyIndex && j != seqIndex && j != describeIndex && headers[j]) {
      langColumns.push({ index: j, lang: headers[j] });
    }
  }

  if (langColumns.length === 0) {
    callback({ success: false, message: `[${sheetName}] 未找到语言列` });
    return;
  }

  const langData = {};
  const files = {};

  langColumns.forEach(col => {
    langData[col.lang] = {};
  });

  for (let r = 2; r < jsonData.length; r++) {
    const row = jsonData[r];
    if (!row || row.length < keyIndex + 1) continue;

    const key = String(row[keyIndex] || '').trim();
    if (!key) continue;

    langColumns.forEach(col => {
      if (row.length > col.index) {
        const cell = row[col.index];
        const value = String(cell == null ? '' : cell).trim();
        langData[col.lang][key] = value;
      }
    });
  }

  const projectPath = Editor.Project.path;
  const bundleName = sheetName.includes('_') ? sheetName.replace('_', '/') : sheetName;
  const outputBase = path.join(
    projectPath,
    'assets',
    ...getBundleRootSegments(bundleRoot),
    stripBundleRootFromBundle(bundleName, bundleRoot),
    'src',
    'language',
  );

  if (!fs.existsSync(outputBase)) {
    fs.mkdirSync(outputBase, { recursive: true });
  }

  const langFileNames = {};

  let folderName = '';
  if (bundleName.includes('_')) {
    folderName = bundleName.substring(0, bundleName.indexOf('_'));
  } else if (bundleName.includes('/')) {
    folderName = bundleName.substring(0, bundleName.indexOf('/'));
  } else {
    folderName = bundleName;
  }

  // 按上一次生成的 proxy 里的 key 顺序，把本次 langData 重排：已有 key 保持原位置，
  // 新 key 追加在末尾，已删除 key 自然消失。这一步是为了让运行时 getEnumKeys()
  // 的返回顺序在重复导入之间保持稳定 —— 老 prefab 上序列化的 _key 数字索引依赖这个稳定性。
  const priorOrder = readPriorKeyOrder(outputBase, folderName);
  if (priorOrder.length > 0) {
    const preserved = priorOrder.length;
    const sampleLang = Object.keys(langData)[0];
    const beforeKeys = sampleLang ? Object.keys(langData[sampleLang]) : [];
    for (const lang of Object.keys(langData)) {
      langData[lang] = reorderByPrior(langData[lang], priorOrder);
    }
    if (sampleLang) {
      const afterKeys = Object.keys(langData[sampleLang]);
      const appended = afterKeys.filter((k) => !priorOrder.includes(k));
      const dropped = priorOrder.filter((k) => !beforeKeys.includes(k));
      console.log(
        `[excel-to-lang] [${sheetName}] key 顺序保持 ${preserved} 个; ` +
        `新增 ${appended.length} 个(追加末尾): ${appended.slice(0, 5).join(', ')}${appended.length > 5 ? '...' : ''}; ` +
        `移除 ${dropped.length} 个: ${dropped.slice(0, 5).join(', ')}${dropped.length > 5 ? '...' : ''}`,
      );
    }
  }

  langColumns.forEach(col => {
    const lang = col.lang;
    const langDir = path.join(outputBase, lang);

    if (!fs.existsSync(langDir)) {
      fs.mkdirSync(langDir, { recursive: true });
    }

    const name = `${folderName.charAt(0).toUpperCase()}${folderName.slice(1)}Language${lang.charAt(0).toUpperCase()}${lang.slice(1)}`;
    const langFile = path.join(langDir, `${name}.ts`);
    const content = `export const ${name} = ${formatObject(langData[lang])};\n`;

    fs.writeFileSync(langFile, content, { flag: 'w' });
    if (!files[lang]) files[lang] = [];
    files[lang].push(langFile);

    langFileNames[lang] = name;
  });

  const proxyFilePath = createProxyLanguageFile(outputBase, folderName, langFileNames, langData);

  // 检测目标目录是否为 cc Bundle；非 bundle 则把 import 行 upsert 到
  // framework/language/Extension.ts 的 auto marker 区块，确保静态 import 链触达
  try {
    upsertExtensionImportIfNonBundle(projectPath, outputBase, proxyFilePath);
  } catch (e) {
    console.warn('[excel-to-lang] upsert Extension.ts failed:', e && e.message);
  }

  // Fire-and-forget; the callback below shouldn't block on AssetDB refresh.
  refreshAssetDB(outputBase).catch((e) => {
    console.warn('[excel-to-lang] refresh error:', e && e.message);
  });

  callback({
    success: true,
    message: '转换成功！',
    files,
    langData,
    sheetName,
    languages: langColumns.map(col => col.lang),
    outputPath: path.relative(projectPath, outputBase),
  });
}

function processExcel(filePath, callback, bundleRoot) {
  try {
    const XLSX = getXlsx();
    const workbook = XLSX.readFile(filePath);
    for (const sheetName of workbook.SheetNames) {
      const worksheet = workbook.Sheets[sheetName];
      const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
      processSheetRows(sheetName, jsonData, bundleRoot, callback);
    }
  } catch (error) {
    callback({ success: false, message: `处理失败: ${error.message}` });
  }
}

// --- Lark Open Sheets pipeline ---
//
// URL format the panel accepts:
//   https://<sub>.feishu.cn/sheets/<TOKEN>[?sheet=<SHEET_ID>]
//   https://<sub>.larksuite.com/sheets/<TOKEN>[?sheet=<SHEET_ID>]
// If sheet param is present we only import that sheet; otherwise we import all
// sheets in the spreadsheet (matches the multi-sheet behavior of local xlsx).

function colLetter(n) {
  let s = '';
  let x = n;
  while (x > 0) {
    const r = (x - 1) % 26;
    s = String.fromCharCode(65 + r) + s;
    x = Math.floor((x - 1) / 26);
  }
  return s || 'A';
}

function parseLarkUrl(rawUrl) {
  let u;
  try {
    u = new URL(rawUrl);
  } catch (e) {
    throw new Error('云文档链接格式不合法');
  }
  const m = u.pathname.match(/\/sheets\/([A-Za-z0-9]+)/);
  if (!m) throw new Error('链接中找不到 spreadsheet token (URL 应形如 .../sheets/<token>)');
  const token = m[1];
  const sheetId = u.searchParams.get('sheet') || '';
  const host = u.hostname;
  // feishu.cn = 国内版；larksuite.com = 国际版（含 *.jp.larksuite.com 等区域域名）
  const apiHost = /(^|\.)feishu\.cn$/.test(host) ? 'open.feishu.cn' : 'open.larksuite.com';
  return { token, sheetId, apiHost };
}

function httpsJson(host, pathname, { method = 'GET', headers = {}, body = null } = {}) {
  return new Promise((resolve, reject) => {
    const finalHeaders = Object.assign({}, headers);
    let payload = null;
    if (body) {
      payload = typeof body === 'string' ? body : JSON.stringify(body);
      finalHeaders['Content-Length'] = Buffer.byteLength(payload);
    }
    const req = https.request(
      { host, port: 443, path: pathname, method, headers: finalHeaders },
      (res) => {
        const chunks = [];
        res.on('data', (c) => chunks.push(c));
        res.on('end', () => {
          const text = Buffer.concat(chunks).toString('utf8');
          try {
            resolve(JSON.parse(text));
          } catch (e) {
            reject(new Error(`Lark API 返回非 JSON (${res.statusCode}): ${text.slice(0, 200)}`));
          }
        });
      },
    );
    req.on('error', reject);
    if (payload) req.write(payload);
    req.end();
  });
}

async function getTenantAccessToken(apiHost, appId, appSecret) {
  const res = await httpsJson(apiHost, '/open-apis/auth/v3/tenant_access_token/internal', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json; charset=utf-8' },
    body: { app_id: appId, app_secret: appSecret },
  });
  if (res.code !== 0 || !res.tenant_access_token) {
    throw new Error(`获取 tenant_access_token 失败: ${res.msg || JSON.stringify(res)}`);
  }
  return res.tenant_access_token;
}

async function listLarkSheets(apiHost, token, accessToken) {
  const res = await httpsJson(
    apiHost,
    `/open-apis/sheets/v3/spreadsheets/${token}/sheets/query`,
    { headers: { Authorization: `Bearer ${accessToken}` } },
  );
  if (res.code !== 0) throw new Error(`获取工作表列表失败: ${res.msg || JSON.stringify(res)}`);
  return (res.data && res.data.sheets) || [];
}

// Lark "Read a single range" returns at most ~10K cells per request. We page in
// 200-row batches × 26 columns = 5200 cells/request to stay well under the cap.
const LARK_ROW_BATCH = 200;
const LARK_MAX_COLS = 26;

async function readLarkSheetRows(apiHost, token, sheet, accessToken) {
  const cols = colLetter(Math.min(sheet.grid_properties.column_count || LARK_MAX_COLS, LARK_MAX_COLS));
  const totalRows = sheet.grid_properties.row_count || 0;
  if (totalRows === 0) return [];
  const out = [];
  for (let start = 1; start <= totalRows; start += LARK_ROW_BATCH) {
    const end = Math.min(start + LARK_ROW_BATCH - 1, totalRows);
    const range = `${sheet.sheet_id}!A${start}:${cols}${end}`;
    const res = await httpsJson(
      apiHost,
      `/open-apis/sheets/v2/spreadsheets/${token}/values/${encodeURIComponent(range)}`,
      { headers: { Authorization: `Bearer ${accessToken}` } },
    );
    if (res.code !== 0) {
      throw new Error(`读取 ${range} 失败: ${res.msg || JSON.stringify(res)}`);
    }
    const rows = (res.data && res.data.valueRange && res.data.valueRange.values) || [];
    for (const r of rows) out.push(r);
  }
  return out;
}

async function processLark(url, appId, appSecret, bundleRoot, callback) {
  try {
    if (!url) throw new Error('请输入云文档链接');
    if (!appId || !appSecret) throw new Error('请填写 App ID / App Secret');
    const { token, sheetId, apiHost } = parseLarkUrl(url);
    const accessToken = await getTenantAccessToken(apiHost, appId, appSecret);
    const sheets = await listLarkSheets(apiHost, token, accessToken);
    let targets = sheets;
    if (sheetId) {
      targets = sheets.filter((s) => s.sheet_id === sheetId);
      if (targets.length === 0) {
        throw new Error(`URL 指定的 sheet=${sheetId} 在表格中不存在`);
      }
    }
    if (targets.length === 0) throw new Error('表格中没有可读的 sheet');
    for (const sheet of targets) {
      const rows = await readLarkSheetRows(apiHost, token, sheet, accessToken);
      processSheetRows(sheet.title, rows, bundleRoot, callback);
    }
  } catch (error) {
    callback({ success: false, message: `云文档导入失败: ${error.message}` });
  }
}

// --- CC extension exports ---

module.exports = {
  load() { },
  unload() { },

  methods: {
    openPanel() {
      Editor.Panel.open('excel-to-lang.panel');
    },

    convertExcel(filePath, bundleRoot) {
      processExcel(filePath, (result) => {
        if (result.success) {
          console.log(`生成文件: ${Object.values(result.files).flat().join(',\n ')}`);
        } else {
          console.log(`Excel转多语言: ${result.message}`);
        }
        // Route to panel.conversionResult via the `conversion-result` message
        // declared in contributions.messages (methods: ["panel.conversionResult"]).
        Editor.Message.send('excel-to-lang', 'conversion-result', result);
      }, bundleRoot);
    },

    convertLark(url, appId, appSecret, bundleRoot) {
      processLark(url, appId, appSecret, bundleRoot, (result) => {
        if (result.success) {
          console.log(`[lark] 生成文件: ${Object.values(result.files).flat().join(',\n ')}`);
        } else {
          console.log(`[lark] ${result.message}`);
        }
        Editor.Message.send('excel-to-lang', 'conversion-result', result);
      });
    },
  },
};
