import { useCallback, useEffect, useMemo, useState } from "react";

import type { ObservationSessionReplayLaunch } from "./sessionArchive";
import {
  initialRecordedCameraAdmissions,
  mergeRecordedCameraAdmission,
  nextRecordedCameraPreparationIds,
  recordedCameraDescriptorPreflight,
  recordedSessionAdmissionPhase,
  type RecordedAdmissionPhase,
  type RecordedCameraAdmissionMap,
  type RecordedCameraAdmissionState,
} from "./recordedSessionAdmission";

interface AdmissionSnapshot {
  key: string | null;
  spatialPhase: RecordedAdmissionPhase;
  cameras: Record<string, RecordedCameraAdmissionState>;
}

export interface RecordedSessionAdmissionController {
  key: string;
  phase: RecordedAdmissionPhase;
  cameraSourceIds: readonly string[];
  cameras: RecordedCameraAdmissionMap;
  activeCameraSourceIds: ReadonlySet<string>;
  reportSpatial: (key: string, phase: RecordedAdmissionPhase) => void;
  reportCamera: (
    key: string,
    sourceId: string,
    state: RecordedCameraAdmissionState,
  ) => void;
}

function replayAdmissionKey(replay: ObservationSessionReplayLaunch): string {
  return [
    replay.sessionId,
    replay.sha256,
    ...replay.mediaSources.map((source) => (
      [
        source.id,
        source.manifestGenerationSha256,
        source.byteLength,
        source.timelineStartSeconds,
        source.timelineEndSeconds,
      ].join(":")
    )),
  ].join("|");
}

export function useRecordedSessionAdmission(
  replay: ObservationSessionReplayLaunch | null,
): RecordedSessionAdmissionController | null {
  const key = replay ? replayAdmissionKey(replay) : null;
  const cameraSourceIds = useMemo(
    () => replay?.mediaSources.map(({ id }) => id) ?? [],
    [replay],
  );
  const declaredByteLengths = useMemo(
    () => Object.fromEntries(
      (replay?.mediaSources ?? []).map(({ id, byteLength }) => [id, byteLength]),
    ),
    [replay],
  );
  const [snapshot, setSnapshot] = useState<AdmissionSnapshot>(() => ({
    key,
    spatialPhase: "loading",
    cameras: initialRecordedCameraAdmissions(cameraSourceIds, declaredByteLengths),
  }));

  useEffect(() => {
    setSnapshot({
      key,
      spatialPhase: "loading",
      cameras: initialRecordedCameraAdmissions(cameraSourceIds, declaredByteLengths),
    });
  }, [cameraSourceIds, declaredByteLengths, key]);

  const reportSpatial = useCallback((reportedKey: string, phase: RecordedAdmissionPhase) => {
    setSnapshot((current) => current.key !== reportedKey
      ? current
      : { ...current, spatialPhase: phase });
  }, []);

  const reportCamera = useCallback((
    reportedKey: string,
    sourceId: string,
    state: RecordedCameraAdmissionState,
  ) => {
    setSnapshot((current) => {
      if (current.key !== reportedKey || !(sourceId in current.cameras)) return current;
      const merged = mergeRecordedCameraAdmission(current.cameras[sourceId], state);
      if (merged === current.cameras[sourceId]) return current;
      return { ...current, cameras: { ...current.cameras, [sourceId]: merged } };
    });
  }, []);

  if (!replay || !key) return null;
  const current = snapshot.key === key
    ? snapshot
    : {
        key,
        spatialPhase: "loading" as const,
        cameras: initialRecordedCameraAdmissions(cameraSourceIds, declaredByteLengths),
      };
  const descriptorPhase = recordedCameraDescriptorPreflight(replay.mediaSources);
  const phase = descriptorPhase === "error"
    ? "error"
    : recordedSessionAdmissionPhase(
        current.spatialPhase,
        cameraSourceIds,
        current.cameras,
      );
  const activeCameraSourceIds = new Set(phase === "error"
    ? []
    : nextRecordedCameraPreparationIds(cameraSourceIds, current.cameras));
  return {
    key,
    phase,
    cameraSourceIds,
    cameras: current.cameras,
    activeCameraSourceIds,
    reportSpatial,
    reportCamera,
  };
}
