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

import type { ObservationSourceDescriptor } from "../runtime/contracts";
import {
  closeObservationSource,
  openObservationSource,
  shouldRestartObservationSource,
} from "./layoutPolicy";
import {
  normalizeObservationWindowRect,
  projectObservationLayoutSnapshot,
  validateObservationLayoutSnapshot,
  validateObservationViewportSize,
  type ObservationLayoutSnapshot,
  type ObservationViewportSize,
  type ObservationWindowRect,
} from "./workspaceLayout";

export type { ObservationWindowRect } from "./workspaceLayout";

export type ObservationLayoutPresentationMode = "preserve" | "reset";

export function observationPresentationSourceAfterLayoutApply(
  currentSourceId: string | null,
  mode: ObservationLayoutPresentationMode,
): string | null {
  return mode === "preserve" ? currentSourceId : null;
}

export function visibleSourceIdsAfterRecordedCatalogActivation(
  currentIds: readonly string[],
  sources: readonly ObservationSourceDescriptor[],
): string[] {
  let visibleIds = [...currentIds];
  for (const source of sources) {
    if (source.transport !== "recording" || !canOpenByDefault(source)) continue;
    visibleIds = openObservationSource(visibleIds, source.id, sources).visibleIds;
  }
  return visibleIds;
}

export interface ObservationLayoutController {
  visibleSourceIds: ReadonlySet<string>;
  focusedSourceId: string | null;
  activeFloatingSourceId: string | null;
  maximizedFloatingSourceId: string | null;
  windowRects: Readonly<Record<string, ObservationWindowRect>>;
  viewportSize: ObservationViewportSize | null;
  pendingSourceIds: ReadonlySet<string>;
  toggleSource: (sourceId: string) => Promise<boolean>;
  hideSource: (sourceId: string) => Promise<boolean>;
  setFocusedSourceId: (sourceId: string | null) => void;
  activateFloatingSource: (sourceId: string) => void;
  setFloatingMaximized: (sourceId: string, maximized: boolean) => void;
  setWindowRect: (sourceId: string, rect: ObservationWindowRect) => void;
  setViewportSize: (size: ObservationViewportSize) => void;
  snapshot: () => ObservationLayoutSnapshot | null;
  restore: (snapshot: ObservationLayoutSnapshot) => void;
}

function canOpenByDefault(source: ObservationSourceDescriptor): boolean {
  if (!source.capabilities.defaultVisible) return false;
  if (source.availability === "unavailable" || source.availability === "error") return false;
  return (
    source.modality === "point-cloud" ||
    Boolean(source.previewUrl) ||
    Boolean(source.delivery && (!source.activation || source.activation.selected))
  );
}

function catalogIdentity(sources: readonly ObservationSourceDescriptor[]): string {
  return sources
    .map((source) => [
      source.id,
      source.binding.deviceSessionId,
      source.binding.deviceId,
    ].filter(Boolean).join(":"))
    .sort()
    .join("|");
}

function cloneSnapshot(snapshot: ObservationLayoutSnapshot): ObservationLayoutSnapshot {
  return {
    visibleSourceIds: [...snapshot.visibleSourceIds],
    activeFloatingSourceId: snapshot.activeFloatingSourceId,
    windowRects: Object.fromEntries(
      Object.entries(snapshot.windowRects).map(([sourceId, rect]) => [sourceId, { ...rect }]),
    ),
    viewportSize: { ...snapshot.viewportSize },
  };
}

export function useObservationLayout(
  sources: readonly ObservationSourceDescriptor[],
  setSourceActive?: (sourceId: string, active: boolean) => Promise<boolean>,
): ObservationLayoutController {
  const [visibleIds, setVisibleIds] = useState<string[]>([]);
  const visibleIdsRef = useRef<string[]>([]);
  const [pendingIds, setPendingIds] = useState<string[]>([]);
  const [focusedSourceId, setFocusedSourceIdState] = useState<string | null>(null);
  const [activeFloatingSourceId, setActiveFloatingSourceIdState] = useState<string | null>(null);
  const activeFloatingSourceIdRef = useRef<string | null>(null);
  const [maximizedFloatingSourceId, setMaximizedFloatingSourceId] = useState<string | null>(null);
  const [windowRects, setWindowRectsState] = useState<Record<string, ObservationWindowRect>>({});
  const windowRectsRef = useRef<Record<string, ObservationWindowRect>>({});
  const [viewportSize, setViewportSizeState] = useState<ObservationViewportSize | null>(null);
  const viewportSizeRef = useRef<ObservationViewportSize | null>(null);
  const desiredSnapshotRef = useRef<ObservationLayoutSnapshot | null>(null);
  const restoredLayoutAuthorityRef = useRef(false);
  const initializedCatalog = useRef<string | null>(null);
  const sourceIdList = sources.map((source) => source.id).sort();
  const sourceIdsIdentity = sourceIdList.join("\u0000");
  const sourceIds = useMemo(() => new Set(sourceIdList), [sourceIdsIdentity]);
  const sourceIdsRef = useRef<ReadonlySet<string>>(sourceIds);
  sourceIdsRef.current = sourceIds;
  const sourcesRef = useRef<readonly ObservationSourceDescriptor[]>(sources);
  sourcesRef.current = sources;
  const identity = catalogIdentity(sources);

  const commitVisibleIds = useCallback((next: readonly string[]) => {
    const unique = [...new Set(next)];
    visibleIdsRef.current = unique;
    setVisibleIds(unique);
  }, []);

  const commitActiveFloatingSourceId = useCallback((next: string | null) => {
    activeFloatingSourceIdRef.current = next;
    setActiveFloatingSourceIdState(next);
  }, []);

  const commitWindowRects = useCallback((next: Record<string, ObservationWindowRect>) => {
    windowRectsRef.current = next;
    setWindowRectsState(next);
  }, []);

  const persistLiveLayout = useCallback(() => {
    const currentViewport = viewportSizeRef.current;
    if (!currentViewport) return;
    const currentKnownIds = sourceIdsRef.current;
    const previous = desiredSnapshotRef.current;
    const unknownVisibleIds = previous?.visibleSourceIds.filter(
      (sourceId) => !currentKnownIds.has(sourceId),
    ) ?? [];
    const visibleSourceIds = [...new Set([...unknownVisibleIds, ...visibleIdsRef.current])];
    const unknownRects = Object.entries(previous?.windowRects ?? {}).filter(
      ([sourceId]) => !currentKnownIds.has(sourceId),
    );
    const knownRects = Object.entries(windowRectsRef.current).map(([sourceId, rect]) => [
      sourceId,
      normalizeObservationWindowRect(rect, currentViewport),
    ] as const);
    const previousActive = previous?.activeFloatingSourceId ?? null;
    const activeFloatingSourceId = activeFloatingSourceIdRef.current ?? (
      previousActive && !currentKnownIds.has(previousActive) ? previousActive : null
    );
    desiredSnapshotRef.current = validateObservationLayoutSnapshot({
      visibleSourceIds,
      activeFloatingSourceId: activeFloatingSourceId && visibleSourceIds.includes(activeFloatingSourceId)
        ? activeFloatingSourceId
        : null,
      windowRects: Object.fromEntries([...unknownRects, ...knownRects]),
      viewportSize: currentViewport,
    });
  }, []);

  const applyDesiredSnapshot = useCallback((
    snapshot: ObservationLayoutSnapshot,
    presentationMode: ObservationLayoutPresentationMode,
  ) => {
    const targetViewport = viewportSizeRef.current ?? snapshot.viewportSize;
    const projected = projectObservationLayoutSnapshot(
      snapshot,
      sourceIdsRef.current,
      targetViewport,
    );
    let visibleSourceIds = [...projected.visibleSourceIds];
    let activeFloatingSourceId = projected.activeFloatingSourceId;
    if (visibleSourceIds.length === 0 && sourcesRef.current.length > 0) {
      for (const source of sourcesRef.current.filter(canOpenByDefault)) {
        visibleSourceIds = openObservationSource(
          visibleSourceIds,
          source.id,
          sourcesRef.current,
        ).visibleIds;
      }
      activeFloatingSourceId = sourcesRef.current.find(
        (source) => visibleSourceIds.includes(source.id) && source.capabilities.overlay,
      )?.id ?? null;
    }
    commitVisibleIds(visibleSourceIds);
    commitActiveFloatingSourceId(activeFloatingSourceId);
    commitWindowRects({ ...projected.windowRects });
    setFocusedSourceIdState((current) =>
      observationPresentationSourceAfterLayoutApply(current, presentationMode));
    setMaximizedFloatingSourceId((current) =>
      observationPresentationSourceAfterLayoutApply(current, presentationMode));
  }, [commitActiveFloatingSourceId, commitVisibleIds, commitWindowRects]);

  const clearPresentation = useCallback((removedIds: readonly string[], persist = true) => {
    if (!removedIds.length) return;
    const removed = new Set(removedIds);
    setFocusedSourceIdState((current) => current && removed.has(current) ? null : current);
    if (
      activeFloatingSourceIdRef.current &&
      removed.has(activeFloatingSourceIdRef.current)
    ) {
      commitActiveFloatingSourceId(null);
    }
    setMaximizedFloatingSourceId((current) => current && removed.has(current) ? null : current);
    if (persist) persistLiveLayout();
  }, [commitActiveFloatingSourceId, persistLiveLayout]);

  useEffect(() => {
    const desired = desiredSnapshotRef.current;
    if (desired) {
      applyDesiredSnapshot(desired, "reset");
      return;
    }
    const currentVisible = visibleIdsRef.current;
    const nextVisible = currentVisible.filter((sourceId) => sourceIds.has(sourceId));
    if (nextVisible.length !== currentVisible.length) commitVisibleIds(nextVisible);
    setFocusedSourceIdState((current) => current && sourceIds.has(current) ? current : null);
    if (activeFloatingSourceIdRef.current && !sourceIds.has(activeFloatingSourceIdRef.current)) {
      commitActiveFloatingSourceId(null);
    }
    setMaximizedFloatingSourceId((current) => current && sourceIds.has(current) ? current : null);
    const entries = Object.entries(windowRectsRef.current);
    const nextEntries = entries.filter(([sourceId]) => sourceIds.has(sourceId));
    if (nextEntries.length !== entries.length) commitWindowRects(Object.fromEntries(nextEntries));
  }, [
    applyDesiredSnapshot,
    commitActiveFloatingSourceId,
    commitVisibleIds,
    commitWindowRects,
    sourceIds,
  ]);

  useEffect(() => {
    if (!identity) {
      if (!desiredSnapshotRef.current) initializedCatalog.current = null;
      return;
    }
    const desired = desiredSnapshotRef.current;
    if (desired) {
      const previousIdentity = initializedCatalog.current;
      initializedCatalog.current = identity;
      if (
        previousIdentity !== identity &&
        sources.some((source) => source.transport === "recording")
      ) {
        const nextVisible = visibleSourceIdsAfterRecordedCatalogActivation(
          visibleIdsRef.current,
          sources,
        );
        commitVisibleIds(nextVisible);
        const firstRecordedOverlay = sources.find((source) => (
          source.transport === "recording" &&
          source.capabilities.overlay &&
          nextVisible.includes(source.id)
        ));
        commitActiveFloatingSourceId(firstRecordedOverlay?.id ?? null);
        persistLiveLayout();
      }
      return;
    }
    if (initializedCatalog.current === identity) return;
    initializedCatalog.current = identity;
    let defaults: string[] = [];
    for (const source of sources.filter(canOpenByDefault)) {
      defaults = openObservationSource(defaults, source.id, sources).visibleIds;
    }
    commitVisibleIds(defaults);
    const firstFloating = sources.find(
      (source) => canOpenByDefault(source) && source.capabilities.overlay,
    );
    commitActiveFloatingSourceId(firstFloating?.id ?? null);
    persistLiveLayout();
  }, [
    applyDesiredSnapshot,
    commitActiveFloatingSourceId,
    commitVisibleIds,
    identity,
    persistLiveLayout,
    sources,
  ]);

  const selectedDeliveryIdentity = sources
    .filter((source) => source.capabilities.defaultVisible && source.activation?.selected && source.delivery)
    .map((source) => [
      source.id,
      source.delivery?.id,
      source.activation?.groupId,
      source.activation?.maxActive,
    ].join(":"))
    .sort()
    .join("|");

  useEffect(() => {
    if (restoredLayoutAuthorityRef.current) return;
    const selected = sources.filter(
      (source) => source.capabilities.defaultVisible && source.activation?.selected && source.delivery,
    );
    if (!selected.length) return;
    let change = { visibleIds: visibleIdsRef.current, removedIds: [] as string[] };
    const removed = new Set<string>();
    for (const source of selected) {
      change = openObservationSource(change.visibleIds, source.id, sources);
      change.removedIds.forEach((sourceId) => removed.add(sourceId));
    }
    commitVisibleIds(change.visibleIds);
    clearPresentation([...removed], false);
    persistLiveLayout();
  }, [clearPresentation, commitVisibleIds, persistLiveLayout, selectedDeliveryIdentity]);

  const markPending = useCallback((source: ObservationSourceDescriptor, pending: boolean) => {
    const groupId = source.activation?.groupId;
    const affected = groupId
      ? sources.filter((candidate) => candidate.activation?.groupId === groupId).map(({ id }) => id)
      : [source.id];
    setPendingIds((current) => pending
      ? [...new Set([...current, ...affected])]
      : current.filter((candidate) => !affected.includes(candidate)));
  }, [sources]);

  const hideSource = useCallback(async (sourceId: string) => {
    const source = sources.find((candidate) => candidate.id === sourceId);
    if (!source) return false;
    if (source.activation?.selected && source.activation.controllable) {
      if (!setSourceActive) return false;
      markPending(source, true);
      try {
        if (!await setSourceActive(source.sourceId, false)) return false;
      } finally {
        markPending(source, false);
      }
    }
    restoredLayoutAuthorityRef.current = false;
    const change = closeObservationSource(visibleIdsRef.current, sourceId);
    commitVisibleIds(change.visibleIds);
    clearPresentation(change.removedIds, false);
    persistLiveLayout();
    return true;
  }, [clearPresentation, commitVisibleIds, markPending, persistLiveLayout, setSourceActive, sources]);

  const toggleSource = useCallback(async (sourceId: string) => {
    const source = sources.find((candidate) => candidate.id === sourceId);
    if (!source) return false;
    const restart = shouldRestartObservationSource(source);
    if (visibleIdsRef.current.includes(sourceId) && !restart) return hideSource(sourceId);
    if (source.activation && (!source.activation.selected || restart)) {
      if (!source.activation.controllable || !setSourceActive) return false;
      markPending(source, true);
      try {
        if (!await setSourceActive(source.sourceId, true)) return false;
      } finally {
        markPending(source, false);
      }
    }
    restoredLayoutAuthorityRef.current = false;
    const change = openObservationSource(visibleIdsRef.current, sourceId, sources);
    commitVisibleIds(change.visibleIds);
    clearPresentation(change.removedIds, false);
    commitActiveFloatingSourceId(sourceId);
    persistLiveLayout();
    return true;
  }, [
    clearPresentation,
    commitActiveFloatingSourceId,
    commitVisibleIds,
    hideSource,
    markPending,
    persistLiveLayout,
    setSourceActive,
    sources,
  ]);

  const setFocusedSourceId = useCallback((sourceId: string | null) => {
    setFocusedSourceIdState(sourceId);
    if (sourceId) {
      commitActiveFloatingSourceId(sourceId);
      persistLiveLayout();
    }
  }, [commitActiveFloatingSourceId, persistLiveLayout]);

  const activateFloatingSource = useCallback((sourceId: string) => {
    commitActiveFloatingSourceId(sourceId);
    persistLiveLayout();
  }, [commitActiveFloatingSourceId, persistLiveLayout]);

  const setFloatingMaximized = useCallback((sourceId: string, maximized: boolean) => {
    setMaximizedFloatingSourceId(maximized ? sourceId : null);
    if (maximized) {
      commitActiveFloatingSourceId(sourceId);
      persistLiveLayout();
    }
  }, [commitActiveFloatingSourceId, persistLiveLayout]);

  const setWindowRect = useCallback((sourceId: string, rect: ObservationWindowRect) => {
    if (
      !Object.values(rect).every((value) => Number.isFinite(value)) ||
      rect.width <= 0 ||
      rect.height <= 0
    ) {
      return;
    }
    const currentViewport = viewportSizeRef.current;
    if (currentViewport) normalizeObservationWindowRect(rect, currentViewport);
    commitWindowRects({ ...windowRectsRef.current, [sourceId]: { ...rect } });
    persistLiveLayout();
  }, [commitWindowRects, persistLiveLayout]);

  const setViewportSize = useCallback((size: ObservationViewportSize) => {
    const next = validateObservationViewportSize(size);
    const current = viewportSizeRef.current;
    if (current && current.width === next.width && current.height === next.height) return;
    viewportSizeRef.current = next;
    setViewportSizeState(next);
    const desired = desiredSnapshotRef.current;
    if (desired) {
      desiredSnapshotRef.current = { ...desired, viewportSize: next };
      // Entering fullscreen changes the shell geometry, which triggers this
      // ResizeObserver path. Reproject persistent window rectangles without
      // clearing the transient fullscreen/focus state that caused the resize.
      applyDesiredSnapshot(desiredSnapshotRef.current, "preserve");
    } else if (initializedCatalog.current) {
      persistLiveLayout();
    }
  }, [applyDesiredSnapshot, persistLiveLayout]);

  const snapshot = useCallback((): ObservationLayoutSnapshot | null => {
    if (!viewportSizeRef.current) return null;
    persistLiveLayout();
    const desired = desiredSnapshotRef.current;
    if (!desired) return null;
    return cloneSnapshot(desired);
  }, [persistLiveLayout]);

  const restore = useCallback((saved: ObservationLayoutSnapshot) => {
    const validated = validateObservationLayoutSnapshot(saved);
    const currentViewport = viewportSizeRef.current;
    desiredSnapshotRef.current = cloneSnapshot({
      ...validated,
      viewportSize: currentViewport ?? validated.viewportSize,
    });
    restoredLayoutAuthorityRef.current = true;
    applyDesiredSnapshot(desiredSnapshotRef.current, "reset");
  }, [applyDesiredSnapshot]);

  const visibleSourceIds = useMemo(() => new Set(visibleIds), [visibleIds]);
  const pendingSourceIds = useMemo(() => new Set(pendingIds), [pendingIds]);

  return {
    visibleSourceIds,
    focusedSourceId,
    activeFloatingSourceId,
    maximizedFloatingSourceId,
    windowRects,
    viewportSize,
    pendingSourceIds,
    toggleSource,
    hideSource,
    setFocusedSourceId,
    activateFloatingSource,
    setFloatingMaximized,
    setWindowRect,
    setViewportSize,
    snapshot,
    restore,
  };
}
