/**
 * Sticky-backend resolver for proxy worker jobs.
 *
 * Design (per
 *   docs/superpowers/specs/2026-05-18-multi-team-gpu-pool-and-node-status-design.md
 * Open Question #3 case 1, "最初に確定したら以後 update しない"):
 *
 * - For a proxy worker, every LLM call may resolve to a different
 *   physical backend (LiteLLM rebalances per request). The UI pet should
 *   not flicker, so we record only the FIRST backend a job sees.
 * - Persistence happens via `updateJob({ lastBackendId })`. If that DB
 *   write FAILS, we must remain in the unset state so the next
 *   `onBackendResolved` event has a chance to retry. If we set the local
 *   sticky variable BEFORE persisting, a transient DB error would lose
 *   the binding permanently for the lifetime of the job (sticky check
 *   short-circuits all subsequent events) and the UI would never see
 *   the worker → backend mapping.
 *
 * This module isolates the "set sticky only after persist succeeds"
 * invariant from `Worker.buildPieceRunCallbacks`, which already has a
 * dozen other concerns and is hard to unit-test in isolation.
 */

export interface StickyBackendLogger {
  debug: (msg: string) => void;
  info: (msg: string) => void;
  warn: (msg: string) => void;
}

export interface StickyBackendEvent {
  backendId: string;
  cacheKey: string | null;
}

/**
 * Build the `onBackendResolved` callback. `persist(backendId)` is the
 * DB write (typically `repo.updateJob(jobId, { lastBackendId })`); it
 * must reject on failure so we can keep sticky unset for retry.
 *
 * Returns an async function the agent-loop can call without awaiting —
 * errors are caught internally and logged. The function resolves once
 * either: (a) sticky was already set and we short-circuited, (b) the
 * persist succeeded and sticky is now set, or (c) the persist failed
 * and sticky remains unset for the next event to retry.
 */
export function createStickyBackendResolver(opts: {
  initial: string | null;
  persist: (backendId: string) => Promise<void>;
  logger: StickyBackendLogger;
  workerId: string;
  jobId: string;
}): (event: StickyBackendEvent) => Promise<void> {
  const { initial, persist, logger, workerId, jobId } = opts;
  let sticky: string | null = initial;

  return async function onBackendResolved({ backendId, cacheKey }: StickyBackendEvent): Promise<void> {
    if (sticky) {
      if (sticky !== backendId) {
        logger.debug(
          `[worker:${workerId}] job ${jobId} backend re-resolved to ${backendId} (sticky=${sticky}, cache=${cacheKey ?? 'miss'}); keeping sticky`,
        );
      }
      return;
    }
    try {
      await persist(backendId);
      // Only set sticky AFTER persist succeeds. If we set first and persist
      // failed, the next event would short-circuit on the sticky check and
      // we'd never recover — the UI would render "no backend" forever.
      sticky = backendId;
      logger.info(
        `[worker:${workerId}] job ${jobId} backend resolved: ${backendId} cache=${cacheKey ?? 'miss'}`,
      );
    } catch (err) {
      logger.warn(
        `[worker:${workerId}] failed to persist lastBackendId for job ${jobId}: ${err} — sticky left unset for retry`,
      );
      // Intentionally do NOT set sticky. Next event retries.
    }
  };
}
