"use client";

import { useState, useCallback, useEffect, useRef } from "react";
import { type DashboardLayoutConfig, type DashboardWidgetType } from "@nexus/shared/types";
import {
  createDashboardWidget,
  createDefaultDashboardLayout,
  getNextDashboardWidgetY,
  normalizeDashboardLayout,
} from "@nexus/shared/schemas";
import { trpc } from "~/lib/trpc/client.js";

/** Returns a user-scoped localStorage key to prevent cross-user data bleed. */
function storageKey(userId: string): string {
  return `nexus_dashboard_v1_${userId}`;
}

function generateWidgetId() {
  return `widget-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
}

function loadFromStorage(userId: string): DashboardLayoutConfig | null {
  if (typeof window === "undefined") return null;
  try {
    const raw = localStorage.getItem(storageKey(userId));
    if (!raw) return null;
    return normalizeDashboardLayout(JSON.parse(raw));
  } catch {
    return null;
  }
}

function saveToStorage(userId: string, config: DashboardLayoutConfig) {
  if (typeof window === "undefined") return;
  try {
    localStorage.setItem(storageKey(userId), JSON.stringify(config));
  } catch {}
}

export function shouldHydrateDashboardFromDb(params: {
  remoteLayout: DashboardLayoutConfig | null | undefined;
  hasHydratedFromDb: boolean;
  hasLocalChangesBeforeHydration: boolean;
}): boolean {
  const { remoteLayout, hasHydratedFromDb, hasLocalChangesBeforeHydration } = params;
  return (
    remoteLayout !== null &&
    remoteLayout !== undefined &&
    !hasHydratedFromDb &&
    !hasLocalChangesBeforeHydration
  );
}

export function useDashboardLayout() {
  const { data: meData } = trpc.user.me.useQuery() as { data: { id?: string } | null | undefined };
  const userId = meData?.id ?? null;

  // Initial state: load from user-scoped localStorage once we have the userId.
  // Before userId resolves, fall back to the default layout so the page is not blank.
  const [config, setConfig] = useState<DashboardLayoutConfig>(createDefaultDashboardLayout);
  // isHydrated: true once we have applied the best available layout (localStorage or DB).
  // While false, callers should show a skeleton instead of the default 1-widget layout.
  const [isHydrated, setIsHydrated] = useState(false);
  const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const hasHydratedFromDbRef = useRef(false);
  const hasLocalChangesBeforeHydrationRef = useRef(false);
  const hasHydratedFromStorageRef = useRef(false);
  // Holds the DB config that needs to be persisted to localStorage once userId is available.
  const pendingStorageSaveRef = useRef<ReturnType<typeof normalizeDashboardLayout> | null>(null);
  // Holds the latest layout that has been queued for DB save but not yet flushed.
  const pendingLayoutSaveRef = useRef<DashboardLayoutConfig | null>(null);

  // Once userId is known, hydrate from user-scoped localStorage (if no DB data yet).
  useEffect(() => {
    if (
      !userId ||
      hasHydratedFromStorageRef.current ||
      hasHydratedFromDbRef.current ||
      hasLocalChangesBeforeHydrationRef.current
    ) {
      return;
    }
    const stored = loadFromStorage(userId);
    hasHydratedFromStorageRef.current = true;
    if (stored) {
      setConfig(stored);
      // Treat localStorage data as authoritative local state so a stale DB
      // response (e.g. from a save that was cancelled by page navigation within
      // the 2-second debounce window) cannot overwrite it.
      hasLocalChangesBeforeHydrationRef.current = true;
    }
    // localStorage check is complete — show whatever we have now.
    setIsHydrated(true);
  }, [userId]);

  const { data: dbData } = trpc.user.getDashboardLayout.useQuery(undefined, {
    staleTime: 30_000,
  }) as { data: { layout: DashboardLayoutConfig | null; updatedAt: unknown } | null | undefined };

  const [saveStatus, setSaveStatus] = useState<"idle" | "saved">("idle");
  const saveStatusTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  const saveMutation = trpc.user.saveDashboardLayout.useMutation({
    onSuccess: () => {
      setSaveStatus("saved");
      if (saveStatusTimerRef.current) clearTimeout(saveStatusTimerRef.current);
      saveStatusTimerRef.current = setTimeout(() => setSaveStatus("idle"), 2500);
    },
  });

  // Sync from DB on load (DB wins if it has data).
  useEffect(() => {
    // Wait until the query has settled (undefined = still in-flight).
    if (dbData === undefined || hasHydratedFromDbRef.current) return;

    const remoteLayout = dbData?.layout ?? null;

    if (remoteLayout === null) {
      // DB has no saved layout — don't block localStorage hydration.
      // If localStorage already ran, we're done; otherwise localStorage will call setIsHydrated.
      if (hasHydratedFromStorageRef.current) {
        setIsHydrated(true);
      }
      return;
    }

    if (
      !shouldHydrateDashboardFromDb({
        remoteLayout,
        hasHydratedFromDb: hasHydratedFromDbRef.current,
        hasLocalChangesBeforeHydration: hasLocalChangesBeforeHydrationRef.current,
      })
    ) {
      // DB data present but local changes already in-flight — keep local state, mark done.
      hasHydratedFromDbRef.current = true;
      setIsHydrated(true);
      return;
    }

    const dbConfig = normalizeDashboardLayout(remoteLayout);
    hasHydratedFromDbRef.current = true;
    setConfig(dbConfig);
    setIsHydrated(true);
    if (userId) {
      saveToStorage(userId, dbConfig);
    } else {
      // userId not yet resolved — defer the localStorage write until it is.
      pendingStorageSaveRef.current = dbConfig;
    }
  }, [dbData, userId]);

  // When userId becomes available, flush any pending localStorage save from DB hydration.
  useEffect(() => {
    if (!userId || !pendingStorageSaveRef.current) return;
    saveToStorage(userId, pendingStorageSaveRef.current);
    pendingStorageSaveRef.current = null;
  }, [userId]);

  // Keep a ref to saveMutation so the unmount cleanup can call it without
  // capturing a stale closure.
  const saveMutationRef = useRef(saveMutation);
  saveMutationRef.current = saveMutation;

  // Flush any pending debounced DB save when the component unmounts so that
  // navigating away within the 2-second window doesn't silently lose changes.
  useEffect(
    () => () => {
      if (saveTimeoutRef.current) {
        clearTimeout(saveTimeoutRef.current);
        saveTimeoutRef.current = null;
        if (pendingLayoutSaveRef.current) {
          saveMutationRef.current.mutate({ layout: pendingLayoutSaveRef.current });
          pendingLayoutSaveRef.current = null;
        }
      }
    },
    [],
  );

  const persist = useCallback(
    (nextConfig: DashboardLayoutConfig) => {
      if (!hasHydratedFromDbRef.current) {
        hasLocalChangesBeforeHydrationRef.current = true;
      }
      const newConfig = normalizeDashboardLayout(nextConfig);
      if (userId) saveToStorage(userId, newConfig);
      pendingLayoutSaveRef.current = newConfig;
      if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
      saveTimeoutRef.current = setTimeout(() => {
        pendingLayoutSaveRef.current = null;
        saveMutation.mutate({ layout: newConfig });
      }, 2000);
    },
    [saveMutation, userId],
  );

  const addWidget = useCallback(
    (type: DashboardWidgetType) => {
      setConfig((prev) => {
        const newConfig = {
          ...prev,
          widgets: [
            ...prev.widgets,
            createDashboardWidget(type, {
              id: generateWidgetId(),
              x: 0,
              y: getNextDashboardWidgetY(prev.widgets),
            }),
          ],
        };
        persist(newConfig);
        return newConfig;
      });
    },
    [persist],
  );

  const removeWidget = useCallback(
    (id: string) => {
      setConfig((prev) => {
        const newConfig = { ...prev, widgets: prev.widgets.filter((w) => w.id !== id) };
        persist(newConfig);
        return newConfig;
      });
    },
    [persist],
  );

  const updateWidgetConfig = useCallback(
    (id: string, configUpdate: Record<string, unknown>) => {
      setConfig((prev) => {
        const newConfig = {
          ...prev,
          widgets: prev.widgets.map((w) =>
            w.id === id ? { ...w, config: { ...w.config, ...configUpdate } } : w,
          ),
        };
        persist(newConfig);
        return newConfig;
      });
    },
    [persist],
  );

  const onLayoutChange = useCallback(
    (layout: { i: string; x: number; y: number; w: number; h: number }[]) => {
      setConfig((prev) => {
        const layoutMap = new Map(layout.map((item) => [item.i, item]));
        const previousWidgetMap = new Map(prev.widgets.map((widget) => [widget.id, widget]));
        const updatedWidgets = prev.widgets.map((w) => {
          const item = layoutMap.get(w.id);
          if (!item) return w;
          return { ...w, x: item.x, y: item.y, w: item.w, h: item.h };
        });

        // Only persist when the user actually moved/resized something.
        // react-grid-layout fires onLayoutChange on mount too — we skip that
        // to avoid overwriting saved positions with compacted coordinates.
        const changed = updatedWidgets.some((w) => {
          const orig = previousWidgetMap.get(w.id);
          return orig && (w.x !== orig.x || w.y !== orig.y || w.w !== orig.w || w.h !== orig.h);
        });

        // Return the same reference when nothing changed so React skips
        // the re-render. Without this guard, setConfig always produces a new
        // object → re-render → new layouts prop → react-grid-layout fires
        // onLayoutChange again → infinite oscillation loop.
        if (!changed) return prev;

        const newConfig = { ...prev, widgets: updatedWidgets };
        persist(newConfig);
        return newConfig;
      });
    },
    [persist],
  );

  const resetLayout = useCallback(() => {
    const defaultConfig = createDefaultDashboardLayout();
    setConfig(defaultConfig);
    persist(defaultConfig);
  }, [persist]);

  return {
    config,
    isHydrated,
    saveStatus,
    addWidget,
    removeWidget,
    updateWidgetConfig,
    onLayoutChange,
    resetLayout,
  };
}
