/**
 * Shared cancellation-reason plumbing between the worker's per-job guard and the
 * agent loop.
 *
 * A job can end for two very different "abort the shared AbortController" reasons:
 *
 *   1. The user pressed cancel (DB job status flips to `cancelled`).
 *   2. The hard execution deadline fired (`safety.max_job_minutes`).
 *
 * Both used to surface identically as `Movement cancelled by caller`, so the UI
 * could not tell "you cancelled this" apart from "the system force-terminated it
 * after N minutes". The guard now aborts with a distinguishable *reason* for the
 * deadline case; the loop reads that reason to pick the right abort code and the
 * right user-facing message.
 */

/** AbortController.abort() reason used exclusively by the hard-deadline guard. */
export const DEADLINE_ABORT_REASON = 'deadline_exceeded';

/** abortCode carried on a MovementResult when the user cancelled the job. */
export const ABORT_CODE_CANCELLED = 'cancelled';

/** abortCode carried on a MovementResult when the hard deadline fired. */
export const ABORT_CODE_DEADLINE = 'deadline_exceeded';

/**
 * Forward an external abort (the job's cancel/deadline signal) onto a local
 * AbortController — used by tools that create their own per-request controller
 * (a timeout) so the job-level deadline/cancel actually interrupts an in-flight
 * request instead of being ignored until the tool's own timeout fires.
 *
 * Returns a cleanup function to detach the listener (call it in a finally, after
 * clearing the local timeout). If the external signal is already aborted the
 * local controller is aborted immediately.
 */
export function forwardAbort(target: AbortController, source?: AbortSignal | null): () => void {
  if (!source) return () => {};
  if (source.aborted) {
    target.abort(source.reason);
    return () => {};
  }
  const onAbort = () => target.abort(source.reason);
  source.addEventListener('abort', onAbort, { once: true });
  return () => source.removeEventListener('abort', onAbort);
}

/** True when the signal was aborted specifically by the hard-deadline guard. */
export function isDeadlineAbort(signal?: AbortSignal | null): boolean {
  return !!signal && signal.aborted && signal.reason === DEADLINE_ABORT_REASON;
}

/**
 * True when an abortCode represents a cancellation (user cancel OR deadline) as
 * opposed to a genuine failure. piece-runner uses this to map the movement
 * result to the `cancelled` job status — replacing the old brittle
 * `output.includes('cancelled')` string match.
 */
export function isCancellationAbortCode(abortCode?: string): boolean {
  return abortCode === ABORT_CODE_CANCELLED || abortCode === ABORT_CODE_DEADLINE;
}

/** Outcome of {@link raceWithGrace}: the work settled, or it was hard-killed. */
export type GraceRaceOutcome<T> =
  | { ok: true; value: T }
  | { ok: false; deadline: boolean };

/**
 * ②B hard-kill primitive. Resolve as soon as `work` settles ({ ok: true }); or,
 * if `work` does not settle within `graceMs` *after the abort signal fires*,
 * resolve as a hard-kill ({ ok: false, deadline }) so the caller can force a
 * terminal state and release the resource `work` was holding. A genuine
 * rejection from `work` is re-thrown (the caller's existing error path handles
 * it). The grace timer arms only once the abort fires — a run that is never
 * aborted races nothing and settles exactly as `work` does.
 *
 * `deadline` reflects whether the abort carried the hard-deadline reason, so the
 * caller can preserve the deadline-vs-user-cancel distinction (see ①).
 *
 * The detached `work` keeps executing after a hard-kill (accepted zombie
 * residual); a later settle is swallowed so it can't raise an unhandled
 * rejection.
 */
export function raceWithGrace<T>(
  work: Promise<T>,
  signal: AbortSignal,
  graceMs: number,
): Promise<GraceRaceOutcome<T>> {
  return new Promise((resolve, reject) => {
    let settled = false;
    let graceTimer: ReturnType<typeof setTimeout> | undefined;
    const clearGrace = () => { if (graceTimer) clearTimeout(graceTimer); };
    work.then(
      (value) => { if (settled) return; settled = true; clearGrace(); resolve({ ok: true, value }); },
      (err) => { if (settled) return; settled = true; clearGrace(); reject(err); },
    );
    const arm = () => {
      if (graceTimer || settled) return;
      graceTimer = setTimeout(() => {
        if (settled) return;
        settled = true;
        resolve({ ok: false, deadline: isDeadlineAbort(signal) });
      }, graceMs);
    };
    if (signal.aborted) arm();
    else signal.addEventListener('abort', arm, { once: true });
  });
}
