/**
 * 회의 전사/원문에 등장한 토큰만 임직원 명단과 퍼지 매칭해,
 * 회의록 LLM 요청에「이번 텍스트 한정 표기 통일」블록만 붙인다(전체 명단을 시스템 프롬프트에 넣지 않음).
 */
const fs = require("fs");
const path = require("path");

const DEFAULT_NAMES_PATH = path.join(__dirname, "..", "data", "meeting-employee-names.txt");

/** 회의 맥락에서 이름이 아닌 짧은 단어(오탐 감소) */
const STOPWORDS = new Set([
  "대표님",
  "팀장",
  "담당",
  "참석",
  "회의",
  "논의",
  "결정",
  "검토",
  "파일럿",
  "라이선스",
  "도입",
  "확산",
  "목표",
  "일정",
  "비용",
  "운영",
  "현황",
  "보고",
  "전사",
  "고객",
  "프로젝트",
  "진행",
  "확인",
  "준비",
  "완료",
  "다음",
  "오늘",
  "내일",
  "이번",
  "주간",
  "월간",
  "분기",
  "연간",
  "슬랙",
  "노션",
  "클로드",
  "커서",
  "구글",
  "엔터프라이즈",
  "계정",
  "사용",
  "적용",
  "전환",
  "시연",
  "발표",
  "자료",
  "문서",
  "보안",
  "네트워크",
  "장비",
  "미정",
  "즉시",
  "가능",
  "필요",
  "관련",
  "내용",
  "사항",
  "기준",
  "방안",
  "계획",
  "요청",
  "제안",
  "결과",
  "챔피언",
  "담당자",
  "참석자",
  "목적",
  "배경",
  "이슈",
  "리스크",
  "효과",
  "대시보드",
  "지표",
  "활용",
  "추가",
  "삭제",
  "수정",
  "작성",
  "제출",
  "공유",
  "연동",
  "설정",
  "구매",
  "계약",
  "예산",
  "절감",
  "확정",
  "선발",
  "명단",
  "기한",
  "우선",
  "순위",
  "단계",
  "초기",
  "전체",
  "일부",
  "해당",
  "각각",
  "모든",
  "기타",
]);

let _rosterCache = null;
let _rosterCachePath = null;
let _rosterCacheMtime = 0;

function resolveNamesPath() {
  const env = (process.env.MEETING_EMPLOYEE_NAMES_FILE || "").trim();
  if (env) {
    return path.isAbsolute(env) ? env : path.join(process.cwd(), env);
  }
  return DEFAULT_NAMES_PATH;
}

/**
 * 한 줄 한 이름(또는 쉼표 구분). # 으로 시작하는 줄·빈 줄 무시.
 * @returns {string[]}
 */
function loadEmployeeRoster() {
  if ((process.env.MEETING_NAME_NORMALIZATION || "1").trim() === "0") {
    return [];
  }
  const filePath = resolveNamesPath();
  try {
    const st = fs.statSync(filePath);
    if (_rosterCache != null && _rosterCachePath === filePath && st.mtimeMs === _rosterCacheMtime) {
      return _rosterCache;
    }
    const raw = fs.readFileSync(filePath, "utf8");
    const names = [];
    const seen = new Set();
    for (const line of raw.split(/\r?\n/)) {
      const t = line.trim();
      if (!t || t.startsWith("#")) continue;
      for (const part of t.split(/[,，]/)) {
        const clean = part.replace(/\s+/g, "").replace(/·/g, "").trim();
        if (!clean || seen.has(clean)) continue;
        if (!/^[가-힣]{2,5}$/.test(clean)) continue;
        seen.add(clean);
        names.push(clean);
      }
    }
    _rosterCache = names;
    _rosterCachePath = filePath;
    _rosterCacheMtime = st.mtimeMs;
    return names;
  } catch {
    return [];
  }
}

function invalidateRosterCache() {
  _rosterCache = null;
  _rosterCachePath = null;
  _rosterCacheMtime = 0;
}

/**
 * @param {string} a
 * @param {string} b
 * @returns {number}
 */
function levenshtein(a, b) {
  const m = a.length;
  const n = b.length;
  if (m === 0) return n;
  if (n === 0) return m;
  const v0 = new Array(n + 1);
  const v1 = new Array(n + 1);
  for (let j = 0; j <= n; j++) v0[j] = j;
  for (let i = 0; i < m; i++) {
    v1[0] = i + 1;
    for (let j = 0; j < n; j++) {
      const cost = a.charAt(i) === b.charAt(j) ? 0 : 1;
      v1[j + 1] = Math.min(v1[j] + 1, v0[j + 1] + 1, v0[j] + cost);
    }
    for (let j = 0; j <= n; j++) v0[j] = v1[j];
  }
  return v0[n];
}

/**
 * @param {string} text
 * @returns {Set<string>}
 */
function extractHangulNameLikeTokens(text) {
  const s = String(text || "");
  const out = new Set();
  const parts = s.split(/[\s,，.。:：;；·\[\]()（）「」『』"'`\-_/\\|\n\r\t]+/);
  for (const p of parts) {
    const t = p.trim();
    if (!t) continue;
    if (!/^[가-힣]{2,4}$/.test(t)) continue;
    if (STOPWORDS.has(t)) continue;
    out.add(t);
  }
  return out;
}

/**
 * 토큰별 명단에서 최소 편집거리 후보. 동점 다수면 스킵(모호).
 * @param {string} token
 * @param {string[]} roster
 * @returns {{ name: string, dist: number } | null}
 */
function bestRosterMatch(token, roster) {
  const maxDist = token.length <= 2 ? 1 : token.length <= 3 ? 1 : 2;
  let bestDist = Infinity;
  const winners = [];
  for (const r of roster) {
    if (Math.abs(r.length - token.length) > maxDist) continue;
    const d = levenshtein(token, r);
    if (d > maxDist) continue;
    if (d < bestDist) {
      bestDist = d;
      winners.length = 0;
      winners.push(r);
    } else if (d === bestDist) {
      winners.push(r);
    }
  }
  if (bestDist === Infinity || bestDist === 0) return null;
  const uniq = [...new Set(winners)];
  if (uniq.length !== 1) return null;
  return { name: uniq[0], dist: bestDist };
}

/**
 * 전사/원문에 대해 전사 표기 → 표준 표기 매핑 생성
 * @param {string} transcript
 * @param {string[]} roster
 * @returns {Array<{ from: string, to: string }>}
 */
function buildNormalizationMappings(transcript, roster) {
  if (!roster.length) return [];
  const tokens = extractHangulNameLikeTokens(transcript);
  /** @type {Map<string, string>} */
  const fromTo = new Map();

  for (const t of tokens) {
    if (roster.includes(t)) continue;
    const hit = bestRosterMatch(t, roster);
    if (!hit || hit.dist === 0) continue;
    if (hit.name === t) continue;
    const existing = fromTo.get(t);
    if (existing && existing !== hit.name) continue;
    fromTo.set(t, hit.name);
  }

  return [...fromTo.entries()]
    .map(([from, to]) => ({ from, to }))
    .sort((a, b) => a.from.localeCompare(b.from, "ko"));
}

/**
 * LLM user 메시지 앞에 붙일 짧은 블록(매핑 없으면 빈 문자열)
 * @param {string} transcript
 * @param {string[]} [roster]
 */
function buildNameNormalizationUserPrefix(transcript, roster) {
  const r = roster || loadEmployeeRoster();
  const maps = buildNormalizationMappings(transcript, r);
  if (!maps.length) {
    return "";
  }
  const lines = maps.map((m) => `- 전사·원문에 「${m.from}」로 보이면, 회의록 인명은 「${m.to}」로 통일합니다.`);
  return (
    "【이번 원문/전사 한정 · 임직원 표기 통일】\n" +
    "아래 줄은 **이번 입력 텍스트에 실제로 등장한 토큰**만 명단과 대조한 결과입니다. 해당할 때만 표기를 바꾸고, 전사에 없는 사람을 새로 만들지 마세요.\n" +
    lines.join("\n") +
    "\n\n---\n\n"
  );
}

module.exports = {
  loadEmployeeRoster,
  buildNormalizationMappings,
  buildNameNormalizationUserPrefix,
  invalidateRosterCache,
  resolveNamesPath,
  DEFAULT_NAMES_PATH,
};
