/**
 * engine/abandoned-pr-reconciliation.js — One-shot startup reconciliation pass
 * for `abandoned` PRs. Pairs with W-mp5trwh60008386d's forward-looking 404
 * hardening in engine/github.js: that fix prevents *future* false-flips, but
 * does nothing for PRs already wrongly marked `abandoned` before it shipped
 * (e.g. the 16 PRs hit by the 2026-05-14 gh-auth-flip incident).
 *
 * `pollPrStatus` only iterates PRs in `PR_POLLABLE_STATUSES = {active, linked}`
 * (engine/shared.js), so once a PR is `abandoned` the regular poll loop never
 * visits it again. This module is the explicit one-time re-probe.
 *
 * ## Why one-shot, not periodic?
 *
 * Periodic re-probing of terminal-status PRs has real costs:
 *   - GitHub/ADO API quota burn on every restart (50+ PRs × 2 hosts).
 *   - Race risk against late-arriving merge events (a PR closed 30s before
 *     the pass would mis-classify based on stale local state).
 *   - Boot latency that grows linearly with abandoned-PR count.
 *
 * The version-gated one-shot model gives us exactly what we need: re-probe
 * once per release that bumps `ENGINE_DEFAULTS.abandonedReconciliationVersion`,
 * then never again until the next bump. First boot after this lands cleans up
 * the historical false-flips; subsequent boots are a single state.json read +
 * version compare → no-op.
 *
 * ## Why a separate WI from W-mp5trwh60008386d?
 *
 * Forward-fix (don't false-flip new PRs) and retroactive-heal (un-flip already
 * damaged PRs) are different concerns with different risk profiles. The 404
 * hardening fix is per-tick and ships independently; this reconciliation is
 * boot-only and depends on the hardening fix existing in master so we don't
 * fork the abandonment-confirmation logic.
 *
 * ## Boot wiring
 *
 * Called from engine/cli.js between the recovery sweep and the initial tick.
 * Errors here must NOT block boot — the catch in cli.js logs and continues.
 */

const path = require('path');
const shared = require('./shared');

const { ENGINE_DEFAULTS, log, mutateEngineState, readEngineState } = shared;

/**
 * Run the version-gated startup reconciliation pass. Idempotent across reruns
 * at the same code version: state.json gates re-execution.
 *
 * @param {object} config — engine config (used to enumerate projects)
 * @param {object} [options]
 * @param {boolean} [options.skipVersionCheck] — bypass the version gate (tests only)
 * @param {object}  [options.githubModule] — injectable for tests (default: require './github')
 * @param {object}  [options.adoModule]    — injectable for tests (default: require './ado')
 * @returns {Promise<{ skipped: boolean, totals: { flipped, confirmedDeleted, skipped, errored } }>}
 */
async function runStartupReconciliation(config, options = {}) {
  const targetVersion = Number(ENGINE_DEFAULTS.abandonedReconciliationVersion) || 0;
  const skipVersionCheck = options.skipVersionCheck === true;

  if (!skipVersionCheck) {
    const state = readEngineState();
    const lastVersion = Number(state.lastAbandonedReconciliationVersion) || 0;
    if (lastVersion >= targetVersion) {
      // Already reconciled at this version — no-op. Don't even log; this runs
      // every boot and the silent path is the common one.
      return {
        skipped: true,
        totals: { flipped: 0, confirmedDeleted: 0, skipped: 0, errored: 0 },
      };
    }
  }

  const github = options.githubModule || require('./github');
  const ado = options.adoModule || require('./ado');

  // Pre-count abandoned PRs (only those without the confirmed-404 marker) so
  // the start-of-pass log line is meaningful. Wrapped in try/catch — the
  // pre-count is observability, not correctness.
  let preCount = 0;
  let projectCount = 0;
  try {
    const projects = shared.getProjects(config);
    projectCount = projects.length;
    for (const p of projects) {
      const prs = shared.safeJsonArr(shared.projectPrPath(p));
      preCount += prs.filter(pr =>
        pr.status === shared.PR_STATUS.ABANDONED && !pr._reconciliation404Confirmed
      ).length;
    }
  } catch { /* observability only */ }

  log('info', `Abandoned PR reconciliation pass: scanning ${preCount} PR${preCount === 1 ? '' : 's'} across ${projectCount} project${projectCount === 1 ? '' : 's'} (version ${targetVersion})`);

  // Run both reconcilers. We do NOT use Promise.allSettled here because
  // adoFetch and ghApi share the global rate-limit counters — running them
  // serially keeps log ordering deterministic and avoids interleaved
  // throttle messages.
  let ghTotals = { flipped: 0, confirmedDeleted: 0, skipped: 0, errored: 0 };
  let adoTotals = { flipped: 0, confirmedDeleted: 0, skipped: 0, errored: 0 };
  try {
    ghTotals = await github.reconcileAbandonedPrs(config);
  } catch (err) {
    log('warn', `Abandoned PR reconciliation: GitHub pass threw: ${err.message}`);
  }
  try {
    adoTotals = await ado.reconcileAbandonedPrs(config);
  } catch (err) {
    log('warn', `Abandoned PR reconciliation: ADO pass threw: ${err.message}`);
  }

  const totals = {
    flipped: ghTotals.flipped + adoTotals.flipped,
    confirmedDeleted: ghTotals.confirmedDeleted + adoTotals.confirmedDeleted,
    skipped: ghTotals.skipped + adoTotals.skipped,
    errored: ghTotals.errored + adoTotals.errored,
  };
  log('info', `Abandoned PR reconciliation: flipped ${totals.flipped}, confirmed-deleted ${totals.confirmedDeleted}, skipped ${totals.skipped}, errored ${totals.errored}`);

  if (!skipVersionCheck) {
    // Persist the version marker even when the pass found nothing (preCount=0)
    // — the marker means "we ran reconciliation at this code version", not
    // "we found something to fix". Without the write, every restart would
    // re-run the no-op pass forever.
    try {
      mutateEngineState((state) => {
        state.lastAbandonedReconciliationVersion = targetVersion;
        state.lastAbandonedReconciliationAt = new Date().toISOString();
        return state;
      });
    } catch (err) {
      // State.json write failure is unfortunate but non-fatal: next boot will
      // re-run the pass, which is idempotent (already-flipped PRs aren't
      // abandoned anymore, confirmed-404s carry the marker). Log and continue.
      log('warn', `Abandoned PR reconciliation: failed to persist version marker: ${err.message}`);
    }
  }

  return { skipped: false, totals };
}

module.exports = {
  runStartupReconciliation,
};
