import axios from 'axios';
import fs from 'fs';
import os from 'os';
import { join } from 'path';
import {
  GITEA_TOKEN,
  GITEA_COMMENT_TOKEN,
  GITEA_SERVER_URL,
  GITEA_REPOSITORY,
  PR_NUMBER,
  getInsecureHttpsAgent,
  getLLMConfig,
} from './config.js';
import { verifyRemoteAccess } from './git.js';
import { step, line, ok, error, result } from './log.js';

// codex 內部用來取得帳號可用模型清單的端點；auth 失效時會回 HTTP 401。
const CODEX_MODELS_ENDPOINT = 'https://chatgpt.com/backend-api/codex/models';

const httpsAgent = getInsecureHttpsAgent();
/**
 * 組出 Gitea REST API v1 的完整網址。
 *
 * 會將模組層級的 GITEA_SERVER_URL 尾端斜線去除後串接 `/api/v1` 與傳入路徑。
 * @param {string} path - 以 `/` 開頭的 API 子路徑，例如 `/repos/owner/name`。
 * @returns {string} 完整可請求的 API URL。
 * @remarks 依賴模組層級常數 GITEA_SERVER_URL；若該值為空會丟出 TypeError。
 */
const api = (path) => `${GITEA_SERVER_URL.replace(/\/$/, '')}/api/v1${path}`;
/**
 * 產生呼叫 Gitea API 用的 HTTP headers。
 *
 * Authorization 採 Gitea 的 `token <token>` 認證格式。
 * @param {string} token - Gitea 個人存取權杖（personal access token）。
 * @returns {{Authorization: string, 'Content-Type': string}} 可直接交給 axios 的 headers 物件。
 */
const giteaHeaders = (token) => ({ Authorization: `token ${token}`, 'Content-Type': 'application/json' });
/**
 * 將（axios）錯誤格式化為易讀的訊息字串。
 *
 * 有 HTTP 回應狀態碼時輸出 `HTTP <status> <message>`，否則僅輸出 message。
 * @param {Error & {response?: {status?: number}, message: string}} e - 捕捉到的錯誤物件。
 * @returns {string} 格式化後的錯誤描述。
 */
function giteaErr(e) {
  const status = e.response?.status;
  return status ? `HTTP ${status} ${e.message}` : e.message;
}

/**
 * 檢查 code review 所需的必要環境變數是否齊全。
 *
 * 用法：preflight 第一關，缺任何一項即視為不通過並列出缺少項目。
 * @param {object} [opts] - 覆寫值，供測試注入；省略時各欄取模組層級常數預設值。
 * @param {string} [opts.token=GITEA_TOKEN] - Gitea token。
 * @param {string} [opts.repo=GITEA_REPOSITORY] - `owner/name` 形式的 repo。
 * @param {string|number} [opts.pr=PR_NUMBER] - PR 編號。
 * @returns {{ok: boolean, missing: string[]}} ok 表是否全部齊全；missing 列出缺少的環境變數名稱。
 */
export function checkRequiredEnv({ token = GITEA_TOKEN, repo = GITEA_REPOSITORY, pr = PR_NUMBER } = {}) {
  const missing = [];
  if (!token) missing.push('GITEA_TOKEN');
  if (!repo) missing.push('GITEA_REPOSITORY');
  if (!pr) missing.push('PR_NUMBER');
  return { ok: missing.length === 0, missing };
}

/**
 * 驗證 Gitea token 有效且對指定 repo 有讀取權限。
 *
 * 透過唯讀的 `GET /repos/{repo}` 探測；任何錯誤都被攔截並轉為回傳值，不會 throw。
 * 採用 rejectUnauthorized:false 的 httpsAgent（不驗證 TLS 憑證）。
 * @param {string} [token=GITEA_TOKEN] - Gitea token，可注入供測試。
 * @param {string} [repo=GITEA_REPOSITORY] - `owner/name` 形式的 repo，可注入供測試。
 * @returns {Promise<{ok: true}|{ok: false, error: string}>} 成功僅含 ok；失敗含格式化錯誤訊息。
 */
export async function verifyGiteaToken(token = GITEA_TOKEN, repo = GITEA_REPOSITORY) {
  try {
    await axios.get(api(`/repos/${repo}`), { headers: giteaHeaders(token), timeout: 30000, httpsAgent });
    return { ok: true };
  } catch (e) {
    return { ok: false, error: giteaErr(e) };
  }
}

/**
 * 驗證選用的 comment token（GITEA_COMMENT_TOKEN）是否可用。
 *
 * 未提供 token 時直接視為通過並標記 skipped:true（之後 comment 會沿用主 token）；
 * 有提供則以 `GET /user` 探測。錯誤被攔截轉為回傳值，不會 throw。
 * @param {string} [token=GITEA_COMMENT_TOKEN] - 專用於發布 comment 的 token，可注入供測試。
 * @returns {Promise<{ok: true, skipped?: true}|{ok: false, error: string}>} skipped 表示未提供而略過。
 */
export async function verifyCommentToken(token = GITEA_COMMENT_TOKEN) {
  if (!token) return { ok: true, skipped: true };
  try {
    await axios.get(api('/user'), { headers: giteaHeaders(token), timeout: 30000, httpsAgent });
    return { ok: true };
  } catch (e) {
    return { ok: false, error: giteaErr(e) };
  }
}

/**
 * 讀取本機 codex 認證檔，向模型清單端點確認帳號目前可用的模型 slug。
 *
 * 用途：preflight 期即時分辨「auth 失效（HTTP 401）」與「模型無權限（不在清單）」，
 * 不必等到 Step5 每個角色送 prompt 才神秘失敗。只讀清單、不送 prompt，不消耗生成額度。
 * 所有錯誤都被攔截並轉為回傳值，不會 throw。
 *
 * @param {object} [deps] - 可注入相依，供測試避免真的讀檔/打網路。
 * @param {typeof fetch} [deps.fetchImpl=fetch] - HTTP 取得函式。
 * @param {string} [deps.authPath=~/.codex/auth.json] - codex 認證檔路徑。
 * @param {string} [deps.clientVersion] - 帶給端點的 client_version 查詢參數。
 * @returns {Promise<{ok: true, slugs: string[]}|{ok: false, error: string}>}
 *   成功回傳可用模型 slug 陣列；失敗回傳格式化錯誤訊息。
 */
export async function fetchCodexModels({
  fetchImpl = fetch,
  authPath = join(os.homedir(), '.codex', 'auth.json'),
  clientVersion = '0.142.5',
} = {}) {
  let auth;
  try {
    auth = JSON.parse(fs.readFileSync(authPath, 'utf8'));
  } catch (e) {
    return { ok: false, error: `無法讀取 codex 認證檔（${authPath}）: ${e.message}` };
  }
  const tokens = auth.tokens || {};
  if (!tokens.access_token) return { ok: false, error: 'codex 認證檔缺少 tokens.access_token' };

  const headers = { Authorization: `Bearer ${tokens.access_token}` };
  if (tokens.account_id) headers['chatgpt-account-id'] = tokens.account_id;

  let resp;
  try {
    resp = await fetchImpl(`${CODEX_MODELS_ENDPOINT}?client_version=${clientVersion}`, { headers });
  } catch (e) {
    return { ok: false, error: `codex 模型清單查詢連線錯誤: ${e.message}` };
  }
  if (resp.status === 401) {
    return { ok: false, error: 'codex 認證失效（HTTP 401）——token 已被撤銷或過期，請重新登入 codex 並更新 LLM_OAUTH secret' };
  }
  if (!resp.ok) {
    return { ok: false, error: `codex 模型清單查詢失敗（HTTP ${resp.status}）` };
  }
  let data;
  try {
    data = await resp.json();
  } catch (e) {
    return { ok: false, error: `codex 模型清單回應解析失敗: ${e.message}` };
  }
  const slugs = Array.isArray(data.models) ? data.models.map(m => m.slug).filter(Boolean) : [];
  return { ok: true, slugs };
}

/**
 * 驗證 LLM（AI 助理 CLI）設定可用。
 *
 * 確認目前環境可偵測到支援的 CLI 且已解析出 model；provider 為 codex 時，
 * 額外向模型清單端點確認 auth 有效且設定的 model 在可用清單內（不送 prompt）。
 * @param {object} [deps] - 可注入相依，供測試。
 * @param {Function} [deps.fetchCodexModelsFn=fetchCodexModels] - codex 模型清單取得函式。
 * @returns {Promise<
 *   {ok: true, provider: string, command: string, model: string, models?: string[]} |
 *   {ok: false, provider?: string, command?: string, model?: string, error: string}
 * >}
 *   通過時含 provider、command、model（codex 另含 models 清單）；未設定 provider 的失敗分支不含 provider。
 * @remarks 設定來源為 config.js 的 getLLMConfig()。
 */
export async function verifyLLM({ fetchCodexModelsFn = fetchCodexModels } = {}) {
  const { provider, command, model } = getLLMConfig();
  if (!provider || !command) return { ok: false, error: '未偵測到可用 AI 助理 CLI，請安裝 codex、claude、antigravity 或 opencode' };
  if (!model) return { ok: false, provider, error: '未設定 MODEL' };

  if (provider === 'codex') {
    const models = await fetchCodexModelsFn();
    if (!models.ok) return { ok: false, provider, command, model, error: models.error };
    if (!models.slugs.includes(model)) {
      return { ok: false, provider, command, model, error: `模型 ${model} 不在 codex 可用清單: [${models.slugs.join(', ')}]` };
    }
    return { ok: true, provider, command, model, models: models.slugs };
  }

  return { ok: true, provider, command, model };
}

/**
 * 執行所有前置驗證（Step2）：環境變數、Gitea token、comment token、git 遠端、LLM CLI。
 *
 * 全程唯讀，不發布任何 comment；任一檢查失敗即記錄錯誤並回傳 false。
 * 各檢查可經 deps 注入覆寫，方便單元測試。
 * @param {string} [workspace=process.env.GITHUB_WORKSPACE||'/workspace'] - git 遠端驗證用的工作目錄。
 * @param {object} [deps] - 依賴注入，覆寫各檢查函式（預設為本模組/ git.js 的實作）。
 * @param {Function} [deps.checkEnv=checkRequiredEnv] - 環境變數檢查。
 * @param {Function} [deps.verifyToken=verifyGiteaToken] - Gitea token / repo 讀取驗證。
 * @param {Function} [deps.verifyComment=verifyCommentToken] - comment token 驗證。
 * @param {Function} [deps.verifyRemote=verifyRemoteAccess] - git 遠端（ls-remote）認證驗證。
 * @param {Function} [deps.verifyLLMFn=verifyLLM] - LLM（AI 助理 CLI）驗證。
 * @returns {Promise<boolean>} 全部通過為 true，任一失敗為 false。
 * @remarks 透過 log.js 輸出 step/ok/line/error/result 記錄；不會 throw（前提是注入的檢查函式皆自行攔截錯誤）。
 */
export async function runPreflight(workspace = process.env.GITHUB_WORKSPACE || '/workspace', deps = {}) {
  const {
    checkEnv = checkRequiredEnv,
    verifyToken = verifyGiteaToken,
    verifyComment = verifyCommentToken,
    verifyRemote = verifyRemoteAccess,
    verifyLLMFn = verifyLLM,
  } = deps;
  step('Step2', '前置驗證（驗證相關設定）');

  const env = checkEnv();
  if (!env.ok) {
    error(`缺少必要環境變數: ${env.missing.join(', ')}`);
    return false;
  }
  ok('必要環境變數齊全 (GITEA_TOKEN, GITEA_REPOSITORY, PR_NUMBER)');

  const gitea = await verifyToken();
  if (!gitea.ok) {
    error(`GITEA_TOKEN 驗證失敗（無法讀取 repo ${GITEA_REPOSITORY}）: ${gitea.error}`);
    return false;
  }
  ok(`GITEA_TOKEN 可讀取 repo ${GITEA_REPOSITORY}`);

  const comment = await verifyComment();
  if (!comment.ok) {
    error(`GITEA_COMMENT_TOKEN 驗證失敗: ${comment.error}`);
    return false;
  }
  if (comment.skipped) line('未提供 GITEA_COMMENT_TOKEN，comment 將沿用 GITEA_TOKEN');
  else ok('GITEA_COMMENT_TOKEN 可用');

  const remote = verifyRemote(workspace);
  if (!remote.ok) {
    error(`git push 認證/連線驗證失敗（ls-remote）: ${remote.error}`);
    return false;
  }
  ok('git remote 認證可用（ls-remote 成功）');

  const llm = await verifyLLMFn();
  if (!llm.ok) {
    error(`LLM 驗證失敗: ${llm.error}`);
    return false;
  }
  ok(`LLM CLI 可用（command=${llm.command}, provider=${llm.provider}, model=${llm.model}）`);
  if (llm.models) line(`模型已確認在可用清單內（共 ${llm.models.length} 個可用模型）`);

  result(true, '前置驗證通過');
  return true;
}
