import {
  MAX_RECORDED_CAMERA_SOURCES,
} from "./recordedSessionAdmission";

export type ObservationSessionStatus =
  | "recording"
  | "ready"
  | "degraded"
  | "interrupted"
  | "failed";

export interface ObservationLabInstance {
  labId: string;
  sourceSessionId: string;
  resultKind: string;
  resultId: string;
  sourceResultId: string | null;
  configSha256: string | null;
  runCreatedAtUtc: string;
  publishedAtUtc: string;
  provenance: Readonly<Record<string, unknown>>;
}

export interface ObservationSessionSummary {
  id: string;
  label: string;
  startedAtUtc: string;
  completedAtUtc: string | null;
  status: ObservationSessionStatus;
  modalities: readonly string[];
  durationSeconds: number;
  replayable: boolean;
  preparation: ObservationSessionCatalogPreparation | null;
  lab: ObservationLabInstance | null;
}

export interface ObservationSessionCatalog {
  items: readonly ObservationSessionSummary[];
}

export interface ObservationSessionReplayLaunch {
  kind: "rerun-recording";
  sessionId: string;
  sourceUrl: string;
  viewerSourceUrl: string;
  mediaType: "application/vnd.rerun.rrd";
  timeline: "session_time";
  timelineStartSeconds: number;
  timelineEndSeconds: number;
  seekable: true;
  byteLength: number;
  sha256: string;
  playback: {
    speed: number;
    loop: boolean;
  };
  mediaSources: readonly ObservationRecordedMediaSource[];
}

export type ObservationSessionPreparationState =
  | "queued"
  | "validating"
  | "exporting"
  | "finalizing"
  | "failed"
  | "cancelled";

export interface ObservationSessionPreparation {
  preparationId: string;
  sessionId: string;
  state: ObservationSessionPreparationState;
  progress: number | null;
  updatedAtUtc: string;
  statusUrl: string;
  cancellable: boolean;
  retryable: boolean;
  error: string | null;
}

export interface ObservationSessionCatalogPreparation {
  preparationId: string;
  state: ObservationSessionPreparationState | "ready";
  progress: number | null;
  updatedAtUtc: string;
  cancellable: boolean;
  retryable: boolean;
  error: string | null;
}

export type ObservationSessionReplayResponse =
  | { kind: "ready"; launch: ObservationSessionReplayLaunch }
  | { kind: "preparing"; preparation: ObservationSessionPreparation };

export interface ObservationRecordedMediaSource {
  id: string;
  label: string;
  modality: "video";
  manifestUrl: string;
  manifestGenerationSha256: string;
  byteLength: number;
  mediaType: "video/mp4";
  timelineStartSeconds: number;
  timelineEndSeconds: number;
  seekable: true;
  synchronization: "host-arrival-best-effort";
}

export interface ObservationRecordedMediaEpoch {
  ordinal: number;
  timelineStartSeconds: number;
  timelineEndSeconds: number;
  mediaType: string;
  byteLength: number;
  streamUrl: string;
}

export interface ObservationRecordedMediaManifest {
  sourceId: string;
  generationSha256: string;
  byteLength: number;
  timelineStartSeconds: number;
  timelineEndSeconds: number;
  synchronization: "host-arrival-best-effort";
  epochs: readonly ObservationRecordedMediaEpoch[];
}

export type ObservationSessionFetch = (
  input: RequestInfo | URL,
  init?: RequestInit,
) => Promise<Response>;

const SESSION_STATUSES = new Set<ObservationSessionStatus>([
  "recording",
  "ready",
  "degraded",
  "interrupted",
  "failed",
]);

const CATALOG_KEYS = new Set(["items"]);
const ITEM_KEYS = new Set([
  "id",
  "label",
  "started_at_utc",
  "completed_at_utc",
  "status",
  "modalities",
  "duration_seconds",
  "replayable",
  "preparation",
  "lab",
]);
const LAB_KEYS = new Set([
  "lab_id",
  "source_session_id",
  "result_kind",
  "result_id",
  "source_result_id",
  "config_sha256",
  "run_created_at_utc",
  "published_at_utc",
  "provenance",
]);
const CATALOG_PREPARATION_KEYS = new Set([
  "preparation_id",
  "state",
  "progress",
  "updated_at_utc",
  "cancellable",
  "retryable",
  "error",
]);
const REPLAY_KEYS = new Set(["schema_version", "launch"]);
const REPLAY_LAUNCH_KEYS = new Set([
  "kind",
  "session_id",
  "source_url",
  "viewer_source_url",
  "media_type",
  "timeline",
  "timeline_start_seconds",
  "timeline_end_seconds",
  "seekable",
  "byte_length",
  "sha256",
  "playback",
  "media_sources",
]);
const REPLAY_PLAYBACK_KEYS = new Set(["speed", "loop"]);
const PREPARATION_KEYS = new Set(["schema_version", "preparation"]);
const PREPARATION_VALUE_KEYS = new Set([
  "preparation_id",
  "session_id",
  "state",
  "progress",
  "updated_at_utc",
  "status_url",
  "cancellable",
  "retryable",
  "error",
]);
const RECORDED_MEDIA_SOURCE_KEYS = new Set([
  "id",
  "label",
  "modality",
  "manifest_url",
  "manifest_generation_sha256",
  "byte_length",
  "media_type",
  "timeline_start_seconds",
  "timeline_end_seconds",
  "seekable",
  "synchronization",
]);
const RECORDED_MEDIA_MANIFEST_KEYS = new Set([
  "schema_version",
  "source_id",
  "generation_sha256",
  "byte_length",
  "timeline_start_seconds",
  "timeline_end_seconds",
  "synchronization",
  "epochs",
]);
const RECORDED_MEDIA_EPOCH_KEYS = new Set([
  "ordinal",
  "timeline_start_seconds",
  "timeline_end_seconds",
  "media_type",
  "byte_length",
  "stream_url",
]);
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const SAFE_MODALITY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
const ISO_WITH_TIMEZONE = /^\d{4}-\d{2}-\d{2}T.+(?:Z|[+-]\d{2}:\d{2})$/;
const SHA256 = /^[a-f0-9]{64}$/;
const SAFE_RECORDING_URL = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9._%:-]+\/recording\.rrd$/;
const SAFE_PREPARATION_URL = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9._%:-]+\/recording-preparation$/;
const SAFE_MEDIA_MANIFEST_URL = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9._%:-]+\/(?:media\/[A-Za-z0-9._%-]+|perception-media\/result-[a-f0-9]{64})\/manifest$/;
const SAFE_MEDIA_STREAM_URL = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9._%:-]+\/(?:media\/[A-Za-z0-9._%-]+\/epochs\/[1-9][0-9]*|perception-media\/result-[a-f0-9]{64})\/recording\.mp4\?generation=[a-f0-9]{64}$/;
const SAFE_MP4_MEDIA_TYPE = /^video\/mp4(?:; codecs="[A-Za-z0-9.,_-]+")?$/;

export class ObservationSessionContractError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "ObservationSessionContractError";
  }
}

export class ObservationSessionApiError extends Error {
  readonly status: number;

  constructor(message: string, status = 0) {
    super(message);
    this.name = "ObservationSessionApiError";
    this.status = status;
  }
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function assertExactKeys(
  value: Record<string, unknown>,
  allowed: ReadonlySet<string>,
  context: string,
): void {
  const unknown = Object.keys(value).filter((key) => !allowed.has(key));
  if (unknown.length > 0) {
    throw new ObservationSessionContractError(
      `${context} содержит неизвестные поля: ${unknown.sort().join(", ")}.`,
    );
  }
}

function requireString(
  value: unknown,
  field: string,
  maximumLength: number,
): string {
  if (typeof value !== "string") {
    throw new ObservationSessionContractError(`Поле ${field} должно быть строкой.`);
  }
  const normalized = value.trim();
  if (!normalized || normalized.length > maximumLength) {
    throw new ObservationSessionContractError(`Поле ${field} имеет недопустимую длину.`);
  }
  return normalized;
}

function requireUtcTimestamp(value: unknown, field: string): string {
  const timestamp = requireString(value, field, 64);
  if (!ISO_WITH_TIMEZONE.test(timestamp) || !Number.isFinite(Date.parse(timestamp))) {
    throw new ObservationSessionContractError(
      `Поле ${field} должно быть ISO-датой с часовым поясом.`,
    );
  }
  return timestamp;
}

function optionalUtcTimestamp(value: unknown, field: string): string | null {
  return value === null ? null : requireUtcTimestamp(value, field);
}

function requireStatus(value: unknown): ObservationSessionStatus {
  if (typeof value !== "string" || !SESSION_STATUSES.has(value as ObservationSessionStatus)) {
    throw new ObservationSessionContractError("Поле status содержит неизвестное состояние сессии.");
  }
  return value as ObservationSessionStatus;
}

function requireModalities(value: unknown): string[] {
  if (!Array.isArray(value)) {
    throw new ObservationSessionContractError("Поле modalities должно быть массивом.");
  }
  const modalities: string[] = [];
  for (const entry of value) {
    const modality = requireString(entry, "modalities[]", 64);
    if (!SAFE_MODALITY.test(modality)) {
      throw new ObservationSessionContractError(
        `Модальность ${modality} не является безопасным идентификатором.`,
      );
    }
    if (!modalities.includes(modality)) modalities.push(modality);
  }
  return modalities;
}

function decodeItem(value: unknown, index: number): ObservationSessionSummary {
  if (!isRecord(value)) {
    throw new ObservationSessionContractError(`Элемент sessions[${index}] должен быть объектом.`);
  }
  assertExactKeys(value, ITEM_KEYS, `Элемент sessions[${index}]`);

  const id = requireString(value.id, "id", 128);
  if (!SAFE_ID.test(id)) {
    throw new ObservationSessionContractError(
      `Идентификатор сессии ${id} не является безопасным непрозрачным идентификатором.`,
    );
  }
  const startedAtUtc = requireUtcTimestamp(value.started_at_utc, "started_at_utc");
  const completedAtUtc = optionalUtcTimestamp(value.completed_at_utc, "completed_at_utc");
  if (completedAtUtc && Date.parse(completedAtUtc) < Date.parse(startedAtUtc)) {
    throw new ObservationSessionContractError(
      `Сессия ${id} завершилась раньше времени начала.`,
    );
  }
  if (
    typeof value.duration_seconds !== "number" ||
    !Number.isFinite(value.duration_seconds) ||
    value.duration_seconds < 0
  ) {
    throw new ObservationSessionContractError(
      `Поле duration_seconds сессии ${id} должно быть неотрицательным числом.`,
    );
  }
  if (typeof value.replayable !== "boolean") {
    throw new ObservationSessionContractError(`Поле replayable сессии ${id} должно быть boolean.`);
  }

  return {
    id,
    label: requireString(value.label, "label", 256),
    startedAtUtc,
    completedAtUtc,
    status: requireStatus(value.status),
    modalities: requireModalities(value.modalities),
    durationSeconds: value.duration_seconds,
    replayable: value.replayable,
    preparation: decodeCatalogPreparation(value.preparation, id),
    lab: decodeLabInstance(value.lab, id),
  };
}

function decodeLabInstance(
  value: unknown,
  sessionId: string,
): ObservationLabInstance | null {
  if (value === null || value === undefined) return null;
  if (!isRecord(value)) {
    throw new ObservationSessionContractError(
      `LAB-привязка сессии ${sessionId} должна быть объектом или null.`,
    );
  }
  assertExactKeys(value, LAB_KEYS, `LAB-привязка сессии ${sessionId}`);
  const labId = requireString(value.lab_id, `lab(${sessionId}).lab_id`, 36);
  if (!/^LAB [A-Z][A-Z0-9._-]{0,31}$/.test(labId)) {
    throw new ObservationSessionContractError("Каталог содержит некорректный LAB-маркер.");
  }
  const sourceSessionId = requireString(
    value.source_session_id,
    `lab(${sessionId}).source_session_id`,
    128,
  );
  const resultKind = requireString(value.result_kind, `lab(${sessionId}).result_kind`, 128);
  const resultId = requireString(value.result_id, `lab(${sessionId}).result_id`, 128);
  const sourceResultId = value.source_result_id === null
    ? null
    : requireString(value.source_result_id, `lab(${sessionId}).source_result_id`, 128);
  for (const [field, entry] of [
    ["source_session_id", sourceSessionId],
    ["result_kind", resultKind],
    ["result_id", resultId],
    ["source_result_id", sourceResultId],
  ] as const) {
    if (entry !== null && !SAFE_ID.test(entry)) {
      throw new ObservationSessionContractError(
        `Поле lab.${field} содержит небезопасный идентификатор.`,
      );
    }
  }
  const configSha256 = value.config_sha256 === null
    ? null
    : requireString(value.config_sha256, `lab(${sessionId}).config_sha256`, 64);
  if (configSha256 !== null && !SHA256.test(configSha256)) {
    throw new ObservationSessionContractError("LAB-конфигурация не содержит SHA-256.");
  }
  if (!isRecord(value.provenance)) {
    throw new ObservationSessionContractError("LAB provenance должен быть JSON-объектом.");
  }
  return {
    labId,
    sourceSessionId,
    resultKind,
    resultId,
    sourceResultId,
    configSha256,
    runCreatedAtUtc: requireUtcTimestamp(
      value.run_created_at_utc,
      `lab(${sessionId}).run_created_at_utc`,
    ),
    publishedAtUtc: requireUtcTimestamp(
      value.published_at_utc,
      `lab(${sessionId}).published_at_utc`,
    ),
    provenance: value.provenance,
  };
}

function decodeCatalogPreparation(
  value: unknown,
  sessionId: string,
): ObservationSessionCatalogPreparation | null {
  if (value === null || value === undefined) return null;
  if (!isRecord(value)) {
    throw new ObservationSessionContractError(
      `Подготовка сессии ${sessionId} должна быть объектом или null.`,
    );
  }
  assertExactKeys(value, CATALOG_PREPARATION_KEYS, `Подготовка сессии ${sessionId}`);
  const preparationId = requireString(
    value.preparation_id,
    `preparation(${sessionId}).preparation_id`,
    128,
  );
  if (!SAFE_ID.test(preparationId)) {
    throw new ObservationSessionContractError("Каталог содержит небезопасный preparation id.");
  }
  const allowedStates = new Set([...PREPARATION_STATES, "ready"]);
  if (typeof value.state !== "string" || !allowedStates.has(value.state)) {
    throw new ObservationSessionContractError("Каталог содержит неизвестную фазу подготовки.");
  }
  const progress = value.progress === null
    ? null
    : requireFiniteNumber(value.progress, `preparation(${sessionId}).progress`, { maximum: 1 });
  if (typeof value.cancellable !== "boolean" || typeof value.retryable !== "boolean") {
    throw new ObservationSessionContractError(
      "Каталог содержит некорректные флаги подготовки.",
    );
  }
  if (
    value.error !== undefined &&
    value.error !== null &&
    (typeof value.error !== "string" || !value.error.trim() || value.error.length > 1_000)
  ) {
    throw new ObservationSessionContractError("Каталог содержит некорректную ошибку подготовки.");
  }
  return {
    preparationId,
    state: value.state as ObservationSessionCatalogPreparation["state"],
    progress,
    updatedAtUtc: requireUtcTimestamp(
      value.updated_at_utc,
      `preparation(${sessionId}).updated_at_utc`,
    ),
    cancellable: value.cancellable,
    retryable: value.retryable,
    error: typeof value.error === "string" ? value.error.trim() : null,
  };
}

export function sortObservationSessions(
  items: readonly ObservationSessionSummary[],
): ObservationSessionSummary[] {
  return [...items].sort((left, right) => {
    const timestampOrder = Date.parse(right.startedAtUtc) - Date.parse(left.startedAtUtc);
    return timestampOrder || left.id.localeCompare(right.id);
  });
}

export function decodeObservationSessionCatalog(payload: unknown): ObservationSessionCatalog {
  if (!isRecord(payload)) {
    throw new ObservationSessionContractError("Каталог сессий должен быть объектом.");
  }
  assertExactKeys(payload, CATALOG_KEYS, "Каталог сессий");
  if (!Array.isArray(payload.items)) {
    throw new ObservationSessionContractError("Каталог сессий не содержит массив items.");
  }

  const items = payload.items.map(decodeItem);
  const ids = new Set<string>();
  for (const item of items) {
    if (ids.has(item.id)) {
      throw new ObservationSessionContractError(
        `Каталог сессий содержит повторяющийся id ${item.id}.`,
      );
    }
    ids.add(item.id);
  }
  return { items: sortObservationSessions(items) };
}

function requireFiniteNumber(
  value: unknown,
  field: string,
  {
    minimum = 0,
    maximum = Number.POSITIVE_INFINITY,
    integer = false,
  }: { minimum?: number; maximum?: number; integer?: boolean } = {},
): number {
  if (
    typeof value !== "number" ||
    !Number.isFinite(value) ||
    value < minimum ||
    value > maximum ||
    (integer && !Number.isInteger(value))
  ) {
    throw new ObservationSessionContractError(`Поле ${field} содержит недопустимое число.`);
  }
  return value;
}

export function decodeObservationSessionReplay(
  payload: unknown,
): ObservationSessionReplayLaunch {
  if (!isRecord(payload)) {
    throw new ObservationSessionContractError("Ответ запуска повтора должен быть объектом.");
  }
  assertExactKeys(payload, REPLAY_KEYS, "Ответ запуска повтора");
  if (payload.schema_version !== "missioncore.observation-session-replay/v2") {
    throw new ObservationSessionContractError("Сервер вернул неподдерживаемый контракт повтора.");
  }
  if (!isRecord(payload.launch)) {
    throw new ObservationSessionContractError("Ответ запуска не содержит descriptor записи.");
  }
  const launch = payload.launch;
  assertExactKeys(launch, REPLAY_LAUNCH_KEYS, "Descriptor записи");
  if (launch.kind !== "rerun-recording") {
    throw new ObservationSessionContractError("Сервер не вернул seekable Rerun recording.");
  }
  const sessionId = requireString(launch.session_id, "launch.session_id", 128);
  if (!SAFE_ID.test(sessionId)) {
    throw new ObservationSessionContractError("Descriptor содержит небезопасный session id.");
  }
  const sourceUrl = requireString(launch.source_url, "launch.source_url", 512);
  const expectedSourceUrl = `/api/v1/observation-sessions/${encodeURIComponent(sessionId)}/recording.rrd`;
  if (
    !SAFE_RECORDING_URL.test(sourceUrl) ||
    sourceUrl !== expectedSourceUrl ||
    sourceUrl.includes("..")
  ) {
    throw new ObservationSessionContractError(
      "Запись должна публиковаться по безопасному same-origin API URL.",
    );
  }
  if (launch.media_type !== "application/vnd.rerun.rrd" || launch.timeline !== "session_time") {
    throw new ObservationSessionContractError("Descriptor записи содержит неподдерживаемый формат.");
  }
  if (launch.seekable !== true) {
    throw new ObservationSessionContractError("Сохранённая сессия должна быть seekable.");
  }
  const timelineStartSeconds = requireFiniteNumber(
    launch.timeline_start_seconds,
    "launch.timeline_start_seconds",
  );
  const timelineEndSeconds = requireFiniteNumber(
    launch.timeline_end_seconds,
    "launch.timeline_end_seconds",
  );
  if (timelineEndSeconds < timelineStartSeconds) {
    throw new ObservationSessionContractError("Диапазон времени записи задан в обратном порядке.");
  }
  if (typeof launch.sha256 !== "string" || !SHA256.test(launch.sha256)) {
    throw new ObservationSessionContractError("Descriptor записи не содержит SHA-256.");
  }
  const viewerSourceUrl = requireString(
    launch.viewer_source_url,
    "launch.viewer_source_url",
    640,
  );
  const expectedViewerSourceUrl = `${expectedSourceUrl}?generation=${launch.sha256}`;
  if (viewerSourceUrl !== expectedViewerSourceUrl) {
    throw new ObservationSessionContractError(
      "URL визуализатора должен быть канонически привязан к SHA-256 поколения записи.",
    );
  }
  if (!isRecord(launch.playback)) {
    throw new ObservationSessionContractError("Descriptor записи не содержит playback.");
  }
  assertExactKeys(launch.playback, REPLAY_PLAYBACK_KEYS, "Параметры playback");
  const speed = requireFiniteNumber(launch.playback.speed, "launch.playback.speed", {
    maximum: 100,
  });
  if (typeof launch.playback.loop !== "boolean") {
    throw new ObservationSessionContractError("Поле launch.playback.loop должно быть boolean.");
  }
  if (
    !Array.isArray(launch.media_sources) ||
    launch.media_sources.length > MAX_RECORDED_CAMERA_SOURCES
  ) {
    throw new ObservationSessionContractError(
      "Descriptor записи содержит некорректный каталог медиаканалов.",
    );
  }
  const mediaSources = launch.media_sources.map((entry, index) =>
    decodeRecordedMediaSource(
      entry,
      index,
      sessionId,
      timelineStartSeconds,
      timelineEndSeconds,
    ));
  if (new Set(mediaSources.map(({ id }) => id)).size !== mediaSources.length) {
    throw new ObservationSessionContractError(
      "Descriptor записи содержит повторяющиеся медиаканалы.",
    );
  }
  return {
    kind: "rerun-recording",
    sessionId,
    sourceUrl,
    viewerSourceUrl,
    mediaType: "application/vnd.rerun.rrd",
    timeline: "session_time",
    timelineStartSeconds,
    timelineEndSeconds,
    seekable: true,
    byteLength: requireFiniteNumber(launch.byte_length, "launch.byte_length", {
      minimum: 1,
      maximum: Number.MAX_SAFE_INTEGER,
      integer: true,
    }),
    sha256: launch.sha256,
    playback: { speed, loop: launch.playback.loop },
    mediaSources,
  };
}

const PREPARATION_STATES = new Set<ObservationSessionPreparationState>([
  "queued",
  "validating",
  "exporting",
  "finalizing",
  "failed",
  "cancelled",
]);

export function decodeObservationSessionPreparation(
  payload: unknown,
  expectedSessionId: string,
): ObservationSessionPreparation {
  if (!SAFE_ID.test(expectedSessionId)) {
    throw new ObservationSessionContractError(
      "Идентификатор подготавливаемой сессии имеет недопустимый формат.",
    );
  }
  if (!isRecord(payload)) {
    throw new ObservationSessionContractError("Ответ подготовки записи должен быть объектом.");
  }
  assertExactKeys(payload, PREPARATION_KEYS, "Ответ подготовки записи");
  if (payload.schema_version !== "missioncore.observation-session-preparation/v1") {
    throw new ObservationSessionContractError("Сервер вернул неподдерживаемый контракт подготовки.");
  }
  if (!isRecord(payload.preparation)) {
    throw new ObservationSessionContractError("Ответ не содержит состояние подготовки записи.");
  }
  const value = payload.preparation;
  assertExactKeys(value, PREPARATION_VALUE_KEYS, "Состояние подготовки записи");
  const preparationId = requireString(value.preparation_id, "preparation.preparation_id", 128);
  const sessionId = requireString(value.session_id, "preparation.session_id", 128);
  if (!SAFE_ID.test(preparationId) || !SAFE_ID.test(sessionId) || sessionId !== expectedSessionId) {
    throw new ObservationSessionContractError(
      "Состояние подготовки содержит несовместимый идентификатор.",
    );
  }
  if (
    typeof value.state !== "string" ||
    !PREPARATION_STATES.has(value.state as ObservationSessionPreparationState)
  ) {
    throw new ObservationSessionContractError("Состояние подготовки неизвестно клиенту.");
  }
  const state = value.state as ObservationSessionPreparationState;
  const terminal = state === "failed" || state === "cancelled";
  const progress = value.progress === null
    ? null
    : requireFiniteNumber(value.progress, "preparation.progress", { maximum: 1 });
  const updatedAtUtc = requireUtcTimestamp(value.updated_at_utc, "preparation.updated_at_utc");
  const statusUrl = requireString(value.status_url, "preparation.status_url", 512);
  const expectedStatusUrl =
    `/api/v1/observation-sessions/${encodeURIComponent(sessionId)}/recording-preparation`;
  if (
    !SAFE_PREPARATION_URL.test(statusUrl) ||
    statusUrl !== expectedStatusUrl ||
    statusUrl.includes("..")
  ) {
    throw new ObservationSessionContractError(
      "Статус подготовки должен использовать канонический same-origin API URL.",
    );
  }
  if (typeof value.cancellable !== "boolean") {
    throw new ObservationSessionContractError("Поле preparation.cancellable должно быть boolean.");
  }
  if (value.retryable !== undefined && typeof value.retryable !== "boolean") {
    throw new ObservationSessionContractError("Поле preparation.retryable должно быть boolean.");
  }
  if (
    value.error !== undefined &&
    value.error !== null &&
    (typeof value.error !== "string" || !value.error.trim() || value.error.length > 1_000)
  ) {
    throw new ObservationSessionContractError("Поле preparation.error имеет недопустимый формат.");
  }
  if (!terminal && (value.retryable === true || (value.error !== undefined && value.error !== null))) {
    throw new ObservationSessionContractError(
      "Незавершённая подготовка не должна содержать terminal-поля.",
    );
  }
  if (terminal && value.cancellable) {
    throw new ObservationSessionContractError(
      "Завершённую подготовку нельзя помечать как cancellable.",
    );
  }
  return {
    preparationId,
    sessionId,
    state,
    progress,
    updatedAtUtc,
    statusUrl,
    cancellable: value.cancellable,
    retryable: terminal && value.retryable === true,
    error: terminal && typeof value.error === "string" ? value.error.trim() : null,
  };
}

function decodeRecordedMediaSource(
  value: unknown,
  index: number,
  sessionId: string,
  recordingStartSeconds: number,
  recordingEndSeconds: number,
): ObservationRecordedMediaSource {
  if (!isRecord(value)) {
    throw new ObservationSessionContractError(`Медиаканал ${index} должен быть объектом.`);
  }
  assertExactKeys(value, RECORDED_MEDIA_SOURCE_KEYS, `Медиаканал ${index}`);
  const id = requireString(value.id, `media_sources[${index}].id`, 128);
  if (
    !SAFE_ID.test(id) ||
    (!id.startsWith("recorded.camera.") && !id.startsWith("recorded.perception."))
  ) {
    throw new ObservationSessionContractError("Медиаканал содержит небезопасный opaque id.");
  }
  if (value.modality !== "video" || value.media_type !== "video/mp4") {
    throw new ObservationSessionContractError("Медиаканал имеет неподдерживаемый формат.");
  }
  const manifestUrl = requireString(
    value.manifest_url,
    `media_sources[${index}].manifest_url`,
    512,
  );
  if (
    typeof value.manifest_generation_sha256 !== "string" ||
    !SHA256.test(value.manifest_generation_sha256)
  ) {
    throw new ObservationSessionContractError(
      "Медиаканал не содержит immutable generation SHA-256.",
    );
  }
  const sessionBase = `/api/v1/observation-sessions/${encodeURIComponent(sessionId)}/`;
  if (
    !SAFE_MEDIA_MANIFEST_URL.test(manifestUrl) ||
    !manifestUrl.startsWith(sessionBase) ||
    manifestUrl.includes("..")
  ) {
    throw new ObservationSessionContractError(
      "Manifest медиаканала должен использовать безопасный same-origin API URL.",
    );
  }
  const timelineStartSeconds = requireFiniteNumber(
    value.timeline_start_seconds,
    `media_sources[${index}].timeline_start_seconds`,
  );
  const timelineEndSeconds = requireFiniteNumber(
    value.timeline_end_seconds,
    `media_sources[${index}].timeline_end_seconds`,
  );
  if (
    timelineEndSeconds <= timelineStartSeconds ||
    timelineStartSeconds < recordingStartSeconds ||
    timelineEndSeconds > recordingEndSeconds
  ) {
    throw new ObservationSessionContractError("Временной диапазон медиаканала некорректен.");
  }
  if (value.seekable !== true || value.synchronization !== "host-arrival-best-effort") {
    throw new ObservationSessionContractError("Медиаканал не поддерживает ожидаемый повтор.");
  }
  return {
    id,
    label: requireString(value.label, `media_sources[${index}].label`, 160),
    modality: "video",
    manifestUrl,
    manifestGenerationSha256: value.manifest_generation_sha256,
    byteLength: requireFiniteNumber(
      value.byte_length,
      `media_sources[${index}].byte_length`,
      { minimum: 1, maximum: Number.MAX_SAFE_INTEGER, integer: true },
    ),
    mediaType: "video/mp4",
    timelineStartSeconds,
    timelineEndSeconds,
    seekable: true,
    synchronization: "host-arrival-best-effort",
  };
}

export function decodeObservationRecordedMediaManifest(
  payload: unknown,
  source: ObservationRecordedMediaSource,
): ObservationRecordedMediaManifest {
  if (!isRecord(payload)) {
    throw new ObservationSessionContractError("Manifest записанного видео должен быть объектом.");
  }
  assertExactKeys(payload, RECORDED_MEDIA_MANIFEST_KEYS, "Manifest записанного видео");
  if (
    payload.schema_version !== "missioncore.observation-recorded-media/v3" ||
    payload.source_id !== source.id ||
    typeof payload.generation_sha256 !== "string" ||
    !SHA256.test(payload.generation_sha256) ||
    payload.generation_sha256 !== source.manifestGenerationSha256 ||
    payload.synchronization !== "host-arrival-best-effort" ||
    !Array.isArray(payload.epochs) ||
    payload.epochs.length < 1
  ) {
    throw new ObservationSessionContractError("Manifest записанного видео несовместим.");
  }
  const manifestBase = source.manifestUrl.slice(0, -"/manifest".length);
  const manifestByteLength = requireFiniteNumber(payload.byte_length, "manifest.byte_length", {
    minimum: 1,
    maximum: Number.MAX_SAFE_INTEGER,
    integer: true,
  });
  if (manifestByteLength !== source.byteLength) {
    throw new ObservationSessionContractError(
      "Размер manifest не совпадает с launch descriptor медиаканала.",
    );
  }
  const manifestTimelineStartSeconds = requireFiniteNumber(
    payload.timeline_start_seconds,
    "manifest.timeline_start_seconds",
  );
  const manifestTimelineEndSeconds = requireFiniteNumber(
    payload.timeline_end_seconds,
    "manifest.timeline_end_seconds",
  );
  if (
    manifestTimelineStartSeconds !== source.timelineStartSeconds ||
    manifestTimelineEndSeconds !== source.timelineEndSeconds ||
    manifestTimelineEndSeconds <= manifestTimelineStartSeconds
  ) {
    throw new ObservationSessionContractError(
      "Временной диапазон manifest не совпадает с launch descriptor медиаканала.",
    );
  }
  let previousEnd = Number.NEGATIVE_INFINITY;
  let declaredBytes = 0;
  const epochs = payload.epochs.map((entry, index): ObservationRecordedMediaEpoch => {
    if (!isRecord(entry)) {
      throw new ObservationSessionContractError(`Codec epoch ${index} должен быть объектом.`);
    }
    assertExactKeys(entry, RECORDED_MEDIA_EPOCH_KEYS, `Codec epoch ${index}`);
    const ordinal = requireFiniteNumber(entry.ordinal, `epochs[${index}].ordinal`, {
      minimum: 1,
      maximum: Number.MAX_SAFE_INTEGER,
      integer: true,
    });
    if (ordinal !== index + 1) {
      throw new ObservationSessionContractError("Codec epoch имеет непоследовательный ordinal.");
    }
    const timelineStartSeconds = requireFiniteNumber(
      entry.timeline_start_seconds,
      `epochs[${index}].timeline_start_seconds`,
    );
    const timelineEndSeconds = requireFiniteNumber(
      entry.timeline_end_seconds,
      `epochs[${index}].timeline_end_seconds`,
    );
    if (
      timelineEndSeconds <= timelineStartSeconds ||
      timelineStartSeconds < previousEnd ||
      timelineStartSeconds < source.timelineStartSeconds ||
      timelineEndSeconds > source.timelineEndSeconds
    ) {
      throw new ObservationSessionContractError("Codec epoch нарушает порядок временной шкалы.");
    }
    previousEnd = timelineEndSeconds;
    const mediaType = requireString(entry.media_type, `epochs[${index}].media_type`, 128);
    if (!SAFE_MP4_MEDIA_TYPE.test(mediaType)) {
      throw new ObservationSessionContractError("Codec epoch содержит небезопасный media type.");
    }
    const byteLength = requireFiniteNumber(
      entry.byte_length,
      `epochs[${index}].byte_length`,
      { minimum: 1, maximum: Number.MAX_SAFE_INTEGER, integer: true },
    );
    const streamUrl = requireString(entry.stream_url, `epochs[${index}].stream_url`, 768);
    const expectedEpochBase = source.id.startsWith("recorded.perception.")
      ? manifestBase
      : `${manifestBase}/epochs/${ordinal}`;
    const expectedStreamUrl =
      `${expectedEpochBase}/recording.mp4?generation=${payload.generation_sha256}`;
    if (
      !SAFE_MEDIA_STREAM_URL.test(streamUrl) ||
      streamUrl !== expectedStreamUrl ||
      streamUrl.includes("..")
    ) {
      throw new ObservationSessionContractError("Codec epoch содержит небезопасный API URL.");
    }
    declaredBytes += byteLength;
    if (!Number.isSafeInteger(declaredBytes)) {
      throw new ObservationSessionContractError(
        "Суммарный размер codec epoch выходит за точный числовой диапазон клиента.",
      );
    }
    return {
      ordinal,
      timelineStartSeconds,
      timelineEndSeconds,
      mediaType,
      byteLength,
      streamUrl,
    };
  });
  if (declaredBytes !== manifestByteLength) {
    throw new ObservationSessionContractError(
      "Сумма codec epoch не совпадает с размером immutable manifest.",
    );
  }
  if (
    epochs[0]?.timelineStartSeconds !== manifestTimelineStartSeconds ||
    epochs[epochs.length - 1]?.timelineEndSeconds !== manifestTimelineEndSeconds
  ) {
    throw new ObservationSessionContractError(
      "Codec epochs не покрывают граничные точки immutable manifest.",
    );
  }
  return {
    sourceId: source.id,
    generationSha256: payload.generation_sha256,
    byteLength: manifestByteLength,
    timelineStartSeconds: manifestTimelineStartSeconds,
    timelineEndSeconds: manifestTimelineEndSeconds,
    synchronization: "host-arrival-best-effort",
    epochs,
  };
}

export async function fetchObservationRecordedMediaManifest(
  source: ObservationRecordedMediaSource,
  {
    signal,
    fetcher = globalThis.fetch,
    expectedGenerationSha256,
  }: {
    signal?: AbortSignal;
    fetcher?: ObservationSessionFetch;
    expectedGenerationSha256?: string;
  } = {},
): Promise<ObservationRecordedMediaManifest> {
  const boundGenerationSha256 = expectedGenerationSha256 ?? source.manifestGenerationSha256;
  if (
    !SHA256.test(boundGenerationSha256) ||
    boundGenerationSha256 !== source.manifestGenerationSha256
  ) {
    throw new ObservationSessionContractError(
      "Ожидаемая generation записанного видео имеет некорректный SHA-256.",
    );
  }
  let response: Response;
  try {
    response = await fetcher(source.manifestUrl, {
      method: "GET",
      cache: "no-store",
      headers: {
        Accept: "application/json",
        "If-Match": `"sha256:${boundGenerationSha256}"`,
      },
      signal,
    });
  } catch (error) {
    if (error instanceof DOMException && error.name === "AbortError") throw error;
    throw new ObservationSessionApiError("Не удалось загрузить manifest записанного видео.");
  }
  const body = await responseBody(response);
  if (!response.ok) {
    throw new ObservationSessionApiError(
      apiErrorMessage(body, `Manifest видео вернул HTTP ${response.status}.`),
      response.status,
    );
  }
  const manifest = decodeObservationRecordedMediaManifest(body, source);
  if (manifest.generationSha256 !== boundGenerationSha256) {
    throw new ObservationSessionContractError(
      "Manifest записанного видео был заменён во время загрузки.",
    );
  }
  const expectedEtag = `"sha256:${manifest.generationSha256}"`;
  if (response.headers.get("ETag") !== expectedEtag) {
    throw new ObservationSessionContractError(
      "Manifest записанного видео не привязан к immutable generation.",
    );
  }
  return manifest;
}

async function responseBody(response: Response): Promise<unknown> {
  const text = await response.text();
  if (!text) return undefined;
  try {
    return JSON.parse(text) as unknown;
  } catch {
    return text;
  }
}

function apiErrorMessage(body: unknown, fallback: string): string {
  if (isRecord(body) && typeof body.detail === "string" && body.detail.trim()) {
    return body.detail.trim();
  }
  if (typeof body === "string" && body.trim()) return body.trim();
  return fallback;
}

function preparationEtag(preparationId: string): string {
  if (!SAFE_ID.test(preparationId)) {
    throw new ObservationSessionContractError("Preparation id нельзя использовать как ETag.");
  }
  return `"${preparationId}"`;
}

function requirePreparationEtag(
  response: Response,
  preparationId: string,
  context: string,
): void {
  const expected = preparationEtag(preparationId);
  const received = response.headers.get("ETag");
  if (received !== expected) {
    throw new ObservationSessionContractError(
      `${context} не подтвердил актуальный preparation ETag.`,
    );
  }
}

export async function fetchObservationSessionCatalog({
  signal,
  limit,
  fetcher = globalThis.fetch,
}: {
  signal?: AbortSignal;
  limit?: number;
  fetcher?: ObservationSessionFetch;
} = {}): Promise<ObservationSessionCatalog> {
  const query = Number.isInteger(limit) && Number(limit) >= 1 && Number(limit) <= 100
    ? `?limit=${Number(limit)}`
    : "";
  let response: Response;
  try {
    response = await fetcher(`/api/v1/observation-sessions${query}`, {
      method: "GET",
      headers: { Accept: "application/json" },
      signal,
    });
  } catch (error) {
    if (error instanceof DOMException && error.name === "AbortError") throw error;
    throw new ObservationSessionApiError("Не удалось загрузить каталог сохранённых сессий.");
  }
  const body = await responseBody(response);
  if (!response.ok) {
    throw new ObservationSessionApiError(
      apiErrorMessage(body, `Каталог сессий вернул HTTP ${response.status}.`),
      response.status,
    );
  }
  return decodeObservationSessionCatalog(body);
}

export async function deleteObservationSession(
  sessionId: string,
  {
    signal,
    fetcher = globalThis.fetch,
  }: { signal?: AbortSignal; fetcher?: ObservationSessionFetch } = {},
): Promise<void> {
  if (!SAFE_ID.test(sessionId)) {
    throw new ObservationSessionContractError(
      "Идентификатор удаляемой сессии имеет недопустимый формат.",
    );
  }
  let response: Response;
  try {
    response = await fetcher(
      `/api/v1/observation-sessions/${encodeURIComponent(sessionId)}`,
      {
        method: "DELETE",
        headers: { Accept: "application/json" },
        signal,
      },
    );
  } catch (error) {
    if (error instanceof DOMException && error.name === "AbortError") throw error;
    throw new ObservationSessionApiError("Не удалось удалить сохранённую сессию.");
  }
  const body = await responseBody(response);
  if (!response.ok || response.status !== 204) {
    throw new ObservationSessionApiError(
      apiErrorMessage(body, `Удаление сессии вернуло HTTP ${response.status}.`),
      response.status,
    );
  }
  if (body !== undefined) {
    throw new ObservationSessionContractError(
      "Сервер вернул данные после подтверждённого удаления сессии.",
    );
  }
}

export async function replayObservationSession(
  sessionId: string,
  {
    signal,
    fetcher = globalThis.fetch,
  }: { signal?: AbortSignal; fetcher?: ObservationSessionFetch } = {},
): Promise<ObservationSessionReplayResponse> {
  if (!SAFE_ID.test(sessionId)) {
    throw new ObservationSessionContractError(
      "Идентификатор запускаемой сессии имеет недопустимый формат.",
    );
  }
  let response: Response;
  try {
    response = await fetcher(
      `/api/v1/observation-sessions/${encodeURIComponent(sessionId)}/replay`,
      {
        method: "POST",
        headers: { Accept: "application/json" },
        signal,
      },
    );
  } catch (error) {
    if (error instanceof DOMException && error.name === "AbortError") throw error;
    throw new ObservationSessionApiError("Не удалось запустить повтор сохранённой сессии.");
  }
  const body = await responseBody(response);
  if (response.status === 200) {
    const launch = decodeObservationSessionReplay(body);
    if (launch.sessionId !== sessionId) {
      throw new ObservationSessionContractError(
        "Сервер подготовил повтор другой сессии.",
      );
    }
    return { kind: "ready", launch };
  }
  if (response.status === 202 || response.status === 409) {
    const preparation = decodeObservationSessionPreparation(body, sessionId);
    requirePreparationEtag(response, preparation.preparationId, "Запуск подготовки");
    const terminal = preparation.state === "failed" || preparation.state === "cancelled";
    if ((response.status === 409) !== terminal) {
      throw new ObservationSessionContractError(
        "HTTP-статус не соответствует состоянию подготовки записи.",
      );
    }
    return { kind: "preparing", preparation };
  }
  if (!response.ok) {
    throw new ObservationSessionApiError(
      apiErrorMessage(body, `Запуск повтора вернул HTTP ${response.status}.`),
      response.status,
    );
  }
  throw new ObservationSessionContractError(
    `Запуск повтора вернул неожиданный HTTP ${response.status}.`,
  );
}

export async function fetchObservationSessionPreparation(
  preparation: Pick<ObservationSessionPreparation, "sessionId" | "preparationId" | "statusUrl">,
  {
    signal,
    fetcher = globalThis.fetch,
  }: { signal?: AbortSignal; fetcher?: ObservationSessionFetch } = {},
): Promise<ObservationSessionReplayResponse> {
  const expectedUrl =
    `/api/v1/observation-sessions/${encodeURIComponent(preparation.sessionId)}/recording-preparation`;
  if (
    !SAFE_ID.test(preparation.sessionId) ||
    !SAFE_ID.test(preparation.preparationId) ||
    preparation.statusUrl !== expectedUrl ||
    !SAFE_PREPARATION_URL.test(preparation.statusUrl)
  ) {
    throw new ObservationSessionContractError("Нельзя опросить небезопасный URL подготовки.");
  }
  let response: Response;
  try {
    response = await fetcher(preparation.statusUrl, {
      method: "GET",
      headers: {
        Accept: "application/json",
        "If-Match": preparationEtag(preparation.preparationId),
      },
      signal,
    });
  } catch (error) {
    if (error instanceof DOMException && error.name === "AbortError") throw error;
    throw new ObservationSessionApiError("Не удалось проверить подготовку записи.");
  }
  const body = await responseBody(response);
  if (response.status === 200) {
    requirePreparationEtag(response, preparation.preparationId, "Готовая запись");
    const launch = decodeObservationSessionReplay(body);
    if (launch.sessionId !== preparation.sessionId) {
      throw new ObservationSessionContractError("Подготовлен повтор другой сессии.");
    }
    return { kind: "ready", launch };
  }
  if (response.status === 202 || response.status === 409) {
    const next = decodeObservationSessionPreparation(body, preparation.sessionId);
    requirePreparationEtag(response, preparation.preparationId, "Статус подготовки");
    if (
      next.preparationId !== preparation.preparationId ||
      next.statusUrl !== preparation.statusUrl
    ) {
      throw new ObservationSessionContractError("Сервер подменил идентификатор подготовки.");
    }
    const terminal = next.state === "failed" || next.state === "cancelled";
    if ((response.status === 409) !== terminal) {
      throw new ObservationSessionContractError(
        "HTTP-статус не соответствует состоянию подготовки записи.",
      );
    }
    return { kind: "preparing", preparation: next };
  }
  throw new ObservationSessionApiError(
    apiErrorMessage(body, `Проверка подготовки вернула HTTP ${response.status}.`),
    response.status,
  );
}

export async function cancelObservationSessionPreparation(
  preparation: Pick<ObservationSessionPreparation, "sessionId" | "preparationId" | "statusUrl">,
  {
    signal,
    fetcher = globalThis.fetch,
  }: { signal?: AbortSignal; fetcher?: ObservationSessionFetch } = {},
): Promise<ObservationSessionPreparation | null> {
  const expectedUrl =
    `/api/v1/observation-sessions/${encodeURIComponent(preparation.sessionId)}/recording-preparation`;
  if (
    !SAFE_ID.test(preparation.sessionId) ||
    !SAFE_ID.test(preparation.preparationId) ||
    preparation.statusUrl !== expectedUrl ||
    !SAFE_PREPARATION_URL.test(preparation.statusUrl)
  ) {
    throw new ObservationSessionContractError("Нельзя отменить небезопасную подготовку.");
  }
  let response: Response;
  try {
    response = await fetcher(preparation.statusUrl, {
      method: "DELETE",
      headers: {
        Accept: "application/json",
        "If-Match": preparationEtag(preparation.preparationId),
      },
      signal,
    });
  } catch (error) {
    if (error instanceof DOMException && error.name === "AbortError") throw error;
    throw new ObservationSessionApiError("Не удалось отменить подготовку записи.");
  }
  if (response.status === 204) return null;
  const body = await responseBody(response);
  if (response.status === 200 || response.status === 202 || response.status === 409) {
    const next = decodeObservationSessionPreparation(body, preparation.sessionId);
    requirePreparationEtag(response, preparation.preparationId, "Отмена подготовки");
    if (
      next.preparationId !== preparation.preparationId ||
      next.statusUrl !== preparation.statusUrl
    ) {
      throw new ObservationSessionContractError("Сервер подменил отменяемую подготовку.");
    }
    return next;
  }
  throw new ObservationSessionApiError(
    apiErrorMessage(body, `Отмена подготовки вернула HTTP ${response.status}.`),
    response.status,
  );
}
