import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import yaml from 'js-yaml';
import { warn } from './log.js';

const ROLES_DIR = path.join(fileURLToPath(import.meta.url), '..', 'prompts', 'roles');

/**
 * 解析單一角色 Markdown 檔內容，拆出前置 YAML frontmatter 與本文。
 *
 * 會先將 CRLF 正規化為 LF，再以 `---` 分隔線切出 frontmatter（徽章、代表色、
 * 面向、個性等欄位）與其後的本文（審查重點 / 裁決準則）。frontmatter 欄位會
 * 被攤平到回傳物件，本文則放入 `body`（已去除頭尾空白）。
 *
 * @param {string} content - 角色 `.md` 檔的完整文字內容。
 * @returns {{ name?: string, side?: string, focus?: string, badge?: string,
 *   color?: string, personality?: string, body: string,
 *   [key: string]: unknown }} 合併 frontmatter 與本文後的角色物件。
 * @throws {Error} 當內容缺少合法 `---` frontmatter 區塊時拋出「角色檔缺少 frontmatter」。
 * @throws {import('js-yaml').YAMLException} 當 frontmatter 不是合法 YAML 時（由 `yaml.load` 拋出，未攔截）。
 *
 * @remarks 純字串處理，無任何檔案 IO；frontmatter 中若自帶 `body` 欄位會被本文覆蓋。
 */
export function parseRoleFile(content) {
  const normalized = content.replace(/\r\n/g, '\n');
  const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
  if (!match) throw new Error('角色檔缺少 frontmatter');
  const meta = yaml.load(match[1]) || {};
  return { ...meta, body: match[2].trim() };
}

let cachedRoles = null;

/**
 * 讀取並解析 `ROLES_DIR` 下所有角色 `.md` 檔，依檔名排序後回傳角色陣列。
 *
 * 結果快取於模組層級（`cachedRoles`），同一程序生命週期內只讀檔一次；之後即使
 * 角色檔有變動也不會重新載入，需重啟程序才會生效。單一檔案解析失敗（壞 YAML、
 * 缺 frontmatter 等）只記錄警告並略過，不會中斷其他角色的載入。
 *
 * @returns {Array<ReturnType<typeof parseRoleFile>>} 已解析的角色物件陣列（依檔名排序）。
 *
 * @remarks 模組私有函式；使用同步檔案 IO。目錄不存在或無權限時，`fs.readdirSync`
 *   會在容錯範圍外拋出錯誤。
 */
function readRoleFiles() {
  if (cachedRoles) return cachedRoles;
  const roles = [];
  for (const f of fs.readdirSync(ROLES_DIR).filter(f => f.endsWith('.md')).sort()) {
    try {
      roles.push(parseRoleFile(fs.readFileSync(path.join(ROLES_DIR, f), 'utf8')));
    } catch (e) {
      warn(`角色檔解析失敗，已略過: ${f}（${e.message}）`);
    }
  }
  cachedRoles = roles;
  return cachedRoles;
}

/**
 * 載入所有「攻擊方」角色（frontmatter `side === 'attack'`），依檔名排序。
 *
 * 供 Step3 產生 findings 階段使用。防守方角色（如 Paladin）不在回傳之列，
 * 其裁決邏輯由去重 / 誤報過濾流程處理。
 *
 * @returns {Array<ReturnType<typeof parseRoleFile>>} 攻擊方角色物件陣列。
 *
 * @remarks 透過 `readRoleFiles` 取得快取後的全部角色再過濾，首次呼叫會觸發檔案讀取。
 */
export function loadRoles() {
  return readRoleFiles().filter(r => r.side === 'attack');
}

/**
 * 依 frontmatter `name` 取得單一角色（比對不分大小寫），找不到回傳 `null`。
 *
 * 不分攻擊方 / 防守方，所有已成功載入的角色皆可查得。
 *
 * @param {string} name - 角色名稱（大小寫不拘）。
 * @returns {ReturnType<typeof parseRoleFile> | null} 對應角色物件，無對應時為 `null`。
 *
 * @remarks 透過 `readRoleFiles` 取得快取角色清單，首次呼叫會觸發檔案讀取。
 */
export function loadRole(name) {
  const target = String(name).toLowerCase();
  return readRoleFiles().find(r => String(r.name).toLowerCase() === target) || null;
}

/**
 * 由攻擊方角色定義組出其分析用 system prompt。
 *
 * 套用角色的徽章、名稱、面向（focus，缺省為「綜合」）、個性（personality，可選）
 * 與審查重點本文（body），並附上固定指示：分析 Git Diff 僅針對新增/修改處找問題，
 * 並以固定 JSON 陣列格式（level / role / location / problem / suggestion）回傳 findings，
 * 強制每條問題帶 `檔案路徑:行號`。
 *
 * @param {ReturnType<typeof parseRoleFile>} role - 攻擊方角色物件（需含 `name`、`body`；`badge`/`focus`/`personality` 可選）。
 * @returns {string} 組裝完成的多行 system prompt 文字。
 * @throws {TypeError} 當 `role` 為 `null`/`undefined` 時（未做防呆，存取屬性即拋出）。
 *
 * @remarks 純字串組裝，無副作用；空白分段行在串接前會被過濾移除。
 */
export function buildAnalysisPrompt(role) {
  return [
    `你是 ${role.badge ? role.badge + ' ' : ''}${role.name}，負責「${role.focus || '綜合'}」面向的程式碼審查（攻擊方）。`,
    role.personality ? `個性：${role.personality}` : '',
    '',
    role.body,
    '',
    '---',
    '',
    '請分析以下 Git Diff，只針對新增/修改處，依你的面向找出所有問題。',
    '回傳 JSON 陣列，每個問題格式如下：',
    '{',
    '  "level": "critical|warning|info",',
    `  "role": "${role.name}",`,
    '  "location": "檔案路徑:行號（行號為必填，例如 app/foo.js:42）",',
    '  "problem": "繁體中文（台灣用語）說明審查員認為這裡有問題的原因，不要只填檔案路徑或行號",',
    '  "suggestion": "繁體中文（台灣用語）的具體修改建議"',
    '}',
    '',
    '等級定義：',
    '- critical：嚴重且應立即處理的問題',
    '- warning：建議修正的問題',
    '- info：可選的改善建議',
    '',
    'location 規則（務必遵守）：',
    '- **每一條問題都必須帶行號**，格式一律為 `檔案路徑:行號`（單一行號，例如 `app/foo.js:42`）。',
    '- 嚴禁只給檔名而省略行號；行號請取該問題在 Git Diff 新增/修改處的實際行號。',
    '- 一條問題只對應一個檔案與一個行號，不要用逗號列多個檔案。',
    '',
    '只回傳 JSON 陣列，不要有其他文字。如果沒有問題，回傳空陣列 []。',
  ].filter(l => l !== '').join('\n');
}

/**
 * 組出「補行號」用的 system prompt。
 *
 * 用於某角色先前提出的 finding 其 `location` 只有檔名、缺行號的情境：請 LLM 對照
 * 該檔 Git Diff 找出問題對應的實際行號，並只回 `{"line": 數字}`（找不到回 `{"line": 0}`）。
 *
 * @param {ReturnType<typeof parseRoleFile> | null | undefined} [role] - 角色物件；可省略或為 null，
 *   此時名稱退回 `'AI Review'` 且不帶徽章與面向子句。
 * @returns {string} 組裝完成的多行 system prompt 文字。
 *
 * @remarks 使用選擇性串接（`?.`），對 `role` 為空值具防呆，不會拋出例外。
 */
export function buildLocateLinePrompt(role) {
  const name = role?.name || 'AI Review';
  const badge = role?.badge ? `${role.badge} ` : '';
  return [
    `你是 ${badge}${name}${role?.focus ? `（負責「${role.focus}」面向）` : ''}。`,
    '你先前提出了一個問題，但 location 只給了檔名、沒有行號。請對照下方提供的該檔案 Git Diff，找出這個問題對應的**實際行號**（新增/修改處在該檔案中的行號）。',
    '只回傳 JSON 物件：{"line": 數字}，不要有其他文字。若 diff 中確實找不到對應行，回傳 {"line": 0}。',
  ].join('\n');
}

/**
 * 由防守方角色定義組出「單條 finding 誤報裁決」用的 system prompt。
 *
 * `role` 存在時套用其徽章、名稱、面向（focus，缺省「裁決」）、個性與裁決準則本文（body）；
 * `role` 為空值時退回固定的通用裁判 persona（🛡️ Paladin 聖騎士）。prompt 要求對一條
 * 攻擊方 finding 判定「成立 / 誤報」，並只回 `{"verdict", "reason"}`；無法確定時一律回
 * `"confirmed"`（寧可保留、不冤枉）。
 *
 * @param {ReturnType<typeof parseRoleFile> | null | undefined} role - 防守方角色物件；為空值時改用通用裁判 persona。
 * @param {string} [exclusionHint=''] - 額外的排除 / 已知誤報提示文字；為空字串時該行會被略過。
 * @returns {string} 組裝完成的多行 system prompt 文字。
 *
 * @remarks 純字串組裝，無副作用；空白分段行在串接前會被過濾移除。
 */
export function buildVerdictPrompt(role, exclusionHint = '') {
  const persona = role
    ? [
        `你是 ${role.badge ? role.badge + ' ' : ''}${role.name}，負責「${role.focus || '裁決'}」的程式碼審查裁決（防守方）。`,
        role.personality ? `個性：${role.personality}` : '',
        '',
        role.body,
      ]
    : ['你是 🛡️ Paladin（聖騎士），公正的裁判。不冤枉無辜的程式碼，也不放水。'];

  return [
    ...persona,
    '',
    '---',
    '',
    '以下提供一條攻擊方的 finding（JSON）。請依你的裁決準則與原始碼脈絡，判斷它是「成立」還是「誤報／不適用」（例如：已正確使用 secrets、CI/CD 必要權限、他處已妥善處理、語義其實正確）。',
    exclusionHint,
    '只回傳 JSON 物件：{"verdict": "confirmed" | "false_positive", "reason": "繁體中文（台灣用語）理由"}，不要有其他文字。無法確定時一律回 "confirmed"（不冤枉、寧可保留）。',
  ].filter(l => l !== '').join('\n');
}

/**
 * 由角色陣列產生「AI Code Review 團隊」介紹用的 Markdown 表格。
 *
 * 表格含三欄：角色（粗體，含徽章）、面向（focus）、個性（personality）；缺省欄位以空字串呈現。
 * 通常用於 PR 留言 / 審查報告開頭呈現參與審查的角色陣容。
 *
 * @param {Array<ReturnType<typeof parseRoleFile>>} roles - 角色物件陣列（每個可含 `badge`/`name`/`focus`/`personality`）。
 * @returns {string} Markdown 格式的多行表格字串。
 * @throws {TypeError} 當 `roles` 非可迭代值（如 `null`/`undefined`）時，`for...of` 會拋出。
 *
 * @remarks 純字串組裝，無副作用；傳入空陣列會得到只有標題與表頭的表格。
 */
export function getRoleIntro(roles) {
  const lines = [
    '## 🤖 AI Code Review 團隊', '',
    '| 👤 角色 | 🎯 面向 | 🧠 個性 |',
    '|--------|--------|--------|',
  ];
  for (const r of roles) {
    const badge = r.badge ? `${r.badge} ` : '';
    lines.push(`| **${badge}${r.name}** | ${r.focus || ''} | ${r.personality || ''} |`);
  }
  return lines.join('\n');
}
