/**
 * Worktree work-at-risk assessment (split out of `worktrees.ts`, v2.7 audit
 * reshape). The git-worktree create/diff/remove lifecycle stays in `worktrees.ts`;
 * this module owns the orthogonal "would deleting this worktree lose work?" gate
 * the server consults before a session delete, plus the recoverable stash escape.
 *
 * Session delete itself lives in apps/server (Docker), which CANNOT see the host
 * worktree dirs or run git on them — only BooCoder (host systemd) can — so the
 * server calls the routes that wrap these helpers. Behavior is unchanged from the
 * original worktrees.ts implementation.
 */
import type { WorktreeRiskReport } from '@boocode/contracts/worktree-risk';
import { hostExec } from './host-exec.js';

/**
 * Resolve the project's default branch as a git-usable ref (e.g. "origin/main").
 *
 * `refs/remotes/origin/HEAD` lives in the repo's COMMON git dir and is shared
 * across every linked worktree, so reading it from the session worktree returns
 * the REMOTE's default branch — never this worktree's own `session-<id>` branch
 * (that would be `symbolic-ref HEAD`, a different ref). Falls back to probing
 * common defaults by verified existence when origin/HEAD isn't set (e.g. a repo
 * that never ran `git remote set-head`). Returns null if none resolve, in which
 * case the unmerged check is skipped (dirty + unpushed still protect the work).
 */
async function detectDefaultBranchRef(
  worktreePath: string,
  opts?: { signal?: AbortSignal },
): Promise<string | null> {
  const head = await hostExec(
    `git -C ${shellEscape(worktreePath)} symbolic-ref --short refs/remotes/origin/HEAD`,
    { signal: opts?.signal, timeoutMs: 10_000 },
  );
  if (head.exitCode === 0) {
    const ref = head.stdout.trim(); // e.g. "origin/main"
    if (ref) {
      const verify = await hostExec(
        `git -C ${shellEscape(worktreePath)} rev-parse --verify --quiet ${shellEscape(ref + '^{commit}')}`,
        { signal: opts?.signal, timeoutMs: 10_000 },
      );
      if (verify.exitCode === 0 && verify.stdout.trim()) return ref;
    }
  }
  // origin/HEAD unset or unresolvable — probe common defaults. Prefer the
  // remote-tracking ref (always resolvable in a fresh worktree) over the local
  // head, which may not exist if the default branch lives only in the main tree.
  for (const cand of ['origin/main', 'origin/master', 'main', 'master']) {
    const verify = await hostExec(
      `git -C ${shellEscape(worktreePath)} rev-parse --verify --quiet ${shellEscape(cand + '^{commit}')}`,
      { signal: opts?.signal, timeoutMs: 10_000 },
    );
    if (verify.exitCode === 0 && verify.stdout.trim()) return cand;
  }
  return null;
}

/**
 * Inspect a worktree for work that would be lost if its session were deleted.
 * Three checks, all via the audited hostExec + shellEscape path (every
 * interpolated value — paths, refs — is single-quote-escaped; no bare
 * interpolation). Any unexpected git failure is treated as at-risk, never a
 * silent pass.
 */
export async function checkWorktreeWorkAtRisk(
  worktreePath: string,
  opts?: { signal?: AbortSignal },
): Promise<WorktreeRiskReport> {
  // Branch name — also doubles as the "is this still a git worktree?" probe.
  const br = await hostExec(
    `git -C ${shellEscape(worktreePath)} rev-parse --abbrev-ref HEAD`,
    { signal: opts?.signal, timeoutMs: 10_000 },
  );
  if (br.exitCode !== 0) {
    return {
      worktreePath,
      branch: '',
      dirty: false,
      unpushed: 0,
      unmerged: 0,
      atRisk: true,
      error: `git rev-parse failed: ${br.stderr.trim() || 'not a git worktree'}`,
    };
  }
  const branch = br.stdout.trim();

  // (a) Uncommitted (dirty working tree, including untracked files).
  const st = await hostExec(
    `git -C ${shellEscape(worktreePath)} status --porcelain`,
    { signal: opts?.signal, timeoutMs: 15_000 },
  );
  if (st.exitCode !== 0) {
    return {
      worktreePath,
      branch,
      dirty: false,
      unpushed: 0,
      unmerged: 0,
      atRisk: true,
      error: `git status failed: ${st.stderr.trim()}`,
    };
  }
  const dirty = st.stdout.trim().length > 0;

  // (b) Unpushed commits. No upstream configured => work exists only locally;
  // treat as unpushed-by-definition (-1) rather than an error.
  const up = await hostExec(
    `git -C ${shellEscape(worktreePath)} rev-list --count ${shellEscape('@{u}..HEAD')}`,
    { signal: opts?.signal, timeoutMs: 15_000 },
  );
  const unpushed = up.exitCode === 0 ? (parseInt(up.stdout.trim() || '0', 10) || 0) : -1;

  // (c) Unmerged commits — on this branch but not in the project default branch.
  const defaultRef = await detectDefaultBranchRef(worktreePath, opts);
  let unmerged = 0;
  if (defaultRef) {
    const rl = await hostExec(
      `git -C ${shellEscape(worktreePath)} rev-list --count ${shellEscape(defaultRef + '..HEAD')}`,
      { signal: opts?.signal, timeoutMs: 15_000 },
    );
    if (rl.exitCode === 0) unmerged = parseInt(rl.stdout.trim() || '0', 10) || 0;
  }

  // unpushed only contributes when an upstream actually exists. Session branches
  // (session-<id>) never have one (unpushed === -1), and any real local-only work
  // there already surfaces as unmerged > 0 — so the no-upstream case adds no
  // protection, only friction (it flagged every pristine worktree-backed session).
  // The unpushed > 0 arm stays forward-compatible with P1.5 pushable branches.
  const hasUpstream = unpushed !== -1;
  const atRisk = dirty || unmerged > 0 || (hasUpstream && unpushed > 0);
  return { worktreePath, branch, dirty, unpushed, unmerged, atRisk };
}

/**
 * Stash a worktree's uncommitted changes (including untracked, via -u) so the
 * working tree is clean. Stash entries live in the repo's common git dir, so
 * they survive worktree-dir removal — this is the recoverable, safe-by-default
 * escape. Note it only clears the *dirty* risk; unpushed/unmerged commits
 * remain on the branch, so a re-attempted delete may still block on those.
 */
export async function stashWorktree(
  worktreePath: string,
  opts?: { signal?: AbortSignal },
): Promise<{ stashed: boolean; error?: string }> {
  const r = await hostExec(
    `git -C ${shellEscape(worktreePath)} stash push -u -m ${shellEscape('boocode: pre-delete stash')}`,
    { signal: opts?.signal, timeoutMs: 30_000 },
  );
  if (r.exitCode !== 0) {
    return { stashed: false, error: r.stderr.trim() || r.stdout.trim() };
  }
  // "No local changes to save" => exit 0, nothing stashed — not an error.
  const stashed = !/no local changes to save/i.test(r.stdout);
  return { stashed };
}

/** Minimal shell escape for paths (single-quote wrapping). */
function shellEscape(s: string): string {
  // Replace single quotes with escaped version, wrap in single quotes
  return "'" + s.replace(/'/g, "'\\''") + "'";
}
