import { request } from 'undici';
import { setTimeout as sleep } from 'node:timers/promises';
import { createWriteStream } from 'node:fs';
import { rm, rename } from 'node:fs/promises';
import { Transform } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { createHash } from 'node:crypto';

export function retryDelay(value, attempt, maxMs = 5000, now = Date.now()) {
  if (value !== undefined) {
    const text = String(value).trim();
    const ms = /^\d+$/.test(text) ? Number(text) * 1000 : Date.parse(text) - now;
    // Do not retry earlier than a Retry-After beyond our permitted budget.
    if (Number.isFinite(ms)) return ms > maxMs ? null : Math.max(0, ms);
  }
  return Math.min(maxMs, 100 * 2 ** (attempt - 1) + Math.floor(Math.random() * 50));
}
export async function getWithRetry(url, { maxAttempts = 3, signal = AbortSignal.timeout(3000), dispatcher, maxDelay = 1000, onAttempt = () => {} } = {}) {
  // Deliberately GET-only. Repeating mutation requests needs an explicit server contract.
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const res = await request(url, { dispatcher, method: 'GET', signal, headersTimeout: 800, bodyTimeout: 800 });
    const data = await res.body.json(); // These fixtures are small JSON; consume before retry.
    onAttempt({ attempt, status: res.statusCode, data });
    if (![429, 503].includes(res.statusCode) || attempt === maxAttempts) return { status: res.statusCode, data, attempts: attempt };
    const delay = retryDelay(res.headers['retry-after'], attempt, maxDelay);
    if (delay === null) return { status: res.statusCode, data, attempts: attempt, stop: 'retry_after_exceeds_budget' };
    await sleep(delay, undefined, { signal });
  }
  throw new RangeError('maxAttempts must be positive');
}
export async function download(url, destination, { maxBytes = 1024 * 1024, signal = AbortSignal.timeout(5000), dispatcher, onProgress = () => {} } = {}) {
  const partial = `${destination}.part`; let bytes = 0; const hash = createHash('sha256');
  // The caller supplies a private, unique destination; never a user-controlled filesystem path.
  try {
    const res = await request(url, { dispatcher, signal, headersTimeout: 1000, bodyTimeout: 1000 });
    if (res.statusCode !== 200) { await res.body.dump(); throw new Error(`HTTP_${res.statusCode}`); }
    const sizeGuard = new Transform({ transform(chunk, encoding, cb) {
      bytes += chunk.length;
      if (bytes > maxBytes) { cb(Object.assign(new Error('Download exceeds byte limit'), { code: 'MAX_BYTES' })); return; }
      hash.update(chunk); onProgress({ bytes }); cb(null, chunk);
    } });
    await pipeline(res.body, sizeGuard, createWriteStream(partial, { flags: 'wx' }), { signal });
    await rename(partial, destination);
    return { bytes, sha256: hash.digest('hex'), complete: true };
  } catch (error) { await rm(partial, { force: true }); throw error; }
}
export function safeLog({ id, path, status, ms, error }) {
  // Whitelist fields. Do not serialize a request, headers, cookies, URL query, or body.
  return { requestId: String(id), route: String(path).split('?')[0], status: status ?? null, durationMs: Math.round(ms), errorCode: error?.code ?? null };
}
