import { isSseEvent, type SseEvent } from "@dada/shared-contracts";

export interface EventSyncDependencies {
  bootstrap: () => Promise<unknown>;
  refetchEntity: (entityRef: string) => Promise<unknown>;
  refetchModels: (input: {
    configSetVersion?: number;
    reason: "config" | "runtime";
    runtimeAvailabilityVersion?: number;
  }) => Promise<unknown>;
}

export interface EventSyncController {
  handleDisconnect: () => Promise<void>;
  handleEvent: (event: unknown) => Promise<void>;
}

async function refetchEventTruth(dependencies: EventSyncDependencies, event: SseEvent) {
  if ("config_set_version" in event) {
    await dependencies.refetchModels({
      configSetVersion: event.config_set_version,
      reason: "config",
    });
    return;
  }
  if ("runtime_availability_version" in event) {
    await dependencies.refetchModels({
      reason: "runtime",
      runtimeAvailabilityVersion: event.runtime_availability_version,
    });
    return;
  }
  await dependencies.refetchEntity(event.entity_ref);
}

export function createEventSyncController(dependencies: EventSyncDependencies): EventSyncController {
  let lastEventId: number | undefined;
  let recoveryInProgress = false;

  async function recoverOnce() {
    if (recoveryInProgress) return;
    recoveryInProgress = true;
    await dependencies.bootstrap();
  }

  return {
    async handleDisconnect() {
      await recoverOnce();
    },
    async handleEvent(event: unknown) {
      if (!isSseEvent(event)) throw new Error("SSE event failed the non-sensitive schema.");

      if (lastEventId !== undefined && event.event_id !== lastEventId + 1) {
        await recoverOnce();
      }
      lastEventId = event.event_id;

      await refetchEventTruth(dependencies, event);

      recoveryInProgress = false;
    },
  };
}

export function connectEventSource(url: string, controller: EventSyncController) {
  const source = new EventSource(url, { withCredentials: true });
  source.onmessage = (message) => {
    void controller.handleEvent(JSON.parse(message.data) as SseEvent);
  };
  source.onerror = () => {
    void controller.handleDisconnect();
  };
  return source;
}
