// translate.mjs（DeepL 版）の差し替え。ローカルの PLaMo 翻訳ゲートウェイを使う。
//
//   node translate-plamo.mjs health
//   node translate-plamo.mjs push-glossary          -- glossary.tsv をゲートウェイへ送る
//   node translate-plamo.mjs run <in.json> [limit]  -- 翻訳して tm.json に追記
//
// DeepL 版と同じ tm.json を読み書きするので、途中から乗り換えられる。
// 文字数の上限はない（自分の GPU なので）。
import fs from 'fs';

const GATEWAY = process.env.PLAMO_GATEWAY || 'http://127.0.0.1:8080';
const TM = 'tm.json';
const PROFILE = process.env.PLAMO_PROFILE || 'game';
const CONCURRENCY = +(process.env.PLAMO_CONCURRENCY || 1);
const CHUNK = 25;

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

async function health() {
  const r = await fetch(GATEWAY + '/api/health');
  const j = await r.json();
  console.log(JSON.stringify(j, (k, v) => (k === 'detail' ? undefined : v), 1));
  if (!j.ok) { console.error('llama-server が動いていません。F:\\PLaMo\\start-plamo.ps1 を実行してください。'); process.exitCode = 1; }
  return j;
}

// glossary.tsv（DeepL 用に作ったもの）をそのままゲートウェイの用語集にする。
// ゲートウェイ側では用語を伏せ字にして訳語を差し戻すので、DeepL の用語集より
// 強制力がある（モデルが訳語を選び直す余地がない）。
async function pushGlossary() {
  const src = fs.existsSync('glossary.tsv') ? fs.readFileSync('glossary.tsv', 'utf8') : '';
  const gl = {};
  for (const line of src.split(/\r?\n/)) {
    const [a, b] = line.split('\t');
    if (a && b) gl[a.trim()] = b.trim();
  }
  // 手訳の上書き層のうち、固有名詞や UI 語も用語集に載せておくと揺れない
  if (fs.existsSync('overrides.json')) {
    const ov = JSON.parse(fs.readFileSync('overrides.json', 'utf8'));
    for (const [k, v] of Object.entries(ov)) {
      if (k.startsWith('_')) continue;
      if (k.length < 3 || k.length > 40) continue;
      if (!/^[A-Za-z][A-Za-z0-9 '’\-()]*$/.test(k)) continue;
      gl[k] = v;
    }
  }

  // 用語集に載せてよいのは『名詞の言い換え』だけ。動詞や活用語を伏せ字にすると
  // 文が壊れる（retire→引退する を伏せると「引退する した後に」になった）。
  const verbJA = v => /(する|した|して|される|られる|せる|れる|った|んだ|めた|ける|かる|わる|いる|ある)$/.test(v);
  const verbEN = new Set(['retire', 'mastered', 'Recover', 'Return', 'Carry up', 'Carry down',
    'Appraise and return', 'Put down', 'Dispatched', 'Cut down', 'Brought down', 'Felled', 'Finished',
    'Slew', 'Struck down', 'Laid low', 'Hewed down', 'Ran through', 'Bested', 'Overcame',
    'Made an end of', 'Accounted for', 'Put paid to', 'Beat down', 'Downed', 'Ended', 'Stilled',
    'Laid out', 'Did for', 'Unmade', 'Made short work of', 'Had the better of', 'Saw off', 'Wrote off',
    'Struck off', 'Settled with', 'Closed the account on', 'Tracking', 'Cornering', 'Stalking',
    'Facing', 'Flushing out', 'Running down', 'Thin out the', 'Drive back the', 'Account for the',
    'Put down the', 'Clear the stair of the', 'Clear the crag road of the']);

  // 小文字1語でも、この作品の中核語は訳語を固定したい。既定だと「普通名詞」として
  // モデル任せになるので、明示的に hard を立てる。
  const PROMOTE = ['chronicle', 'prestige', 'bestiary', 'hireling', 'muster', 'warden', 'writ',
    'seal', 'feat', 'trophy', 'boon', 'knack', 'descent', 'capstone', 'plate', 'arc', 'act'];

  const out = {};
  let dropped = 0;
  for (const [k, v] of Object.entries(gl)) {
    const ja = typeof v === 'object' && v ? v.ja : v;
    if (verbEN.has(k) || verbJA(ja)) { dropped++; continue; }
    out[k] = PROMOTE.includes(k) ? { ja, hard: true } : ja;
  }

  const r = await fetch(GATEWAY + '/api/glossary', {
    method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify(out),
  });
  console.log('用語集を送りました:', (await r.json()).terms, '語（動詞など', dropped, '語は除外）');
}

async function run(file, limit) {
  const src = JSON.parse(fs.readFileSync(file, 'utf8'));
  const tm = loadTM();
  const todo = src.filter(s => !(s in tm));
  const pick = limit ? todo.slice(0, +limit) : todo;
  console.log(`${file}: ${src.length} 件中 ${todo.length} 件が未訳、今回 ${pick.length} 件`);
  if (!pick.length) return;

  await health();
  const t0 = Date.now();
  let done = 0, model = 0, warned = 0, noted = 0;

  for (let i = 0; i < pick.length; i += CHUNK) {
    const slice = pick.slice(i, i + CHUNK);
    const r = await fetch(GATEWAY + '/api/translate/batch', {
      method: 'POST', headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ texts: slice, src: 'en', tgt: 'ja', profile: PROFILE, concurrency: CONCURRENCY }),
    });
    if (!r.ok) throw new Error('gateway ' + r.status + ' ' + (await r.text()).slice(0, 200));
    const j = await r.json();
    j.results.forEach((res, k) => {
      // コード側のプレースホルダを落とした訳だけは採用しない（ゲームが壊れる）。
      // 用語集の固定が外れただけの訳は使う（note が付くが訳文としては正しい）。
      if (res.warning || res.error) { warned++; return; }
      if (res.note) noted++;
      tm[slice[k]] = res.text;
    });
    model += j.stats.fromModel;
    done += slice.length;
    saveTM(tm);
    const rate = done / ((Date.now() - t0) / 1000);
    process.stdout.write(`\r  ${done}/${pick.length}  ${rate.toFixed(1)} 件/秒  要確認 ${warned}   `);
  }
  console.log(`\n完了。モデル生成 ${model} 件 / 要確認 ${warned} 件 / ${((Date.now() - t0) / 1000).toFixed(0)} 秒`);
  if (warned) console.log('要確認の分は tm.json に入れていません（英語のまま残ります）。');
}

const [, , cmd, a, b] = process.argv;
if (cmd === 'health') await health();
else if (cmd === 'push-glossary') await pushGlossary();
else if (cmd === 'run') await run(a, b);
else console.log('cmd: health | push-glossary | run <file.json> [limit]');
