import {
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useSyncExternalStore,
} from "react";
import {
  EmbeddingsReindexProgressType,
  FrigateCameraState,
  FrigateEvent,
  FrigateReview,
  ModelState,
  ToggleableSetting,
  TrackedObjectUpdateReturnType,
  TriggerStatus,
  FrigateAudioDetections,
  Job,
} from "@/types/ws";
import { FrigateStats } from "@/types/stats";
import { isEqual } from "lodash";
import { WsSendContext } from "./wsContext";
import type { Update, WsSend } from "./wsContext";

export type { Update };

export type WsFeedMessage = {
  topic: string;
  payload: unknown;
  timestamp: number;
  id: string;
};

type WsState = {
  [topic: string]: unknown;
};

// External store for WebSocket state using useSyncExternalStore
type Listener = () => void;

const wsState: WsState = {};
const wsTopicListeners = new Map<string, Set<Listener>>();

// Reset all module-level state. Called on WsProvider unmount to prevent
// stale data from leaking across mount/unmount cycles (e.g. HMR, logout)
export function resetWsStore() {
  for (const key of Object.keys(wsState)) {
    delete wsState[key];
  }
  wsTopicListeners.clear();
  lastCameraActivityPayload = null;
  wsMessageSubscribers.clear();
  wsMessageIdCounter = 0;
}

// Parse and apply a raw WS message synchronously.
// Called directly from WsProvider's onmessage handler.
export function processWsMessage(raw: string) {
  const data: Update = JSON.parse(raw);
  if (!data) return;

  const { topic, payload } = data;

  if (topic === "camera_activity") {
    applyCameraActivity(payload as string);
  } else {
    applyTopicUpdate(topic, payload);
  }

  if (wsMessageSubscribers.size > 0) {
    wsMessageSubscribers.forEach((cb) =>
      cb({
        topic,
        payload,
        timestamp: Date.now(),
        id: String(wsMessageIdCounter++),
      }),
    );
  }
}

function applyTopicUpdate(topic: string, newVal: unknown) {
  const oldVal = wsState[topic];
  // camera_activity snapshots always re-notify: consumers reconcile local
  // state that may have diverged from an unchanged snapshot
  const isActivitySnapshot = topic.startsWith("camera_activity/");
  // Fast path: === for primitives ("ON"/"OFF", numbers).
  // Fall back to isEqual for objects/arrays.
  const unchanged =
    oldVal === newVal ||
    (typeof newVal === "object" && newVal !== null && isEqual(oldVal, newVal));
  if (unchanged && !isActivitySnapshot) return;

  wsState[topic] = newVal;
  // Snapshot the Set — a listener may trigger unmount that modifies it.
  const listeners = wsTopicListeners.get(topic);
  if (listeners) {
    for (const l of Array.from(listeners)) l();
  }
}

// Subscriptions

export function subscribeWsTopic(
  topic: string,
  listener: Listener,
): () => void {
  let set = wsTopicListeners.get(topic);
  if (!set) {
    set = new Set();
    wsTopicListeners.set(topic, set);
  }
  set.add(listener);
  return () => {
    set!.delete(listener);
    if (set!.size === 0) wsTopicListeners.delete(topic);
  };
}

export function getWsTopicValue(topic: string): unknown {
  return wsState[topic];
}

// Feed message subscribers
const wsMessageSubscribers = new Set<(msg: WsFeedMessage) => void>();
let wsMessageIdCounter = 0;

// Camera activity expansion
//
// Cache the last raw camera_activity JSON string so we can skip JSON.parse
// and the entire expansion when nothing has changed. This avoids creating
// fresh objects (which defeat Object.is and force expensive isEqual deep
// traversals) on every flush — critical with many cameras.
let lastCameraActivityPayload: string | null = null;

// Make the next camera_activity snapshot fully apply even when byte-identical
// to the previous one — local state may have diverged while no messages flowed
export function invalidateCameraActivityCache() {
  lastCameraActivityPayload = null;
}

// Collapse same-task resync requests (one hook per camera card) into a
// single onConnect round-trip
let resyncScheduled = false;
function requestCameraActivityResync(sendOnConnect: () => void) {
  if (resyncScheduled) return;
  resyncScheduled = true;
  queueMicrotask(() => {
    resyncScheduled = false;
    invalidateCameraActivityCache();
    sendOnConnect();
  });
}

function applyCameraActivity(payload: string) {
  // Fast path: if the raw JSON string is identical, nothing changed.
  if (payload === lastCameraActivityPayload) return;
  lastCameraActivityPayload = payload;

  let activity: { [key: string]: Partial<FrigateCameraState> };

  try {
    activity = JSON.parse(payload);
  } catch {
    return;
  }

  if (Object.keys(activity).length === 0) return;

  for (const [name, state] of Object.entries(activity)) {
    applyTopicUpdate(`camera_activity/${name}`, state);

    const cameraConfig = state?.config;
    if (!cameraConfig) continue;

    const {
      record,
      detect,
      enabled,
      snapshots,
      audio,
      audio_transcription,
      notifications,
      notifications_suspended,
      autotracking,
      alerts,
      detections,
      object_descriptions,
      review_descriptions,
    } = cameraConfig;

    applyTopicUpdate(`${name}/recordings/state`, record ? "ON" : "OFF");
    applyTopicUpdate(`${name}/enabled/state`, enabled ? "ON" : "OFF");
    applyTopicUpdate(`${name}/detect/state`, detect ? "ON" : "OFF");
    applyTopicUpdate(`${name}/snapshots/state`, snapshots ? "ON" : "OFF");
    applyTopicUpdate(`${name}/audio/state`, audio ? "ON" : "OFF");
    applyTopicUpdate(
      `${name}/audio_transcription/state`,
      audio_transcription ? "ON" : "OFF",
    );
    applyTopicUpdate(
      `${name}/notifications/state`,
      notifications ? "ON" : "OFF",
    );
    applyTopicUpdate(
      `${name}/notifications/suspended`,
      String(notifications_suspended ?? 0),
    );
    applyTopicUpdate(
      `${name}/ptz_autotracker/state`,
      autotracking ? "ON" : "OFF",
    );
    applyTopicUpdate(`${name}/review_alerts/state`, alerts ? "ON" : "OFF");
    applyTopicUpdate(
      `${name}/review_detections/state`,
      detections ? "ON" : "OFF",
    );
    applyTopicUpdate(
      `${name}/object_descriptions/state`,
      object_descriptions ? "ON" : "OFF",
    );
    applyTopicUpdate(
      `${name}/review_descriptions/state`,
      review_descriptions ? "ON" : "OFF",
    );
  }
}

// Hooks
export function useWsUpdate(): WsSend {
  const send = useContext(WsSendContext);
  if (!send) {
    throw new Error("useWsUpdate must be used within WsProvider");
  }
  return send;
}

// Subscribe to a single WS topic with proper bail-out.
// Only re-renders when the topic's value changes (Object.is comparison).
// Uses useSyncExternalStore — zero useEffect, so no PassiveMask flags
// propagate through the fiber tree.
export function useWs(watchTopic: string, publishTopic: string) {
  const payload = useSyncExternalStore(
    useCallback(
      (listener: Listener) => subscribeWsTopic(watchTopic, listener),
      [watchTopic],
    ),
    useCallback(() => wsState[watchTopic], [watchTopic]),
  );

  const sendJsonMessage = useWsUpdate();

  const value = { payload: payload ?? null };

  const send = useCallback(
    (payload: unknown, retain = false) => {
      sendJsonMessage({
        topic: publishTopic || watchTopic,
        payload,
        retain,
      });
    },
    [sendJsonMessage, watchTopic, publishTopic],
  );

  return { value, send };
}

// Convenience hooks

export function useEnabledState(camera: string): {
  payload: ToggleableSetting;
  send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(`${camera}/enabled/state`, `${camera}/enabled/set`);
  return { payload: payload as ToggleableSetting, send };
}

export function useDetectState(camera: string): {
  payload: ToggleableSetting;
  send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(`${camera}/detect/state`, `${camera}/detect/set`);
  return { payload: payload as ToggleableSetting, send };
}

export function useRecordingsState(camera: string): {
  payload: ToggleableSetting;
  send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(`${camera}/recordings/state`, `${camera}/recordings/set`);
  return { payload: payload as ToggleableSetting, send };
}

export function useSnapshotsState(camera: string): {
  payload: ToggleableSetting;
  send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(`${camera}/snapshots/state`, `${camera}/snapshots/set`);
  return { payload: payload as ToggleableSetting, send };
}

export function useAudioState(camera: string): {
  payload: ToggleableSetting;
  send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(`${camera}/audio/state`, `${camera}/audio/set`);
  return { payload: payload as ToggleableSetting, send };
}

export function useAudioTranscriptionState(camera: string): {
  payload: ToggleableSetting;
  send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(
    `${camera}/audio_transcription/state`,
    `${camera}/audio_transcription/set`,
  );
  return { payload: payload as ToggleableSetting, send };
}

export function useAutotrackingState(camera: string): {
  payload: ToggleableSetting;
  send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(`${camera}/ptz_autotracker/state`, `${camera}/ptz_autotracker/set`);
  return { payload: payload as ToggleableSetting, send };
}

export function useAlertsState(camera: string): {
  payload: ToggleableSetting;
  send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(`${camera}/review_alerts/state`, `${camera}/review_alerts/set`);
  return { payload: payload as ToggleableSetting, send };
}

export function useDetectionsState(camera: string): {
  payload: ToggleableSetting;
  send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(
    `${camera}/review_detections/state`,
    `${camera}/review_detections/set`,
  );
  return { payload: payload as ToggleableSetting, send };
}

export function useObjectDescriptionState(camera: string): {
  payload: ToggleableSetting;
  send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(
    `${camera}/object_descriptions/state`,
    `${camera}/object_descriptions/set`,
  );
  return { payload: payload as ToggleableSetting, send };
}

export function useReviewDescriptionState(camera: string): {
  payload: ToggleableSetting;
  send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(
    `${camera}/review_descriptions/state`,
    `${camera}/review_descriptions/set`,
  );
  return { payload: payload as ToggleableSetting, send };
}

export function useMotionMaskState(
  camera: string,
  maskName: string,
): {
  payload: ToggleableSetting;
  send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(
    `${camera}/motion_mask/${maskName}/state`,
    `${camera}/motion_mask/${maskName}/set`,
  );
  return { payload: payload as ToggleableSetting, send };
}

export function useObjectMaskState(
  camera: string,
  maskName: string,
): {
  payload: ToggleableSetting;
  send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(
    `${camera}/object_mask/${maskName}/state`,
    `${camera}/object_mask/${maskName}/set`,
  );
  return { payload: payload as ToggleableSetting, send };
}

export function useZoneState(
  camera: string,
  zoneName: string,
): {
  payload: ToggleableSetting;
  send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(
    `${camera}/zone/${zoneName}/state`,
    `${camera}/zone/${zoneName}/set`,
  );
  return { payload: payload as ToggleableSetting, send };
}

export function usePtzCommand(camera: string): {
  payload: string;
  send: (payload: string, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(`${camera}/ptz`, `${camera}/ptz`);
  return { payload: payload as string, send };
}

export function useRestart(): {
  payload: string;
  send: (payload: string, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs("restart", "restart");
  return { payload: payload as string, send };
}

export function useFrigateEvents(): { payload: FrigateEvent } {
  const {
    value: { payload },
  } = useWs("events", "");
  const parsed = useMemo(
    () => (payload ? JSON.parse(payload as string) : undefined),
    [payload],
  );
  return { payload: parsed };
}

export function useAudioDetections(): { payload: FrigateAudioDetections } {
  const {
    value: { payload },
  } = useWs("audio_detections", "");
  const parsed = useMemo(
    () => (payload ? JSON.parse(payload as string) : undefined),
    [payload],
  );
  return { payload: parsed };
}

export function useFrigateReviews(): FrigateReview {
  const {
    value: { payload },
  } = useWs("reviews", "");
  return useMemo(
    () => (payload ? JSON.parse(payload as string) : undefined),
    [payload],
  );
}

export function useFrigateStats(): FrigateStats {
  const {
    value: { payload },
  } = useWs("stats", "");
  return useMemo(
    () => (payload ? JSON.parse(payload as string) : undefined),
    [payload],
  );
}

export function useInitialCameraState(
  camera: string,
  revalidateOnFocus: boolean,
): {
  payload: FrigateCameraState;
} {
  const {
    value: { payload },
    send: sendCommand,
  } = useWs(`camera_activity/${camera}`, "onConnect");

  // camera_activity sub-topic payload is already parsed by expandCameraActivity
  const data = payload as FrigateCameraState | undefined;

  // the cached snapshot is only written on onConnect and can be stale by the
  // time this hook mounts — re-request on mount and when the user tabs back in
  useEffect(() => {
    if (!revalidateOnFocus) return;

    requestCameraActivityResync(() => sendCommand("onConnect"));

    const listener = () => {
      if (document.visibilityState === "visible") {
        requestCameraActivityResync(() => sendCommand("onConnect"));
      }
    };
    addEventListener("visibilitychange", listener);

    return () => {
      removeEventListener("visibilitychange", listener);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [revalidateOnFocus]);

  return { payload: data as FrigateCameraState };
}

export function useModelState(
  model: string,
  revalidateOnFocus: boolean = true,
): { payload: ModelState } {
  const {
    value: { payload },
    send: sendCommand,
  } = useWs("model_state", "modelState");

  const data = useMemo(
    () => (payload ? JSON.parse(payload as string) : undefined),
    [payload],
  );

  useEffect(() => {
    let listener = undefined;
    if (revalidateOnFocus) {
      sendCommand("modelState");
      listener = () => {
        if (document.visibilityState == "visible") {
          sendCommand("modelState");
        }
      };
      addEventListener("visibilitychange", listener);
    }

    return () => {
      if (listener) {
        removeEventListener("visibilitychange", listener);
      }
    };
    // we know that these deps are correct
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [revalidateOnFocus]);

  return { payload: data ? data[model] : undefined };
}

export function useEmbeddingsReindexProgress(
  revalidateOnFocus: boolean = true,
): {
  payload: EmbeddingsReindexProgressType;
} {
  const {
    value: { payload },
    send: sendCommand,
  } = useWs("embeddings_reindex_progress", "embeddingsReindexProgress");

  const data = useMemo(
    () => (payload ? JSON.parse(payload as string) : undefined),
    [payload],
  );

  useEffect(() => {
    let listener = undefined;
    if (revalidateOnFocus) {
      sendCommand("embeddingsReindexProgress");
      listener = () => {
        if (document.visibilityState == "visible") {
          sendCommand("embeddingsReindexProgress");
        }
      };
      addEventListener("visibilitychange", listener);
    }

    return () => {
      if (listener) {
        removeEventListener("visibilitychange", listener);
      }
    };
    // we know that these deps are correct
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [revalidateOnFocus]);

  return { payload: data };
}

export function useAudioTranscriptionProcessState(
  revalidateOnFocus: boolean = true,
): { payload: string } {
  const {
    value: { payload },
    send: sendCommand,
  } = useWs("audio_transcription_state", "audioTranscriptionState");

  const data = useMemo(
    () => (payload ? (JSON.parse(payload as string) as string) : "idle"),
    [payload],
  );

  useEffect(() => {
    let listener = undefined;
    if (revalidateOnFocus) {
      sendCommand("audioTranscriptionState");
      listener = () => {
        if (document.visibilityState == "visible") {
          sendCommand("audioTranscriptionState");
        }
      };
      addEventListener("visibilitychange", listener);
    }
    return () => {
      if (listener) {
        removeEventListener("visibilitychange", listener);
      }
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [revalidateOnFocus]);

  return { payload: data || "idle" };
}

export function useBirdseyeLayout(revalidateOnFocus: boolean = true): {
  payload: string;
} {
  const {
    value: { payload },
    send: sendCommand,
  } = useWs("birdseye_layout", "birdseyeLayout");

  const data = useMemo(
    () => (payload ? JSON.parse(payload as string) : undefined),
    [payload],
  );

  useEffect(() => {
    let listener = undefined;
    if (revalidateOnFocus) {
      sendCommand("birdseyeLayout");
      listener = () => {
        if (document.visibilityState == "visible") {
          sendCommand("birdseyeLayout");
        }
      };
      addEventListener("visibilitychange", listener);
    }

    return () => {
      if (listener) {
        removeEventListener("visibilitychange", listener);
      }
    };
    // we know that these deps are correct
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [revalidateOnFocus]);

  return { payload: data };
}

export function useMotionActivity(camera: string): { payload: string } {
  const {
    value: { payload },
  } = useWs(`${camera}/motion`, "");
  return { payload: payload as string };
}

export function useAudioActivity(camera: string): { payload: number } {
  const {
    value: { payload },
  } = useWs(`${camera}/audio/rms`, "");
  return { payload: payload as number };
}

export function useAudioLiveTranscription(camera: string): {
  payload: string;
} {
  const {
    value: { payload },
  } = useWs(`${camera}/audio/transcription`, "");
  return { payload: payload as string };
}

export function useMotionThreshold(camera: string): {
  payload: string;
  send: (payload: number, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(
    `${camera}/motion_threshold/state`,
    `${camera}/motion_threshold/set`,
  );
  return { payload: payload as string, send };
}

export function useMotionContourArea(camera: string): {
  payload: string;
  send: (payload: number, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(
    `${camera}/motion_contour_area/state`,
    `${camera}/motion_contour_area/set`,
  );
  return { payload: payload as string, send };
}

export function useImproveContrast(camera: string): {
  payload: ToggleableSetting;
  send: (payload: string, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(
    `${camera}/improve_contrast/state`,
    `${camera}/improve_contrast/set`,
  );
  return { payload: payload as ToggleableSetting, send };
}

export function useTrackedObjectUpdate(): {
  payload: TrackedObjectUpdateReturnType;
} {
  const {
    value: { payload },
  } = useWs("tracked_object_update", "");
  const parsed = useMemo(
    () =>
      payload
        ? JSON.parse(payload as string)
        : { type: "", id: "", camera: "" },
    [payload],
  );
  return { payload: parsed };
}

export function useNotifications(camera: string): {
  payload: ToggleableSetting;
  send: (payload: string, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(`${camera}/notifications/state`, `${camera}/notifications/set`);
  return { payload: payload as ToggleableSetting, send };
}

export function useNotificationSuspend(camera: string): {
  payload: string;
  send: (payload: number, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs(
    `${camera}/notifications/suspended`,
    `${camera}/notifications/suspend`,
  );
  return { payload: String(payload ?? 0), send };
}

export function useNotificationTest(): {
  payload: string;
  send: (payload: string, retain?: boolean) => void;
} {
  const {
    value: { payload },
    send,
  } = useWs("notification_test", "notification_test");
  return { payload: payload as string, send };
}

export function useTriggers(): { payload: TriggerStatus } {
  const {
    value: { payload },
  } = useWs("triggers", "");
  const parsed = useMemo(
    () =>
      payload
        ? JSON.parse(payload as string)
        : { name: "", camera: "", event_id: "", type: "", score: 0 },
    [payload],
  );
  return { payload: parsed };
}

export function useJobStatus<TResults = unknown>(
  jobType: string,
  revalidateOnFocus: boolean = true,
): { payload: Job<TResults> | null } {
  const {
    value: { payload },
    send: sendCommand,
  } = useWs("job_state", "jobState");

  const jobData = useMemo(
    () => (payload && typeof payload === "string" ? JSON.parse(payload) : {}),
    [payload],
  );
  const currentJob = jobData[jobType] || null;

  useEffect(() => {
    let listener: (() => void) | undefined;
    if (revalidateOnFocus) {
      sendCommand("jobState");
      listener = () => {
        if (document.visibilityState === "visible") {
          sendCommand("jobState");
        }
      };
      addEventListener("visibilitychange", listener);
    }

    return () => {
      if (listener) {
        removeEventListener("visibilitychange", listener);
      }
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [revalidateOnFocus]);

  return { payload: currentJob as Job<TResults> | null };
}

export function useWsMessageSubscribe(callback: (msg: WsFeedMessage) => void) {
  const callbackRef = useRef(callback);
  callbackRef.current = callback;

  useEffect(() => {
    const handler = (msg: WsFeedMessage) => callbackRef.current(msg);
    wsMessageSubscribers.add(handler);
    return () => {
      wsMessageSubscribers.delete(handler);
    };
  }, []);
}