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

import type { BackendStatus, ViewerSettings } from "@mission-core/plugin-sdk";

import {
  ApiError,
  xgridsK1Api,
  openEventSocket,
  type ConnectRequest,
  type EventSocketStatus,
  type OpenApplicationControlSessionRequest,
  type OperatorPresenceConfirmation,
  type PrepareAcquisitionRequest,
  type ReplayRequest,
  type XgridsOperation,
  type XgridsApplicationControlPhase,
  type XgridsK1State,
} from "./api";
import {
  controlSessionEntryPlan,
  isTerminalAcquisitionState,
  liveStartPlan,
  operationByIdempotencyKey,
  operationNeedsReconciliation,
  isSoftwareCommandedAcquisition,
} from "./lifecycle";
import { localizeRuntimeMessage } from "./messages";
import {
  awaitWhileIntentCurrent,
  OperatorIntentGeneration,
} from "./operatorIntentGeneration";
import { selectMonotonicXgridsState } from "./stateOrdering";

export type PendingAction =
  | "scan"
  | "connect"
  | "control"
  | "live"
  | "replay"
  | "stop"
  | "abort"
  | "camera"
  | "viewer";

export interface CanonicalLiveStartRequest {
  control: OpenApplicationControlSessionRequest;
  acquisition: PrepareAcquisitionRequest;
  physicalAcceptance: OperatorPresenceConfirmation;
}

const CONTROL_STATE_READ_INTERVAL_MS = 250;

function controlPhase(state: XgridsK1State): XgridsApplicationControlPhase {
  return state.application_control_session?.state ?? "idle";
}

function controlFailure(state: XgridsK1State): ApiError {
  const failure = state.application_control_session?.failure;
  const localizedDetail = localizeRuntimeMessage(failure?.message);
  const reasonLabels: Record<string, string> = {
    application_authority_unavailable:
      "Локальный допуск управления устройством недоступен; команды не отправлялись.",
    mqtt_connect_call_failed:
      "Управляющее MQTT-соединение со сканером не открылось.",
    mqtt_connect_rejected:
      "Сканер отклонил управляющее MQTT-соединение.",
    mqtt_connection_timeout:
      "Подключение или подписки MQTT не подтвердились вовремя.",
    mqtt_subscription_failed:
      "Сканер не подтвердил канонические MQTT-подписки.",
    mqtt_response_timeout:
      "Ожидаемый ответ сканера не пришёл до безопасной границы ожидания.",
    response_identity_decode_failed:
      "Ответ сканера не удалось безопасно разобрать и привязать к операции.",
    modeling_response_decode_failed:
      "Ответ START/STOP не удалось безопасно разобрать.",
    duplicate_application_response:
      "Сканер прислал повторный ответ на уже завершённую операцию.",
    unexpected_response_identity:
      "Получен ответ неизвестной операции; диалог остановлен.",
    response_identity_mismatch:
      "Идентичность ответа не совпала с ожидаемой операцией.",
    response_session_mismatch:
      "Session ответа не совпал с точной подготовительной операцией.",
    response_device_identity_mismatch:
      "Ответ относится не к тому экземпляру устройства.",
    response_authority_mismatch:
      "Ответ не совпал с локальным допуском приложения.",
    response_rejected:
      "Сканер отклонил подготовительную операцию.",
    compatibility_profile_mismatch:
      "Живой DeviceInfo не соответствует выбранному профилю модели, platform type, прошивки или активации.",
    scan_initialization_timeout:
      "После подтверждённого START сканер не завершил инициализацию в безопасный срок.",
    operation_reuse_forbidden:
      "Повтор уже использованной операции заблокирован.",
  };
  const reasonDetail = failure?.reason_code
    ? reasonLabels[failure.reason_code]
    : undefined;
  const stageLabels: Record<string, string> = {
    connecting: "подключение и первичный диалог",
    connection: "первичный диалог",
    "connection-ready": "подключение подтверждено",
    "workspace-requested": "вход в рабочую область",
    "workspace-ready": "рабочая область готова",
    "project-requested": "подготовка проекта",
    "project-ready": "проект готов",
    "start-requested": "подтверждение запуска",
    "start-attempted": "команда запуска",
    initializing: "калибровка оборудования",
    scanning: "сканирование",
    "stop-requested": "подтверждение остановки",
    "stop-attempted": "команда остановки",
    stopping: "остановка",
  };
  const stageCode = failure?.dialogue_stage || failure?.failed_phase;
  const stage = stageCode
    ? stageLabels[stageCode] ?? "неопознанный этап канонического диалога"
    : "этап не зафиксирован";
  const commandStatus = failure?.modeling_command_attempted === true
    ? "Команда START или STOP могла быть отправлена; автоматический повтор запрещён."
    : failure?.modeling_command_attempted === false
      ? "Команды START и STOP не отправлялись."
      : "Факт отправки START или STOP диагностически не подтверждён; повтор запрещён.";
  const retryStatus = failure?.safe_to_retry
    ? "Новая попытка возможна только отдельным нажатием оператора."
    : "Повтор заблокирован до ручной проверки состояния.";
  const exchanges = typeof failure?.publish_attempts === "number"
    ? `MQTT-публикаций до остановки: ${failure.publish_attempts}.`
    : "Количество MQTT-публикаций не зафиксировано.";
  const failedProtocolEvidence =
    failure?.compatibility_failure ?? failure?.correlation_failure;
  const failedOperation = failedProtocolEvidence?.operation_key
    ? `Шаг протокола: ${failedProtocolEvidence.operation_key}.`
    : "";
  const diagnostics = failure?.diagnostic_evidence_unavailable?.length
    ? "Часть диагностических доказательств недоступна; результат считается неизвестным."
    : "";
  return new ApiError(
    `${reasonDetail || localizedDetail || "Канонический диалог K1 остановлен."} Этап: ${stage}. ${failedOperation} ${exchanges} ${commandStatus} ${diagnostics} ${retryStatus}`,
  );
}

async function waitForControlPhase(
  expected: XgridsApplicationControlPhase,
  acceptState: (state: XgridsK1State) => void,
  assertOperatorIntentCurrent: () => void,
): Promise<XgridsK1State> {
  for (;;) {
    assertOperatorIntentCurrent();
    const nextState = await awaitWhileIntentCurrent(
      assertOperatorIntentCurrent,
      () => xgridsK1Api.getState(),
    );
    acceptState(nextState);
    const phase = controlPhase(nextState);
    if (phase === expected) return nextState;
    if (phase === "failed") throw controlFailure(nextState);
    if (["idle", "closed", "completed"].includes(phase)) {
      throw new ApiError(
        `Управляющая сессия K1 завершилась до ожидаемого этапа «${expected}».`,
      );
    }
    // This cadence only reads local server state. It never schedules, retries,
    // or times a K1 command; every next write remains gated by device response.
    await awaitWhileIntentCurrent(
      assertOperatorIntentCurrent,
      () => new Promise<void>((resolve) => {
        window.setTimeout(resolve, CONTROL_STATE_READ_INTERVAL_MS);
      }),
    );
  }
}

function messageFor(error: unknown): string {
  if (error instanceof ApiError) {
    // Domain errors are already written for the operator. Only HTTP details
    // originate at the backend and need vendor/runtime normalization.
    const message = error.status
      ? localizeRuntimeMessage(error.message) ?? error.message
      : error.message;
    return error.status
      ? `${message} (HTTP ${error.status})`
      : message;
  }
  return "Запрос к локальному сервису устройства завершился ошибкой.";
}

function networkProvisionFailureMessage(
  operation: XgridsOperation | null | undefined,
): string | null {
  if (!operation || operation.status !== "failed") return null;
  const code = operation.error?.code;
  if (typeof code !== "string") return null;

  const messages: Record<string, string> = {
    "network-not-found":
      "Точка доступа выбранного K1 не найдена. Команда включения точки не повторялась; проверьте питание и состояние K1.",
    "credential-entry-cancelled":
      "Первичная регистрация пароля K1 отменена. Получите пароль сохранённой сети этого K1 на авторизованном устройстве и повторите подключение отдельным действием.",
    "credential-invalid":
      "Пароль точки доступа K1 имеет недопустимую длину. Получите сохранённый пароль этого K1 в LixelGO/iPhone и повторите подключение.",
    "host-wifi-operation-timeout":
      "Первичное системное подключение к K1 не было завершено вовремя. BLE-команда автоматически не повторялась; получите пароль сохранённой сети этого K1 и запустите подключение заново.",
    "profile-ssid-mismatch":
      "Сохранённый профиль относится к другому K1. Подключение остановлено без повторной команды сканеру.",
    "corewlan-error":
      "macOS не смогла подключиться к точке доступа K1. Проверьте пароль сохранённой сети этого K1; автоматического повтора не было.",
    "wifi-interface-unavailable":
      "Системный Wi-Fi-интерфейс macOS недоступен. Команда сканеру автоматически не повторялась.",
    "unsupported-platform":
      "Для этой операционной системы адаптер подключения к точке K1 ещё не реализован.",
  };
  return messages[code] ?? null;
}

function measuredLatency(state: XgridsK1State | null): number | null {
  if (state?.source_mode !== "live") return null;
  const metrics = state?.metrics;
  if (!metrics) return null;

  const reported = metrics.pipeline_ms ?? metrics.end_to_end_ms;
  if (typeof reported === "number" && Number.isFinite(reported)) return reported;

  const segments = [
    metrics.mqtt_to_decode_ms,
    metrics.publish_ms,
  ].filter((value): value is number => typeof value === "number" && Number.isFinite(value));

  return segments.length === 2 ? segments.reduce((total, value) => total + value, 0) : null;
}

export function useXgridsK1Runtime(enabled: boolean) {
  const [state, setState] = useState<XgridsK1State | null>(null);
  const [backendStatus, setBackendStatus] = useState<BackendStatus>("checking");
  const [eventStatus, setEventStatus] = useState<EventSocketStatus>("connecting");
  const [pendingAction, setPendingAction] = useState<PendingAction | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [latencyHistory, setLatencyHistory] = useState<number[]>([]);
  const operatorIntents = useRef(new OperatorIntentGeneration());
  const actionSequence = useRef(0);
  const actionInFlight = useRef<{
    runtimeGeneration: number;
    actionSequence: number;
  } | null>(null);

  const acceptState = useCallback((nextState: XgridsK1State) => {
    setState((currentState) => selectMonotonicXgridsState(currentState, nextState));
    setBackendStatus("online");
  }, []);

  const refresh = useCallback(async (reportErrors = true) => {
    const runtimeToken = operatorIntents.current.captureRuntime();
    if (!enabled || !runtimeToken) return;
    const [healthResult, stateResult] = await Promise.allSettled([
      xgridsK1Api.getHealth(),
      xgridsK1Api.getState(),
    ]);

    if (!operatorIntents.current.isRuntimeCurrent(runtimeToken)) return;

    if (stateResult.status === "fulfilled") {
      acceptState(stateResult.value);
      if (reportErrors) setError(null);
    }

    if (healthResult.status === "fulfilled") {
      const health = healthResult.value;
      const healthy = health.ok !== false && health.status !== "error";
      setBackendStatus(healthy && stateResult.status === "fulfilled" ? "online" : "degraded");
    } else if (stateResult.status === "rejected") {
      setBackendStatus("offline");
    }

    if (stateResult.status === "rejected" && reportErrors) {
      setError(messageFor(stateResult.reason));
    }
  }, [acceptState, enabled]);

  const run = useCallback(
    async (action: PendingAction, operation: () => Promise<XgridsK1State>) => {
      const runtimeToken = operatorIntents.current.captureRuntime();
      if (!enabled || !runtimeToken) return false;
      if (
        actionInFlight.current?.runtimeGeneration
        === runtimeToken.runtimeGeneration
      ) return false;
      actionSequence.current += 1;
      const actionToken = {
        runtimeGeneration: runtimeToken.runtimeGeneration,
        actionSequence: actionSequence.current,
      };
      actionInFlight.current = actionToken;
      setPendingAction(action);
      setError(null);

      try {
        if (!operatorIntents.current.isRuntimeCurrent(runtimeToken)) return false;
        const nextState = await operation();
        if (!operatorIntents.current.isRuntimeCurrent(runtimeToken)) return false;
        acceptState(nextState);
        return true;
      } catch (operationError) {
        if (operatorIntents.current.isRuntimeCurrent(runtimeToken)) {
          setError(messageFor(operationError));
          if (
            operationError instanceof ApiError
            && operationError.transportUnavailable
          ) {
            setBackendStatus("offline");
          }
        }
        return false;
      } finally {
        if (
          actionInFlight.current?.runtimeGeneration === actionToken.runtimeGeneration
          && actionInFlight.current.actionSequence === actionToken.actionSequence
        ) {
          actionInFlight.current = null;
          if (operatorIntents.current.isRuntimeCurrent(runtimeToken)) {
            setPendingAction(null);
          }
        }
      }
    },
    [acceptState, enabled],
  );

  const scan = useCallback(
    () => run("scan", () => xgridsK1Api.scanBle({ duration_seconds: 6 })),
    [run],
  );

  const connect = useCallback(
    (request: ConnectRequest) =>
      run("connect", async () => {
        const previous = operationByIdempotencyKey(
          state,
          "network.provision",
          request.idempotency_key,
        );
        if (previous?.status === "succeeded" && state) return state;
        if (operationNeedsReconciliation(previous)) {
          throw new ApiError(
            "Предыдущая запись настроек завершилась с неопределённым результатом. Автоматический повтор заблокирован; измените параметры только после проверки устройства.",
          );
        }

        let nextState: XgridsK1State;
        try {
          nextState = await xgridsK1Api.connect(request);
        } catch (connectError) {
          // A failed network action is terminal and persisted by the backend.
          // Re-read state only; never replay the device command. This replaces
          // an opaque HTTP 502 banner with the exact host-association outcome.
          let failedState: XgridsK1State;
          try {
            failedState = await xgridsK1Api.getState();
          } catch {
            throw connectError;
          }
          acceptState(failedState);
          const failedOperation = operationByIdempotencyKey(
            failedState,
            "network.provision",
            request.idempotency_key,
          );
          const failureMessage = networkProvisionFailureMessage(failedOperation);
          if (failureMessage) throw new ApiError(failureMessage);
          throw connectError;
        }
        const operation = operationByIdempotencyKey(
          nextState,
          "network.provision",
          request.idempotency_key,
        );
        if (operationNeedsReconciliation(operation)) {
          throw new ApiError(
            "Результат записи настроек требует ручной проверки. Повторная аппаратная запись не выполнена.",
          );
        }
        if (operation && operation.status !== "succeeded") {
          throw new ApiError("Запись настроек ещё выполняется; дождитесь обновления состояния.");
        }
        return nextState;
      }),
    [acceptState, run, state],
  );

  const openApplicationControlSession = useCallback(
    (request: OpenApplicationControlSessionRequest) =>
      run("control", () => xgridsK1Api.openApplicationControlSession(request)),
    [run],
  );

  const enterApplicationWorkspace = useCallback(
    () =>
      run("control", () =>
        xgridsK1Api.enterApplicationWorkspace({ operator_confirmed: true }),
      ),
    [run],
  );

  const closeApplicationControlSession = useCallback(
    () => run("control", () => xgridsK1Api.closeApplicationControlSession()),
    [run],
  );

  const startCanonicalAcquisition = useCallback(
    (request: CanonicalLiveStartRequest) =>
      run("live", async () => {
        const intentToken = operatorIntents.current.beginOperatorIntent();
        if (!intentToken) {
          throw new ApiError(
            "Экран управления закрыт; дальнейшие команды канонического диалога не отправлялись.",
          );
        }
        const assertOperatorIntentCurrent = () => {
          if (!operatorIntents.current.isOperatorIntentCurrent(intentToken)) {
            throw new ApiError(
              "Операторское действие завершено или заменено; дальнейшие команды канонического диалога не отправлялись.",
            );
          }
        };
        let nextState = await awaitWhileIntentCurrent(
          assertOperatorIntentCurrent,
          () => xgridsK1Api.getState(),
        );
        let openedControlSession = false;
        acceptState(nextState);
        const plan = liveStartPlan(nextState);
        if (plan === "blocked") {
          throw new ApiError("Сначала завершите текущий приём или повтор записи.");
        }
        if (plan === "already-running") return nextState;

        for (;;) {
          assertOperatorIntentCurrent();
          const phase = controlPhase(nextState);
          const acquisition = nextState.acquisition;
          const entryPlan = controlSessionEntryPlan(
            phase,
            openedControlSession,
            nextState.application_control_session?.can_open === true,
          );

          if (entryPlan === "failed") {
            // A failed dialogue always ends this operator intent. Even when
            // backend reconciliation says a fresh attempt may be safe, that
            // attempt requires another explicit click.
            throw controlFailure(nextState);
          }

          if (entryPlan === "duplicate-open") {
            throw new ApiError(
              "Управляющая сессия завершилась сразу после открытия. Автоматический повтор заблокирован; проверьте состояние и повторите только отдельным нажатием.",
            );
          }

          if (entryPlan === "open") {
            if (acquisition && !isTerminalAcquisitionState(acquisition.state)) {
              throw new ApiError(
                "Незавершённая подготовка не привязана к открытой control-сессии. Отмените её перед новым запуском.",
              );
            }
            openedControlSession = true;
            nextState = await awaitWhileIntentCurrent(
              assertOperatorIntentCurrent,
              () => xgridsK1Api.openApplicationControlSession(request.control),
            );
            acceptState(nextState);
            continue;
          }

          if (phase === "connecting") {
            nextState = await awaitWhileIntentCurrent(
              assertOperatorIntentCurrent,
              () => waitForControlPhase(
                "connection-ready",
                acceptState,
                assertOperatorIntentCurrent,
              ),
            );
            continue;
          }

          if (phase === "connection-ready") {
            nextState = await awaitWhileIntentCurrent(
              assertOperatorIntentCurrent,
              () => xgridsK1Api.enterApplicationWorkspace({
                operator_confirmed: true,
              }),
            );
            acceptState(nextState);
            continue;
          }

          if (phase === "workspace-requested") {
            nextState = await awaitWhileIntentCurrent(
              assertOperatorIntentCurrent,
              () => waitForControlPhase(
                "workspace-ready",
                acceptState,
                assertOperatorIntentCurrent,
              ),
            );
            continue;
          }

          if (phase === "workspace-ready") {
            if (!acquisition || isTerminalAcquisitionState(acquisition.state)) {
              nextState = await awaitWhileIntentCurrent(
                assertOperatorIntentCurrent,
                () => xgridsK1Api.prepareAcquisition(request.acquisition),
              );
              acceptState(nextState);
              continue;
            }
            if (acquisition.state !== "prepared" || acquisition.control_mode !== "plugin-commanded") {
              throw new ApiError(
                "Текущая подготовка не принадлежит канонической control-сессии K1.",
              );
            }
            nextState = await awaitWhileIntentCurrent(
              assertOperatorIntentCurrent,
              () => waitForControlPhase(
                "project-ready",
                acceptState,
                assertOperatorIntentCurrent,
              ),
            );
            continue;
          }

          if (phase === "project-requested") {
            nextState = await awaitWhileIntentCurrent(
              assertOperatorIntentCurrent,
              () => waitForControlPhase(
                "project-ready",
                acceptState,
                assertOperatorIntentCurrent,
              ),
            );
            continue;
          }

          if (phase === "project-ready") {
            if (!acquisition || acquisition.state !== "prepared") {
              throw new ApiError("Локальный приём не подготовлен к каноническому START.");
            }
            nextState = await awaitWhileIntentCurrent(
              assertOperatorIntentCurrent,
              () => xgridsK1Api.startAcquisition({
                acquisition_id: acquisition.acquisition_id,
                expected_state_revision: acquisition.state_revision,
                physical_acceptance: request.physicalAcceptance,
              }),
            );
            acceptState(nextState);
            return nextState;
          }

          if (["start-requested", "initializing", "scanning"].includes(phase)) {
            return nextState;
          }

          throw new ApiError(`Запуск K1 недоступен из состояния «${phase}».`);
        }
      }),
    [acceptState, run],
  );

  const prepareAcquisition = useCallback(
    (request: PrepareAcquisitionRequest) =>
      run("live", async () => {
        const plan = liveStartPlan(state);
        if (plan === "blocked") {
          throw new ApiError(
            "Сначала завершите текущий приём или повтор записи.",
          );
        }
        if (plan === "already-running" && state) return state;

        return (
          plan === "resume-prepared" && state
            ? state
            : await xgridsK1Api.prepareAcquisition(request)
        );
      }),
    [run, state],
  );

  const startPreparedAcquisition = useCallback(
    (physicalAcceptance: OperatorPresenceConfirmation) =>
      run("live", async () => {
        const acquisition = state?.acquisition;
        if (!acquisition?.acquisition_id || acquisition.state !== "prepared") {
          throw new ApiError("Сначала сохраните проект и подготовьте локальный приём.");
        }
        return xgridsK1Api.startAcquisition({
          acquisition_id: acquisition.acquisition_id,
          expected_state_revision: acquisition.state_revision,
          physical_acceptance: physicalAcceptance,
        });
      }),
    [run, state],
  );

  const startReplay = useCallback(
    (request: ReplayRequest) => run("replay", () => xgridsK1Api.startReplay(request)),
    [run],
  );

  const stop = useCallback(
    (physicalAcceptance?: OperatorPresenceConfirmation) =>
      run("stop", () => {
        const acquisition = state?.acquisition;
        const acquisitionTerminal = isTerminalAcquisitionState(acquisition?.state);
        if (acquisition && !acquisitionTerminal) {
          const softwareCommanded = isSoftwareCommandedAcquisition(state);
          if (softwareCommanded && !physicalAcceptance) {
            throw new ApiError(
              "Подтвердите присутствие рядом с K1 перед каноническим STOP.",
            );
          }
          return xgridsK1Api.stopAcquisition({
            acquisition_id: acquisition.acquisition_id,
            mode: softwareCommanded ? "graceful" : "capture-only",
            ...(physicalAcceptance
              ? { physical_acceptance: physicalAcceptance }
              : {}),
          });
        }
        return xgridsK1Api.stopSessionCompatibility();
      }),
    [run, state],
  );

  const abort = useCallback(() => {
    const acquisition = state?.acquisition;
    if (!acquisition || isTerminalAcquisitionState(acquisition.state)) {
      return Promise.resolve(false);
    }
    return run("abort", () =>
      xgridsK1Api.abortAcquisition({ acquisition_id: acquisition.acquisition_id }),
    );
  }, [run, state?.acquisition]);

  const setObservationSourceActive = useCallback(
    (sourceId: string, active: boolean) =>
      run("camera", async () => {
        if (!state) {
          throw new ApiError("Состояние устройства ещё не загружено.");
        }
        const deviceSessionId = state.device_session?.device_session_id?.trim();
        if (!deviceSessionId) {
          throw new ApiError("Для камеры нет активной сессии устройства.");
        }
        const catalogSource = state.sensor_catalog?.streams?.find(
          (candidate) => candidate.source_id === sourceId,
        );
        if (!catalogSource || catalogSource.activation?.controllable !== true) {
          throw new ApiError("Плагин не разрешает управление выбранным видеоканалом.");
        }

        if (active) {
          if (
            catalogSource.activation.selected &&
            state.camera_preview?.active_source_id === sourceId &&
            state.camera_preview?.phase !== "error" &&
            state.camera_preview?.delivery
          ) {
            return state;
          }
          return xgridsK1Api.selectCameraPreview({
            source_id: sourceId,
            device_session_id: deviceSessionId,
          });
        }

        if (state.camera_preview?.active_source_id !== sourceId) return state;
        const generation = state.camera_preview.generation;
        if (!Number.isInteger(generation) || (generation ?? 0) < 1) {
          throw new ApiError("Плагин не вернул поколение активной camera-preview сессии.");
        }
        return xgridsK1Api.stopCameraPreview({
          device_session_id: deviceSessionId,
          generation: generation as number,
        });
      }),
    [run, state],
  );

  const updateViewerSettings = useCallback(
    (request: ViewerSettings) => run("viewer", () => xgridsK1Api.updateViewerSettings(request)),
    [run],
  );

  useEffect(() => {
    if (!enabled) {
      operatorIntents.current.deactivateRuntime();
      setState(null);
      setBackendStatus("checking");
      setEventStatus("closed");
      setPendingAction(null);
      setError(null);
      setLatencyHistory([]);
      return;
    }

    operatorIntents.current.activateRuntime();
    void refresh(true);
    const poll = window.setInterval(() => void refresh(false), 4_000);

    return () => {
      operatorIntents.current.deactivateRuntime();
      window.clearInterval(poll);
    };
  }, [enabled, refresh]);

  useEffect(() => {
    if (!enabled) return;

    let dispose: (() => void) | undefined;
    let retry: number | undefined;
    let cancelled = false;

    const connectEvents = () => {
      if (cancelled) return;
      dispose = openEventSocket(acceptState, (status) => {
        if (cancelled) return;
        setEventStatus(status);
        if ((status === "closed" || status === "error") && retry === undefined) {
          retry = window.setTimeout(() => {
            retry = undefined;
            connectEvents();
          }, 3_000);
        }
      });
    };

    connectEvents();

    return () => {
      cancelled = true;
      if (retry !== undefined) window.clearTimeout(retry);
      dispose?.();
    };
  }, [acceptState, enabled]);

  useEffect(() => {
    const latency = measuredLatency(state);
    if (latency === null) {
      setLatencyHistory((values) => (values.length ? [] : values));
      return;
    }
    setLatencyHistory((values) => [...values.slice(-23), latency]);
  }, [state]);

  return {
    state,
    backendStatus,
    eventStatus,
    pendingAction,
    error,
    latencyHistory,
    refresh: () => refresh(true),
    clearError: () => setError(null),
    scan,
    connect,
    openApplicationControlSession,
    enterApplicationWorkspace,
    closeApplicationControlSession,
    startCanonicalAcquisition,
    prepareAcquisition,
    startPreparedAcquisition,
    startReplay,
    stop,
    abort,
    setObservationSourceActive,
    updateViewerSettings,
  };
}
