import path from 'path';
import { GITEA_REPOSITORY, PR_NUMBER, PR_HEAD_BRANCH, PR_BASE_BRANCH, getLLMConfig, FINDINGS_PATH, EXCLUSIONS_PATH } from './config.js';
import { loadRoles, getRoleIntro } from './roles.js';
import { getPRDiff, postComment, getCommitMessageBySha, getBotReviewOutcome, shouldSkipBotCommit } from './gitea.js';
import { analyzeWithRole, loadOldFindings, mergeFindings, sortByLevel, deduplicateWithAI, loadExclusions, applyExclusions, filterFalsePositivesWithAI, appendExclusions, resolveMissingLineNumbers } from './findings.js';
import { reconcileConversations, dropResolvedFindings, addCarriedFindings } from './resolve.js';
import { saveFindings, postFindingsReview, formatFindingsStatsLine } from './comments.js';
import { getRunUsage, getRateLimit, fetchAccountQuota, formatUsageStats, formatUsageStatsLine } from './usage.js';
import { cloneRepo, commitAndPush, getRepoState } from './git.js';
import { validateJSONArrayFile, ensureJSONArrayFileExists } from './json.js';
import { runPreflight } from './preflight.js';
import { mapWithConcurrency, LLM_CONCURRENCY } from './llm.js';
import { section, step, line, input, output, result, warn, error } from './log.js';

const WORKSPACE = process.env.GITHUB_WORKSPACE || '/workspace';

/**
 * AI Code Review Pipeline 的總指揮（orchestrator）。
 *
 * 依序串接 Step1~Step11：啟動參數讀取、前置驗證、自動提交檢查、PR 對話收斂、
 * 角色平行分析產生 findings、新舊 findings 合併與語意去重、排除規則與誤報過濾、
 * 寫入 findings 並發布 Gitea Review、findings/exclusions JSON 格式驗證、
 * 記憶區 commit/push，以及嚴重問題把關。
 *
 * 結果主要透過 `process.exit()` 決定 workflow 成敗，而非以回傳值傳遞。
 *
 * @async
 * @returns {Promise<void>} 流程正常走完（無嚴重問題）時 resolve；多數結束路徑會直接
 *   呼叫 `process.exit()` 結束程序，函式不會以回傳值回報審查結果。
 * @throws {Error} 內部未被個別 try/catch 攔截的未預期例外會向上拋出，
 *   由頂層 `main().catch(...)` 接住並以 `process.exit(1)` 結束。
 *
 * @remarks
 * 流程階段（Step1~Step11）：
 * - Step1 啟動：讀取 repo / PR / 分支等基本參數。
 * - Step2 前置驗證：`runPreflight`，未通過則 exit 1。
 * - Step3 自動提交檢查：偵測上輪 bot `[failure]`（exit 1）或本次為 bot 自動提交（exit 0 跳過）。
 * - Step4 PR 對話收斂：關閉未解決 comment 並將 finding 分流為已修復 / 誤報 / 仍成立（失敗則降級繼續）。
 * - Step5 角色分析：載入角色、取 PR diff，平行產生 findings 並補齊缺漏行號；
 *   未設定 API Key 或取 diff 失敗 exit 1，diff 為空 exit 0。
 * - Step6 合併去重：舊 findings + 對話收斂結果 + 新 findings → 語意去重並排序。
 * - Step7 過濾：套用排除規則 + 防守方 AI 誤報裁決。
 * - Step8 發布：寫入 findings、組裝使用量，發布 Gitea Review（失敗則降級繼續）。
 * - Step9 JSON 驗證：驗證 findings/exclusions 檔，格式錯誤 exit 1，缺檔則建立空陣列檔。
 * - Step10 記憶區 commit/push：依是否有 critical 計算 reviewOutcome 後推回來源分支。
 * - Step11 嚴重問題把關：有 critical 則 exit 1，否則正常結束。
 *
 * 退出行為：
 * - exit 1：前置驗證未過、上輪 bot failure、未設定 LLM Key、取 diff 失敗、JSON 格式錯誤、發現嚴重問題、頂層未預期例外。
 * - exit 0：本次為 bot 自動提交、diff 為空、正常走完無嚴重問題。
 *
 * 降級處理：Step4 對話收斂、Step5 角色介紹 comment 與個別角色分析、Step6 clone repo、
 * Step8 Review 發布等非致命步驟失敗時，僅 `warn` 後繼續執行。
 */
export async function main() {
  section('AI Code Review Pipeline');

  // Step1 啟動
  step('Step1', '啟動');
  input(`repo=${GITEA_REPOSITORY}  PR=#${PR_NUMBER}  ${PR_HEAD_BRANCH} → ${PR_BASE_BRANCH}`);
  output('參數讀取完成');

  // Step2 前置驗證（step 標題與逐項檢查由 runPreflight 內部輸出）
  if (!(await runPreflight(WORKSPACE))) {
    result(false, '前置驗證未通過，終止流程');
    section('Pipeline 結束');
    process.exit(1);
  }

  // Step3 自動提交檢查：判斷本次 PR head 是否為 bot 自動提交
  step('Step3', '自動提交檢查');
  const headSha = process.env.PR_HEAD_SHA || process.env.GITHUB_SHA || '';
  input(`PR head sha=${headSha ? headSha.slice(0, 7) : 'empty'}`);
  const headMessage = await getCommitMessageBySha(headSha);
  if (headMessage.includes('[ai-review-bot]') && getBotReviewOutcome(headMessage) === 'failure') {
    result(false, '偵測到 [ai-review-bot][failure]，讓 workflow 失敗');
    section('Pipeline 結束');
    process.exit(1);
  }
  if (await shouldSkipBotCommit()) {
    result(true, '本次為 [ai-review-bot] 自動提交，跳過審查並結束');
    section('Pipeline 結束');
    process.exit(0);
  }
  output('非自動提交，繼續審查');

  // Step4 PR 對話收斂：關閉所有未解決 comment，並把對應 finding 分流
  step('Step4', 'PR 對話收斂');
  let reconcile = { resolvedFindings: [], excludedFindings: [], carriedFindings: [], resolvedCount: 0, falsePositiveCount: 0, openCount: 0, closedCount: 0 };
  try {
    reconcile = await reconcileConversations();
    output(`關閉 comment ${reconcile.closedCount}；findings 已修復 ${reconcile.resolvedCount}、誤報 ${reconcile.falsePositiveCount}、加回仍成立 ${reconcile.carriedFindings.length}`);
  } catch (e) {
    warn(`對話收斂失敗（繼續執行）: ${e.message}`);
  }

  // Step5 角色分析：載入角色、取 diff，讓各角色平行產生 findings
  step('Step5', '角色分析產生 findings');
  const { provider, apiKeys, baseURL, model } = getLLMConfig();
  if (!provider) {
    result(false, '未設定任何 LLM API Key，請檢查 action inputs');
    process.exit(1);
  }
  const roles = loadRoles();
  let diff;
  try {
    diff = await getPRDiff();
  } catch (e) {
    result(false, `取得 PR diff 失敗: ${e.message}`);
    process.exit(1);
  }
  if (!diff.trim()) {
    result(true, 'diff 為空，無需審查');
    section('Pipeline 結束');
    process.exit(0);
  }
  input(`LLM=${provider}/${model}；角色=[${roles.map(r => r.name).join(', ')}]；diff=${diff.length} 字元`);
  try {
    await postComment(getRoleIntro(roles) + `\n\n> 🔍 服務：${provider}  模型：${model}`);
    line('角色介紹 comment 已發布');
  } catch (e) {
    warn(`角色介紹 comment 發布失敗（繼續執行）: ${e.message}`);
  }
  // 各角色以獨立 LLM 子行程並行分析（併發上限見 LLM_CONCURRENCY），單一角色失敗僅 warn 後跳過。
  const newFindings = [];
  let fulfilledAnalyses = 0;
  const roleResults = await mapWithConcurrency(roles, LLM_CONCURRENCY, async (role) => {
    try {
      return await analyzeWithRole(role, diff);
    } catch (e) {
      warn(`[${role.name}] 分析失敗（跳過）: ${e.message}`);
      return null;
    }
  });
  for (const findings of roleResults) {
    if (findings) {
      fulfilledAnalyses += 1;
      newFindings.push(...findings);
    }
  }
  if (fulfilledAnalyses === 0) {
    result(false, '所有角色分析皆失敗，終止流程以避免誤判為審查通過');
    process.exit(1);
  }
  // 對只有檔名、缺行號的問題，反問原角色補上行號（最多重試數次），確保後續能行內標註
  await resolveMissingLineNumbers(newFindings, diff);
  output(`新 findings ${newFindings.length} 筆（${formatFindingsStatsLine(newFindings)}）`);

  // Step6 合併與去重：舊問題 + 對話收斂結果 + 新問題 → 語意去重
  step('Step6', 'Findings 合併與語意去重');
  let repoDir;
  try {
    repoDir = cloneRepo(WORKSPACE);
  } catch (e) {
    warn(`clone repo 失敗（繼續執行）: ${e.message}`);
  }
  const repoState = repoDir ? getRepoState(repoDir) : null;
  if (repoState) line(`repo: branch=${repoState.branch || 'detached'} commit=${repoState.shortSha || 'unknown'}`);
  let oldFindings = loadOldFindings(repoDir || WORKSPACE);
  const beforeReconcile = oldFindings.length;
  oldFindings = dropResolvedFindings(oldFindings, [...reconcile.resolvedFindings, ...reconcile.excludedFindings]);
  oldFindings = addCarriedFindings(oldFindings, reconcile.carriedFindings);
  input(`舊 findings ${beforeReconcile} 筆（套用對話收斂後 ${oldFindings.length}）＋新 findings ${newFindings.length} 筆`);
  const mergedFindings = mergeFindings(oldFindings, newFindings);
  const deduped = await deduplicateWithAI(mergedFindings);
  const sorted = sortByLevel(deduped);
  output(`合併 ${mergedFindings.length} → 去重後 ${sorted.length} 筆（${formatFindingsStatsLine(sorted)}）`);

  // Step7 過濾：套用排除規則 + 防守方 AI 誤報裁決
  step('Step7', '排除規則與誤報過濾');
  if (reconcile.excludedFindings.length > 0) {
    // 以 repoDir 為主（即將提交回去的來源分支副本），WORKSPACE 為鏡像；
    // 順序須與下方 loadExclusions 一致，否則會讀到空的 WORKSPACE 而把既有排除規則覆蓋掉。
    appendExclusions(repoDir || WORKSPACE, reconcile.excludedFindings, WORKSPACE);
  }
  const exclusions = loadExclusions(repoDir || WORKSPACE, repoState, WORKSPACE);
  input(`待過濾 ${sorted.length} 筆；排除規則 ${exclusions.length} 條`);
  const ruleFiltered = applyExclusions(sorted, exclusions);
  const filtered = await filterFalsePositivesWithAI(ruleFiltered, exclusions);
  output(`保留 ${filtered.length} 筆（規則排除 ${sorted.length - ruleFiltered.length}、誤報剔除 ${ruleFiltered.length - filtered.length}）`);

  // Step8 寫入 findings 並發布 Gitea Review（附使用量）
  step('Step8', '寫入 findings 與發布 Review');
  const reviewDir = repoDir || WORKSPACE;
  saveFindings(WORKSPACE, filtered, reviewDir);
  const runUsage = getRunUsage();
  const quota = await fetchAccountQuota(provider, { apiKeys, baseURL });
  const rate = getRateLimit();
  const usageSection = formatUsageStats(provider, model, runUsage, quota, rate);
  input(`findings ${filtered.length} 筆（${formatFindingsStatsLine(filtered)}）`);
  line(`使用量: ${formatUsageStatsLine(provider, model, runUsage, quota, rate)}`);
  try {
    await postFindingsReview(filtered, { summaryFindings: filtered, commentFindings: filtered, usageSection });
    output('Gitea Review 已發布');
  } catch (e) {
    warn(`Review 發布失敗（繼續執行）: ${e.message}`);
  }

  // Step9 JSON 格式驗證
  step('Step9', 'findings/exclusions JSON 格式驗證');
  const missingPaths = [];
  for (const relPath of [FINDINGS_PATH, EXCLUSIONS_PATH]) {
    const fullPath = path.join(reviewDir, relPath);
    try {
      const r = await validateJSONArrayFile(fullPath, relPath);
      if (!r.exists) missingPaths.push({ fullPath, relPath });
    } catch {
      result(false, `${relPath} JSON 格式錯誤，終止流程`);
      process.exit(1);
    }
  }
  for (const { fullPath, relPath } of missingPaths) ensureJSONArrayFileExists(fullPath, relPath);
  result(true, '兩個檔案 JSON 格式皆正確');

  // Step10 記憶區 Commit/Push
  step('Step10', '記憶區 Commit/Push');
  const reviewOutcome = filtered.some(f => f.level === 'critical') ? 'failure' : 'success';
  input(`review outcome=${reviewOutcome}`);
  // clone 失敗（repoDir 為 undefined）時不可把 WORKSPACE（非來源分支 git repo）當 repoDir，
  // 否則會在錯誤的工作目錄嘗試 commit/push，findings/exclusions 無法持久化到 PR 分支。
  if (!repoDir) {
    warn('來源分支 clone 失敗，略過 findings/exclusions 持久化（不以 WORKSPACE 當 repoDir）');
  } else {
    await commitAndPush(WORKSPACE, repoDir, undefined, undefined, reviewOutcome);
  }

  // Step11 嚴重問題把關
  step('Step11', '嚴重問題把關');
  const criticalCount = filtered.filter(f => f.level === 'critical').length;
  if (criticalCount > 0) {
    result(false, `發現 ${criticalCount} 個嚴重問題，workflow 失敗（exit 1）`);
    section('Pipeline 結束');
    process.exit(1);
  }
  result(true, '無嚴重問題，審查通過');
  section('Pipeline 結束');
}

// 預設一律自動啟動 pipeline（正式以 node 執行、或由 Gitea node action runtime 載入時皆會執行）。
// 僅單元測試會設 AI_REVIEW_SKIP_MAIN=1 略過自動執行，改為手動呼叫 export 的 main() 注入 mock 測試。
if (!process.env.AI_REVIEW_SKIP_MAIN) {
  main().catch(e => {
    error(`Runner failed: ${e.message}`);
    process.exit(1);
  });
}
