import fs from 'fs';
import path from 'path';
import { postComment, postPullReviewComment, postPullReview } from './gitea.js';
import { FINDINGS_PATH } from './config.js';
import { ok, line, warn } from './log.js';

const LEVEL_EMOJI = { critical: '🔴', warning: '🟡', info: '🔵' };
const LEVEL_LABEL = { critical: '嚴重', warning: '警告', info: '建議' };
const LEVEL_ORDER = ['critical', 'warning', 'info'];
// 預先把等級對應到排序索引，bySeverity 排序時直接 O(1) 取值，省去每次比較的 includes + indexOf 掃描。
const LEVEL_RANK = new Map(LEVEL_ORDER.map((level, index) => [level, index]));

/**
 * 將單一 finding 格式化為 Markdown 表格的一列（等級｜審查員｜位置｜建議）。
 *
 * @param {{ level?: string, role?: string, location?: string, suggestion?: string }} f
 *   單筆審查問題物件。`level` 若不在 critical/warning/info 之內，emoji 留空、標籤回退為原始 level 值；
 *   `role`、`location`、`suggestion` 直接內嵌字串（未定義時會輸出 undefined 字樣）。傳入 null/undefined 時回傳空字串（已防呆，不會拋例外）。
 * @returns {string} 形如 `| 🔴 嚴重 | role | location | suggestion |` 的表格列字串；`f` 為空值時回傳空字串。
 * @remarks 內部輔助函式，供 {@link buildTable} 逐列組裝表格使用，本身不含換行。
 */
function findingRow(f) {
  if (!f) return '';
  return `| ${LEVEL_EMOJI[f.level] || ''} ${LEVEL_LABEL[f.level] || f.level} | ${f.role} | ${f.location} | ${f.suggestion} |`;
}

/**
 * 將多筆 findings 組成完整的 Markdown 表格（含表頭與分隔列）。
 *
 * @param {Array<object>} findings 審查問題陣列；空陣列時僅輸出表頭與分隔列。每筆物件格式見 {@link findingRow}。
 * @returns {string} 完整的 Markdown 表格字串（表頭：等級｜審查員｜位置｜建議）。
 * @remarks 內部輔助函式，供發布舊問題、新問題（非嚴重）、單筆嚴重問題等 comment 內文使用。
 */
function buildTable(findings) {
  const rows = findings.map(findingRow).join('\n');
  return `| 等級 | 審查員 | 位置 | 建議 |\n|------|--------|------|------|\n${rows}`;
}

/**
 * 取得 finding 等級的人類可讀字串（emoji + 中文標籤），已去除頭尾空白。
 *
 * @param {{ level?: string }} f 單筆審查問題物件。`level` 查無對應時 emoji 留空、標籤回退為原始 level 值。
 * @returns {string} 例如 `🔴 嚴重`；無法對應時回退為原始 level 字串（無 emoji）。
 * @remarks 內部輔助函式，供 {@link inlineCommentBody} 與 {@link reviewCommentBody} 組裝 comment 內文使用。
 */
const levelText = f => `${LEVEL_EMOJI[f.level] || ''} ${LEVEL_LABEL[f.level] || f.level}`.trim();
/**
 * findings 排序比較器：先依嚴重等級（critical < warning < info < 其他），同級再依 location 字串排序。
 *
 * @param {{ level?: string, location?: string }} a 比較項 A。
 * @param {{ level?: string, location?: string }} b 比較項 B。
 * @returns {number} 負值代表 a 排在 b 之前，正值代表之後，0 代表相等（供 Array.prototype.sort 使用）。
 * @remarks 不在 LEVEL_ORDER 內的等級一律視為最低優先（排在最後）；location 未定義時以空字串參與比較，因此排序穩定不會丟例外。
 */
const bySeverity = (a, b) => {
  const aLevel = LEVEL_RANK.has(a.level) ? LEVEL_RANK.get(a.level) : LEVEL_ORDER.length;
  const bLevel = LEVEL_RANK.has(b.level) ? LEVEL_RANK.get(b.level) : LEVEL_ORDER.length;
  if (aLevel !== bLevel) return aLevel - bLevel;
  return String(a.location || '').localeCompare(String(b.location || ''));
};

/**
 * 解析 finding 的 location 取出檔案與行號，供行內 comment 標註使用。
 * 支援 "file:19" 與 "file:70-82"（取起始行）；無行號或含多個檔案（逗號）時回傳 null。
 */
export function parseLocation(location) {
  if (typeof location !== 'string') return null;
  const trimmed = location.trim();
  if (trimmed.includes(',')) return null;
  const match = trimmed.match(/^(.+?):(\d+)(?:-\d+)?$/);
  if (!match) return null;
  const line = Number(match[2]);
  return line > 0 ? { file: match[1], line } : null;
}

/** 行內 comment 內容：等級／審查員／建議 */
function inlineCommentBody(f) {
  return `**等級**：${levelText(f)}\n**審查員**：${f.role}\n**建議**：${f.suggestion}`;
}

/**
 * 從 finding 取出問題原因描述，依序嘗試多個可能欄位。
 *
 * @param {{ problem?: string, reason?: string, description?: string, detail?: string, title?: string, message?: string }} f
 *   單筆審查問題物件；依序取第一個有值（truthy）的欄位。所有欄位皆無值時回退為「未提供問題原因」。
 * @returns {string} 問題原因字串。
 * @remarks 內部輔助函式，供 {@link reviewCommentBody} 組裝 comment 內文使用，用以容忍不同來源 finding 的欄位命名差異。
 */
function problemText(f) {
  return f.problem || f.reason || f.description || f.detail || f.title || f.message || '未提供問題原因';
}

/**
 * 產生 review comment 內文（嚴重等級／審查員／問題／建議四行）。
 *
 * @param {{ level?: string, role?: string, suggestion?: string, problem?: string, reason?: string, description?: string, detail?: string, title?: string, message?: string }} f
 *   單筆審查問題物件。
 * @returns {string} 多行 Markdown 字串。
 * @remarks 內部輔助函式，供 {@link toReviewComment} 產生批次 review comment 內文使用。比 {@link inlineCommentBody} 多了「問題」一行。
 */
function reviewCommentBody(f) {
  return [
    `**嚴重等級**：${levelText(f)}`,
    `**審查員**：${f.role}`,
    `**問題**：${problemText(f)}`,
    `**建議**：${f.suggestion}`,
  ].join('\n');
}

/**
 * 計算陣列中符合條件的元素數量。
 *
 * @param {Array<T>} findings 待計數的陣列。
 * @param {(item: T) => boolean} predicate 判斷函式；回傳 true 的元素計入。
 * @returns {number} 符合條件的元素數量。
 * @template T
 * @remarks 內部輔助函式，供 {@link formatFindingsStats} 與 {@link formatFindingsStatsLine} 統計各等級筆數使用。
 */
function countBy(findings, predicate) {
  return findings.filter(predicate).length;
}

/**
 * 過濾出新問題（is_new 不等於 false 者）。
 *
 * @param {Array<{ is_new?: boolean }>} findings 審查問題陣列。
 * @returns {Array<object>} 新問題子集合。
 * @remarks 內部輔助函式。判定採 `is_new !== false`，因此未設定 is_new（undefined）的 finding 也視為新問題；僅明確 `is_new === false` 會被排除。供統計與 review 發布判斷使用。
 */
function newFindingsOnly(findings) {
  return findings.filter(f => f.is_new !== false);
}

/**
 * 判斷 finding 等級是否無法歸入 critical/warning/info（無法標示）。
 *
 * @param {{ level?: string }} f 單筆審查問題物件。
 * @returns {boolean} 等級不在 LEVEL_ORDER 內時為 true。
 * @remarks 內部輔助函式，供統計表的「⚪ 無法標示」欄位計數使用。
 */
const isUnclassified = f => !LEVEL_ORDER.includes(f.level);

/**
 * 產生 findings 統計的 Markdown 表格（新問題／舊問題 × 嚴重/警告/建議/無法標示）。
 *
 * @param {Array<{ is_new?: boolean, level?: string }>} findings 審查問題陣列；
 *   `is_new === false` 計入舊問題，其餘計入新問題。
 * @returns {string} 含表頭、分隔列與兩資料列的 Markdown 表格字串。
 * @remarks 供 {@link buildReviewSummary} 組裝 review 統計本文使用。空陣列時仍輸出表格（各欄為 0 筆）。
 */
export function formatFindingsStats(findings) {
  const oldFindings = findings.filter(f => f.is_new === false);
  const newFindings = newFindingsOnly(findings);
  const row = (label, items) => `| ${label} | ${countBy(items, f => f.level === 'critical')} 筆 | ${countBy(items, f => f.level === 'warning')} 筆 | ${countBy(items, f => f.level === 'info')} 筆 | ${countBy(items, isUnclassified)} 筆 |`;

  return [
    '| 類型 | 🔴 嚴重 | 🟡 警告 | 🔵 建議 | ⚪ 無法標示 |',
    '| --- | --- | --- | --- | --- |',
    row('新問題', newFindings),
    row('舊問題', oldFindings),
  ].join('\n');
}

/**
 * 產生 findings 統計的單行文字摘要（供 log 使用）。
 *
 * @param {Array<{ is_new?: boolean, level?: string }>} findings 審查問題陣列；
 *   `is_new === false` 計入舊問題，其餘計入新問題。
 * @returns {string} 形如 `新: 嚴重1 / 警告0 / 建議2 / 無法標示0；舊: ...` 的單行字串。
 * @remarks 供 {@link postFindingsReview} 在 log 輸出統計時呼叫。內容與 {@link formatFindingsStats} 一致，僅格式為單行純文字。
 */
export function formatFindingsStatsLine(findings) {
  const oldFindings = findings.filter(f => f.is_new === false);
  const newFindings = newFindingsOnly(findings);
  const row = items => `嚴重${countBy(items, f => f.level === 'critical')} / 警告${countBy(items, f => f.level === 'warning')} / 建議${countBy(items, f => f.level === 'info')} / 無法標示${countBy(items, isUnclassified)}`;
  return `新: ${row(newFindings)}；舊: ${row(oldFindings)}`;
}

/**
 * 組裝 review 本文：標題 + findings 統計表 +（選擇性）用量區塊。
 *
 * @param {Array<object>} findings 用於統計的審查問題陣列。
 * @param {string} [usageSection=''] 額外附加的用量／token 統計區塊；空字串時不附加。
 * @returns {string} review 本文（Markdown）。
 * @remarks 內部輔助函式，供 {@link postFindingsReview} 產生整批 review 的 body。
 */
function buildReviewSummary(findings, usageSection = '') {
  const parts = [
    '## AI Code Review 統計',
    '',
    formatFindingsStats(findings),
  ];
  if (usageSection) parts.push('', usageSection);
  return parts.join('\n');
}

/**
 * 將 finding 轉為 Gitea review comment 物件（含檔案路徑、內文、行號）。
 *
 * @param {{ location?: string, level?: string, role?: string, suggestion?: string }} f 單筆審查問題物件。
 * @returns {{ path: string, body: string, new_position: number } | null}
 *   可定位時回傳 comment 物件；location 無法解析出行號時回傳 null。
 * @remarks 內部輔助函式，供 {@link postFindingsReview} 在 map 後以 `filter(Boolean)` 濾除無法定位的項目。
 */
function toReviewComment(f) {
  const loc = parseLocation(f.location);
  if (!loc) return null;
  return {
    path: loc.file,
    body: reviewCommentBody(f),
    new_position: loc.line,
  };
}

/**
 * 發布單一 Gitea review：
 * - summaryFindings 只用來統計本文數字（含新舊問題）
 * - commentFindings 用來產生 review comments，並依嚴重等級排序；
 *   只為新問題加上行內標註，舊問題（is_new === false）僅計入統計、不再重複標註檔案與行數
 */
export async function postFindingsReview(findings, deps = {}) {
  const {
    postReview = postPullReview,
    postInline = postPullReviewComment,
    postIssue = postComment,
    summaryFindings = findings,
    commentFindings = findings,
    usageSection = '',
  } = deps;
  const sortedComments = [...commentFindings].sort(bySeverity);
  const comments = sortedComments.filter(f => f.is_new !== false).map(toReviewComment).filter(Boolean);
  const body = buildReviewSummary(summaryFindings, usageSection);
  try {
    await postReview({ body, comments });
  } catch (e) {
    warn(`整批 review 發布失敗，改用 summary + 逐筆行內 comment: ${e.message}`);
    try {
      await postReview({ body, comments: [] });
    } catch (summaryErr) {
      warn(`review summary 發布失敗，改用一般 comment: ${summaryErr.message}`);
      await postIssue(body);
    }
    for (const comment of comments) {
      try {
        await postInline({ path: comment.path, line: comment.new_position, body: comment.body });
      } catch (commentErr) {
        warn(`行內 review comment 發布失敗（略過）: ${comment.path}:${comment.new_position} error=${commentErr.message}`);
      }
    }
  }
  ok(`review 發布: summary=${summaryFindings.length} total=${sortedComments.length} commentable=${comments.length}`);
  line(`review summary 統計: ${formatFindingsStatsLine(summaryFindings)}`);
  line(`review comments 統計: ${formatFindingsStatsLine(sortedComments)}`);
}

/**
 * 寫入 findings.json。
 * 預設寫到 workspace；若提供 mirrorDir，則同步寫入另一份供 repo commit 使用。
 */
export function saveFindings(workspace, findings, mirrorDir = null) {
  const targets = [workspace];
  if (mirrorDir && mirrorDir !== workspace) targets.push(mirrorDir);

  for (const targetDir of targets) {
    const fullPath = path.join(targetDir, FINDINGS_PATH);
    fs.mkdirSync(path.dirname(fullPath), { recursive: true });
    fs.writeFileSync(fullPath, JSON.stringify(findings, null, 2) + '\n', 'utf8');
    ok(`findings 寫入: ${fullPath} (${findings.length} 筆)`);
  }
}

/**
 * 發布所有舊問題 comment（一次發布，依等級排序）
 */
export async function postOldFindingsComment(findings) {
  const old = findings.filter(f => !f.is_new);
  if (old.length === 0) {
    line('無舊問題，跳過');
    return;
  }
  const body = `## 📋 舊有未解決問題（${old.length} 筆）\n\n${buildTable(old)}`;
  await postComment(body);
  ok(`舊問題 comment 發布 (${old.length} 筆)`);
}

/**
 * 發布新問題中非 critical 的 comment（一次發布）
 */
export async function postNewNonCriticalComment(findings) {
  const items = findings.filter(f => f.is_new && f.level !== 'critical');
  if (items.length === 0) {
    line('無新的非嚴重問題，跳過');
    return;
  }
  const body = `## 🔍 新發現問題（${items.length} 筆）\n\n${buildTable(items)}`;
  await postComment(body);
  ok(`新問題（非嚴重）comment 發布 (${items.length} 筆)`);
}

/**
 * 每個新 critical 問題各發一個 comment。
 * 優先用 Gitea 行內 review comment 標註問題檔案與行數（內容為等級／審查員／建議）；
 * 若 location 無法解析出行號，或行內發布失敗（例如該行不在 diff 範圍），則降級為一般 comment。
 */
export async function postNewCriticalComments(findings, deps = {}) {
  const { postInline = postPullReviewComment, postIssue = postComment } = deps;
  const criticals = findings.filter(f => f.is_new && f.level === 'critical');
  if (criticals.length === 0) {
    line('無新的嚴重問題，跳過');
    return;
  }
  for (const f of criticals) {
    const loc = parseLocation(f.location);
    if (loc) {
      try {
        await postInline({ path: loc.file, line: loc.line, body: inlineCommentBody(f) });
        ok(`嚴重問題 行內 comment 發布: [${f.role}] ${loc.file}:${loc.line}`);
        continue;
      } catch (e) {
        warn(`行內 comment 發布失敗，改用一般 comment: [${f.role}] ${f.location} error=${e.message}`);
      }
    }
    await postIssue(`## 🚨 嚴重問題\n\n${buildTable([f])}`);
    ok(`嚴重問題 comment 發布: [${f.role}] ${f.location}`);
  }
}
