• Sync page component for the Kenmei to AniList sync tool.

    Handles synchronization preview, configuration, execution, and results display for the user.

    Returns Element

    export function SyncPage() {
    const navigate = useNavigate();
    const { authState, isOnline } = useAuthState();
    const token = authState.accessToken || "";
    const [state, actions] = useSynchronization();
    const {
    failedOperations,
    isLoadingFailedOps,
    retryFailedOperation,
    retryAllFailedOperations,
    clearFailedOperation,
    } = actions;
    const [viewMode, setViewMode] = useState<ViewMode>("preview");
    const { rateLimitState, setRateLimit } = useRateLimit();
    const { completeStep } = useOnboarding();

    // Retry state
    const [retryingOperations, setRetryingOperations] = useState<Set<string>>(
    new Set(),
    );

    // Authentication and validation states
    const [authError, setAuthError] = useState(false);
    const [matchDataError, setMatchDataError] = useState(false);
    const [validMatchesError, setValidMatchesError] = useState(false);

    // Authentication and data validation check
    useEffect(() => {
    // Check if user is authenticated
    if (!authState.isAuthenticated || !token) {
    console.debug("[SyncPage] User not authenticated, showing auth error");
    setAuthError(true);
    return;
    } else {
    setAuthError(false);
    }

    // Check if there are match results to sync
    const savedResults = getSavedMatchResults();
    if (
    !savedResults ||
    !Array.isArray(savedResults) ||
    savedResults.length === 0
    ) {
    console.debug(
    "[SyncPage] No match results found, showing match data error",
    );
    setMatchDataError(true);
    return;
    } else {
    setMatchDataError(false);
    }

    // Validate that there are actual matches (not just skipped entries)
    const validMatches = savedResults.filter(
    (match) => match.status === "matched" || match.status === "manual",
    );

    if (validMatches.length === 0) {
    console.debug(
    "[SyncPage] No valid matches found, showing valid matches error",
    );
    setValidMatchesError(true);
    return;
    } else {
    setValidMatchesError(false);
    }

    console.debug(
    `[SyncPage] Found ${validMatches.length} valid matches for synchronization`,
    );
    }, [authState.isAuthenticated, token]);

    // Sync configuration options
    const [syncConfig, setSyncConfig] = useState<SyncConfig>(getSyncConfig());
    // Track if we're using a custom threshold
    const [isCustomThresholdEnabled, setIsCustomThresholdEnabled] =
    useState<boolean>(
    ![1, 7, 14, 30, 60, 90, 180, 365].includes(syncConfig.autoPauseThreshold),
    );

    /**
    * Toggles a sync configuration option and persists the updated config to storage.
    * @param option - The sync configuration option key to toggle.
    * @source
    */
    const handleToggleOption = (option: keyof SyncConfig) => {
    setSyncConfig((prev) => {
    const newConfig = {
    ...prev,
    [option]: !prev[option],
    };

    // Save the updated config to storage
    saveSyncConfig(newConfig);

    return newConfig;
    });
    };

    /**
    * Refreshes the user's AniList library from the server.
    * Delegates to utility function with appropriate state handlers.
    * @source
    */
    const handleLibraryRefresh = () => {
    handleLibraryRefreshUtil({
    token,
    setLibraryLoading,
    setLibraryError,
    setRetryCount,
    setRateLimit,
    setUserLibrary,
    });
    };

    // View mode for displaying manga entries
    const [displayMode, setDisplayMode] = useState<DisplayMode>("cards");

    // State to hold manga matches
    const [mangaMatches, setMangaMatches] = useState<MangaMatchResult[]>([]);

    // Pagination and loading state
    const [visibleItems, setVisibleItems] = useState(20);
    const [isLoadingMore, setIsLoadingMore] = useState(false);
    // State to hold user's AniList library
    const [userLibrary, setUserLibrary] = useState<UserMediaList>({});
    const [libraryLoading, setLibraryLoading] = useState(false);
    const [libraryError, setLibraryError] = useState<string | null>(null);
    const [retryCount, setRetryCount] = useState(0);
    const maxRetries = 3;

    // Track initial manga load to show skeletons
    const [isInitialMangaLoad, setIsInitialMangaLoad] = useState(true);

    // Sorting and filtering options
    const [sortOption, setSortOption] = useState<SortOption>({
    field: "title",
    direction: "asc",
    });

    const [filters, setFilters] = useState<FilterOptions>({
    status: "all", // 'all', 'reading', 'completed', 'planned', 'paused', 'dropped'
    changes: "with-changes", // 'all', 'with-changes', 'no-changes'
    library: "all", // 'all', 'new', 'existing'
    });

    // Load manga matches from the app's storage system
    useEffect(() => {
    const savedResults = getSavedMatchResults();
    if (savedResults && Array.isArray(savedResults)) {
    console.debug(
    `[SyncPage] Loaded ${savedResults.length} match results from storage`,
    );
    setMangaMatches(savedResults as MangaMatchResult[]);
    } else {
    captureError(
    ErrorType.STORAGE,
    "No match results found in storage",
    new Error("No match results"),
    {},
    );
    }
    setIsInitialMangaLoad(false);
    }, []);

    // Auto-retry failed operations when app comes online
    const isAutoRetryInProgressRef = useRef(false);

    useEffect(() => {
    const handleAppOnline = async () => {
    // Gate: skip if auto-retry is already in progress
    if (isAutoRetryInProgressRef.current) {
    console.debug(
    "[SyncPage] Auto-retry already in progress, skipping duplicate trigger",
    );
    return;
    }

    // Bail if sync is already active to avoid competing with active sync
    if (state.isActive) {
    console.info(
    `[SyncPage] App came online, but sync is already active. Skipping auto-retry.`,
    );
    return;
    }

    if (failedOperations.length > 0) {
    console.info(
    `[SyncPage] App came online with ${failedOperations.length} failed operations, attempting retry`,
    );

    // Set gate to prevent concurrent retries during flapping connectivity
    isAutoRetryInProgressRef.current = true;

    try {
    // Small delay to allow for other reconnection handlers to complete
    await new Promise((resolve) => setTimeout(resolve, 500));
    const succeeded = await retryAllFailedOperations();
    console.info(
    `[SyncPage] Retried failed operations: ${succeeded} succeeded`,
    );
    } catch (err) {
    captureError(
    ErrorType.NETWORK,
    "Error during auto-retry on reconnect",
    err instanceof Error ? err : new Error(String(err)),
    {},
    );
    } finally {
    // Reset gate after completion
    isAutoRetryInProgressRef.current = false;
    }
    }
    };

    globalThis.addEventListener("app:online", handleAppOnline);
    return () => {
    globalThis.removeEventListener("app:online", handleAppOnline);
    };
    }, [state.isActive, failedOperations.length, retryAllFailedOperations]);

    // Handle rate limit errors
    type ApiError = {
    isRateLimited?: boolean;
    status?: number;
    retryAfter?: number;
    message?: string;
    name?: string;
    };

    /**
    * Handles rate limit errors from the AniList API with scheduled retry.
    * @param error - The API error containing rate limit information.
    * @param controller - AbortController to manage request cancellation.
    * @param fetchLibrary - Callback to retry fetching the library.
    * @returns Cleanup function to clear timeout if needed.
    * @source
    */
    const handleRateLimitError = (
    error: ApiError,
    controller: AbortController,
    fetchLibrary: (attempt: number) => void,
    ) => {
    const err = error;

    console.warn("[SyncPage] 📛 DETECTED RATE LIMIT:", {
    isRateLimited: err.isRateLimited,
    status: err.status,
    retryAfter: err.retryAfter,
    });

    const retryDelay = err.retryAfter ? err.retryAfter : 60;

    setRateLimit(
    true,
    retryDelay,
    "AniList API rate limit reached. Waiting to retry...",
    );
    setLibraryLoading(false);
    setLibraryError("AniList API rate limit reached. Waiting to retry...");

    const timer = setTimeout(() => {
    if (!controller.signal.aborted) {
    console.info("[SyncPage] Rate limit timeout complete, retrying...");
    setLibraryLoading(true);
    setLibraryError(null);
    fetchLibrary(0);
    }
    }, retryDelay * 1000);

    return () => clearTimeout(timer);
    };

    /**
    * Handles server errors with exponential backoff retry logic.
    * @param error - The API error to handle.
    * @param attempt - Current retry attempt number.
    * @param controller - AbortController to manage request cancellation.
    * @param fetchLibrary - Callback to retry fetching the library.
    * @returns Cleanup function if retry is scheduled, false otherwise.
    * @source
    */
    const handleServerError = (
    error: ApiError,
    attempt: number,
    controller: AbortController,
    fetchLibrary: (attempt: number) => void,
    ) => {
    const err = error;

    const message = err.message || "";
    const isServerError =
    message.includes("500") ||
    message.includes("502") ||
    message.includes("503") ||
    message.includes("504") ||
    message.toLowerCase().includes("network error");

    if (!isServerError || attempt >= maxRetries) {
    return false;
    }

    const backoffDelay = Math.pow(2, attempt) * 1000;
    setLibraryError(
    `AniList server error. Retrying in ${backoffDelay / 1000} seconds (${attempt + 1}/${maxRetries})...`,
    );

    const timer = setTimeout(() => {
    if (!controller.signal.aborted) {
    fetchLibrary(attempt + 1);
    }
    }, backoffDelay);

    return () => clearTimeout(timer);
    };

    /**
    * Handles successful library data fetch from AniList API.
    * @param library - The user's AniList media list.
    * @source
    */
    const handleFetchSuccess = (library: UserMediaList) => {
    console.info(
    `[SyncPage] Loaded ${Object.keys(library).length} entries from user's AniList library`,
    );
    setUserLibrary(library);
    setLibraryLoading(false);
    setLibraryError(null);
    setRetryCount(0);
    };

    /**
    * Handles errors from library fetch with appropriate retry logic.
    * Delegates to specific error handlers for rate limit and server errors.
    * @param error - The API error from library fetch.
    * @param attempt - Current retry attempt number.
    * @param controller - AbortController to manage request cancellation.
    * @param fetchLibrary - Callback to retry fetching the library.
    * @source
    */
    const handleFetchError = (
    error: ApiError,
    attempt: number,
    controller: AbortController,
    fetchLibrary: (attempt: number) => void,
    ) => {
    const err = error;
    if (err.name === "AbortError") return;

    captureError(
    ErrorType.NETWORK,
    "Failed to load user library",
    err instanceof Error ? err : new Error(String(err)),
    { token: !!token },
    );
    console.debug(
    "[SyncPage] Error object structure:",
    JSON.stringify(err, null, 2),
    );

    // Check for rate limiting
    if (err.isRateLimited || err.status === 429) {
    return handleRateLimitError(err, controller, fetchLibrary);
    }

    // Check for server error
    const serverErrorResult = handleServerError(
    err,
    attempt,
    controller,
    fetchLibrary,
    );
    if (serverErrorResult) {
    return serverErrorResult;
    }

    // Default error handling
    const errorMessage =
    err.message ||
    "Failed to load your AniList library. Synchronization can still proceed, but comparison data will not be shown.";

    setLibraryError(errorMessage);
    setUserLibrary({});
    setLibraryLoading(false);

    // Show recovery notification for library fetch failures
    showErrorNotification(
    createError(
    ErrorType.NETWORK,
    "Failed to load AniList library",
    err instanceof Error ? err : new Error(String(err)),
    "LIBRARY_FETCH_FAILED",
    ErrorRecoveryAction.RETRY,
    "Your sync can still proceed, but comparison data won't be shown. Click retry to load your library.",
    ),
    {
    onRetry: () => fetchLibrary(0),
    duration: 8000,
    },
    );
    };

    // Fetch the user's AniList library for comparison
    useEffect(() => {
    if (token && mangaMatches.length > 0) {
    setLibraryLoading(true);
    setLibraryError(null);

    const controller = new AbortController();

    const fetchLibrary = (attempt = 0) => {
    console.debug(
    `[SyncPage] Fetching AniList library (attempt ${attempt + 1}/${maxRetries + 1})`,
    );
    setRetryCount(attempt);

    getUserMangaList(token, controller.signal)
    .then(handleFetchSuccess)
    .catch((error) => {
    handleFetchError(error, attempt, controller, fetchLibrary);
    });
    };

    fetchLibrary(0);

    return () => controller.abort();
    }
    }, [token, mangaMatches, maxRetries, setRateLimit]);

    // Reset visible items when changing display mode
    useEffect(() => {
    setVisibleItems(20);
    }, [displayMode]);

    // Apply filters to manga matches
    const filteredMangaMatches = useMemo(() => {
    return filterMangaMatches(mangaMatches, filters, userLibrary, syncConfig);
    }, [mangaMatches, filters, userLibrary, syncConfig]);

    // Apply sorting to filtered manga matches
    const sortedMangaMatches = useMemo(() => {
    return sortMangaMatches(
    filteredMangaMatches,
    sortOption,
    userLibrary,
    syncConfig,
    );
    }, [filteredMangaMatches, sortOption, userLibrary, syncConfig]);

    // Compute all entries to sync (unfiltered, all with changes)
    const allEntriesToSync = useMemo(() => {
    return prepareAllEntriesToSync(mangaMatches, userLibrary, syncConfig);
    }, [mangaMatches, userLibrary, syncConfig]);

    // Only sync entries with actual changes
    const entriesWithChanges = useMemo(
    () => allEntriesToSync.filter((entry) => hasChanges(entry, syncConfig)),
    [allEntriesToSync, syncConfig],
    );

    const totalMatchedManga = useMemo(
    () => mangaMatches.filter((match) => match.status !== "skipped").length,
    [mangaMatches],
    );

    const newEntriesCount = useMemo(
    () =>
    mangaMatches.filter(
    (match) => match.selectedMatch && !userLibrary[match.selectedMatch.id],
    ).length,
    [mangaMatches, userLibrary],
    );

    const manualMatchesCount = useMemo(
    () => mangaMatches.filter((match) => match.status === "manual").length,
    [mangaMatches],
    );

    const queuedPercentage = useMemo(() => {
    if (totalMatchedManga === 0) {
    return 0;
    }

    return Math.round((entriesWithChanges.length / totalMatchedManga) * 100);
    }, [entriesWithChanges.length, totalMatchedManga]);

    const heroStats = useMemo(
    () => [
    {
    label: "Ready to sync",
    value: entriesWithChanges.length,
    helper: "entries queued",
    icon: CheckCircle2,
    accent:
    "from-emerald-400/80 via-emerald-400/10 to-transparent dark:from-emerald-500/60 dark:via-emerald-500/5",
    },
    {
    label: "Total matches",
    value: totalMatchedManga,
    helper: `${queuedPercentage}% prepared`,
    icon: Layers,
    accent:
    "from-sky-400/70 via-sky-400/10 to-transparent dark:from-sky-500/50 dark:via-sky-500/5",
    },
    {
    label: "New additions",
    value: newEntriesCount,
    helper: "not yet in AniList",
    icon: UserPlus,
    accent:
    "from-purple-400/80 via-purple-400/10 to-transparent dark:from-purple-500/60 dark:via-purple-500/5",
    },
    ],
    [
    entriesWithChanges.length,
    totalMatchedManga,
    newEntriesCount,
    manualMatchesCount,
    queuedPercentage,
    ],
    );

    /**
    * Initiates sync view transition when entries with changes are ready.
    * @source
    */
    const handleStartSync = () => {
    if (entriesWithChanges.length === 0) {
    return;
    }
    setViewMode("sync");
    };

    /**
    * Generates button title text based on current sync state.
    * @returns Descriptive title or undefined if sync can proceed.
    * @source
    */
    const getStartSyncButtonTitle = () => {
    if (isOnline === false) {
    return "Cannot sync while offline";
    }
    if (state.resumeAvailable) {
    return "Resume or discard the interrupted sync first";
    }
    return undefined;
    };

    /**
    * Handles completion of the sync process and transitions to results view.
    * @source
    */
    const handleSyncComplete = () => {
    completeStep("sync");
    setViewMode("results");
    };

    // Handle sync cancellation
    const [wasCancelled, setWasCancelled] = useState(false);

    /**
    * Handles sync cancellation or navigation back to review.
    * Cancels active sync if in progress, otherwise returns to review page.
    * @source
    */
    const handleCancel = () => {
    if (viewMode === "sync") {
    // If sync has not started, go back to preview
    if (!state.isActive && !state.report) {
    setViewMode("preview");
    return;
    }
    actions.cancelSync();
    setWasCancelled(true);
    setViewMode("results");
    return;
    }
    // Navigate back to the matching page
    navigate({ to: "/review" });
    };

    /**
    * Navigates home after viewing sync results, resetting sync state.
    * @source
    */
    const handleGoHome = () => {
    actions.reset();
    setWasCancelled(false);
    navigate({ to: "/" });
    };

    /**
    * Refreshes the user's AniList library from the server.
    * @source
    */
    const refreshUserLibrary = () => {
    refreshUserLibraryUtil({
    token,
    setLibraryLoading,
    setLibraryError,
    setRetryCount,
    setRateLimit,
    setUserLibrary,
    });
    };

    /**
    * Navigates back to the matching review page after sync completion.
    * @source
    */
    const handleBackToReview = () => {
    actions.reset();
    setWasCancelled(false);
    refreshUserLibrary();
    setViewMode("preview");
    };

    /**
    * Resumes sync from the last checkpoint.
    * @source
    */
    const handleResumeSync = () => {
    if (!state.resumeAvailable) {
    console.warn("[SyncPage] ⚠️ No resume checkpoint available");
    return;
    }

    const remainingCount = state.resumeMetadata?.remainingMediaIds.length || 0;
    console.info(
    `[SyncPage] ▶️ Resuming sync from checkpoint (${remainingCount} entries remaining)`,
    );

    actions.resumeSync(allEntriesToSync, token);
    setViewMode("sync");
    };

    /**
    * Discards the sync checkpoint and clears resume state.
    * @source
    */
    const handleDiscardCheckpoint = () => {
    console.info("[SyncPage] 🗑️ Discarding sync checkpoint");
    actions.reset();
    // Show toast notification (if toast system is available)
    console.log("Sync checkpoint discarded. Starting fresh.");
    };

    /**
    * Checks if an error message indicates a logical conflict.
    * @param errorMsg - The error message to check.
    * @returns True if the error indicates a conflict.
    * @source
    */
    const isConflictError = (errorMsg: string): boolean => {
    const normalized = errorMsg.toLowerCase();
    return (
    normalized.includes("conflict") ||
    normalized.includes("differs") ||
    normalized.includes("outdated") ||
    normalized.includes("mismatch")
    );
    };

    /**
    * Shows appropriate error notification based on conflict detection.
    * @param operationId - Optional operation ID for retry callback.
    * @param error - The error that occurred.
    * @source
    */
    const showSyncRetryError = (
    operationId: string | null,
    error: unknown,
    ): void => {
    const errorMsg = error instanceof Error ? error.message : String(error);
    const isConflict = isConflictError(errorMsg);

    showErrorNotification(
    createError(
    isConflict ? ErrorType.SERVER : ErrorType.NETWORK,
    isConflict
    ? "This entry conflicts with AniList's current state"
    : "Failed to retry sync operation",
    error instanceof Error ? error : new Error(String(error)),
    isConflict ? "SYNC_CONFLICT" : "SYNC_RETRY_FAILED",
    isConflict ? ErrorRecoveryAction.NONE : ErrorRecoveryAction.RETRY,
    isConflict
    ? "The entry has changed on AniList since this sync started. Open the item to review the current state and resolve the conflict."
    : "The operation could not be retried. You can try again or skip this entry.",
    ),
    {
    ...(isConflict
    ? { duration: 8000 }
    : {
    onRetry: operationId
    ? () => handleRetryOperation(operationId)
    : handleRetryAll,
    duration: 6000,
    }),
    },
    );
    };

    /**
    * Handles retrying a single failed sync operation.
    * Updates retry state and calls the retry handler.
    * Detects logical conflicts and displays conflict-specific recovery guidance.
    * @param operationId - The ID of the failed operation to retry.
    * @source
    */
    const handleRetryOperation = async (operationId: string) => {
    setRetryingOperations((prev) => new Set([...prev, operationId]));
    try {
    const success = await retryFailedOperation(operationId);
    if (success) {
    console.log("Operation succeeded");
    } else {
    console.log("Operation still failed");

    // Get the failed operation to check error details for conflicts
    const failedOp = failedOperations.find((op) => op.id === operationId);
    const errorMsg = failedOp?.error || "";

    if (isConflictError(errorMsg)) {
    // Show conflict-specific error with NONE action (no auto-retry)
    showErrorNotification(
    createError(
    ErrorType.UNKNOWN,
    "This entry conflicts with AniList's current state",
    new Error("Sync conflict detected"),
    "SYNC_CONFLICT",
    ErrorRecoveryAction.NONE,
    "The entry has changed on AniList since this sync started. Open the item to review the current state and resolve the conflict.",
    ),
    { duration: 8000 },
    );
    } else {
    // Show error notification for general failed retry
    showErrorNotification(
    createError(
    ErrorType.UNKNOWN,
    "Failed to retry sync operation",
    new Error("Retry operation failed"),
    "SYNC_RETRY_FAILED",
    ErrorRecoveryAction.RETRY,
    "The operation could not be retried. You can try again or skip this entry.",
    ),
    {
    onRetry: () => handleRetryOperation(operationId),
    duration: 6000,
    },
    );
    }
    }
    } catch (error) {
    const errorMsg = error instanceof Error ? error.message : String(error);
    captureError(
    ErrorType.UNKNOWN,
    isConflictError(errorMsg)
    ? "Sync conflict detected"
    : "Error retrying operation",
    error instanceof Error ? error : new Error(String(error)),
    { operationId },
    );
    showSyncRetryError(operationId, error);
    } finally {
    setRetryingOperations((prev) => {
    const next = new Set(prev);
    next.delete(operationId);
    return next;
    });
    }
    };

    /**
    * Shows appropriate error notification for batch retry based on conflict detection.
    * @param error - The error that occurred.
    * @source
    */
    const showBatchRetryError = (error: unknown): void => {
    const errorMsg = error instanceof Error ? error.message : String(error);
    const isConflict = isConflictError(errorMsg);

    showErrorNotification(
    createError(
    isConflict ? ErrorType.SERVER : ErrorType.NETWORK,
    isConflict
    ? "Some entries conflict with AniList's current state"
    : "Failed to retry all operations",
    error instanceof Error ? error : new Error(String(error)),
    isConflict ? "SYNC_BATCH_CONFLICT" : "SYNC_RETRY_ALL_FAILED",
    isConflict ? ErrorRecoveryAction.NONE : ErrorRecoveryAction.RETRY,
    isConflict
    ? "Some entries have changed on AniList. Review the failed operations individually to resolve conflicts."
    : "Some operations could not be retried. Check your connection and try again.",
    ),
    {
    ...(isConflict
    ? { duration: 8000 }
    : { onRetry: handleRetryAll, duration: 6000 }),
    },
    );
    };

    /**
    * Handles retrying all failed sync operations at once.
    * Detects conflicts and displays appropriate recovery messaging.
    * @source
    */
    const handleRetryAll = async () => {
    try {
    const succeeded = await retryAllFailedOperations();
    console.log(`Retried all operations: ${succeeded} succeeded`);
    } catch (error) {
    const errorMsg = error instanceof Error ? error.message : String(error);
    captureError(
    ErrorType.UNKNOWN,
    isConflictError(errorMsg)
    ? "Sync conflicts detected"
    : "Error retrying all operations",
    error instanceof Error ? error : new Error(String(error)),
    );
    showBatchRetryError(error);
    }
    };

    /**
    * Handles clearing a single failed operation from the retry queue.
    * @param operationId - The ID of the operation to clear.
    * @source
    */
    const handleClearOperation = (operationId: string) => {
    clearFailedOperation(operationId);
    };

    /**
    * Handles sync reset from error boundary.
    * @source
    */
    const handleSyncReset = () => {
    actions.reset();
    setViewMode("preview");
    setRetryingOperations(new Set());
    };

    /**
    * Handles retrying failed operations from error boundary.
    * @source
    */
    const handleRetryFailedFromBoundary = async () => {
    try {
    await handleRetryAll();
    } catch (error) {
    captureError(
    ErrorType.UNKNOWN,
    "Failed to retry operations from error boundary",
    error instanceof Error ? error : new Error(String(error)),
    {},
    );
    }
    };

    /**
    * Handles canceling sync from error boundary.
    * @source
    */
    const handleCancelSyncFromBoundary = () => {
    actions.cancelSync();
    setViewMode("preview");
    };

    /**
    * Renders a badge indicating manga status change from Kenmei to AniList.
    * @param statusWillChange - Whether the status will be updated.
    * @param userEntry - Current AniList entry for the manga.
    * @param kenmei - Kenmei manga data with status.
    * @param syncConfig - Sync configuration options.
    * @returns JSX badge element or null if no change.
    * @source
    */
    const renderStatusBadge = (
    statusWillChange: boolean,
    userEntry: UserMediaEntry | undefined,
    kenmei: KenmeiManga,
    syncConfig: SyncConfig,
    ) => {
    if (!statusWillChange) return null;

    const fromStatus = userEntry?.status || "None";
    const toStatus = getEffectiveStatus(kenmei, syncConfig);

    if (fromStatus === toStatus) return null;

    return (
    <Badge
    variant="outline"
    className="border-blue-400/70 bg-blue-50/60 px-2 py-0 text-[10px] text-blue-600 shadow-sm dark:border-blue-500/40 dark:bg-blue-900/40 dark:text-blue-300"
    >
    {fromStatus} → {toStatus}
    </Badge>
    );
    };

    /**
    * Renders a badge showing progress (chapters) changes from Kenmei to AniList.
    * @param progressWillChange - Whether progress will be updated.
    * @param userEntry - Current AniList entry for the manga.
    * @param kenmei - Kenmei manga data with chapter count.
    * @param syncConfig - Sync configuration options.
    * @returns JSX badge element or null if no change.
    * @source
    */
    const renderProgressBadge = (
    progressWillChange: boolean,
    userEntry: UserMediaEntry | undefined,
    kenmei: KenmeiManga,
    syncConfig: SyncConfig,
    ) => {
    if (!progressWillChange) return null;

    const fromProgress = userEntry?.progress || 0;
    let toProgress: number;

    if (syncConfig.prioritizeAniListProgress) {
    if (userEntry?.progress && userEntry.progress > 0) {
    toProgress = Math.max(
    kenmei.chaptersRead || 0,
    userEntry.progress || 0,
    );
    } else {
    toProgress = kenmei.chaptersRead || 0;
    }
    } else {
    toProgress = kenmei.chaptersRead || 0;
    }

    if (fromProgress === toProgress) return null;

    return (
    <Badge
    variant="outline"
    className="border-green-400/70 bg-green-50/60 px-2 py-0 text-[10px] text-green-600 shadow-sm dark:border-green-500/40 dark:bg-green-900/30 dark:text-green-300"
    >
    {fromProgress} → {toProgress} ch
    </Badge>
    );
    };

    /**
    * Renders a badge showing score/rating changes from Kenmei to AniList.
    * @param scoreWillChange - Whether score will be updated.
    * @param userEntry - Current AniList entry for the manga.
    * @param kenmei - Kenmei manga data with score.
    * @returns JSX badge element or null if no change.
    * @source
    */
    const renderScoreBadge = (
    scoreWillChange: boolean,
    userEntry: UserMediaEntry | undefined,
    kenmei: KenmeiManga,
    ) => {
    if (!scoreWillChange) return null;

    const fromScore = userEntry?.score || 0;
    const toScore = kenmei.score || 0;

    if (fromScore === toScore) return null;

    return (
    <Badge
    variant="outline"
    className="border-amber-400/70 bg-amber-50/60 px-2 py-0 text-[10px] text-amber-600 shadow-sm dark:border-amber-500/40 dark:bg-amber-900/30 dark:text-amber-300"
    >
    {fromScore} → {toScore}/10
    </Badge>
    );
    };

    /**
    * Renders a badge indicating privacy setting changes for the manga entry.
    * @param userEntry - Current AniList entry for the manga.
    * @param syncConfig - Sync configuration options.
    * @returns JSX badge element or null if no privacy change.
    * @source
    */
    const renderPrivacyBadge = (
    userEntry: UserMediaEntry | undefined,
    syncConfig: SyncConfig,
    ) => {
    const shouldShowBadge = userEntry
    ? syncConfig.setPrivate && !userEntry.private
    : syncConfig.setPrivate;

    if (!shouldShowBadge) return null;

    return (
    <Badge
    variant="outline"
    className="border-purple-400/70 bg-purple-50/60 px-2 py-0 text-[10px] text-purple-600 shadow-sm dark:border-purple-500/40 dark:bg-purple-900/30 dark:text-purple-300"
    >
    {userEntry ? "Yes" : "No"}
    </Badge>
    );
    };

    /**
    * Renders privacy display text with styling reflecting privacy status and pending changes.
    * @param userEntry - Current AniList entry for the manga.
    * @param syncConfig - Sync configuration options.
    * @param isCurrentAniList - Whether displaying current AniList data.
    * @returns Formatted privacy display text.
    * @source
    */
    const renderPrivacyDisplay = (
    userEntry: UserMediaEntry | undefined,
    syncConfig: SyncConfig,
    isCurrentAniList: boolean,
    ) => {
    let privacyClass = "text-xs font-medium";
    const willChange = userEntry
    ? syncConfig.setPrivate && !userEntry.private
    : syncConfig.setPrivate;

    if (isCurrentAniList) {
    if (willChange) {
    privacyClass += " text-muted-foreground line-through";
    }
    return (
    <span className={privacyClass}>
    {userEntry?.private ? "Yes" : "No"}
    </span>
    );
    } else {
    if (willChange) {
    privacyClass += " text-blue-700 dark:text-blue-300";
    }
    const privacyDisplay =
    syncConfig.setPrivate || userEntry?.private ? "Yes" : "No";
    return <span className={privacyClass}>{privacyDisplay}</span>;
    }
    };

    /**
    * Renders progress display for after-sync state with current/total chapters.
    * @param userEntry - Current AniList entry for the manga.
    * @param kenmei - Kenmei manga data with chapter count.
    * @param anilist - AniList manga data with total chapters.
    * @param syncConfig - Sync configuration options.
    * @param progressWillChange - Whether progress will be updated.
    * @returns Formatted progress text.
    * @source
    */
    const renderAfterSyncProgress = (
    userEntry: UserMediaEntry | undefined,
    kenmei: KenmeiManga,
    anilist: AniListManga | undefined,
    syncConfig: SyncConfig,
    progressWillChange: boolean,
    ) => {
    let afterSyncProgress: number;
    if (syncConfig.prioritizeAniListProgress) {
    if (userEntry?.progress && userEntry.progress > 0) {
    afterSyncProgress = Math.max(
    kenmei.chaptersRead || 0,
    userEntry.progress || 0,
    );
    } else {
    afterSyncProgress = kenmei.chaptersRead || 0;
    }
    } else {
    afterSyncProgress = kenmei.chaptersRead || 0;
    }

    return (
    <span
    className={`text-xs font-medium ${
    progressWillChange ? "text-blue-700 dark:text-blue-300" : ""
    }`}
    >
    {afterSyncProgress} ch
    {anilist?.chapters ? ` / ${anilist.chapters}` : ""}
    </span>
    );
    };

    /**
    * Renders score display for after-sync state with highlighting for changes.
    * @param userEntry - Current AniList entry for the manga.
    * @param kenmei - Kenmei manga data with score.
    * @param scoreWillChange - Whether score will be updated.
    * @returns Formatted score text.
    * @source
    */
    const renderAfterSyncScore = (
    userEntry: UserMediaEntry | undefined,
    kenmei: KenmeiManga,
    scoreWillChange: boolean,
    ) => {
    let scoreDisplay: string;
    if (scoreWillChange) {
    scoreDisplay = kenmei.score ? `${kenmei.score}/10` : "None";
    } else {
    scoreDisplay = userEntry?.score ? `${userEntry.score}/10` : "None";
    }

    return (
    <span
    className={`text-xs font-medium ${
    scoreWillChange ? "text-blue-700 dark:text-blue-300" : ""
    }`}
    >
    {scoreDisplay}
    </span>
    );
    };

    /**
    * Renders manga cover image with status badges for new entries and completed items.
    * @param anilist - AniList manga data with cover image.
    * @param isNewEntry - Whether this is a new entry being added.
    * @param isCompleted - Whether this manga is marked as completed.
    * @returns JSX element with cover image and status badges.
    * @source
    */
    const renderMangaCover = (
    anilist: AniListManga | undefined,
    isNewEntry: boolean,
    isCompleted: boolean,
    ) => {
    return (
    <div className="relative flex h-[200px] w-full shrink-0 items-center justify-center bg-slate-50 pl-3 sm:w-auto sm:bg-transparent dark:bg-slate-900/50 sm:dark:bg-transparent">
    {anilist?.coverImage?.large || anilist?.coverImage?.medium ? (
    <motion.div
    layout="position"
    animate={{
    transition: { type: false },
    }}
    >
    <img
    src={anilist?.coverImage?.large || anilist?.coverImage?.medium}
    alt={anilist?.title?.romaji || ""}
    className="h-full w-[145px] rounded-sm object-cover"
    />
    </motion.div>
    ) : (
    <div className="flex h-[200px] items-center justify-center rounded-sm bg-slate-200 dark:bg-slate-800">
    <span className="text-muted-foreground text-xs">No Cover</span>
    </div>
    )}

    {/* Status Badges */}
    <div className="absolute left-4 top-2 flex flex-col gap-1">
    {isNewEntry && <Badge className="bg-emerald-500">New</Badge>}
    {isCompleted && (
    <Badge
    variant="outline"
    className="border-amber-500 text-amber-700 dark:text-amber-400"
    >
    Completed
    </Badge>
    )}
    </div>
    </div>
    );
    };

    /**
    * Renders change indicator badges for status, progress, and score updates.
    * @param statusWillChange - Whether status will be updated.
    * @param progressWillChange - Whether progress will be updated.
    * @param scoreWillChange - Whether score will be updated.
    * @returns JSX element with change badges or null if no changes.
    * @source
    */
    const renderChangeBadges = (
    statusWillChange: boolean,
    progressWillChange: boolean,
    scoreWillChange: boolean,
    userEntry: UserMediaEntry | undefined,
    syncConfig: SyncConfig,
    ) => {
    return (
    <div className="mt-1 flex flex-wrap gap-1">
    {statusWillChange && (
    <Badge
    variant="outline"
    className="border-blue-400/70 bg-blue-50/60 px-1.5 py-0 text-xs text-blue-600 shadow-sm dark:border-blue-500/50 dark:bg-blue-900/40 dark:text-blue-300"
    >
    Status
    </Badge>
    )}
    {progressWillChange && (
    <Badge
    variant="outline"
    className="border-green-400/70 bg-green-50/60 px-1.5 py-0 text-xs text-green-600 shadow-sm dark:border-green-500/50 dark:bg-green-900/30 dark:text-green-300"
    >
    Progress
    </Badge>
    )}
    {scoreWillChange && (
    <Badge
    variant="outline"
    className="border-amber-400/70 bg-amber-50/60 px-1.5 py-0 text-xs text-amber-600 shadow-sm dark:border-amber-500/40 dark:bg-amber-900/30 dark:text-amber-300"
    >
    Score
    </Badge>
    )}
    {userEntry
    ? syncConfig.setPrivate &&
    !userEntry.private && (
    <Badge
    variant="outline"
    className="border-purple-400/70 bg-purple-50/60 px-1.5 py-0 text-xs text-purple-600 shadow-sm dark:border-purple-500/50 dark:bg-purple-900/30 dark:text-purple-300"
    >
    Privacy
    </Badge>
    )
    : syncConfig.setPrivate && (
    <Badge
    variant="outline"
    className="border-purple-400/70 bg-purple-50/60 px-1.5 py-0 text-xs text-purple-600 shadow-sm dark:border-purple-500/50 dark:bg-purple-900/30 dark:text-purple-300"
    >
    Privacy
    </Badge>
    )}
    </div>
    );
    };

    /**
    * Renders current AniList entry data with status, progress, score, and privacy information.
    * Displays data with visual indication of pending changes.
    * @param userEntry - Current AniList entry for the manga.
    * @param anilist - AniList manga data.
    * @param statusWillChange - Whether status will be updated.
    * @param progressWillChange - Whether progress will be updated.
    * @param scoreWillChange - Whether score will be updated.
    * @param syncConfig - Sync configuration options.
    * @returns JSX element displaying formatted AniList entry information.
    * @source
    */
    const renderCurrentAniListData = (
    userEntry: UserMediaEntry | undefined,
    anilist: AniListManga | undefined,
    statusWillChange: boolean,
    progressWillChange: boolean,
    scoreWillChange: boolean,
    syncConfig: SyncConfig,
    ) => {
    return (
    <div className="space-y-2">
    <div className="flex items-center justify-between">
    <span className="text-muted-foreground text-xs">Status:</span>
    <span
    className={`text-xs font-medium ${statusWillChange ? "text-muted-foreground line-through" : ""}`}
    >
    {userEntry?.status || "None"}
    </span>
    </div>
    <div className="flex items-center justify-between">
    <span className="text-muted-foreground text-xs">Progress:</span>
    <span
    className={`text-xs font-medium ${progressWillChange ? "text-muted-foreground line-through" : ""}`}
    >
    {userEntry?.progress || 0} ch
    {anilist?.chapters ? ` / ${anilist.chapters}` : ""}
    </span>
    </div>
    <div className="flex items-center justify-between">
    <span className="text-muted-foreground text-xs">Score:</span>
    <span
    className={`text-xs font-medium ${scoreWillChange ? "text-muted-foreground line-through" : ""}`}
    >
    {userEntry?.score ? `${userEntry.score}/10` : "None"}
    </span>
    </div>
    <div className="flex items-center justify-between">
    <span className="text-muted-foreground text-xs">Private:</span>
    {renderPrivacyDisplay(userEntry, syncConfig, true)}
    </div>
    </div>
    );
    };

    // If any error condition is true, show the appropriate error message
    if (authError || matchDataError || validMatchesError) {
    return (
    <ErrorStateDisplay
    authError={authError}
    matchDataError={matchDataError}
    validMatchesError={validMatchesError}
    />
    );
    }

    // If no manga matches are loaded yet, show loading state
    if (isInitialMangaLoad && mangaMatches.length === 0) {
    return (
    <motion.div
    className="container mx-auto flex h-full max-w-full flex-col px-4 py-6 md:px-6"
    variants={pageVariants}
    initial="hidden"
    animate="visible"
    >
    <motion.div className="mb-8 space-y-4">
    <h1 className="text-3xl font-semibold tracking-tight">
    Sync Preview
    </h1>
    <p className="text-sm text-slate-600 dark:text-slate-400">
    Loading your matched manga...
    </p>
    </motion.div>
    <motion.div className="grid grid-cols-1 gap-6 md:grid-cols-3">
    {Array.from({ length: 3 }).map((_, index) => (
    <motion.div key={`skeleton-stat-${index + 1}`}>
    <SkeletonCard />
    </motion.div>
    ))}
    </motion.div>
    <motion.div className="mt-8">
    <SkeletonList items={8} />
    </motion.div>
    </motion.div>
    );
    }

    if (mangaMatches.length === 0) {
    return (
    <AnimatePresence>
    <EmptyState
    icon={<History className="h-10 w-10" />}
    title="No matches to sync"
    description="Complete the matching process first to see sync preview."
    actionLabel="Go to Matching"
    onAction={() => navigate({ to: "/review" })}
    variant="info"
    />
    </AnimatePresence>
    );
    }

    // If library is loading, show loading state
    if (libraryLoading) {
    return (
    <LoadingStateDisplay
    type="library"
    isRateLimited={rateLimitState.isRateLimited}
    retryCount={retryCount}
    maxRetries={maxRetries}
    />
    );
    }

    // Render preview view
    const renderPreviewView = () => {
    return (
    <motion.div
    className="space-y-6"
    key="preview"
    initial="hidden"
    animate="visible"
    exit="exit"
    variants={staggerContainerVariants}
    >
    <motion.div variants={cardVariants} className="space-y-6">
    {/* Offline indicator */}
    {!isOnline && (
    <div className="rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800 dark:border-amber-800/60 dark:bg-amber-900/20 dark:text-amber-300">
    <div className="flex items-center gap-2">
    <AlertCircle className="h-5 w-5 shrink-0" />
    <div>
    <p className="font-medium">You are currently offline</p>
    <p className="text-xs text-amber-700 dark:text-amber-400">
    Sync operations will be queued and retried when your
    connection is restored.
    </p>
    </div>
    </div>
    </div>
    )}

    {/* Resume notification for interrupted syncs */}
    {state.resumeAvailable && state.resumeMetadata && (
    <SyncResumeNotification
    remainingCount={state.resumeMetadata.remainingMediaIds.length}
    totalCount={
    state.progress?.total ||
    state.resumeMetadata.remainingMediaIds.length
    }
    lastSyncTime={state.resumeMetadata.timestamp}
    onResume={handleResumeSync}
    onDiscard={handleDiscardCheckpoint}
    />
    )}

    {/* Failed operations panel */}
    {failedOperations.length > 0 && (
    <motion.div
    initial={{ opacity: 0, y: 20 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ duration: 0.45 }}
    className="relative overflow-hidden rounded-lg border border-red-200 bg-red-50 p-4 dark:border-red-900/40 dark:bg-red-950/20"
    >
    <div className="flex items-center justify-between">
    <div className="flex items-center gap-3">
    <AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400" />
    <div>
    <h3 className="font-medium text-red-900 dark:text-red-300">
    Failed Operations ({failedOperations.length})
    </h3>
    <p className="text-xs text-red-700 dark:text-red-400">
    These operations failed and can be retried
    </p>
    </div>
    </div>
    <Button
    size="sm"
    variant="outline"
    onClick={handleRetryAll}
    disabled={isLoadingFailedOps}
    className="border-red-300 text-red-700 hover:bg-red-100 dark:border-red-700 dark:text-red-300 dark:hover:bg-red-900/30"
    >
    <RefreshCw className="mr-2 h-4 w-4" />
    Retry All
    </Button>
    </div>

    <div className="mt-4 max-h-96 space-y-2 overflow-y-auto">
    {failedOperations.map((op) => {
    const isRetrying = retryingOperations.has(op.id);
    const payload = op.payload as Record<string, unknown>;
    const canRetry = op.retryCount < MAX_RETRY_ATTEMPTS;

    return (
    <div
    key={op.id}
    className="flex items-start justify-between gap-3 rounded-md bg-white/50 p-3 text-sm dark:bg-slate-800/30"
    >
    <div className="min-w-0 flex-1">
    <p className="font-medium text-slate-900 dark:text-slate-100">
    {(payload.title as string) || "Unknown"}
    </p>
    <p className="truncate text-xs text-red-600 dark:text-red-400">
    {op.error}
    </p>
    <p className="text-xs text-slate-500 dark:text-slate-400">
    Retried {op.retryCount}/{MAX_RETRY_ATTEMPTS} times
    </p>
    </div>
    <div className="flex gap-2">
    <Button
    size="sm"
    variant="ghost"
    onClick={() => handleRetryOperation(op.id)}
    disabled={isRetrying || !canRetry}
    className="h-8 w-8 p-0"
    >
    {isRetrying ? (
    <Loader2 className="h-4 w-4 animate-spin" />
    ) : (
    <RefreshCw className="h-4 w-4" />
    )}
    </Button>
    <Button
    size="sm"
    variant="ghost"
    onClick={() => handleClearOperation(op.id)}
    className="h-8 w-8 p-0"
    >
    <Trash2 className="h-4 w-4" />
    </Button>
    </div>
    </div>
    );
    })}
    </div>
    </motion.div>
    )}

    <motion.section
    initial={{ opacity: 0, y: 20 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ duration: 0.45 }}
    className="relative overflow-hidden rounded-3xl border border-slate-200/60 bg-white/80 p-6 shadow-xl backdrop-blur-xl dark:border-slate-800/70 dark:bg-slate-950/60"
    >
    <div className="pointer-events-none absolute inset-0 opacity-80">
    <div className="bg-linear-to-r absolute -top-24 left-1/2 h-56 w-96 -translate-x-1/2 rounded-full from-blue-400/50 to-indigo-400/50 blur-3xl dark:from-blue-500/25 dark:to-indigo-500/25" />
    <div className="bg-linear-to-br absolute -bottom-16 -right-12 h-40 w-40 rounded-full from-slate-200/70 to-transparent blur-3xl dark:from-slate-800/40" />
    </div>
    <div className="relative flex flex-col gap-6 md:flex-row md:items-center md:justify-between">
    <div className="max-w-xl space-y-3">
    <h1 className="text-3xl font-semibold tracking-tight text-slate-900 dark:text-slate-100">
    Finalize your AniList library updates
    </h1>
    <p className="text-sm text-slate-600 dark:text-slate-400">
    Review detected changes, tune your syncing preferences, and
    push a polished update to AniList.
    </p>
    </div>
    <div className="grid w-full gap-3 sm:grid-cols-3 md:w-auto">
    {heroStats.map((stat) => {
    const Icon = stat.icon;
    return (
    <div
    key={stat.label}
    className="relative overflow-hidden rounded-2xl border border-white/70 bg-white/80 p-4 shadow-sm backdrop-blur-lg dark:border-slate-800/70 dark:bg-slate-950/70"
    >
    <div
    className={`bg-linear-to-br pointer-events-none absolute inset-0 ${stat.accent} opacity-80`}
    />
    <div className="relative flex flex-col gap-2">
    <div className="flex items-center justify-between">
    <span className="pr-2 text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
    {stat.label}
    </span>
    <Icon className="h-4 w-4 text-slate-400 dark:text-slate-500" />
    </div>
    <span className="text-2xl font-semibold text-slate-900 dark:text-slate-100">
    {stat.value}
    </span>
    <span className="text-xs text-slate-500 dark:text-slate-400">
    {stat.helper}
    </span>
    </div>
    </div>
    );
    })}
    </div>
    </div>
    </motion.section>

    <Card className="border border-slate-200/70 bg-white/80 shadow-2xl backdrop-blur-xl dark:border-slate-800/70 dark:bg-slate-950/60">
    <CardHeader className="space-y-2">
    <CardTitle className="bg-linear-to-r from-slate-900 via-slate-700 to-slate-900 bg-clip-text text-2xl font-semibold text-transparent dark:from-slate-100 dark:via-slate-300 dark:to-slate-100">
    Sync Preview
    </CardTitle>
    <CardDescription className="text-sm text-slate-600 dark:text-slate-400">
    Review the changes that will be applied to your AniList account
    </CardDescription>
    </CardHeader>
    <CardContent>
    <div className="space-y-6">
    <div className="rounded-2xl border border-slate-200/70 bg-slate-50/80 p-4 shadow-sm dark:border-slate-800/70 dark:bg-slate-900/40">
    <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
    <span className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
    Sync readiness
    </span>
    <div className="mt-1 text-lg font-semibold text-slate-900 dark:text-slate-100">
    {entriesWithChanges.length} of {totalMatchedManga}{" "}
    entries prepared
    </div>
    <p className="text-xs text-slate-500 dark:text-slate-400">
    Automatically excludes skipped matches and completed
    entries preserved per your settings.
    </p>
    </div>
    <div className="flex w-full items-center gap-3 sm:w-auto">
    <div className="flex h-14 w-14 items-center justify-center rounded-full border-2 border-blue-200/80 bg-white/80 text-sm font-semibold text-blue-600 dark:border-blue-700/80 dark:bg-blue-900/20 dark:text-blue-300">
    {queuedPercentage}%
    </div>
    <div className="min-w-[140px] flex-1">
    <div className="h-2 w-full overflow-hidden rounded-full bg-slate-200/80 dark:bg-slate-800/60">
    <div
    className="bg-linear-to-r h-full rounded-full from-blue-500 via-indigo-500 to-purple-500 transition-all duration-300"
    style={{
    width: `${Math.min(queuedPercentage, 100)}%`,
    }}
    />
    </div>
    </div>
    </div>
    </div>
    </div>

    <SyncConfigurationPanel
    syncConfig={syncConfig}
    setSyncConfig={setSyncConfig}
    isCustomThresholdEnabled={isCustomThresholdEnabled}
    setIsCustomThresholdEnabled={setIsCustomThresholdEnabled}
    handleToggleOption={handleToggleOption}
    />

    <ChangesSummary
    entriesWithChanges={entriesWithChanges.length}
    libraryLoading={libraryLoading}
    libraryError={libraryError}
    isRateLimited={rateLimitState.isRateLimited}
    onLibraryRefresh={handleLibraryRefresh}
    userLibrary={userLibrary}
    mangaMatches={mangaMatches}
    syncConfig={syncConfig}
    />

    <ViewControls
    displayMode={displayMode}
    setDisplayMode={setDisplayMode}
    sortOption={sortOption}
    setSortOption={setSortOption}
    filters={filters}
    setFilters={setFilters}
    />

    {sortedMangaMatches.length !== mangaMatches.length && (
    <div className="flex items-center justify-between rounded-xl border border-slate-200/70 bg-slate-50/70 px-3 py-2 text-sm shadow-sm dark:border-slate-800/60 dark:bg-slate-900/50">
    <div>
    Showing{" "}
    <span className="font-medium">
    {sortedMangaMatches.length}
    </span>{" "}
    of{" "}
    <span className="font-medium">
    {
    mangaMatches.filter(
    (match) => match.status !== "skipped",
    ).length
    }
    </span>{" "}
    manga
    {Object.values(filters).some((v) => v !== "all") && (
    <span className="text-muted-foreground ml-1">
    (filtered)
    </span>
    )}
    </div>
    <Button
    variant="ghost"
    size="sm"
    className="h-7 rounded-full border border-transparent px-3 text-xs hover:border-slate-300 hover:bg-slate-100 dark:hover:border-slate-700 dark:hover:bg-slate-800/60"
    onClick={() => {
    setSortOption({ field: "title", direction: "asc" });
    setFilters({
    status: "all",
    changes: "with-changes",
    library: "all",
    });
    }}
    >
    Clear Filters
    </Button>
    </div>
    )}

    <div className="max-h-[60vh] overflow-y-auto">
    <AnimatePresence
    initial={false}
    mode="wait"
    key={displayMode}
    >
    {displayMode === "cards"
    ? renderCardsView()
    : renderCompactView()}
    </AnimatePresence>
    </div>
    </div>
    </CardContent>
    <CardFooter className="flex flex-col gap-3 border-t border-slate-200/60 pt-6 sm:flex-row sm:items-center sm:justify-between dark:border-slate-800/80">
    <div className="flex items-center gap-2 text-xs text-slate-500 dark:text-slate-400">
    <span
    className={`inline-flex h-2 w-2 rounded-full ${entriesWithChanges.length > 0 ? "bg-emerald-500" : "bg-amber-500"}`}
    ></span>
    {entriesWithChanges.length > 0
    ? `${entriesWithChanges.length} entries ready to sync`
    : "No actionable changes detected yet"}
    </div>
    <div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row">
    <Button
    variant="outline"
    onClick={handleCancel}
    className="w-full border-slate-300/70 text-slate-700 hover:border-slate-400 hover:bg-slate-100 sm:w-auto dark:border-slate-700/70 dark:text-slate-200 dark:hover:border-slate-600 dark:hover:bg-slate-900"
    >
    Cancel
    </Button>
    <Button
    onClick={handleStartSync}
    disabled={
    entriesWithChanges.length === 0 ||
    libraryLoading ||
    state.resumeAvailable ||
    !isOnline
    }
    className="bg-linear-to-r group relative w-full overflow-hidden rounded-md from-blue-500 via-indigo-500 to-purple-500 px-6 py-2 font-semibold text-white shadow-lg transition hover:shadow-xl disabled:cursor-not-allowed disabled:opacity-70 sm:w-auto"
    data-onboarding="sync-button"
    title={getStartSyncButtonTitle()}
    >
    <span className="absolute inset-0 bg-white/20 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
    <span className="relative flex items-center justify-center gap-2">
    <Sparkles className="h-4 w-4" />
    Launch Sync
    </span>
    </Button>
    </div>
    </CardFooter>
    </Card>
    </motion.div>
    </motion.div>
    );
    };

    // Render cards view
    const renderCardsView = () => {
    return (
    <motion.div
    key="cards-view"
    variants={fadeVariants}
    initial="hidden"
    animate="visible"
    exit="exit"
    transition={viewModeTransition}
    className="grid grid-cols-1 gap-4"
    >
    <AnimatePresence initial={false}>
    {sortedMangaMatches
    .slice(0, visibleItems)
    .map((match, index) => renderCardItem(match, index))}
    </AnimatePresence>
    {renderLoadMoreButton()}
    </motion.div>
    );
    };

    // Render compact view
    const renderCompactView = () => {
    return (
    <motion.div
    key="compact-view"
    variants={fadeVariants}
    initial="hidden"
    animate="visible"
    exit="exit"
    transition={viewModeTransition}
    className="space-y-1 overflow-hidden rounded-2xl border border-slate-200/70 bg-white/70 shadow-sm backdrop-blur-sm dark:border-slate-800/60 dark:bg-slate-950/50"
    >
    <AnimatePresence initial={false}>
    {sortedMangaMatches
    .slice(0, visibleItems)
    .map((match, index) => renderCompactItem(match, index))}
    </AnimatePresence>
    {renderLoadMoreButtonCompact()}
    </motion.div>
    );
    };

    // Render individual card item
    const renderCardItem = (match: MangaMatchResult, index: number) => {
    const kenmei = match.kenmeiManga;
    const anilist = match.selectedMatch;
    const userEntry = anilist ? userLibrary[anilist.id] : undefined;

    const {
    statusWillChange,
    progressWillChange,
    scoreWillChange,
    isNewEntry,
    isCompleted,
    changeCount,
    } = calculateSyncChanges(kenmei, userEntry, syncConfig);

    return (
    <motion.div
    key={`${anilist?.id}-${index}`}
    layout="position"
    initial={{ opacity: 0, y: 20 }}
    animate={{ opacity: 1, y: 0 }}
    exit={{ opacity: 0, scale: 0.9 }}
    transition={{ duration: 0.3 }}
    layoutId={undefined}
    >
    <Card className="group overflow-hidden border border-slate-200/70 bg-white/80 shadow-sm backdrop-blur-sm transition-all duration-300 hover:-translate-y-1 hover:border-blue-200/70 hover:shadow-xl dark:border-slate-800/60 dark:bg-slate-950/60">
    <div className="flex flex-col sm:flex-row">
    {renderMangaCover(anilist, isNewEntry, isCompleted)}
    <div className="flex-1 p-4">
    <div className="flex items-start justify-between">
    <div>
    <h3 className="line-clamp-2 max-w-[580px] text-base font-semibold">
    {anilist?.title.romaji || kenmei.title}
    </h3>
    {changeCount > 0 && !isCompleted ? (
    renderChangeBadges(
    statusWillChange,
    progressWillChange,
    scoreWillChange,
    userEntry,
    syncConfig,
    )
    ) : (
    <div className="mt-1">
    <Badge
    variant="outline"
    className="border-slate-200/70 bg-slate-100/70 px-1.5 py-0 text-xs text-slate-500 dark:border-slate-700/60 dark:bg-slate-800/50 dark:text-slate-400"
    >
    {isCompleted ? "Preserving Completed" : "No Changes"}
    </Badge>
    </div>
    )}
    </div>
    {changeCount > 0 && !isCompleted && (
    <div className="rounded-full bg-blue-100/80 px-2 py-1 text-xs font-medium text-blue-600 shadow-sm dark:bg-blue-900/30 dark:text-blue-300">
    {changeCount} change{changeCount === 1 ? "" : "s"}
    </div>
    )}
    </div>
    <div className="mt-4 grid grid-cols-2 gap-2 text-sm">
    <div className="rounded-xl border border-slate-200/60 bg-white/70 p-3 shadow-sm dark:border-slate-800/60 dark:bg-slate-900/40">
    <h4 className="text-muted-foreground mb-2 text-xs font-medium">
    {isNewEntry ? "Not in Library" : "Current AniList"}
    </h4>
    {isNewEntry ? (
    <div className="text-muted-foreground py-4 text-center text-xs">
    New addition to your library
    </div>
    ) : (
    renderCurrentAniListData(
    userEntry,
    anilist,
    statusWillChange,
    progressWillChange,
    scoreWillChange,
    syncConfig,
    )
    )}
    </div>
    <div className="rounded-xl border border-blue-100/60 bg-blue-50/70 p-3 shadow-sm dark:border-blue-900/40 dark:bg-blue-900/20">
    <h4 className="mb-2 text-xs font-medium text-blue-600 dark:text-blue-300">
    After Sync
    </h4>
    <div className="space-y-2">
    <div className="flex items-center justify-between">
    <span className="text-xs text-blue-500 dark:text-blue-400">
    Status:
    </span>
    <span
    className={`text-xs font-medium ${statusWillChange ? "text-blue-700 dark:text-blue-300" : ""}`}
    >
    {getEffectiveStatus(kenmei, syncConfig)}
    </span>
    </div>
    <div className="flex items-center justify-between">
    <span className="text-xs text-blue-500 dark:text-blue-400">
    Progress:
    </span>
    {renderAfterSyncProgress(
    userEntry,
    kenmei,
    anilist,
    syncConfig,
    progressWillChange,
    )}
    </div>
    <div className="flex items-center justify-between">
    <span className="text-xs text-blue-500 dark:text-blue-400">
    Score:
    </span>
    {renderAfterSyncScore(userEntry, kenmei, scoreWillChange)}
    </div>
    <div className="flex items-center justify-between">
    <span className="text-xs text-blue-500 dark:text-blue-400">
    Private:
    </span>
    {renderPrivacyDisplay(userEntry, syncConfig, false)}
    </div>
    </div>
    </div>
    </div>
    </div>
    </div>
    </Card>
    </motion.div>
    );
    };

    // Render individual compact item
    const renderCompactItem = (match: MangaMatchResult, index: number) => {
    const kenmei = match.kenmeiManga;
    const anilist = match.selectedMatch;
    const userEntry = anilist ? userLibrary[anilist.id] : undefined;

    const {
    statusWillChange,
    progressWillChange,
    scoreWillChange,
    isNewEntry,
    isCompleted,
    changeCount,
    } = calculateSyncChanges(kenmei, userEntry, syncConfig);

    const baseRowClasses =
    "group flex items-center rounded-xl px-3 py-2 transition-colors duration-200";
    let backgroundClass = "";
    if (isCompleted) {
    backgroundClass = "bg-amber-50/70 dark:bg-amber-950/20";
    } else if (isNewEntry) {
    backgroundClass = "bg-emerald-50/70 dark:bg-emerald-950/20";
    } else if (index % 2 === 0) {
    backgroundClass = "bg-white/70 dark:bg-slate-900/40";
    } else {
    backgroundClass = "bg-white/60 dark:bg-slate-900/30";
    }

    return (
    <motion.div
    key={
    (anilist?.id ? String(anilist.id) : "unknown-" + index) + "-" + index
    }
    layout="position"
    initial={{ opacity: 0, y: 5 }}
    animate={{ opacity: 1, y: 0 }}
    exit={{ opacity: 0 }}
    transition={{ duration: 0.2 }}
    layoutId={undefined}
    >
    <div
    className={`${baseRowClasses} ${backgroundClass} hover:bg-blue-50/70 dark:hover:bg-slate-900/60`}
    >
    <div className="mr-3 flex shrink-0 items-center pl-2">
    {anilist &&
    (anilist.coverImage?.large || anilist.coverImage?.medium) ? (
    <motion.div
    layout="position"
    animate={{
    transition: { type: false },
    }}
    >
    <img
    src={anilist.coverImage?.large || anilist.coverImage?.medium}
    alt={anilist.title.romaji || ""}
    className="h-12 w-8 rounded-sm object-cover"
    />
    </motion.div>
    ) : (
    <div className="flex h-12 w-8 items-center justify-center rounded-sm bg-slate-200 dark:bg-slate-800">
    <span className="text-muted-foreground text-[8px]">
    No Cover
    </span>
    </div>
    )}
    </div>
    <div className="mr-2 min-w-0 flex-1">
    <div className="truncate text-sm font-medium">
    {anilist?.title.romaji || kenmei.title}
    </div>
    <div className="mt-0.5 flex items-center gap-1">
    {isNewEntry && (
    <Badge className="bg-emerald-500/80 px-2 py-0 text-[10px] text-white shadow-sm">
    New
    </Badge>
    )}
    {isCompleted && (
    <Badge
    variant="outline"
    className="border-amber-400/70 bg-amber-50/60 px-2 py-0 text-[10px] text-amber-600 shadow-sm dark:border-amber-500/40 dark:bg-amber-900/30 dark:text-amber-300"
    >
    Completed
    </Badge>
    )}
    </div>
    </div>
    <div className="flex shrink-0 items-center gap-1">
    {!isNewEntry && !isCompleted && (
    <>
    {renderStatusBadge(
    statusWillChange,
    userEntry,
    kenmei,
    syncConfig,
    )}
    {renderProgressBadge(
    progressWillChange,
    userEntry,
    kenmei,
    syncConfig,
    )}
    {renderScoreBadge(scoreWillChange, userEntry, kenmei)}
    {renderPrivacyBadge(userEntry, syncConfig)}
    {changeCount === 0 && (
    <span className="px-1 text-[10px] text-slate-500 dark:text-slate-400">
    No Changes
    </span>
    )}
    </>
    )}
    {isNewEntry && (
    <span className="text-[10px] font-medium text-emerald-600 dark:text-emerald-400">
    Adding to Library
    </span>
    )}
    {isCompleted && (
    <span className="text-[10px] font-medium text-amber-600 dark:text-amber-400">
    Preserving Completed
    </span>
    )}
    </div>
    </div>
    </motion.div>
    );
    };

    // Render load more button for cards view
    const renderLoadMoreButton = () => {
    if (sortedMangaMatches.length > visibleItems) {
    return (
    <div className="py-4 text-center">
    <Button
    onClick={() => {
    setIsLoadingMore(true);
    const newValue = Math.min(
    visibleItems + 20,
    sortedMangaMatches.length,
    );
    setTimeout(() => {
    setVisibleItems(newValue);
    setIsLoadingMore(false);
    }, 300);
    }}
    variant="outline"
    className="gap-2"
    disabled={isLoadingMore}
    >
    {isLoadingMore && (
    <div className="text-primary inline-block h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></div>
    )}
    {isLoadingMore
    ? "Loading..."
    : `Load More (${visibleItems} of ${sortedMangaMatches.length})`}
    </Button>
    </div>
    );
    }
    if (
    sortedMangaMatches.length > 0 &&
    sortedMangaMatches.length <= visibleItems
    ) {
    return (
    <div className="py-4 text-center">
    <span className="text-muted-foreground text-xs">
    All items loaded
    </span>
    </div>
    );
    }
    return null;
    };

    // Render load more button for compact view
    const renderLoadMoreButtonCompact = () => {
    if (sortedMangaMatches.length === 0) return null;

    return (
    <div className="bg-muted/20 py-3 text-center">
    {sortedMangaMatches.length > visibleItems ? (
    <Button
    onClick={() => {
    setIsLoadingMore(true);
    const newValue = Math.min(
    visibleItems + 20,
    sortedMangaMatches.length,
    );
    setTimeout(() => {
    setVisibleItems(newValue);
    setIsLoadingMore(false);
    }, 300);
    }}
    variant="outline"
    size="sm"
    className="gap-2"
    disabled={isLoadingMore}
    >
    {isLoadingMore && (
    <div className="text-primary inline-block h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></div>
    )}
    {isLoadingMore
    ? "Loading..."
    : `Load More (${visibleItems} of ${sortedMangaMatches.length})`}
    </Button>
    ) : (
    <span className="text-muted-foreground text-xs">
    All items loaded
    </span>
    )}
    </div>
    );
    };

    // Render sync view
    const renderSyncView = () => {
    return (
    <motion.div
    variants={pageVariants}
    initial="hidden"
    animate="visible"
    exit="exit"
    >
    <SyncManager
    entries={entriesWithChanges}
    token={token || ""}
    onComplete={handleSyncComplete}
    onCancel={handleCancel}
    autoStart={false}
    syncState={state}
    syncActions={{
    ...actions,
    startSync: (entries, token, _unused, displayOrderMediaIds) =>
    actions.startSync(entries, token, _unused, displayOrderMediaIds),
    }}
    incrementalSync={syncConfig.incrementalSync}
    onIncrementalSyncChange={(value) => {
    const newConfig = { ...syncConfig, incrementalSync: value };
    setSyncConfig(newConfig);
    saveSyncConfig(newConfig);
    }}
    displayOrderMediaIds={entriesWithChanges
    .filter(Boolean)
    .map((e) => e.mediaId)}
    />
    </motion.div>
    );
    };

    // Render results view
    const renderResultsView = () => {
    if (state.report || wasCancelled) {
    return (
    <motion.div
    variants={pageVariants}
    initial="hidden"
    animate="visible"
    exit="exit"
    >
    {state.report ? (
    <SyncResultsView
    report={state.report}
    onClose={handleGoHome}
    onExportErrors={() =>
    state.report && exportSyncErrorLog(state.report)
    }
    />
    ) : (
    <div className="flex min-h-[300px] flex-col items-center justify-center">
    <div className="mb-4">
    <Loader2 className="h-10 w-10 animate-spin text-blue-500" />
    </div>
    <div className="text-lg font-medium text-blue-700 dark:text-blue-300">
    Loading synchronization results...
    </div>
    </div>
    )}
    <div className="mt-6 flex justify-center gap-4">
    <Button onClick={handleGoHome} variant="default">
    Go Home
    </Button>
    <Button onClick={handleBackToReview} variant="outline">
    Back to Sync Review
    </Button>
    </div>
    {wasCancelled && (
    <div className="mt-4 text-center text-amber-600 dark:text-amber-400">
    Synchronization was cancelled. No further entries will be
    processed.
    </div>
    )}
    </motion.div>
    );
    }

    return (
    <motion.div
    variants={pageVariants}
    initial="hidden"
    animate="visible"
    exit="exit"
    >
    <Card className="mx-auto w-full max-w-md p-6 text-center">
    <CardContent>
    <AlertCircle className="mx-auto mb-4 h-12 w-12 text-red-500" />
    <h3 className="text-lg font-medium">Synchronization Error</h3>
    <p className="mt-2 text-sm text-slate-500">
    {state.error ||
    "An unknown error occurred during synchronization."}
    </p>
    </CardContent>
    <CardFooter className="justify-center gap-4">
    <Button onClick={handleGoHome}>Go Home</Button>
    <Button onClick={handleBackToReview} variant="outline">
    Back to Sync Review
    </Button>
    </CardFooter>
    </Card>
    </motion.div>
    );
    };

    // Render the appropriate view based on state
    const renderContent = () => {
    switch (viewMode) {
    case "preview":
    return renderPreviewView();

    case "sync":
    return renderSyncView();

    case "results":
    return renderResultsView();
    }
    };

    return (
    <div className="relative min-h-0 overflow-hidden">
    <SyncErrorBoundary
    onReset={handleSyncReset}
    onRetryFailed={handleRetryFailedFromBoundary}
    onCancelSync={handleCancelSyncFromBoundary}
    >
    <div className="pointer-events-none absolute inset-0 -z-10">
    <div className="absolute left-[8%] top-[-10%] h-64 w-64 rounded-full bg-blue-200/50 blur-3xl dark:bg-blue-500/20" />
    <div className="absolute right-[-12%] top-1/3 h-80 w-80 rounded-full bg-indigo-200/40 blur-3xl dark:bg-indigo-500/15" />
    <div className="h-104 w-160 bg-linear-to-t absolute bottom-[-20%] left-1/2 -translate-x-1/2 from-slate-100 via-transparent to-transparent opacity-80 dark:from-slate-900/40" />
    </div>
    <motion.div
    className="container relative z-10 py-10"
    initial="hidden"
    animate="visible"
    variants={pageVariants}
    >
    <AnimatePresence mode="wait">{renderContent()}</AnimatePresence>
    </motion.div>
    </SyncErrorBoundary>
    </div>
    );
    }