import { useReducer, useCallback, useRef, useEffect } from "react";
import { nanoid } from "nanoid";
import type {
  DashboardLayout,
  LayoutItem,
  Widget,
} from "~/presenters/v3/MetricDashboardPresenter.server";
import type { WidgetData, QueryWidgetConfig } from "~/components/metrics/QueryWidget";

// ============================================================================
// Types
// ============================================================================

type EditorMode = null | { type: "add" } | { type: "edit"; widgetId: string; widget: WidgetData };

type DashboardState = {
  /** The layout items (positions/sizes) */
  layout: LayoutItem[];
  /** The widget configurations keyed by widget ID */
  widgets: Record<string, Widget>;
  /** Current editor mode (add/edit/closed) */
  editorMode: EditorMode;
};

// ============================================================================
// Actions
// ============================================================================

type DashboardAction =
  | { type: "ADD_WIDGET"; payload: { id: string; widget: Widget; layoutItem: LayoutItem } }
  | { type: "UPDATE_WIDGET"; payload: { id: string; widget: Widget } }
  | { type: "RENAME_WIDGET"; payload: { id: string; title: string } }
  | { type: "DELETE_WIDGET"; payload: { id: string } }
  | { type: "DUPLICATE_WIDGET"; payload: { id: string; newId: string } }
  | { type: "UPDATE_LAYOUT"; payload: { layout: LayoutItem[] } }
  | { type: "RESET_STATE"; payload: { layout: LayoutItem[]; widgets: Record<string, Widget> } }
  | { type: "OPEN_ADD_EDITOR" }
  | { type: "OPEN_EDIT_EDITOR"; payload: { widgetId: string; widget: WidgetData } }
  | { type: "CLOSE_EDITOR" };

// ============================================================================
// Reducer
// ============================================================================

function dashboardReducer(state: DashboardState, action: DashboardAction): DashboardState {
  switch (action.type) {
    case "ADD_WIDGET":
      return {
        ...state,
        layout: [...state.layout, action.payload.layoutItem],
        widgets: {
          ...state.widgets,
          [action.payload.id]: action.payload.widget,
        },
        editorMode: null,
      };

    case "UPDATE_WIDGET":
      return {
        ...state,
        widgets: {
          ...state.widgets,
          [action.payload.id]: action.payload.widget,
        },
        editorMode: null,
      };

    case "RENAME_WIDGET": {
      const existingWidget = state.widgets[action.payload.id];
      if (!existingWidget) return state;
      return {
        ...state,
        widgets: {
          ...state.widgets,
          [action.payload.id]: {
            ...existingWidget,
            title: action.payload.title,
          },
        },
      };
    }

    case "DELETE_WIDGET": {
      const { [action.payload.id]: _, ...remainingWidgets } = state.widgets;
      return {
        ...state,
        layout: state.layout.filter((item) => item.i !== action.payload.id),
        widgets: remainingWidgets,
      };
    }

    case "DUPLICATE_WIDGET": {
      const original = state.widgets[action.payload.id];
      const originalLayout = state.layout.find((l) => l.i === action.payload.id);
      if (!original || !originalLayout) return state;

      const maxBottom = Math.max(0, ...state.layout.map((l) => l.y + l.h));

      // Deep copy the widget to ensure no shared references
      // This prevents edits to one widget from affecting the duplicate
      const duplicatedWidget: Widget = {
        title: `${original.title} (Copy)`,
        query: original.query,
        display: JSON.parse(JSON.stringify(original.display)) as QueryWidgetConfig,
      };

      return {
        ...state,
        layout: [
          ...state.layout,
          { ...originalLayout, i: action.payload.newId, y: maxBottom, x: 0 },
        ],
        widgets: {
          ...state.widgets,
          [action.payload.newId]: duplicatedWidget,
        },
      };
    }

    case "UPDATE_LAYOUT":
      return { ...state, layout: action.payload.layout };

    case "RESET_STATE":
      return {
        ...state,
        layout: action.payload.layout,
        widgets: action.payload.widgets,
      };

    case "OPEN_ADD_EDITOR":
      return { ...state, editorMode: { type: "add" } };

    case "OPEN_EDIT_EDITOR":
      return {
        ...state,
        editorMode: {
          type: "edit",
          widgetId: action.payload.widgetId,
          widget: action.payload.widget,
        },
      };

    case "CLOSE_EDITOR":
      return { ...state, editorMode: null };

    default:
      return state;
  }
}

// ============================================================================
// Hook Options
// ============================================================================

export type UseDashboardEditorOptions = {
  /** Initial dashboard layout data from the server */
  initialData: DashboardLayout;
  /** URL for widget actions (add, update, delete, duplicate) */
  widgetActionUrl: string;
  /** URL for layout updates. If empty or not provided, uses current page URL. */
  layoutActionUrl?: string;
  /** Maximum number of widgets allowed per dashboard. If not provided, no limit is enforced. */
  widgetLimit?: number;
  /** Callback when a sync error occurs */
  onSyncError?: (error: Error, action: string) => void;
  /** Callback when a widget action is blocked by the limit */
  onWidgetLimitReached?: () => void;
};

// ============================================================================
// Sync Queue Types
// ============================================================================

type WidgetSyncTask = {
  type: "widget";
  action: string;
  data: Record<string, string>;
};

type LayoutSyncTask = {
  type: "layout";
  layout: LayoutItem[];
};

type SyncTask = WidgetSyncTask | LayoutSyncTask;

// ============================================================================
// Hook
// ============================================================================

export function useDashboardEditor({
  initialData,
  widgetActionUrl,
  layoutActionUrl,
  widgetLimit,
  onSyncError,
  onWidgetLimitReached,
}: UseDashboardEditorOptions) {
  const [state, dispatch] = useReducer(dashboardReducer, {
    layout: initialData.layout,
    widgets: initialData.widgets,
    editorMode: null,
  });

  // Refs for debouncing and tracking initialization
  const layoutDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const isInitializedRef = useRef(false);
  const currentLayoutJsonRef = useRef<string>(JSON.stringify(initialData.layout));

  // Sync queue to prevent race conditions
  const syncQueueRef = useRef<SyncTask[]>([]);
  const isSyncingRef = useRef(false);

  // Reset state when initialData changes (e.g., navigating to different dashboard)
  const initialDataJson = JSON.stringify({
    layout: initialData.layout,
    widgets: initialData.widgets,
  });
  useEffect(() => {
    // Cancel any pending layout save
    if (layoutDebounceRef.current) {
      clearTimeout(layoutDebounceRef.current);
      layoutDebounceRef.current = null;
    }

    // Clear the sync queue when switching dashboards
    syncQueueRef.current = [];

    // Reset state to new initial data
    dispatch({
      type: "RESET_STATE",
      payload: { layout: initialData.layout, widgets: initialData.widgets },
    });

    // Update refs
    currentLayoutJsonRef.current = JSON.stringify(initialData.layout);
    isInitializedRef.current = false;

    // Allow saves after a short delay to skip initial mount callbacks
    const initTimeout = setTimeout(() => {
      isInitializedRef.current = true;
    }, 100);

    return () => {
      clearTimeout(initTimeout);
      if (layoutDebounceRef.current) {
        clearTimeout(layoutDebounceRef.current);
      }
    };
  }, [initialDataJson]);

  // -------------------------------------------------------------------------
  // Sync queue processor - ensures only one sync runs at a time
  // -------------------------------------------------------------------------

  const processNextSync = useCallback(async () => {
    // If already syncing or queue is empty, do nothing
    if (isSyncingRef.current || syncQueueRef.current.length === 0) {
      return;
    }

    isSyncingRef.current = true;
    const task = syncQueueRef.current.shift()!;

    try {
      if (task.type === "widget") {
        const formData = new FormData();
        formData.set("action", task.action);
        Object.entries(task.data).forEach(([k, v]) => formData.set(k, v));

        const response = await fetch(widgetActionUrl, {
          method: "POST",
          body: formData,
        });

        if (!response.ok) {
          const errorText = await response.text();
          throw new Error(`Failed to ${task.action} widget: ${errorText}`);
        }
      } else if (task.type === "layout") {
        const formData = new FormData();
        formData.set("action", "layout");
        formData.set("layout", JSON.stringify(task.layout));

        // Use current page URL if layoutActionUrl is not provided
        const url = layoutActionUrl || window.location.pathname;

        const response = await fetch(url, {
          method: "POST",
          body: formData,
        });

        if (!response.ok) {
          const errorText = await response.text();
          throw new Error("Failed to update layout: " + errorText);
        }
      }
    } catch (error) {
      console.error(`Dashboard sync error:`, error);
      const actionName = task.type === "widget" ? task.action : "layout";
      onSyncError?.(error instanceof Error ? error : new Error(String(error)), actionName);
    } finally {
      isSyncingRef.current = false;
      // Process next item in queue
      processNextSync();
    }
  }, [widgetActionUrl, layoutActionUrl, onSyncError]);

  // -------------------------------------------------------------------------
  // Queue helpers
  // -------------------------------------------------------------------------

  const queueWidgetSync = useCallback(
    (action: string, data: Record<string, string>) => {
      syncQueueRef.current.push({ type: "widget", action, data });
      processNextSync();
    },
    [processNextSync]
  );

  const queueLayoutSync = useCallback(
    (layout: LayoutItem[]) => {
      // For layout syncs, we only care about the latest state
      // Remove any pending layout syncs and add the new one
      syncQueueRef.current = syncQueueRef.current.filter((task) => task.type !== "layout");
      syncQueueRef.current.push({ type: "layout", layout });
      processNextSync();
    },
    [processNextSync]
  );

  // -------------------------------------------------------------------------
  // Action handlers
  // -------------------------------------------------------------------------

  // Count only non-title widgets for limit checks (title widgets are free)
  const countedWidgets = Object.values(state.widgets).filter(
    (w) => w.display.type !== "title"
  ).length;

  const addWidget = useCallback(
    (title: string, query: string, config: QueryWidgetConfig) => {
      // Guard: check widget limit (title widgets don't count)
      if (widgetLimit !== undefined && countedWidgets >= widgetLimit) {
        onWidgetLimitReached?.();
        return;
      }

      const id = nanoid(8);
      const maxBottom = Math.max(0, ...state.layout.map((l) => l.y + l.h));
      const layoutItem: LayoutItem = { i: id, x: 0, y: maxBottom, w: 12, h: 15 };
      const widget: Widget = { title, query, display: config };

      // Update local state immediately
      dispatch({ type: "ADD_WIDGET", payload: { id, widget, layoutItem } });

      // Queue sync to server (processed sequentially)
      // Send the client-generated ID so the server uses the same ID
      queueWidgetSync("add", {
        widgetId: id,
        title,
        query,
        config: JSON.stringify(config),
      });
    },
    [state.layout, countedWidgets, widgetLimit, onWidgetLimitReached, queueWidgetSync]
  );

  const addTitleWidget = useCallback(
    (title: string) => {
      const id = nanoid(8);
      const maxBottom = Math.max(0, ...state.layout.map((l) => l.y + l.h));
      // Title widgets are fixed at h=2 and full width
      const layoutItem: LayoutItem = { i: id, x: 0, y: maxBottom, w: 12, h: 2 };
      const config: QueryWidgetConfig = { type: "title" };
      const widget: Widget = { title, query: "", display: config };

      // Update local state immediately
      dispatch({ type: "ADD_WIDGET", payload: { id, widget, layoutItem } });

      // Queue sync to server (processed sequentially)
      // Send the client-generated ID so the server uses the same ID
      queueWidgetSync("add", {
        widgetId: id,
        title,
        query: "",
        config: JSON.stringify(config),
      });
    },
    [state.layout, queueWidgetSync]
  );

  const updateWidget = useCallback(
    (widgetId: string, title: string, query: string, config: QueryWidgetConfig) => {
      const widget: Widget = { title, query, display: config };

      // Update local state immediately
      dispatch({ type: "UPDATE_WIDGET", payload: { id: widgetId, widget } });

      // Queue sync to server (processed sequentially)
      queueWidgetSync("update", {
        widgetId,
        title,
        query,
        config: JSON.stringify(config),
      });
    },
    [queueWidgetSync]
  );

  const deleteWidget = useCallback(
    (widgetId: string) => {
      // Update local state immediately
      dispatch({ type: "DELETE_WIDGET", payload: { id: widgetId } });

      // Queue sync to server (processed sequentially)
      queueWidgetSync("delete", { widgetId });
    },
    [queueWidgetSync]
  );

  const duplicateWidget = useCallback(
    (widgetId: string) => {
      // Guard: check widget limit (title widgets don't count)
      if (widgetLimit !== undefined && countedWidgets >= widgetLimit) {
        onWidgetLimitReached?.();
        return;
      }

      const newId = nanoid(8);

      // Update local state immediately
      dispatch({ type: "DUPLICATE_WIDGET", payload: { id: widgetId, newId } });

      // Queue sync to server (processed sequentially)
      // Send the client-generated newId so the server uses the same ID for the duplicate
      queueWidgetSync("duplicate", { widgetId, newId });
    },
    [countedWidgets, widgetLimit, onWidgetLimitReached, queueWidgetSync]
  );

  const renameWidget = useCallback(
    (widgetId: string, title: string) => {
      // Update local state immediately
      dispatch({ type: "RENAME_WIDGET", payload: { id: widgetId, title } });

      // Queue sync to server (processed sequentially)
      queueWidgetSync("rename", { widgetId, title });
    },
    [queueWidgetSync]
  );

  const updateLayout = useCallback(
    (newLayout: LayoutItem[]) => {
      // Skip if not yet initialized (prevents saving during mount/navigation)
      if (!isInitializedRef.current) {
        return;
      }

      const newLayoutJson = JSON.stringify(newLayout);

      // Skip if layout hasn't actually changed
      if (newLayoutJson === currentLayoutJsonRef.current) {
        return;
      }

      // Update local state immediately
      dispatch({ type: "UPDATE_LAYOUT", payload: { layout: newLayout } });

      // Clear existing debounce timeout
      if (layoutDebounceRef.current) {
        clearTimeout(layoutDebounceRef.current);
      }

      // Debounce before queueing - this ensures rapid layout changes
      // (like dragging) don't queue up many requests
      layoutDebounceRef.current = setTimeout(() => {
        currentLayoutJsonRef.current = newLayoutJson;
        // Queue layout sync (replaces any pending layout sync in queue)
        queueLayoutSync(newLayout);
      }, 500);
    },
    [queueLayoutSync]
  );

  const openAddEditor = useCallback(() => {
    dispatch({ type: "OPEN_ADD_EDITOR" });
  }, []);

  const openEditEditor = useCallback((widgetId: string, widget: WidgetData) => {
    dispatch({ type: "OPEN_EDIT_EDITOR", payload: { widgetId, widget } });
  }, []);

  const closeEditor = useCallback(() => {
    dispatch({ type: "CLOSE_EDITOR" });
  }, []);

  // -------------------------------------------------------------------------
  // Return value
  // -------------------------------------------------------------------------

  return {
    /** Current dashboard state */
    state,
    /** Action dispatchers */
    actions: {
      addWidget,
      addTitleWidget,
      updateWidget,
      renameWidget,
      deleteWidget,
      duplicateWidget,
      updateLayout,
      openAddEditor,
      openEditEditor,
      closeEditor,
    },
  };
}
