/**
 * engine/live-checkout.js — P-a3f9b203
 *
 * Pure helper for the live-checkout dispatch mode (plan-w-mq5rmtt9000a42f9-2026-06-08).
 *
 * `prepareLiveCheckout({ localPath, branchName, mainRef, gitOpts, dispatchId, wiId, log, _git })`
 *
 * Lifecycle (called by spawnAgent in engine.js when resolveSpawnPaths returns liveMode:true):
 *   1. `git status --porcelain` from localPath. Non-empty output → bail with
 *      { ok:false, reason:'dirty', dirtyFiles:[…] } so spawnAgent can fail the
 *      dispatch non-retryably and write an inbox alert. NO mutating git calls
 *      after this point if dirty.
 *   2. `git fetch origin <mainRef>` — BEST-EFFORT. Failure is logged via the
 *      injected `log` (severity 'warn') and execution continues. Offline
 *      operators can still resume work on a previously-checked-out branch.
 *   3. Branch resolution:
 *        a. `git rev-parse --verify <branchName>` succeeds → branch exists
 *           locally → `git checkout <branchName>` (NO --force, NO auto-pull,
 *           NO fast-forward, NO reset). The operator owns the checkout
 *           state per the live-checkout contract; the engine never
 *           overwrites local commits.
 *        b. rev-parse fails → branch is new → `git checkout -b <branchName>
 *           origin/<mainRef>` (tracks remote main as upstream-of-creation).
 *   4. Returns { ok:true, branch, created:boolean }.
 *
 * PURE — does NOT call completeDispatch, does NOT write inbox alerts. That
 * translation belongs in spawnAgent (engine.js). Keeping this helper free of
 * side effects lets unit tests exercise every branch with a mocked git
 * runner and zero filesystem touching.
 *
 * All git invocations route through `shared.shellSafeGit` (argv-form,
 * shell:false, execFile). Branch and main-ref are validated via
 * `shared.validateGitRef` before any argv is constructed, so a maliciously
 * crafted branch name like "--evil-flag" is rejected before reaching git.
 *
 * Tests inject a mock runner via the private `_git` option (signature
 * `(args:string[], opts:{cwd, gitExtraArgs?:string[]}) => Promise<string>`).
 * Production callers omit `_git` and the helper falls back to
 * `require('./shared').shellSafeGit`.
 */

'use strict';

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

async function prepareLiveCheckout(opts = {}) {
  const {
    localPath,
    branchName,
    mainRef,
    gitOpts,
    dispatchId, // accepted for caller bookkeeping; not used by the helper
    wiId,       // accepted for caller bookkeeping; not used by the helper
    log,
    _git,       // private injection for testing — defaults to shared.shellSafeGit
  } = opts;

  // ── Required-arg guards. Throw rather than return {ok:false} so a
  //    caller's missing wiring fails loudly at the call site.
  if (!localPath || typeof localPath !== 'string') {
    throw new Error('prepareLiveCheckout: localPath is required (got ' + JSON.stringify(localPath) + ')');
  }
  if (!branchName || typeof branchName !== 'string') {
    throw new Error('prepareLiveCheckout: branchName is required (got ' + JSON.stringify(branchName) + ')');
  }
  if (!mainRef || typeof mainRef !== 'string') {
    throw new Error('prepareLiveCheckout: mainRef is required (got ' + JSON.stringify(mainRef) + ')');
  }

  // ── Validate refs before they hit git argv (defense in depth).
  //    shellSafeGit uses execFile (shell:false) so metacharacters are inert,
  //    but a leading-dash ref like "--evil" would still be parsed as a git
  //    flag. validateGitRef throws on those cases (and on other rev-spec
  //    quirks). Wrap so the caller-facing message mentions the field name.
  try { shared.validateGitRef(branchName); }
  catch (e) { throw new Error('prepareLiveCheckout: invalid branchName ref — ' + e.message); }
  try { shared.validateGitRef(mainRef); }
  catch (e) { throw new Error('prepareLiveCheckout: invalid mainRef — ' + e.message); }

  const git = (typeof _git === 'function') ? _git : shared.shellSafeGit;
  const baseOpts = { cwd: localPath, ...(gitOpts || {}) };

  // Small log helper that's safe even when the caller didn't pass `log`.
  const warn = (msg) => {
    if (typeof log !== 'function') return;
    try { log(msg, 'warn'); } catch { /* never let logging crash the helper */ }
  };

  // ── Step 1: git status --porcelain. Bail early on dirty tree. ─────────
  // Porcelain format begins each line with a two-char XY status code (e.g.
  // " M file.js", "?? new.js", "MM stage+unstage.js"). The leading space on
  // unstaged-only lines is SIGNIFICANT, so we split first and strip ONLY
  // trailing whitespace/CR per line — outer-trimming the whole blob would
  // eat the leading XY space on the first line.
  const statusRaw = await git(['status', '--porcelain'], baseOpts);
  const statusStr = typeof statusRaw === 'string' ? statusRaw : '';
  const dirtyFiles = statusStr
    .split(/\r?\n/)
    .map((line) => line.replace(/\s+$/, ''))
    .filter((line) => line.length > 0);
  if (dirtyFiles.length > 0) {
    return { ok: false, reason: 'dirty', dirtyFiles };
  }

  // ── Step 2: best-effort fetch of mainRef. Log + continue on failure. ──
  try {
    await git(['fetch', 'origin', mainRef], baseOpts);
  } catch (e) {
    const reason = (e && e.message) ? e.message : String(e);
    warn('prepareLiveCheckout: git fetch origin ' + mainRef + ' failed (continuing): ' + reason);
  }

  // ── Step 3: branch resolution + checkout. ─────────────────────────────
  let branchExists = false;
  try {
    await git(['rev-parse', '--verify', branchName], baseOpts);
    branchExists = true;
  } catch {
    branchExists = false;
  }

  if (branchExists) {
    // Plain checkout — NO --force, NO -B, NO pull. Operator owns local commits.
    await git(['checkout', branchName], baseOpts);
    return { ok: true, branch: branchName, created: false };
  }

  // New branch — create off of remote main.
  await git(['checkout', '-b', branchName, 'origin/' + mainRef], baseOpts);
  return { ok: true, branch: branchName, created: true };
}

module.exports = { prepareLiveCheckout };
