// Lowmoor 日本語化パイプライン: 翻訳フェーズ (DeepL)
//
// 使い方:
//   node translate.mjs usage                 -- 残り文字数を見る
//   node translate.mjs glossary              -- 用語集を作成/更新し id を保存
//   node translate.mjs run <in.json> [limit] -- 翻訳して tm.json に追記
//
// tm.json は翻訳メモリ (原文 -> 訳文)。既訳はスキップするので、
// 何度実行しても DeepL の文字数を二重に消費しない。
import fs from 'fs';

const KEY = process.env.DEEPL_KEY || fs.readFileSync('.deepl-key', 'utf8').trim();
const API = 'https://api-free.deepl.com/v2';
const TM = 'tm.json';
const GLOSS_ID_FILE = '.glossary-id';

const auth = { Authorization: 'DeepL-Auth-Key ' + KEY };

const loadTM = () => (fs.existsSync(TM) ? JSON.parse(fs.readFileSync(TM, 'utf8')) : {});
const saveTM = tm => fs.writeFileSync(TM, JSON.stringify(tm, null, 1));

async function usage() {
  const r = await fetch(API + '/usage', { method: 'POST', headers: auth });
  const j = await r.json();
  console.log(`used ${j.character_count} / ${j.character_limit}  (remaining ${j.character_limit - j.character_count})`);
  return j;
}

async function makeGlossary() {
  const entries = fs.readFileSync('glossary.tsv', 'utf8').trim();
  // 既存を消してから作り直す (無料プランは本数制限がある)
  try {
    const r = await fetch(API + '/glossaries', { headers: auth });
    const j = await r.json();
    for (const g of j.glossaries || []) {
      if (g.name === 'lowmoor-en-ja') await fetch(API + '/glossaries/' + g.glossary_id, { method: 'DELETE', headers: auth });
    }
  } catch (e) { /* 一覧が取れなくても新規作成は試す */ }
  const body = new URLSearchParams({
    name: 'lowmoor-en-ja', source_lang: 'en', target_lang: 'ja',
    entries, entries_format: 'tsv'
  });
  const r = await fetch(API + '/glossaries', { method: 'POST', headers: auth, body });
  const j = await r.json();
  if (!j.glossary_id) { console.error('glossary failed:', JSON.stringify(j)); return null; }
  fs.writeFileSync(GLOSS_ID_FILE, j.glossary_id);
  console.log('glossary created:', j.glossary_id, j.entry_count, 'entries');
  return j.glossary_id;
}

// DeepL は 1リクエスト最大50テキスト / 128KiB。安全側に寄せる。
const BATCH_N = 40, BATCH_CHARS = 60000;

async function translateBatch(texts, glossId) {
  const body = new URLSearchParams();
  for (const t of texts) body.append('text', t);
  body.append('source_lang', 'EN');
  body.append('target_lang', 'JA');
  body.append('tag_handling', 'html');       // <b> や <span> を壊さない
  body.append('preserve_formatting', '1');
  body.append('split_sentences', 'nonewlines');
  if (glossId) body.append('glossary_id', glossId);
  for (let attempt = 0; attempt < 5; attempt++) {
    const r = await fetch(API + '/translate', { method: 'POST', headers: auth, body });
    if (r.status === 429 || r.status >= 500) { await new Promise(s => setTimeout(s, 2000 * (attempt + 1))); continue; }
    const j = await r.json();
    if (!j.translations) throw new Error('deepl error ' + r.status + ' ' + JSON.stringify(j).slice(0, 300));
    return j.translations.map(t => t.text);
  }
  throw new Error('deepl: retries exhausted');
}

async function run(file, limit) {
  const src = JSON.parse(fs.readFileSync(file, 'utf8'));
  const tm = loadTM();
  const glossId = fs.existsSync(GLOSS_ID_FILE) ? fs.readFileSync(GLOSS_ID_FILE, 'utf8').trim() : null;
  const todo = src.filter(s => !(s in tm));
  const pick = limit ? todo.slice(0, +limit) : todo;
  const cost = pick.reduce((a, s) => a + s.length, 0);
  console.log(`${file}: ${src.length} strings, ${todo.length} untranslated, taking ${pick.length} (${cost} chars)`);
  if (!pick.length) return;

  const u = await usage();
  if (u.character_limit - u.character_count < cost) {
    console.error('REFUSING: not enough DeepL quota for this batch.');
    process.exit(2);
  }

  let done = 0;
  for (let i = 0; i < pick.length;) {
    const batch = [];
    let chars = 0;
    while (i < pick.length && batch.length < BATCH_N && chars + pick[i].length < BATCH_CHARS) {
      batch.push(pick[i]); chars += pick[i].length; i++;
    }
    const out = await translateBatch(batch, glossId);
    batch.forEach((s, k) => { tm[s] = out[k]; });
    done += batch.length;
    saveTM(tm);                                 // 毎バッチ保存: 落ちても払った分は残る
    process.stdout.write(`\r  ${done}/${pick.length}`);
  }
  console.log('\ndone.');
  await usage();
}

const [, , cmd, arg, arg2] = process.argv;
if (cmd === 'usage') await usage();
else if (cmd === 'glossary') await makeGlossary();
else if (cmd === 'run') await run(arg, arg2);
else console.log('cmd: usage | glossary | run <file.json> [limit]');
