import {
  useState,
  useCallback,
  useRef,
  useEffect,
  useSyncExternalStore,
  memo,
  type ReactNode,
} from "react";
import { useMountEffect } from "../../hooks/useMountEffect";
import { useTimelinePlayer, PlayerControls, Timeline, usePlayerStore } from "../../player";
import type { TimelineElement } from "../../player";
import type { BlockedTimelineEditIntent } from "../../player/components/timelineEditing";
import { NLEPreview } from "./NLEPreview";
import { CompositionBreadcrumb } from "./CompositionBreadcrumb";
import { TimelineResizeDivider, MIN_TIMELINE_H, MIN_PREVIEW_H } from "./TimelineResizeDivider";
import { usePreviewBlockDrop } from "./usePreviewBlockDrop";
import { useCompositionStack } from "./useCompositionStack";
import { useTimelineEditContext } from "../../contexts/TimelineEditContext";
import { setCompositionSourceMap } from "../editor/domEditingDom";
import { trackStudioExpandedClipEdit } from "../../telemetry/events";
import {
  TIMELINE_TOGGLE_SHORTCUT_LABEL,
  getTimelineToggleTitle,
} from "../../utils/timelineDiscovery";
import { ensureMotionPathPluginLoaded } from "../../utils/gsapSoftReload";
import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/studioUiPreferences";

interface NLELayoutProps {
  projectId: string;
  portrait?: boolean;
  /** Slot for overlays rendered on top of the preview (cursors, highlights, etc.) */
  previewOverlay?: ReactNode;
  /** Slot rendered above the timeline tracks (toolbar with split, delete, zoom) */
  timelineToolbar?: ReactNode;
  /** Slot rendered below the timeline tracks */
  timelineFooter?: ReactNode;
  /** Increment to force the preview to reload (e.g., after file writes) */
  refreshKey?: number;
  /** Navigate to a specific composition path (e.g., "compositions/intro.html") */
  activeCompositionPath?: string | null;
  /** Callback to expose the iframe ref (for element picker, etc.) */
  onIframeRef?: (iframe: HTMLIFrameElement | null) => void;
  /** Callback when the viewed composition changes (drill-down/back) */
  onCompositionChange?: (compositionPath: string | null) => void;
  /** Custom clip content renderer for timeline (thumbnails, waveforms, etc.) */
  renderClipContent?: (
    element: TimelineElement,
    style: { clip: string; label: string },
  ) => ReactNode;
  onFileDrop?: (
    files: File[],
    placement?: Pick<TimelineElement, "start" | "track">,
  ) => Promise<void> | void;
  onDeleteElement?: (element: TimelineElement) => Promise<void> | void;
  onAssetDrop?: (
    assetPath: string,
    placement: Pick<TimelineElement, "start" | "track">,
  ) => Promise<void> | void;
  onBlockDrop?: (
    blockName: string,
    placement: Pick<TimelineElement, "start" | "track">,
  ) => Promise<void> | void;
  onPreviewBlockDrop?: (
    blockName: string,
    position: { left: number; top: number },
  ) => Promise<void> | void;
  onBlockedEditAttempt?: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
  onSelectTimelineElement?: (element: TimelineElement | null) => void;
  /** Exposes the compIdToSrc map for parent components (e.g., useRenderClipContent) */
  onCompIdToSrcChange?: (map: Map<string, string>) => void;
  /** Whether the timeline panel is visible (default: true) */
  timelineVisible?: boolean;
  /** Callback to toggle timeline visibility */
  onToggleTimeline?: () => void;
  /** Notifies parent when composition loading state changes */
  onCompositionLoadingChange?: (loading: boolean) => void;
}

const DEFAULT_TIMELINE_H = 220;

function subscribeFullscreen(cb: () => void) {
  document.addEventListener("fullscreenchange", cb);
  return () => document.removeEventListener("fullscreenchange", cb);
}

function getFullscreenElement() {
  return document.fullscreenElement;
}

export function shouldDisableTimelineWhileCompositionLoading(compositionLoading: boolean): boolean {
  return compositionLoading;
}

// fallow-ignore-next-line complexity
export const NLELayout = memo(function NLELayout({
  projectId,
  portrait,
  previewOverlay,
  timelineToolbar,
  timelineFooter,
  refreshKey,
  activeCompositionPath,
  onIframeRef,
  onCompositionChange,
  renderClipContent,
  onFileDrop,
  onDeleteElement,
  onAssetDrop,
  onBlockDrop,
  onPreviewBlockDrop,
  onBlockedEditAttempt,
  onSelectTimelineElement,
  onCompIdToSrcChange,
  timelineVisible,
  onToggleTimeline,
  onCompositionLoadingChange: onCompositionLoadingChangeParent,
}: NLELayoutProps) {
  const {
    iframeRef,
    togglePlay,
    seek,
    onIframeLoad: baseOnIframeLoad,
    refreshPlayer,
  } = useTimelinePlayer();

  // Reset timeline state when the project changes. Done in an effect, not during
  // render: reset() updates the player store, and updating another store/component
  // mid-render triggers React's "Cannot update a component while rendering a
  // different component" warning. The effect runs right after commit, so the new
  // project's first frame may briefly show prior timeline state before it clears.
  useEffect(() => {
    usePlayerStore.getState().reset();
  }, [projectId]);

  const stageRefForDrop = useRef<HTMLDivElement | null>(null);
  const handleStageRef = useCallback((ref: React.RefObject<HTMLDivElement | null>) => {
    stageRefForDrop.current = ref.current;
  }, []);

  // Authored composition size measured from the loaded preview — drives drop
  // coordinate mapping so blocks land where the user pointed on any comp size.
  const [previewCompositionSize, setPreviewCompositionSize] = useState<{
    width: number;
    height: number;
  } | null>(null);

  const {
    isDragOver: previewDragOver,
    handleDragEnter: handlePreviewDragEnter,
    handleDragOver: handlePreviewDragOver,
    handleDragLeave: handlePreviewDragLeave,
    handleDrop: handlePreviewDrop,
  } = usePreviewBlockDrop({
    portrait,
    compositionSize: previewCompositionSize,
    stageRef: stageRefForDrop as React.RefObject<HTMLDivElement | null>,
    onBlockDrop: onPreviewBlockDrop,
  });

  // Lightweight reload: change iframe src instead of destroying the Player.
  // refreshPlayer() saves the seek position and appends a cache-busting _t
  // param — the Player instance stays alive so the adapter is available for
  // saveSeekPosition() to read the current time before the reload.
  const prevRefreshKeyRef = useRef(refreshKey);
  useEffect(() => {
    if (refreshKey === prevRefreshKeyRef.current) return;
    prevRefreshKeyRef.current = refreshKey;
    refreshPlayer();
  }, [refreshKey, refreshPlayer]);

  const onIframeLoad = useCallback(() => {
    baseOnIframeLoad();
    // Pre-load + register MotionPathPlugin once so adding a motion path in the
    // studio doesn't take the async plugin-load flash path on the first soft
    // reload (the comp may not ship the plugin until it actually uses one).
    ensureMotionPathPluginLoaded(iframeRef.current);
    onIframeRef?.(iframeRef.current);
  }, [baseOnIframeLoad, iframeRef, onIframeRef]);

  const {
    compositionStack,
    updateCompositionStack,
    handleNavigateComposition,
    handleDrillDown: drillDown,
    masterSeekRef,
    compIdToSrc,
    setCompIdToSrc,
  } = useCompositionStack({
    projectId,
    activeCompositionPath,
    onCompositionChange,
  });

  // Wrap handleDrillDown to also scan the iframe DOM for data-composition-src
  const iframeRef_ = iframeRef;
  const handleDrillDown = useCallback(
    (element: TimelineElement) => {
      if (!element.compositionSrc) return;
      usePlayerStore.getState().setSelectedElementId(null);
      // Check compIdToSrc map first; then scan iframe DOM; then fall through to drillDown
      const compId = element.id;
      let resolvedPath = compIdToSrc.get(compId);
      if (!resolvedPath) {
        try {
          const doc = iframeRef_.current?.contentDocument;
          if (doc) {
            const host = doc.querySelector(
              `[data-composition-id="${CSS.escape(compId)}"][data-composition-src]`,
            );
            if (host) {
              resolvedPath = host.getAttribute("data-composition-src") || undefined;
            }
          }
        } catch {
          /* cross-origin */
        }
      }
      // Delegate with the resolved compositionSrc (may be same as original)
      drillDown({
        id: compId,
        compositionSrc: resolvedPath ?? element.compositionSrc,
      });
    },
    [compIdToSrc, drillDown, iframeRef_],
  );

  // Move/resize/split come from the timeline edit context, not props — the
  // wrappers below intercept expanded clips and must call the *real* handlers.
  // (Delete is a direct prop; it stays that way.)
  const { onMoveElement, onResizeElement, onSplitElement } = useTimelineEditContext();

  // An expanded sub-comp child reaches the normal edit handlers in its own
  // local coordinates: addressed by its real DOM id, with timeline time rebased
  // onto the sub-comp it lives in. The handlers then save + reloadPreview exactly
  // as they do for top-level clips — no separate live-DOM path.
  const toLocalElement = useCallback(
    (element: TimelineElement, basis: number): TimelineElement => ({
      ...element,
      id: element.domId ?? element.id,
      start: element.start - basis,
    }),
    [],
  );

  const handleMoveElement = useCallback(
    (element: TimelineElement, updates: Pick<TimelineElement, "start" | "track">) => {
      const basis = element.expandedParentStart;
      if (basis === undefined) return onMoveElement?.(element, updates);
      trackStudioExpandedClipEdit({ action: "move" });
      onMoveElement?.(toLocalElement(element, basis), {
        ...updates,
        start: Math.max(0, updates.start - basis),
      });
    },
    [onMoveElement, toLocalElement],
  );

  const handleResizeElement = useCallback(
    (
      element: TimelineElement,
      updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
    ) => {
      const basis = element.expandedParentStart;
      if (basis === undefined) return onResizeElement?.(element, updates);
      trackStudioExpandedClipEdit({ action: "resize" });
      onResizeElement?.(toLocalElement(element, basis), {
        ...updates,
        start: Math.max(0, updates.start - basis),
      });
    },
    [onResizeElement, toLocalElement],
  );

  const handleDeleteElement = useCallback(
    (element: TimelineElement) => {
      const basis = element.expandedParentStart;
      if (basis === undefined) return onDeleteElement?.(element);
      trackStudioExpandedClipEdit({ action: "delete" });
      return onDeleteElement?.(toLocalElement(element, basis));
    },
    [onDeleteElement, toLocalElement],
  );

  const handleSplitElement = useCallback(
    (element: TimelineElement, splitTime: number) => {
      const basis = element.expandedParentStart;
      if (basis === undefined) return onSplitElement?.(element, splitTime);
      trackStudioExpandedClipEdit({ action: "split" });
      return onSplitElement?.(toLocalElement(element, basis), Math.max(0, splitTime - basis));
    },
    [onSplitElement, toLocalElement],
  );

  // Composition ID → file path map from raw index.html
  const compIdToSrcRef = useRef(compIdToSrc);
  compIdToSrcRef.current = compIdToSrc;

  useMountEffect(() => {
    fetch(`/api/projects/${projectId}/files/index.html`)
      .then((r) => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        return r.json();
      })
      .then((data: { content?: string }) => {
        const html = data.content || "";
        const map = new Map<string, string>();
        const re =
          /data-composition-id=["']([^"']+)["'][^>]*data-composition-src=["']([^"']+)["']|data-composition-src=["']([^"']+)["'][^>]*data-composition-id=["']([^"']+)["']/g;
        let match;
        while ((match = re.exec(html)) !== null) {
          const id = match[1] || match[4];
          const src = match[2] || match[3];
          if (id && src) map.set(id, src);
        }
        setCompIdToSrc(map);
        // Let DOM source-resolution recover a subcomposition element's source file
        // (the runtime drops the linkage when inlining — see getSourceFileForElement).
        setCompositionSourceMap(map);
        onCompIdToSrcChange?.(map);
      })
      .catch((err: unknown) => {
        // Non-fatal: drill-down still works via the iframe DOM scan; without
        // the map only source-file resolution for sub-comps degrades.
        console.warn("[studio] Couldn't load composition source map from index.html:", err);
      });
  });

  // Patch elements with compositionSrc whenever elements or compIdToSrc change.
  // eslint-disable-next-line no-restricted-syntax
  useEffect(() => {
    if (compIdToSrc.size === 0) return;
    const patchElements = (elements: TimelineElement[]): TimelineElement[] | null => {
      const map = compIdToSrcRef.current;
      if (map.size === 0) return null;
      let patched = false;
      const updated = elements.map((el) => {
        if (el.compositionSrc) return el;
        const src = map.get(el.id) ?? map.get(el.id.replace(/-(host|comp|layer)$/, ""));
        if (src) {
          patched = true;
          return { ...el, compositionSrc: src };
        }
        return el;
      });
      return patched ? updated : null;
    };
    const patched = patchElements(usePlayerStore.getState().elements);
    if (patched) usePlayerStore.getState().setElements(patched);
    let patching = false;
    return usePlayerStore.subscribe((state, prev) => {
      if (patching) return;
      if (state.elements === prev.elements || state.elements.length === 0) return;
      if (state.elements.every((el) => el.compositionSrc)) return;
      patching = true;
      const result = patchElements(state.elements);
      if (result) state.setElements(result);
      patching = false;
    });
  }, [compIdToSrc]);

  // Resizable timeline height — persisted alongside zoom/pan so the user's
  // workspace layout survives reloads.
  const [timelineH, setTimelineH] = useState(() => {
    const stored = readStudioUiPreferences().timelineHeight;
    return stored !== undefined && stored >= MIN_TIMELINE_H ? stored : DEFAULT_TIMELINE_H;
  });
  const persistTimelineH = useCallback((height: number) => {
    writeStudioUiPreferences({ timelineHeight: Math.round(height) });
  }, []);
  // A height persisted on a tall window can exceed this window's container and
  // collapse the flex-1 preview to 0px — clamp once the container is measurable
  // (the drag/keyboard paths already clamp; the restore path must too).
  useEffect(() => {
    const containerH = containerRef.current?.getBoundingClientRect().height;
    if (!containerH) return;
    const max = containerH - MIN_PREVIEW_H;
    setTimelineH((prev) => (prev > max ? Math.max(MIN_TIMELINE_H, max) : prev));
  }, []);
  const hasLoadedOnceRef = useRef(false);
  const [compositionLoading, setCompositionLoadingRaw] = useState(true);
  const setCompositionLoading = useCallback((loading: boolean) => {
    if (!loading) hasLoadedOnceRef.current = true;
    if (loading && hasLoadedOnceRef.current) return;
    setCompositionLoadingRaw(loading);
  }, []);
  const timelineDisabled = shouldDisableTimelineWhileCompositionLoading(compositionLoading);

  useEffect(() => {
    onCompositionLoadingChangeParent?.(compositionLoading);
  }, [compositionLoading, onCompositionLoadingChangeParent]);

  const fullscreenElement = useSyncExternalStore(subscribeFullscreen, getFullscreenElement);
  const isTimelineVisible = timelineVisible ?? true;
  const containerRef = useRef<HTMLDivElement>(null);
  const isFullscreen = fullscreenElement === containerRef.current && fullscreenElement != null;

  const toggleFullscreen = useCallback(() => {
    if (!containerRef.current) return;
    if (document.fullscreenElement) {
      void document.exitFullscreen();
    } else {
      void containerRef.current.requestFullscreen();
    }
  }, []);

  const currentLevel = compositionStack[compositionStack.length - 1];
  const directUrl = compositionStack.length > 1 ? currentLevel.previewUrl : undefined;

  const onIframeRefStable = useRef(onIframeRef);
  onIframeRefStable.current = onIframeRef;
  useEffect(() => {
    onIframeRefStable.current?.(iframeRef.current);
  }, [compositionStack.length, refreshKey, iframeRef]);

  // Keyboard: Escape to pop composition level
  const handleKeyDown = useCallback(
    (e: React.KeyboardEvent) => {
      if (e.key === "Escape" && compositionStack.length > 1) {
        updateCompositionStack((prev) => prev.slice(0, -1));
      }
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [compositionStack.length],
  );

  // Suppress TS unused-var warning for masterSeekRef (used inside useCompositionStack)
  void masterSeekRef;

  return (
    <div
      ref={containerRef}
      className="flex flex-col h-full min-h-0 bg-neutral-950"
      onKeyDown={handleKeyDown}
      tabIndex={-1}
      data-studio-fullscreen-target=""
    >
      {/* Preview + player controls */}
      <div className="flex-1 min-h-0 flex flex-col">
        <div
          className="flex-1 min-h-0 relative overflow-hidden"
          data-preview-pan-surface="true"
          onPointerDown={(e) => {
            const el = iframeRef.current?.parentElement ?? iframeRef.current;
            if (!el) return;
            const rect = el.getBoundingClientRect();
            const inside =
              e.clientX >= rect.left &&
              e.clientX <= rect.right &&
              e.clientY >= rect.top &&
              e.clientY <= rect.bottom;
            if (!inside) onSelectTimelineElement?.(null);
          }}
          onDragEnter={handlePreviewDragEnter}
          onDragOver={handlePreviewDragOver}
          onDragLeave={handlePreviewDragLeave}
          onDrop={handlePreviewDrop}
        >
          <div className="absolute inset-0 overflow-hidden">
            <NLEPreview
              projectId={projectId}
              iframeRef={iframeRef}
              onIframeLoad={onIframeLoad}
              onCompositionLoadingChange={setCompositionLoading}
              portrait={portrait}
              directUrl={directUrl}
              suppressLoadingOverlay={hasLoadedOnceRef.current}
              onStageRef={handleStageRef}
              onCompositionSizeChange={setPreviewCompositionSize}
            />
            {previewDragOver && (
              <div className="absolute inset-2 z-40 rounded-lg border-2 border-dashed border-studio-accent/50 bg-studio-accent/[0.04] pointer-events-none" />
            )}
          </div>
          {!isFullscreen && previewOverlay}
        </div>
        <div className="bg-neutral-950 border-t border-neutral-800/50 flex-shrink-0">
          {!isFullscreen && compositionStack.length > 1 && (
            <CompositionBreadcrumb
              stack={compositionStack}
              onNavigate={handleNavigateComposition}
            />
          )}
          <PlayerControls
            onTogglePlay={togglePlay}
            onSeek={seek}
            disabled={timelineDisabled}
            isFullscreen={isFullscreen}
            onToggleFullscreen={toggleFullscreen}
          />
        </div>
      </div>

      {!isFullscreen && isTimelineVisible ? (
        <>
          <TimelineResizeDivider
            timelineH={timelineH}
            setTimelineH={setTimelineH}
            persistTimelineH={persistTimelineH}
            containerRef={containerRef}
            disabled={timelineDisabled}
          />

          {/* Timeline section */}
          <div
            className="relative flex flex-col flex-shrink-0"
            style={{ height: timelineH }}
            aria-disabled={timelineDisabled || undefined}
          >
            <div
              className="flex flex-col flex-1 min-h-0 overflow-hidden bg-neutral-950"
              onDoubleClick={(e) => {
                if ((e.target as HTMLElement).closest("[data-clip]")) return;
                if (timelineDisabled) return;
                if (compositionStack.length > 1) {
                  updateCompositionStack((prev) => prev.slice(0, -1));
                }
              }}
            >
              <div className="flex-shrink-0">{timelineToolbar}</div>
              <Timeline
                onSeek={seek}
                onDrillDown={handleDrillDown}
                renderClipContent={renderClipContent}
                onFileDrop={onFileDrop}
                onDeleteElement={handleDeleteElement}
                onAssetDrop={onAssetDrop}
                onBlockDrop={onBlockDrop}
                onMoveElement={handleMoveElement}
                onResizeElement={handleResizeElement}
                onBlockedEditAttempt={onBlockedEditAttempt}
                onSplitElement={handleSplitElement}
                onSelectElement={onSelectTimelineElement}
              />
            </div>
            {timelineFooter && <div className="flex-shrink-0">{timelineFooter}</div>}
            {timelineDisabled && (
              <div
                className="absolute inset-0 z-30 cursor-not-allowed bg-black/18 flex items-center justify-center"
                data-testid="timeline-loading-disabled-overlay"
                role="status"
                onPointerDown={(event) => event.preventDefault()}
                onDragOver={(event) => event.preventDefault()}
                onDrop={(event) => event.preventDefault()}
              >
                <span className="rounded-md bg-neutral-900/90 px-2.5 py-1 text-[11px] text-neutral-400">
                  Loading composition…
                </span>
              </div>
            )}
          </div>
        </>
      ) : !isFullscreen && onToggleTimeline ? (
        <div className="flex-shrink-0 border-t border-neutral-800/50 bg-neutral-950/96">
          <div className="flex h-10 items-center justify-between px-3">
            <div className="text-[10px] font-medium uppercase tracking-[0.16em] text-neutral-500">
              Timeline
            </div>
            <button
              type="button"
              onClick={onToggleTimeline}
              className="flex h-7 items-center gap-1.5 rounded-md border border-neutral-800 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-700 hover:bg-neutral-900 hover:text-neutral-100"
              title={getTimelineToggleTitle(false)}
              aria-label="Show timeline editor"
            >
              <svg
                width="13"
                height="13"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                strokeWidth="1.7"
                strokeLinecap="round"
                strokeLinejoin="round"
                aria-hidden="true"
              >
                <rect x="3" y="13" width="18" height="8" rx="1" />
                <path d="M7 9h10" />
                <path d="M8 5h8" />
              </svg>
              <span>Show</span>
              <span className="hidden rounded bg-white/5 px-1 py-0.5 font-mono text-[9px] text-neutral-500 sm:inline">
                {TIMELINE_TOGGLE_SHORTCUT_LABEL}
              </span>
            </button>
          </div>
        </div>
      ) : null}
    </div>
  );
});
