// Camera count and preparation concurrency remain device-agnostic scheduling
// policy. Recorded duration and aggregate bytes are deliberately not admission
// criteria: sealed media is presented through a generation-bound HTTP stream,
// so a one-, three- or ten-hour recording never has to fit in browser memory.
export const MAX_RECORDED_CAMERA_SOURCES = 16;
export const MAX_CONCURRENT_RECORDED_CAMERA_PREPARATIONS = 1;

export type RecordedAdmissionPhase = "loading" | "ready" | "error";
export type RecordedCameraAdmissionPhase = "pending" | "loading" | "ready" | "error";

export interface RecordedCameraAdmissionState {
  phase: RecordedCameraAdmissionPhase;
  byteLength: number | null;
  message: string | null;
  admissionKey?: string | null;
  workerGeneration?: number;
}

export type RecordedCameraAdmissionMap = Readonly<Record<string, RecordedCameraAdmissionState>>;

export interface RecordedCameraAdmissionDescriptor {
  id: string;
  byteLength: number;
}

export function recordedCameraDescriptorPreflight(
  sources: readonly RecordedCameraAdmissionDescriptor[],
): RecordedAdmissionPhase {
  if (sources.length > MAX_RECORDED_CAMERA_SOURCES) return "error";
  if (new Set(sources.map(({ id }) => id)).size !== sources.length) return "error";
  for (const source of sources) {
    if (
      !source.id ||
      !Number.isSafeInteger(source.byteLength) ||
      source.byteLength < 1
    ) return "error";
  }
  return "ready";
}

export function initialRecordedCameraAdmissions(
  sourceIds: readonly string[],
  declaredByteLengths: Readonly<Record<string, number>> = {},
): Record<string, RecordedCameraAdmissionState> {
  return Object.fromEntries(sourceIds.map((sourceId) => [sourceId, {
    phase: "pending" as const,
    byteLength: declaredByteLengths[sourceId] ?? null,
    message: null,
  }]));
}

export function recordedSessionAdmissionPhase(
  spatialPhase: RecordedAdmissionPhase,
  sourceIds: readonly string[],
  cameras: RecordedCameraAdmissionMap,
): RecordedAdmissionPhase {
  if (sourceIds.length > MAX_RECORDED_CAMERA_SOURCES || spatialPhase === "error") {
    return "error";
  }
  for (const sourceId of sourceIds) {
    const camera = cameras[sourceId];
    if (!camera || camera.phase === "error") return "error";
    if (camera.phase === "ready" && camera.byteLength === null) return "error";
    if (camera.byteLength !== null) {
      if (
        !Number.isSafeInteger(camera.byteLength) ||
        camera.byteLength < 1
      ) return "error";
    }
  }
  if (spatialPhase !== "ready") return "loading";
  return sourceIds.every((sourceId) => cameras[sourceId]?.phase === "ready")
    ? "ready"
    : "loading";
}

export function nextRecordedCameraPreparationIds(
  sourceIds: readonly string[],
  cameras: RecordedCameraAdmissionMap,
  preferredSourceIds: ReadonlySet<string> = new Set(),
  concurrency = MAX_CONCURRENT_RECORDED_CAMERA_PREPARATIONS,
): string[] {
  if (!Number.isInteger(concurrency) || concurrency < 1) return [];
  const pending = sourceIds.filter((sourceId) => {
    const phase = cameras[sourceId]?.phase;
    return phase === "pending" || phase === "loading" || phase === undefined;
  });
  pending.sort((left, right) => (
    Number(cameras[right]?.phase === "loading") - Number(cameras[left]?.phase === "loading") ||
    Number(preferredSourceIds.has(right)) - Number(preferredSourceIds.has(left)) ||
    sourceIds.indexOf(left) - sourceIds.indexOf(right)
  ));
  return pending.slice(0, concurrency);
}

export function mergeRecordedCameraAdmission(
  current: RecordedCameraAdmissionState,
  next: RecordedCameraAdmissionState,
): RecordedCameraAdmissionState {
  const normalized = next.byteLength === null && current.byteLength !== null
    ? { ...next, byteLength: current.byteLength }
    : next;
  const currentWorker = current.workerGeneration ?? 0;
  const nextWorker = normalized.workerGeneration ?? currentWorker;
  if (nextWorker < currentWorker) return current;
  if (current.phase === "error") return current;
  if (nextWorker > currentWorker) return normalized;
  if (normalized.phase === "error") return normalized;
  if (current.phase === "ready") return current;
  return normalized;
}
