/**
 * Per-slug single-flight mutex.
 *
 * Guards long-running, mutating operations (backup, restore, upgrade) so that
 * two concurrent CCP calls for the same slug can't trample each other.
 *
 * Usage:
 *   await withSlugLock(slug, 'backup', async () => { ... });
 *
 * If a lock is already held for (slug, op), throws SlugBusyError which the
 * route handler should convert to HTTP 409.
 */

export class SlugBusyError extends Error {
  constructor(public slug: string, public op: string) {
    super(`Slug ${slug} is busy: ${op} already in progress`);
    this.name = 'SlugBusyError';
  }
}

type LockKey = string;
const locks = new Map<LockKey, { op: string; startedAt: number }>();

function key(slug: string, op: string): LockKey {
  return `${slug}::${op}`;
}

/**
 * Run `fn` while holding a single-flight lock on (slug, op).
 * Throws SlugBusyError immediately if another call is already running.
 */
export async function withSlugLock<T>(
  slug: string,
  op: string,
  fn: () => Promise<T>
): Promise<T> {
  const k = key(slug, op);
  if (locks.has(k)) {
    throw new SlugBusyError(slug, op);
  }
  locks.set(k, { op, startedAt: Date.now() });
  try {
    return await fn();
  } finally {
    locks.delete(k);
  }
}

/**
 * Returns true if a lock is currently held for (slug, op).
 */
export function isSlugLocked(slug: string, op: string): boolean {
  return locks.has(key(slug, op));
}

/**
 * Returns debug info about all active locks.
 */
export function listActiveLocks(): Array<{ slug: string; op: string; ageMs: number }> {
  const now = Date.now();
  return Array.from(locks.entries()).map(([k, v]) => {
    const [slug] = k.split('::');
    return { slug: slug ?? '', op: v.op, ageMs: now - v.startedAt };
  });
}
