import { spawnSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, GITEA_COMMENT_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js';
import { line, ok, warn } from './log.js';

const REVIEW_FILE_PATHS = [FINDINGS_PATH, '.gitea/ai-review/exclusions.json'];
const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`;
export const BOT_COMMIT_MARKER = '[ai-review-bot]';

/**
 * 建立一個同步執行 git 子行程的 runner。透過注入 `spawn` 以利測試
 * （正式環境傳入 `child_process.spawnSync`，測試可傳入 stub）。
 *
 * 回傳的 `run(args, cwd, env)` 會以 utf8 編碼執行 `git <args>`，
 * 成功回傳經 trim 的 stdout，失敗則丟出 Error。
 *
 * @param {(cmd: string, args: string[], opts: object) => {error?: Error & {code?: string}, status?: number, stdout?: string, stderr?: string}} spawn
 *        同步 spawn 實作（依賴注入，通常為 `spawnSync`）。
 * @returns {(args: string[], cwd?: string, env?: object) => string}
 *          執行 git 的函式：回傳 trim 後的 stdout。
 * @throws {Error} 找不到 git 指令時（`ENOENT`）丟出中文提示。
 * @throws {Error} git 子行程本身的 `error`（非 ENOENT）原樣丟出。
 * @throws {Error} git 離開碼非 0 時，以 stderr/stdout 內容丟出。
 */
function makeRunner(spawn) {
  return function run(args, cwd, env) {
    const opts = { cwd, encoding: 'utf8' };
    if (env) opts.env = env;
    const result = spawn('git', args, opts);
    if (result.error) {
      if (result.error.code === 'ENOENT') throw new Error('找不到 git 指令，請確認 action image 已安裝 git');
      throw result.error;
    }
    if (result.status !== 0) throw new Error((result.stderr || result.stdout || '').trim());
    return (result.stdout || '').trim();
  };
}

/**
 * 包裝一段需要 git HTTP 認證的工作：先在 workspace 寫出暫時的
 * `.git-askpass.sh`（透過 `GIT_ASKPASS` 提供 token），呼叫 `fn(credEnv)`，
 * 再清除該暫存腳本。
 *
 * 清理時機會依 `fn` 回傳型別自動判斷：
 * 同步回傳會立即清理；回傳 Promise（含 async 回呼）則延後到 Promise
 * settle 後才清理，避免在第一個 await 就刪掉腳本，導致後續 git push
 * 因 `cannot exec .git-askpass.sh` 而失敗。
 *
 * @template T
 * @param {string} workspace 寫入暫存 askpass 腳本的目錄。
 * @param {(credEnv: NodeJS.ProcessEnv) => T} fn 帶入憑證環境變數執行的回呼。
 * @param {string} [token=GITEA_TOKEN] 供 git HTTP 認證使用的 token。預設為自動的
 *        `GITEA_TOKEN`（適用於唯讀的 clone/ls-remote）；需要讓 push 出來的 commit
 *        重新觸發 workflow 時，呼叫端應改傳真人 PAT（`GITEA_COMMENT_TOKEN`），因為
 *        Gitea 不會為「自動 token」推送的 commit 發出事件。
 * @returns {T} 即 `fn` 的回傳值（Promise 會被包成 `.finally(cleanup)` 後回傳）。
 * @throws 透傳 `fn` 丟出的任何例外（同步路徑會先清理暫存腳本再 re-throw）。
 * @remarks askpass 腳本以權限 0o700 寫出；token 由參數帶入並透過 `GIT_TOKEN` 提供給腳本。
 */
function withAskpass(workspace, fn, token = GITEA_TOKEN) {
  const askpassScript = path.join(workspace, '.git-askpass.sh');
  fs.writeFileSync(askpassScript, '#!/bin/sh\necho "$GIT_TOKEN"\n', { mode: 0o700 });
  const credEnv = {
    ...process.env,
    GIT_ASKPASS: askpassScript,
    GIT_USERNAME: 'x-token',
    GIT_TOKEN: token,
  };
  const cleanup = () => { try { fs.unlinkSync(askpassScript); } catch {} };
  let result;
  try {
    result = fn(credEnv);
  } catch (e) {
    cleanup();
    throw e;
  }
  // Defer cleanup until an async callback settles, otherwise the askpass script
  // is deleted at the first `await` and later network ops (e.g. git push) fail
  // with "cannot exec .git-askpass.sh". Sync callbacks clean up immediately.
  if (result && typeof result.then === 'function') {
    return result.finally(cleanup);
  }
  cleanup();
  return result;
}

/**
 * 以容錯方式執行 git 讀取指令：成功回傳 trim 後的輸出，
 * 任何錯誤都吞掉並回傳空字串。適用於「失敗也不該中斷流程」的唯讀查詢
 * （例如取 HEAD SHA、分支名、commit 時間）。
 *
 * @param {(args: string[], cwd?: string, env?: object) => string} run
 *        由 `makeRunner` 產生的 git 執行函式。
 * @param {string[]} args git 子指令與參數。
 * @param {string} [cwd] 執行目錄。
 * @param {object} [env] 環境變數覆寫。
 * @returns {string} git 的 trim 輸出；失敗時回傳空字串。
 * @remarks 不會拋出例外，也不記錄錯誤。
 */
function readGitOutput(run, args, cwd, env) {
  try {
    return run(args, cwd, env);
  } catch {
    return '';
  }
}

/**
 * 讀取指定 repo 目錄的目前狀態（HEAD SHA、短 SHA、目前分支、commit 時間）。
 * 所有查詢皆採容錯讀取，任一失敗對應欄位即為空字串，不會丟出例外。
 *
 * @param {string} repoDir git 工作目錄路徑。
 * @param {typeof import('child_process').spawnSync} [_spawnSync=spawnSync]
 *        測試用依賴注入：覆寫底層的同步 spawn 實作。
 * @returns {{repoDir: string, branch: string, headSha: string, shortSha: string, commitTime: string}}
 *          repo 狀態快照；無法取得的欄位為空字串。
 * @remarks `commitTime` 為 `%cI` 格式（committer date, ISO 8601 嚴格格式）。
 */
export function getRepoState(repoDir, _spawnSync = spawnSync) {
  const run = makeRunner(_spawnSync);
  const headSha = readGitOutput(run, ['rev-parse', 'HEAD'], repoDir);
  const shortSha = readGitOutput(run, ['rev-parse', '--short', 'HEAD'], repoDir);
  const branch = readGitOutput(run, ['branch', '--show-current'], repoDir);
  const commitTime = readGitOutput(run, ['show', '-s', '--format=%cI', 'HEAD'], repoDir);
  return { repoDir, branch, headSha, shortSha, commitTime };
}

/**
 * 取得 HEAD commit 的完整 commit message（`%B`，含 subject 與 body）。
 * 容錯讀取：失敗時回傳空字串。
 *
 * @param {string} repoDir git 工作目錄路徑。
 * @param {typeof import('child_process').spawnSync} [_spawnSync=spawnSync]
 *        測試用依賴注入。
 * @returns {string} HEAD 的完整 commit 訊息；失敗時為空字串。
 */
export function getHeadCommitMessage(repoDir, _spawnSync = spawnSync) {
  const run = makeRunner(_spawnSync);
  return readGitOutput(run, ['show', '-s', '--format=%B', 'HEAD'], repoDir);
}

/**
 * 判斷 HEAD commit 是否為 AI Review 機器人自己產生的自動 commit
 * （commit message 含 `BOT_COMMIT_MARKER`）。常用於避免機器人 commit
 * 反覆觸發新一輪審查。
 *
 * @param {string} repoDir git 工作目錄路徑。
 * @param {typeof import('child_process').spawnSync} [_spawnSync=spawnSync]
 *        測試用依賴注入。
 * @returns {boolean} HEAD 訊息含機器人標記時為 true；讀取失敗時安全地回傳 false。
 */
export function isBotAutoCommit(repoDir, _spawnSync = spawnSync) {
  return getHeadCommitMessage(repoDir, _spawnSync).includes(BOT_COMMIT_MARKER);
}

/**
 * 用與 push 相同的 askpass + remote URL 機制跑一次唯讀的 `git ls-remote`，
 * 驗證 git 對 remote 的認證與連線是否可用（不會寫入任何東西）。
 * 這條路徑與 Gitea REST API 不同，API token 有效不代表 git push 認證一定可用，
 * 所以放在前置驗證可以提前抓出 askpass 無法執行或 HTTP 認證失敗的問題。
 */
export function verifyRemoteAccess(workspace, _spawnSync = spawnSync) {
  const run = makeRunner(_spawnSync);
  try {
    return withAskpass(workspace, credEnv => {
      run(['ls-remote', remoteUrl, PR_HEAD_BRANCH || 'HEAD'], workspace, credEnv);
      return { ok: true };
    });
  } catch (e) {
    return { ok: false, error: e.message };
  }
}

/**
 * Clone PR head branch to workspace/repo (idempotent)
 */
export function cloneRepo(workspace, _spawnSync = spawnSync) {
  const run = makeRunner(_spawnSync);
  const repoDir = path.join(workspace, 'repo');

  return withAskpass(workspace, credEnv => {
    if (!fs.existsSync(repoDir)) {
      run(['clone', '--depth=1', '--branch', PR_HEAD_BRANCH, remoteUrl, repoDir], workspace, credEnv);
      ok(`repo cloned to ${repoDir}`);
    } else {
      run(['fetch', 'origin', PR_HEAD_BRANCH], repoDir, credEnv);
      run(['checkout', PR_HEAD_BRANCH], repoDir);
      ok('repo already exists, fetched latest');
    }
    return repoDir;
  });
}

/**
 * 將 AI 審查產出的 review 檔（findings / exclusions）結轉到 repo，並 commit、
 * push 回 PR head branch。流程：設定機器人 git 身分 → fetch + hard reset 對齊
 * 遠端 → 從 workspace 複製存在的 review 檔到 repo 並 add → 若無變更則跳過 →
 * 以含 `BOT_COMMIT_MARKER` 與結果標籤的訊息 commit → push。
 *
 * 失敗策略：push 失敗只記 warning（commit 已在本地完成）；其餘步驟的例外
 * 由外層捕捉並記 warning，函式整體**不丟出例外**，以免中斷上層流程。
 *
 * @param {string} workspace review 檔來源目錄、askpass 腳本所在目錄。
 * @param {string} repoDir 目標 git repo 目錄（commit/push 的工作目錄）。
 * @param {typeof import('child_process').spawnSync} [_spawnSync=spawnSync]
 *        測試用依賴注入：覆寫底層同步 spawn。
 * @param {string|null} [_sourceRoot=null] 測試用依賴注入保留參數；
 *        目前函式主體未使用（不確定，待確認其他呼叫端是否依賴）。
 * @param {'success'|'failure'} [reviewOutcome='success']
 *        審查結果，決定 commit 訊息標籤（`[success]` / `[failure]`）。
 * @returns {Promise<void>} 無回傳值；所有失敗皆以 log 記錄後吞掉。
 * @remarks `git reset --hard origin/<branch>` 會丟棄本地未對齊變更，請確認
 *          review 檔是在 reset 之後才複製進來（流程已如此安排）。
 * @remarks push 優先使用真人 PAT `GITEA_COMMENT_TOKEN`（無則退回 `GITEA_TOKEN`），
 *          目的是讓 bot commit 能重新觸發 PR 的 workflow：以自動 token 推送的 commit
 *          不會發出 `synchronize` 事件，新 head commit 便拿不到檢查而卡住。改用 PAT
 *          推送後會正常重觸發，重跑時由 main.js Step3 偵測 `[ai-review-bot]` 標記後跳過。
 */
export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync, _sourceRoot = null, reviewOutcome = 'success') {
  const run = makeRunner(_spawnSync);
  const pushToken = GITEA_COMMENT_TOKEN || GITEA_TOKEN;

  try {
    await withAskpass(workspace, async credEnv => {
      run(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir);
      run(['config', 'user.name', 'AI Review Bot'], repoDir);
      if (PR_HEAD_BRANCH) {
        run(['fetch', 'origin', PR_HEAD_BRANCH], repoDir, credEnv);
        run(['reset', '--hard', `origin/${PR_HEAD_BRANCH}`], repoDir);
      }

      const reviewFilePaths = REVIEW_FILE_PATHS.filter(relPath => fs.existsSync(path.join(workspace, relPath)));
      if (reviewFilePaths.length > 0) {
        for (const relPath of reviewFilePaths) {
          const src = path.join(workspace, relPath);
          const dest = path.join(repoDir, relPath);
          fs.mkdirSync(path.dirname(dest), { recursive: true });
          fs.copyFileSync(src, dest);
        }
        run(['add', ...reviewFilePaths], repoDir);
      }

      const status = run(['status', '--porcelain'], repoDir);
      if (!status) {
        line('review files 無變更，跳過 commit');
        return;
      }

      const outcomeTag = reviewOutcome === 'failure' ? '[failure]' : '[success]';
      const out = run(['commit', '-m', `chore: update ai-review findings ${BOT_COMMIT_MARKER}${outcomeTag}`], repoDir);
      const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown';
      try {
        run(['push', remoteUrl, PR_HEAD_BRANCH], repoDir, credEnv);
        ok(`persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH} review_outcome=${reviewOutcome}`);
      } catch (pushErr) {
        warn(`Step8 commit 成功但 push 失敗: commit=${commitHash} push=${PR_HEAD_BRANCH} review_outcome=${reviewOutcome} error=${pushErr.message}`);
      }
    }, pushToken);
  } catch (e) {
    warn(`Runner failed: commit/push 失敗: ${e.message}`);
  }
}
