// 칼바람 컴패니언 1단계 — LCU에서 내 ARAM 개인기록을 로컬 생성(일반450·증강2400). production key 0.
// 실행: League 클라이언트 켠 상태에서  node mayhem-model/companion/lcu_personal.mjs
//       node mayhem-model/companion/lcu_personal.mjs --mock   (League 없이 출력형태 확인)
import { readFile } from "node:fs/promises";
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import os from "node:os";
import https from "node:https";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { execSync } from "node:child_process";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REL_API_DIR = path.resolve(__dirname, "../../aram_mayhem_auto/data/pages_site/static/api");
const ABS_API_DIR = "E:\\프린서버\\plugins\\Skript\\scripts\\aram_mayhem_auto\\data\\pages_site\\static\\api"; // 설치본 폴백
const API_DIR = process.env.MAYHEM_API_DIR || (existsSync(REL_API_DIR) ? REL_API_DIR : ABS_API_DIR);
const DB_PATH = path.resolve(API_DIR, "../../../db.json"); // 증강 누적 저장소(본섭+PBE 전체) — LCU 최근창보다 많음
// 개인기록 캐시 — 어느 PC든 존재하는 홈 폴더에 저장(데이터 디렉토리는 메인PC 전용 하드코딩 경로라 다른 PC선 없어 쓰기 실패함)
const CACHE_DIR = path.join(os.homedir(), ".aram-mayhem");
try { mkdirSync(CACHE_DIR, { recursive: true }); } catch {}
const STD_CACHE_PATH = path.join(CACHE_DIR, "mayhem_companion_std.json"); // 일반(450) LCU 누적(롤 꺼도 다음부터 보이게)
const AUG_CACHE_PATH = path.join(CACHE_DIR, "mayhem_companion_aug.json"); // 증강(2400) LCU 폴백 누적(db.json 없는 다른 PC)
// 옛 위치(데이터 디렉토리)에 누적 캐시가 있으면 새 위치로 1회 이전 — 메인PC 기존 기록 보존
for (const [oldP, newP] of [["../mayhem_companion_std.json", STD_CACHE_PATH], ["../mayhem_companion_aug.json", AUG_CACHE_PATH]]) {
  try { const op = path.resolve(DB_PATH, oldP); if (existsSync(op) && !existsSync(newP)) writeFileSync(newP, readFileSync(op)); } catch {}
}
const Q_STANDARD = 450, Q_AUGMENT = 2400; // 일반칼바람 / 증바람(아수라장)
// 증강 시즌2 술어(server.js makeAugmentSeasonFilter와 동일): gameVersion 16.12+ & 2026-06-04 경계 이후
const AUG_S2_VERNUM = 1612, AUG_S2_BOUNDARY_MS = 1780535674230;
const augVerNum = x => { const v = String((x && x.gameVersion) || "").match(/(\d+)\.(\d+)/); return v ? +v[1] * 100 + +v[2] : 0; };

// 매치카드용 아이콘/추출(필드명은 기존 aram_mayhem_auto/server.js 검증값과 동일)
const DDV = "16.12.1";
const SPELL_ICON = { 1:"SummonerBoost",3:"SummonerExhaust",4:"SummonerFlash",6:"SummonerHaste",7:"SummonerHeal",11:"SummonerSmite",12:"SummonerTeleport",13:"SummonerMana",14:"SummonerDot",21:"SummonerBarrier",30:"SummonerPoroRecall",31:"SummonerPoroThrow",32:"SummonerSnowball",39:"SummonerSnowball" };
const spellIcon = id => SPELL_ICON[Number(id)] ? `https://ddragon.leagueoflegends.com/cdn/${DDV}/img/spell/${SPELL_ICON[Number(id)]}.png` : "";
const itemIconUrl = id => Number(id) > 0 ? `https://ddragon.leagueoflegends.com/cdn/${DDV}/img/item/${Number(id)}.png` : "";
const EXCLUDE_ITEM = new Set([2003,2010,2031,2032,2033,2055,2138,2139,2140,2150,2151,2152,3340,3363,3364,3400,6701]); // 물약·영약·와드 등 소모품 + 6701 기회(삭제된 아이템)
function pickItems(p, s) { const out = []; for (let i = 0; i < 7; i++) { const v = Number((p && p["item" + i]) ?? (s && s["item" + i]) ?? 0); if (v > 0 && !EXCLUDE_ITEM.has(v)) out.push(v); } return out; }
const durStr = sec => { sec = Math.round(sec || 0); return Math.floor(sec / 60) + ":" + String(sec % 60).padStart(2, "0"); };

function findLockfile() {
  // 1) env + 표준 설치 경로(빠름)
  const rel = "/Riot Games/League of Legends/lockfile";
  const known = [process.env.LCU_LOCKFILE,
    "C:" + rel, "D:" + rel, "E:" + rel, "F:" + rel, "G:" + rel,
    "C:/Program Files" + rel, "C:/Program Files (x86)" + rel,
    "D:/Program Files" + rel, "D:/Program Files (x86)" + rel
  ].filter(Boolean).find(p => existsSync(p));
  if (known) return known;
  // 2) 실행 중인 LeagueClientUx 프로세스의 설치경로에서 락파일 도출(어느 경로/드라이브에 깔렸든 찾음)
  try {
    const exe = execSync('powershell -NoProfile -Command "(Get-Process LeagueClientUx -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Path)"',
      { encoding: "utf8", timeout: 6000, windowsHide: true }).trim();
    if (exe) { const lf = path.join(path.dirname(exe), "lockfile"); if (existsSync(lf)) return lf; }
  } catch {}
  return null;
}
function readLockfile() {
  const f = findLockfile(); if (!f) return null;
  const parts = readFileSync(f, "utf8").trim().split(":"); // name:pid:port:password:protocol
  return { port: Number(parts[2]), password: parts[3] };
}
function lcuGet(lcu, p) {
  return new Promise((resolve, reject) => {
    const req = https.request({
      host: "127.0.0.1", port: lcu.port, path: p, method: "GET", rejectUnauthorized: false,
      headers: { Authorization: "Basic " + Buffer.from("riot:" + lcu.password).toString("base64") }
    }, res => { let d = ""; res.on("data", c => d += c); res.on("end", () => { try { resolve(JSON.parse(d)); } catch { reject(new Error("LCU 응답 파싱 실패: " + p)); } }); });
    req.on("error", reject); req.end();
  });
}
async function getMatches(lcu, count = 2000) {
  const out = [], seen = new Set(), PAGE = 100; // 큰 페이지로 한 번에 더 많이(LCU 캐시 깊이만큼)
  for (let beg = 0; beg < count; beg += PAGE) {
    const end = Math.min(beg + PAGE - 1, count - 1);
    const j = await lcuGet(lcu, `/lol-match-history/v1/products/lol/current-summoner/matches?begIndex=${beg}&endIndex=${end}`);
    const g = (j && j.games && j.games.games) || [];
    let added = 0;
    for (const game of g) { const id = game.gameId; if (id != null && seen.has(id)) continue; if (id != null) seen.add(id); out.push(game); added++; } // gameId 중복제거 — LCU 페이징 무시 ×N 부풀림 차단
    if (g.length === 0 || added === 0) break; // 끝 or 새 게임 없음 → 중단
  }
  return out;
}
let _maps = null;
async function champMaps() {
  if (_maps) return _maps; // 25MB 코호트 1회만 읽음(세션 캐시)
  try {
    const j = JSON.parse(await readFile(path.join(API_DIR, "standard-aram-cohort-all.json"), "utf8"));
    const names = new Map(), icons = new Map();
    for (const c of (j.report && j.report.championRows) || []) { const id = Number(c.championId); names.set(id, c.localName || c.displayName); if (c.icon) icons.set(id, c.icon); }
    return (_maps = { names, icons });
  } catch { return { names: new Map(), icons: new Map() }; }
}
function myStat(game, puuid) {
  let p = (game.participants || []).find(x => x.puuid === puuid);
  if (!p) { const pi = (game.participantIdentities || []).find(x => x.player && x.player.puuid === puuid); p = (game.participants || []).find(x => x.participantId === (pi && pi.participantId)); }
  if (!p) return null; // 내 참가자를 못 찾으면 버린다 — 남의 전적 오귀속 금지
  const s = p.stats || {};
  return {
    gid: game.gameId ?? null, championId: p.championId, win: !!s.win, k: s.kills || 0, d: s.deaths || 0, a: s.assists || 0,
    queueId: game.queueId, t: game.gameCreation || 0, dur: game.gameDuration || 0,
    cs: (s.totalMinionsKilled || 0) + (s.neutralMinionsKilled || 0),
    dmg: s.totalDamageDealtToChampions || 0, taken: s.totalDamageTaken || 0, gold: s.goldEarned || 0,
    lvl: s.champLevel || 0, mk: s.largestMultiKill || 0,
    spells: [spellIcon(p.spell1Id ?? s.spell1Id ?? p.summoner1Id ?? s.summoner1Id), spellIcon(p.spell2Id ?? s.spell2Id ?? p.summoner2Id ?? s.summoner2Id)].filter(Boolean),
    items: pickItems(p, s)
  };
}
function buildRecord(rows, m) {
  const names = m.names, icons = m.icons;
  const total = rows.length, wins = rows.filter(r => r.win).length, by = new Map();
  let TK = 0, TD = 0, TA = 0;
  for (const r of rows) {
    const c = by.get(r.championId) || { games: 0, wins: 0, k: 0, d: 0, a: 0, dmg: 0, taken: 0, gold: 0, dur: 0 };
    c.games++; c.wins += r.win ? 1 : 0; c.k += r.k; c.d += r.d; c.a += r.a;
    c.dmg += r.dmg || 0; c.taken += r.taken || 0; c.gold += r.gold || 0; c.dur += r.dur || 0; by.set(r.championId, c);
    TK += r.k; TD += r.d; TA += r.a;
  }
  const nm = id => names.get(Number(id)) || ("챔프#" + id), ic = id => icons.get(Number(id)) || "";
  const fd = t => { const d = new Date(t || 0); return (d.getMonth() + 1) + "/" + d.getDate(); };
  const champs = [...by.entries()].map(([id, c]) => {
    const mins = (c.dur || 0) / 60;
    return {
      name: nm(id), icon: ic(id), games: c.games, thin: c.games < 8, winRate: Math.round(c.wins / c.games * 100),
      kda: `${(c.k / c.games).toFixed(1)}/${(c.d / c.games).toFixed(1)}/${(c.a / c.games).toFixed(1)}`,
      dpm: mins > 0 ? Math.round(c.dmg / mins) : 0, taken: mins > 0 ? Math.round(c.taken / mins) : 0, gpm: mins > 0 ? Math.round(c.gold / mins) : 0 // 분당 개인지표(메타 비교용)
    };
  }).sort((a, b) => b.games - a.games);
  const sorted = [...rows].sort((a, b) => (b.t || 0) - (a.t || 0));
  return {
    total, wins, winRate: total ? Math.round(wins / total * 100) : 0,
    avgKda: total ? `${(TK / total).toFixed(1)}/${(TD / total).toFixed(1)}/${(TA / total).toFixed(1)}` : "-",
    form: sorted.slice(0, 10).map(r => r.win),
    recent: sorted.slice(0, 15).map(r => ({
      name: nm(r.championId), icon: ic(r.championId), win: r.win,
      kda: `${r.k}/${r.d}/${r.a}`, ratio: r.d ? ((r.k + r.a) / r.d).toFixed(2) : "Perfect",
      date: fd(r.t), dur: durStr(r.dur), cs: r.cs || 0, dmg: r.dmg || 0, gold: r.gold || 0, lvl: r.lvl || 0, mk: r.mk || 0,
      spells: r.spells || [], items: (r.items || []).map(itemIconUrl)
    })),
    champs
  };
}
function printRecord(title, rec, hint) {
  console.log(`\n■ ${title} — ${rec.total}판  ${rec.winRate}% (${rec.wins}승)`);
  if (!rec.total && hint) { console.log("   " + hint); return; }
  rec.champs.slice(0, 8).forEach(c => console.log(`   ${String(c.name).padEnd(7)} ${c.games}판  ${c.winRate}%  KDA ${c.kda}`));
}
function mockGames(puuid) {
  const g = (cid, win, k, d, a, q, items, sp, dmg, gold, cs, lvl, mk, dur) => ({
    queueId: q, gameCreation: Date.now(), gameDuration: dur,
    participantIdentities: [{ participantId: 1, player: { puuid } }],
    participants: [{
      participantId: 1, championId: cid, spell1Id: sp[0], spell2Id: sp[1],
      stats: Object.assign({ win, kills: k, deaths: d, assists: a, totalDamageDealtToChampions: dmg, totalDamageTaken: Math.round(dmg * 0.85), goldEarned: gold, totalMinionsKilled: cs, champLevel: lvl, largestMultiKill: mk }, items.reduce((o, it, i) => (o["item" + i] = it, o), {}))
    }]
  });
  return [
    g(63, 1, 12, 8, 20, 450, [6655, 3020, 3157, 3165, 3089, 3135], [4, 32], 31200, 13400, 41, 18, 2, 1140),
    g(63, 0, 5, 11, 14, 450, [6655, 3020, 3157, 3165], [4, 32], 18900, 9800, 28, 15, 0, 980),
    g(99, 1, 8, 6, 25, 450, [3115, 3089, 3157, 3165, 3135], [4, 7], 27600, 12100, 35, 17, 0, 1080),
    g(81, 1, 14, 7, 16, 450, [3142, 3814, 3179, 6694, 3036], [4, 32], 34800, 14200, 44, 18, 3, 1210),
    g(266, 1, 9, 10, 19, 2400, [6630, 3047, 3076, 3193, 3143], [4, 6], 22400, 11600, 0, 18, 0, 1320),
    g(266, 0, 4, 12, 11, 2400, [6630, 3047, 3076], [4, 6], 14100, 8200, 0, 16, 0, 1090),
    g(157, 1, 11, 6, 13, 2400, [6673, 3031, 3094, 3036, 3072], [4, 32], 29800, 13900, 0, 18, 2, 1250)
  ];
}
function readDb() { try { return JSON.parse(readFileSync(DB_PATH, "utf8")); } catch { return null; } }
// 증강 개인기록은 db.json(누적 저장소)에서 — 본섭+PBE 양 계정 통합, 시즌2만. LCU 최근창(92)이 아니라 전체(196).
function augmentRowsFromDb(db) { // db.json → 시즌2 증강 행(buildRecord 호환). 병합용으로 행을 그대로 노출.
  if (!db || !Array.isArray(db.matches)) return [];
  const puuids = new Set((db.accounts || []).map(a => a.puuid).filter(Boolean));
  const seen = new Set();
  return db.matches.filter(x => {
    if (x.queueId !== Q_AUGMENT) return false;
    if (puuids.size && !puuids.has(x.puuid)) return false;
    if (augVerNum(x) < AUG_S2_VERNUM || (x.gameCreation || 0) < AUG_S2_BOUNDARY_MS) return false; // 시즌2만(시즌1 구증강 섞지 않음)
    const id = x.matchId + "|" + x.puuid; if (seen.has(id)) return false; seen.add(id); return true;
  }).map(x => ({
    championId: x.championId, win: !!x.win, k: x.kills || 0, d: x.deaths || 0, a: x.assists || 0,
    queueId: Q_AUGMENT, t: x.gameCreation || 0, dur: x.gameDuration || 0, cs: x.totalMinionsKilled || 0,
    dmg: x.totalDamageDealtToChampions || 0, taken: x.totalDamageTaken || 0, gold: x.goldEarned || 0, lvl: 0, mk: x.largestMultiKill || 0,
    spells: (x.spells || []).map(spellIcon).filter(Boolean),
    items: (x.items || []).filter(id => Number(id) > 0 && !EXCLUDE_ITEM.has(Number(id)))
  }));
}
function augmentRecordFromDb(db, m) { return buildRecord(augmentRowsFromDb(db), m); }
// 증강 개인 집계 = 내 픽만(내 챔피언 통계 열). 시즌2 ~200판 → 카서스 12판 등.
export async function augmentAggregate() {
  const m = await champMaps();
  return augmentRecordFromDb(readDb(), m).champs; // [{name,icon,games,winRate,kda,dpm,taken,gpm}]
}
// 증강 메타 = 게임당 10명 전체(teamParticipants) 집계 — 내 픽이 아니라 전체 표본. 200게임×10명=2000행 → aram-mayhem.com 메타와 같은 의미.
export async function augmentMetaFromTeams() {
  const m = await champMaps(), db = readDb();
  if (!db || !Array.isArray(db.matches)) return [];
  const seen = new Set(), by = new Map();
  for (const x of db.matches) {
    if (x.queueId !== Q_AUGMENT) continue;
    if (augVerNum(x) < AUG_S2_VERNUM || (x.gameCreation || 0) < AUG_S2_BOUNDARY_MS) continue;
    if (seen.has(x.matchId)) continue; seen.add(x.matchId); // 게임당 1회(10명 한 번씩)
    const dur = x.gameDuration || 0;
    for (const p of (x.teamParticipants || [])) {
      const id = Number(p.champion); if (!id) continue;
      const c = by.get(id) || { games: 0, wins: 0, k: 0, d: 0, a: 0, dmg: 0, taken: 0, gold: 0, dur: 0 };
      c.games++; c.wins += p.win ? 1 : 0; c.k += p.kills || 0; c.d += p.deaths || 0; c.a += p.assists || 0;
      c.dmg += p.totalDamageDealtToChampions || 0; c.taken += p.totalDamageTaken || 0; c.gold += p.goldEarned || 0; c.dur += dur;
      by.set(id, c);
    }
  }
  const nm = id => m.names.get(Number(id)) || ("챔프#" + id), ic = id => m.icons.get(Number(id)) || "";
  const list = [...by.entries()].map(([id, c]) => {
    const mins = c.dur / 60;
    return { name: nm(id), icon: ic(id), games: c.games, thin: c.games < 10, winRate: Math.round(c.wins / c.games * 100),
      kda: `${(c.k / c.games).toFixed(1)}/${(c.d / c.games).toFixed(1)}/${(c.a / c.games).toFixed(1)}`,
      dpm: mins > 0 ? Math.round(c.dmg / mins) : 0, taken: mins > 0 ? Math.round(c.taken / mins) : 0, gpm: mins > 0 ? Math.round(c.gold / mins) : 0 };
  }).filter(c => c.name);
  // 티어 배정 — 일반과 일관(적응형 백분위 1~5, cut 6/30/72/96%), 표본보정 점수로 정렬(소표본 50%수축)
  const ranked = list.map(c => ({ c, s: (c.winRate / 100 * c.games + 10) / (c.games + 20) })).sort((a, b) => b.s - a.s);
  ranked.forEach((o, i) => { const p = ranked.length ? i / ranked.length : 1; o.c.tier = p < 0.06 ? 1 : p < 0.30 ? 2 : p < 0.72 ? 3 : p < 0.96 ? 4 : 5; });
  return list.sort((a, b) => b.games - a.games);
}
// 증강 아이템 메타 = 게임당 10명 전체(teamParticipants)의 아이템 집계. 이름/아이콘은 server가 itemRows로 붙임.
export function augmentItemAgg() {
  const db = readDb();
  if (!db || !Array.isArray(db.matches)) return [];
  const seen = new Set(), by = new Map();
  for (const x of db.matches) {
    if (x.queueId !== Q_AUGMENT) continue;
    if (augVerNum(x) < AUG_S2_VERNUM || (x.gameCreation || 0) < AUG_S2_BOUNDARY_MS) continue;
    if (seen.has(x.matchId)) continue; seen.add(x.matchId);
    for (const p of (x.teamParticipants || [])) {
      const ids = [...new Set((p.items || []).map(Number))]; // 게임당 같은 아이템 1회
      for (const id of ids) {
        if (!(id > 0) || EXCLUDE_ITEM.has(id)) continue;
        const c = by.get(id) || { games: 0, wins: 0 }; c.games++; c.wins += p.win ? 1 : 0; by.set(id, c);
      }
    }
  }
  return [...by.entries()].map(([id, c]) => ({ id, games: c.games, wins: c.wins, winRate: Math.round(c.wins / c.games * 100) }));
}
const _norm = s => String(s || "").normalize("NFKC").trim().toLowerCase();
// 증강 메타 = 게임당 10명 전체(teamParticipants)의 증강 집계. value→이름은 db.meta.augmentDetails(505, value=id 정확매핑). 사이트와 일치(전환:프리즘 174판).
export function augmentAugmentMeta() {
  const db = readDb();
  if (!db || !Array.isArray(db.matches)) return [];
  const nameMap = new Map(), iconMap = new Map();
  for (const a of (db.meta && db.meta.augmentDetails) || []) { const id = Number(a.id); if (id) { nameMap.set(id, a.localName || a.name); if (a.icon) iconMap.set(id, a.icon); } } // 이름·아이콘 둘 다 augmentDetails(505 전체)에서 — 본인 미픽 증강도 아이콘 나오게
  for (const x of db.matches) for (const a of (x.augments || [])) { const v = Number(a.value); if (!v) continue; if (a.icon && !iconMap.has(v)) iconMap.set(v, a.icon); if (!nameMap.has(v) && (a.localName || a.name)) nameMap.set(v, a.localName || a.name); } // augmentDetails(communitydragon) 우선, 매치기록 아이콘(metasrc 등 비허용호스트)은 빈 곳만 보충
  const seen = new Set(), by = new Map();
  for (const x of db.matches) {
    if (x.queueId !== Q_AUGMENT || augVerNum(x) < AUG_S2_VERNUM || (x.gameCreation || 0) < AUG_S2_BOUNDARY_MS) continue;
    if (seen.has(x.matchId)) continue; seen.add(x.matchId);
    for (const p of (x.teamParticipants || [])) for (const a of (p.augments || [])) {
      const v = Number(a.value); if (!v) continue;
      const c = by.get(v) || { games: 0, wins: 0 }; c.games++; c.wins += p.win ? 1 : 0; by.set(v, c);
    }
  }
  return [...by.entries()].map(([v, c]) => ({ name: nameMap.get(v) || null, icon: iconMap.get(v) || "", games: c.games, winRate: Math.round(c.wins / c.games * 100), thin: c.games < 10 })).filter(x => x.name).sort((a, b) => b.games - a.games);
}
// 개인 챔프 빌드 — 내가 그 챔프로 찍은 증강·아이템(+승률). 메타와 비교용("나는 이걸 찍었을때 승률").
export async function personalChampBuild(mode, name) {
  const m = await champMaps(), q = _norm(name);
  let champId = null, icon = ""; for (const [id, n] of m.names) if (_norm(n) === q) { champId = id; icon = m.icons.get(id) || ""; break; }
  const aug = mode === "augment";
  let rows;
  if (aug) {
    const db = readDb(); const puuids = new Set((db && db.accounts || []).map(a => a.puuid));
    rows = (db && db.matches || []).filter(x => x.queueId === Q_AUGMENT && puuids.has(x.puuid) && augVerNum(x) >= AUG_S2_VERNUM && (x.gameCreation || 0) >= AUG_S2_BOUNDARY_MS && (Number(x.championId) === champId || _norm(x.championLocalName) === q || _norm(x.championDisplayName) === q));
  } else {
    rows = readStdCache().filter(r => Number(r.championId) === champId);
  }
  const games = rows.length, wins = rows.filter(r => r.win).length;
  const augBy = new Map(), itemBy = new Map();
  for (const x of rows) {
    if (aug) for (const a of (x.augments || [])) { const k = a.localName || a.name; if (!k) continue; const c = augBy.get(k) || { games: 0, wins: 0, icon: a.icon || "" }; c.games++; c.wins += x.win ? 1 : 0; augBy.set(k, c); }
    const its = aug ? (x.itemsDetailed || []).map(d => ({ id: d.id, name: d.localName || d.name, icon: d.icon })) : (x.items || []).map(id => ({ id, name: null, icon: null }));
    for (const it of its) { if (!it.id || EXCLUDE_ITEM.has(Number(it.id))) continue; const c = itemBy.get(it.id) || { games: 0, wins: 0, name: it.name, icon: it.icon }; c.games++; c.wins += x.win ? 1 : 0; itemBy.set(it.id, c); }
  }
  const _adj = (w, g) => Math.round((g ? (w + 10) / (g + 20) : 0.5) * 100);
  let dmg = 0, taken = 0, gold = 0, dur = 0; // 내 분당 지표(메타와 비교)
  for (const x of rows) {
    if (aug) { dmg += x.totalDamageDealtToChampions || 0; taken += x.totalDamageTaken || 0; gold += x.goldEarned || 0; dur += x.gameDuration || 0; }
    else { dmg += x.dmg || 0; taken += x.taken || 0; gold += x.gold || 0; dur += x.dur || 0; }
  }
  const mins = dur / 60;
  const mk = (map) => [...map.entries()].map(([k, c]) => ({ name: c.name || k, icon: c.icon || (typeof k === "number" ? itemIconUrl(k) : ""), id: typeof k === "number" ? k : null, games: c.games, winRate: Math.round(c.wins / c.games * 100), adj: _adj(c.wins, c.games), thin: c.games < 3 })).sort((a, b) => b.games - a.games);
  return { name, icon, games, wins, winRate: games ? Math.round(wins / games * 100) : 0, dpm: mins > 0 ? Math.round(dmg / mins) : 0, taken: mins > 0 ? Math.round(taken / mins) : 0, gpm: mins > 0 ? Math.round(gold / mins) : 0, augments: aug ? mk(augBy) : [], items: mk(itemBy) };
}
// 일반(450) 로컬 누적 캐시 — LCU엔 최근창만, 어디에도 저장 안 되므로 불러올 때마다 합쳐 보존(롤 꺼도 다음부터 보임)
function readStdCache() { try { const j = JSON.parse(readFileSync(STD_CACHE_PATH, "utf8")); return Array.isArray(j.games) ? j.games : []; } catch { return []; } }
function writeStdCache(games) { try { writeFileSync(STD_CACHE_PATH, JSON.stringify({ updatedAt: Date.now(), games })); } catch {} }
function readAugCache() { try { const j = JSON.parse(readFileSync(AUG_CACHE_PATH, "utf8")); return Array.isArray(j.games) ? j.games : []; } catch { return []; } }
function writeAugCache(games) { try { writeFileSync(AUG_CACHE_PATH, JSON.stringify({ updatedAt: Date.now(), games })); } catch {} }
function mergeStd(cache, fresh) {
  const by = new Map();
  const key = r => (r.gid != null ? "g" + r.gid : "x" + r.championId + "_" + r.t);
  for (const r of cache) by.set(key(r), r);
  for (const r of fresh) by.set(key(r), r);
  return [...by.values()];
}
// gameCreation(초) 기준 중복제거 — 같은 게임이면 먼저 들어온 행(db.json)이 이김, LCU의 새 게임만 추가됨.
function dedupeByTime(rows) {
  const by = new Map();
  for (const r of rows) { const k = Math.round((r.t || 0) / 1000); if (!by.has(k)) by.set(k, r); }
  return [...by.values()];
}
// 서버/CLI 공용: 표준=LCU(450)+로컬누적, 증강=db.json(시즌2 본섭+PBE). leagueOff=LCU 못읽음(표준 비는 이유 구분용).
export async function getRecords({ mock = false } = {}) {
  const m = await champMaps();
  if (mock) {
    const stat = mockGames("MOCK").map(g => myStat(g, "MOCK")).filter(Boolean);
    return { who: "(테스트)", leagueOff: false, augmentQueue: Q_AUGMENT,
      standard: buildRecord(stat.filter(s => s.queueId === Q_STANDARD), m),
      augment: buildRecord(stat.filter(s => s.queueId === Q_AUGMENT), m) };
  }
  const db = readDb();
  let who = "나", leagueOff = true, stdGames = readStdCache(), augGames = readAugCache();
  const lcuInfo = { found: false, total: 0, byQueue: {}, standard: 0, error: null };
  try {
    const lcu = readLockfile();
    if (lcu) {
      leagueOff = false; lcuInfo.found = true;
      const me = await lcuGet(lcu, "/lol-summoner/v1/current-summoner");
      who = me.gameName || me.displayName || "나";
      const all = (await getMatches(lcu, 2000)).map(g => myStat(g, me.puuid)).filter(Boolean); // 깊게 긁어 일반 최대수집
      lcuInfo.total = all.length;
      for (const s of all) lcuInfo.byQueue[s.queueId] = (lcuInfo.byQueue[s.queueId] || 0) + 1; // 큐별 진단(진짜 버그/데이터없음 구분)
      const fresh = all.filter(s => s.queueId === Q_STANDARD);
      lcuInfo.standard = fresh.length;
      stdGames = mergeStd(stdGames, fresh); writeStdCache(stdGames); // 누적 보존
      const freshAug = all.filter(s => s.queueId === Q_AUGMENT); augGames = mergeStd(augGames, freshAug); writeAugCache(augGames); // 증강도 누적(다른 PC db.json 폴백용)
    }
  } catch (e) { lcuInfo.error = String((e && e.message) || e); }
  const standard = buildRecord(stdGames, m);
  // who 확정(League 꺼졌으면 db.json 소유자명으로) — 아래 소유자 판정에 필요
  if (who === "나" && db && Array.isArray(db.accounts) && db.accounts.length) {
    who = (db.accounts.find(a => /강준치/.test(a.gameName || "")) || db.accounts.find(a => a.gameName) || {}).gameName || "나";
  }
  // 증강 개인기록 = db.json 전체 히스토리 + LCU 실시간 2400 병합(gameCreation 중복제거). 새 게임이 db.json 갱신 전에도 즉시 반영(동결 버그 해소).
  const dbOwner = !!(db && Array.isArray(db.accounts) && db.accounts.length);
  const isOwner = dbOwner && (!lcuInfo.found || (db.accounts || []).some(a => a.gameName && a.gameName === who)); // 다른 PC/다른 유저면 db.json(방송인) 데이터 안 섞음
  const augDbRows = isOwner ? augmentRowsFromDb(db) : []; // 방송인일 때만 전체 히스토리
  const augLcuRows = augGames.filter(r => (r.t || 0) >= AUG_S2_BOUNDARY_MS); // 현재 LCU 유저의 시즌2 증강(최신, 누적)
  const augMerged = dedupeByTime([...augDbRows, ...augLcuRows]); // db 우선 + LCU의 새 게임 추가
  const augment = augMerged.length ? buildRecord(augMerged, m) : buildRecord(augGames, m);
  return { who, leagueOff, lcuInfo, augmentQueue: Q_AUGMENT, standard, augment };
}

async function main() {
  try {
    const r = await getRecords({ mock: process.argv.includes("--mock") });
    console.log(`\n=== ${r.who} 님 — 칼바람 개인기록 (LCU·로컬, Riot API키 0) ===`);
    printRecord("일반 칼바람 (올해)", r.standard, "올해 일반칼바람 기록 없음.");
    printRecord("증강 칼바람 (증바람)", r.augment, `증바람(큐${Q_AUGMENT}) 기록 0 — 큐ID 다르면 Q_AUGMENT 수정.`);
    console.log("");
  } catch (e) { console.error(e.code === "LEAGUE_OFF" ? "❌ League 실행 후 다시 / 또는 LCU_LOCKFILE 지정." : (e.stack || e)); process.exit(1); }
}
if (process.argv[1] && path.resolve(fileURLToPath(import.meta.url)) === path.resolve(process.argv[1])) main();
