import React, { useCallback, useEffect, useRef } from 'react';

import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
import { useSendScrobble } from '/@/renderer/features/player/mutations/scrobble-mutation';
import {
    getServerById,
    publishScrobbleDebug,
    useAppStore,
    usePlaybackSettings,
    usePlayerSong,
    usePlayerSpeed,
    usePlayerStore,
    useSettingsStore,
    useTimestampStoreBase,
} from '/@/renderer/store';
import { logger } from '/@/renderer/utils/logger';
import { hasFeature } from '/@/shared/api/utils';
import { LibraryItem, QueueSong, ServerType } from '/@/shared/types/domain-types';
import { ServerFeature } from '/@/shared/types/features-types';
import { PlayerStatus } from '/@/shared/types/types';

type ScrobbleManualHandlers = {
    forceSubmitScrobble: () => void;
    resetListenedState: () => void;
};

let scrobbleManualHandlers: null | ScrobbleManualHandlers = null;

export const registerScrobbleManualHandlers = (next: null | ScrobbleManualHandlers) => {
    scrobbleManualHandlers = next;
};

export const invokeScrobbleForceSubmit = () => {
    scrobbleManualHandlers?.forceSubmitScrobble();
};

export const invokeScrobbleResetListenedState = () => {
    scrobbleManualHandlers?.resetListenedState();
};

const getPositionValue = (seconds: number, useTicks: boolean) => {
    if (useTicks) {
        return Math.round(seconds * 1e7);
    }

    return seconds * 1000;
};

/*
 Submission (Last.fm / etc.) eligibility uses accumulated listen time:
  - If listened time meets the required percentage of track duration
  - If listened time meets the required duration (seconds)

Listen time advances only while PLAYING, from consecutive timestamp deltas.
Seeks and other timeline jumps re-baseline the next sample without counting
the jump as listen time; accumulated listen time is kept across seeks.

Listen time and submission state reset when the playhead returns to the start
of the track (position before SCROBBLE_TRACK_BEGIN_SEC), e.g. seek-to-start or
restart-from-near-zero. Song change and repeat still reset for a new play-through.

Jellyfin progress APIs still use playback position (ticks), not listen time:
  - Periodic timeupdate while playing
  - timeupdate on seek
  - pause / unpause

Other events:
  - When the song changes: sends 'stop' for the previous non-Jellyfin track;
    sends 'start' when the new track is playing; clears submission flag and
    listen accumulator for the new track.

  - When the song is restarted (near 0 after 10s+): clears submission flag
    and listen accumulator.

  - When the song is seeked: Jellyfin sends timeupdate (throttled). Seeking from
    at/after the intro into the start of the track clears listen accumulator and
    submission flag; other seeks keep accumulated listen time.
*/

// Positions before this time (seconds) count as the start of the track for listen/scrobble resets.
const SCROBBLE_TRACK_BEGIN_SEC = 5;

// Min previous position (seconds) to treat a jump to the start as a full restart.
const SCROBBLE_RESTART_PREVIOUS_MIN_SEC = 10;

// Max seconds between timestamp samples to count as continuous play (above poll interval, below a teleport).
const MAX_LISTEN_DELTA_SEC = 5;

const checkScrobbleConditions = (args: {
    scrobbleAtDurationMs: number;
    scrobbleAtPercentage: number;
    songCompletedDurationMs: number;
    songDurationMs: number;
}) => {
    const { scrobbleAtDurationMs, scrobbleAtPercentage, songCompletedDurationMs, songDurationMs } =
        args;
    const percentageOfSongCompleted = songDurationMs
        ? (songCompletedDurationMs / songDurationMs) * 100
        : 0;

    const shouldScrobbleBasedOnPercentage = percentageOfSongCompleted >= scrobbleAtPercentage;
    const shouldScrobbleBasedOnDuration = songCompletedDurationMs >= scrobbleAtDurationMs;

    return shouldScrobbleBasedOnPercentage || shouldScrobbleBasedOnDuration;
};

export const useScrobble = () => {
    const scrobbleSettings = usePlaybackSettings().scrobble;
    const isScrobbleEnabled = scrobbleSettings?.enabled;
    const isPrivateModeEnabled = useAppStore((state) => state.privateMode);
    const sendScrobble = useSendScrobble();
    const currentSong = usePlayerSong();
    const playbackRate = usePlayerSpeed();

    const imageUrl = useItemImageUrl({
        id: currentSong?.imageId || undefined,
        imageUrl: currentSong?.imageUrl,
        itemType: LibraryItem.SONG,
        type: 'itemCard',
    });

    const imageUrlRef = useRef<null | string | undefined>(imageUrl);
    const isCurrentSongScrobbledRef = useRef(false);
    const listenedMsRef = useRef(0);
    const lastListenSampleTimeRef = useRef<null | number>(null);
    const scrobbleAtDurationMsRef = useRef(0);
    const scrobbleAtPercentageRef = useRef(75);

    const previousSongRef = useRef<QueueSong | undefined>(undefined);
    const previousTimestampRef = useRef<number>(0);
    const stopPositionRef = useRef<number>(0);
    const stoppedSongIdRef = useRef<string | undefined>(undefined);
    const lastProgressEventRef = useRef<number>(0);
    const lastSeekEventRef = useRef<number>(0);
    const songChangeTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
    const notifyTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);

    useEffect(() => {
        imageUrlRef.current = imageUrl;
    }, [imageUrl]);

    useEffect(() => {
        scrobbleAtDurationMsRef.current = (scrobbleSettings?.scrobbleAtDuration ?? 0) * 1000;
        scrobbleAtPercentageRef.current = scrobbleSettings?.scrobbleAtPercentage ?? 75;
    }, [scrobbleSettings?.scrobbleAtDuration, scrobbleSettings?.scrobbleAtPercentage]);

    const flushScrobbleDebug = useCallback(() => {
        const song = usePlayerStore.getState().getCurrentSong();
        const status = usePlayerStore.getState().player.status;
        const positionSec = useTimestampStoreBase.getState().timestamp;
        const trackDurationMs = song?.duration ?? 0;

        const eligibilityMet = Boolean(
            song?.id &&
            checkScrobbleConditions({
                scrobbleAtDurationMs: scrobbleAtDurationMsRef.current,
                scrobbleAtPercentage: scrobbleAtPercentageRef.current,
                songCompletedDurationMs: listenedMsRef.current,
                songDurationMs: trackDurationMs,
            }),
        );

        publishScrobbleDebug({
            eligibilityMet,
            lastListenSampleTimeSec: lastListenSampleTimeRef.current,
            listenedMs: listenedMsRef.current,
            playerStatus: status,
            positionSec,
            songId: song?.id,
            songName: song?.name,
            submitted: isCurrentSongScrobbledRef.current,
            targetDurationSec: scrobbleAtDurationMsRef.current / 1000,
            targetPercentage: scrobbleAtPercentageRef.current,
            trackDurationMs,
        });
    }, []);

    const sendProgressAfterSubmission = useCallback(
        (song: QueueSong) => {
            const activeSong = usePlayerStore.getState().getCurrentSong();
            const status = usePlayerStore.getState().player.status;

            // Because Jellyfin uses the stop event for submission, we need to send another
            // progress update after submission so that the song continues to progress in the dashboard
            const shouldSendProgress =
                song._serverType === ServerType.JELLYFIN &&
                activeSong?._uniqueId === song._uniqueId &&
                status === PlayerStatus.PLAYING;

            if (!shouldSendProgress) {
                return;
            }

            sendScrobble.mutate(
                {
                    apiClientProps: { serverId: song._serverId || '' },
                    query: {
                        albumId: song.albumId,
                        event: 'unpause',
                        id: song.id,
                        mediaType: song._itemType.includes('song') ? 'song' : 'podcast',
                        playbackRate,
                        position: getPositionValue(
                            useTimestampStoreBase.getState().timestamp,
                            true,
                        ),
                        submission: false,
                    },
                },
                {
                    onSuccess: () => {
                        logger.debug('Scrobbled a timeupdate event', {
                            id: song.id,
                            reason: 'after submission',
                        });
                    },
                },
            );
        },
        [playbackRate, sendScrobble],
    );

    const handleScrobbleFromProgress = useCallback(
        (properties: { timestamp: number }, prev: { timestamp: number }) => {
            if (!isScrobbleEnabled || isPrivateModeEnabled) return;

            const currentSong = usePlayerStore.getState().getCurrentSong();
            const mediaType = currentSong?._itemType.includes('song') ? 'song' : 'podcast';
            const useTicks = currentSong?._serverType === ServerType.JELLYFIN;
            const currentStatus = usePlayerStore.getState().player.status;
            const currentTime = properties.timestamp;
            const previousTime = prev.timestamp;

            if (!currentSong?.id) {
                return;
            }

            if (currentStatus !== PlayerStatus.PLAYING) {
                lastListenSampleTimeRef.current = currentTime;
                return;
            }

            // Detect song restart: when timestamp resets to near 0 and was playing past the intro
            if (
                currentTime < previousTime &&
                currentTime < SCROBBLE_TRACK_BEGIN_SEC &&
                previousTime >= SCROBBLE_RESTART_PREVIOUS_MIN_SEC
            ) {
                isCurrentSongScrobbledRef.current = false;
                lastProgressEventRef.current = 0;
                previousTimestampRef.current = 0;
                listenedMsRef.current = 0;
                lastListenSampleTimeRef.current = null;
                return;
            }

            const lastSample = lastListenSampleTimeRef.current;
            if (lastSample === null) {
                const prevSec = prev.timestamp;
                if (currentTime > prevSec && currentTime - prevSec <= MAX_LISTEN_DELTA_SEC) {
                    listenedMsRef.current += (currentTime - prevSec) * 1000;
                }
                lastListenSampleTimeRef.current = currentTime;
            } else {
                const deltaSec = currentTime - lastSample;
                const jumpedBackToTrackStart =
                    currentTime < lastSample &&
                    currentTime < SCROBBLE_TRACK_BEGIN_SEC &&
                    lastSample >= SCROBBLE_TRACK_BEGIN_SEC;

                if (jumpedBackToTrackStart) {
                    listenedMsRef.current = 0;
                    isCurrentSongScrobbledRef.current = false;
                    lastProgressEventRef.current = 0;
                    lastListenSampleTimeRef.current = currentTime;
                } else if (currentTime < lastSample || deltaSec > MAX_LISTEN_DELTA_SEC) {
                    lastListenSampleTimeRef.current = currentTime;
                } else if (deltaSec > 0) {
                    listenedMsRef.current += deltaSec * 1000;
                    lastListenSampleTimeRef.current = currentTime;
                }
            }

            // Send progress events every 10 seconds
            // if (hasPlaybackReport) {
            //     const timeSinceLastProgress = currentTime - lastProgressEventRef.current;
            //     if (timeSinceLastProgress >= 10) {
            //         sendScrobble.mutate(
            //             {
            //                 apiClientProps: { serverId: serverId || '' },
            //                 query: {
            //                     albumId: currentSong.albumId,
            //                     event: 'timeupdate',
            //                     id: currentSong.id,
            //                     mediaType: mediaType,
            //                     playbackRate,
            //                     position: getPositionValue(currentTime, useTicks),
            //                     submission: false,
            //                 },
            //             },
            //             {
            //                 onSuccess: () => {
            //                     logFn.debug("Scrobbled a timeupdate event", {
            //                         meta: {
            //                             id: currentSong.id,
            //                         },
            //                     });
            //                 },
            //             },
            //         );
            //         lastProgressEventRef.current = currentTime;
            //     }
            // }

            // Check if we should submit scrobble based on listened time
            if (!isCurrentSongScrobbledRef.current) {
                const shouldSubmitScrobble = checkScrobbleConditions({
                    scrobbleAtDurationMs: scrobbleAtDurationMsRef.current,
                    scrobbleAtPercentage: scrobbleAtPercentageRef.current,
                    songCompletedDurationMs: listenedMsRef.current,
                    songDurationMs: currentSong.duration,
                });

                if (shouldSubmitScrobble) {
                    sendScrobble.mutate(
                        {
                            apiClientProps: { serverId: currentSong._serverId || '' },
                            query: {
                                albumId: currentSong.albumId,
                                id: currentSong.id,
                                mediaType: mediaType,
                                playbackRate: playbackRate,
                                position: getPositionValue(currentSong.duration ?? 0, useTicks),
                                submission: true,
                            },
                        },
                        {
                            onSuccess: () => {
                                logger.info('Scrobbled a submission event', {
                                    id: currentSong.id,
                                    reason: 'from listened time',
                                });
                                sendProgressAfterSubmission(currentSong);
                            },
                        },
                    );

                    isCurrentSongScrobbledRef.current = true;
                }
            }
        },
        [
            isScrobbleEnabled,
            isPrivateModeEnabled,
            sendScrobble,
            playbackRate,
            sendProgressAfterSubmission,
        ],
    );

    const handleScrobbleFromSongChange = useCallback(
        (
            properties: { index: number; song: QueueSong | undefined },
            prev: { index: number; song: QueueSong | undefined },
        ) => {
            const currentSong = properties.song;
            const previousSong = previousSongRef.current;
            const previousPositionSec = stopPositionRef.current;
            const mediaType = currentSong?._itemType.includes('song') ? 'song' : 'podcast';
            const previousMediaType = previousSong?._itemType.includes('song') ? 'song' : 'podcast';
            const useTicksForPrevious = previousSong?._serverType === ServerType.JELLYFIN;

            // Handle notifications
            if (scrobbleSettings?.notify && currentSong?.id) {
                clearTimeout(notifyTimeoutRef.current);
                notifyTimeoutRef.current = setTimeout(() => {
                    if (
                        currentSong._uniqueId !== previousSong?._uniqueId ||
                        properties.index !== prev.index
                    ) {
                        const artists =
                            currentSong.artists?.length > 0
                                ? currentSong.artists.map((artist) => artist.name).join(' · ')
                                : currentSong.artistName;

                        try {
                            new Notification(`${currentSong.name}`, {
                                body: `${artists}\n${currentSong.album}`,
                                icon: imageUrlRef.current || undefined,
                                silent: true,
                            });
                        } catch (error) {
                            logger.error('an error occurred while sending a desktop notification', {
                                error: error as Error,
                            });
                        }
                    }
                }, 1000);
            }

            if (!isScrobbleEnabled || isPrivateModeEnabled) {
                previousSongRef.current = currentSong;
                previousTimestampRef.current = 0;
                stopPositionRef.current = 0;
                listenedMsRef.current = 0;
                lastListenSampleTimeRef.current = null;
                flushScrobbleDebug();
                return;
            }

            isCurrentSongScrobbledRef.current = false;
            lastProgressEventRef.current = 0;
            listenedMsRef.current = 0;
            lastListenSampleTimeRef.current = null;

            // Use a timeout to prevent spamming the server when switching songs quickly
            clearTimeout(songChangeTimeoutRef.current);
            songChangeTimeoutRef.current = setTimeout(() => {
                const currentStatus = usePlayerStore.getState().player.status;

                // Send start scrobble when song changes and the new song is playing
                if (currentStatus === PlayerStatus.PLAYING && currentSong?.id) {
                    sendScrobble.mutate(
                        {
                            apiClientProps: { serverId: currentSong._serverId || '' },
                            query: {
                                albumId: currentSong.albumId,
                                event: 'start',
                                id: currentSong.id,
                                mediaType: mediaType,
                                playbackRate: playbackRate,
                                position: 0,
                                submission: false,
                            },
                        },
                        {
                            onSuccess: () => {
                                logger.info('Scrobbled a start event', {
                                    id: currentSong.id,
                                });
                            },
                        },
                    );
                }

                // Jellyfin does not need a stop event when advancing to another song.
                const skipStopScrobble = previousSong?._serverType === ServerType.JELLYFIN;

                if (previousSong?.id && !skipStopScrobble) {
                    sendScrobble.mutate(
                        {
                            apiClientProps: { serverId: previousSong._serverId || '' },
                            query: {
                                albumId: previousSong.albumId,
                                event: 'stop',
                                id: previousSong.id,
                                mediaType: previousMediaType,
                                playbackRate: playbackRate,
                                position: getPositionValue(
                                    previousPositionSec,
                                    useTicksForPrevious,
                                ),
                                submission: false,
                            },
                        },
                        {
                            onSuccess: () => {
                                logger.info('Scrobbled a stop event', {
                                    id: previousSong.id,
                                });
                            },
                        },
                    );
                }
            }, 2000);

            previousSongRef.current = currentSong;
            previousTimestampRef.current = 0;
            stopPositionRef.current = 0;
            flushScrobbleDebug();
        },
        [
            scrobbleSettings?.notify,
            isScrobbleEnabled,
            isPrivateModeEnabled,
            flushScrobbleDebug,
            sendScrobble,
            playbackRate,
        ],
    );

    const handleScrobbleFromSeek = useCallback(
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        (properties: { timestamp: number }, _prev: { timestamp: number }) => {
            if (!isScrobbleEnabled || isPrivateModeEnabled) {
                return;
            }

            const currentSong = usePlayerStore.getState().getCurrentSong();
            const mediaType = currentSong?._itemType.includes('song') ? 'song' : 'podcast';
            const serverId = currentSong?._serverId;
            const server = getServerById(serverId);
            const hasPlaybackReport = hasFeature(server, ServerFeature.REPORT_PLAYBACK);
            const useTicks = currentSong?._serverType === ServerType.JELLYFIN;

            if (!currentSong?.id) {
                return;
            }

            const sampleBeforeSeek = lastListenSampleTimeRef.current;
            lastListenSampleTimeRef.current = properties.timestamp;

            if (
                properties.timestamp < SCROBBLE_TRACK_BEGIN_SEC &&
                (sampleBeforeSeek === null || sampleBeforeSeek >= SCROBBLE_TRACK_BEGIN_SEC)
            ) {
                listenedMsRef.current = 0;
                isCurrentSongScrobbledRef.current = false;
                lastProgressEventRef.current = 0;
            }

            // Position scrobbles are only relevant for Jellyfin
            if (!hasPlaybackReport) {
                flushScrobbleDebug();
                return;
            }

            const now = Date.now();
            const timeSinceLastSeek = now - lastSeekEventRef.current;

            // Only allow seek scrobble once per second
            if (timeSinceLastSeek < 1000) {
                flushScrobbleDebug();
                return;
            }

            lastProgressEventRef.current = properties.timestamp;
            lastSeekEventRef.current = now;

            const currentStatus = usePlayerStore.getState().player.status;

            // Stop resets seek position; the stop event is reported by handleScrobbleFromStatus.
            if (currentStatus === PlayerStatus.STOPPED) {
                flushScrobbleDebug();
                return;
            }

            sendScrobble.mutate(
                {
                    apiClientProps: { serverId: currentSong._serverId || '' },
                    query: {
                        albumId: currentSong.albumId,
                        event: currentStatus === PlayerStatus.PLAYING ? 'unpause' : 'pause',
                        id: currentSong.id,
                        mediaType: mediaType,
                        playbackRate: playbackRate,
                        position: getPositionValue(properties.timestamp, useTicks),
                        submission: false,
                    },
                },
                {
                    onSuccess: () => {
                        logger.debug('Scrobbled a timeupdate event', {
                            id: currentSong.id,
                        });
                    },
                },
            );
            flushScrobbleDebug();
        },
        [isScrobbleEnabled, isPrivateModeEnabled, sendScrobble, playbackRate, flushScrobbleDebug],
    );

    const handleScrobbleFromStatus = useCallback(
        (properties: { status: PlayerStatus }, prev: { status: PlayerStatus }) => {
            if (!isScrobbleEnabled || isPrivateModeEnabled) {
                return;
            }

            const currentSong = usePlayerStore.getState().getCurrentSong();
            const mediaType = currentSong?._itemType.includes('song') ? 'song' : 'podcast';
            const serverId = currentSong?._serverId;
            const server = getServerById(serverId);
            const hasPlaybackReport = hasFeature(server, ServerFeature.REPORT_PLAYBACK);
            const useTicks = currentSong?._serverType === ServerType.JELLYFIN;

            if (!currentSong?.id) {
                return;
            }

            // Only apply to Jellyfin controller scrobble
            if (!hasPlaybackReport) {
                return;
            }

            const currentTimestamp = useTimestampStoreBase.getState().timestamp;

            // Send pause event when status changes to paused
            if (properties.status === PlayerStatus.PAUSED && prev.status === PlayerStatus.PLAYING) {
                sendScrobble.mutate(
                    {
                        apiClientProps: { serverId: currentSong._serverId || '' },
                        query: {
                            albumId: currentSong.albumId,
                            event: 'pause',
                            id: currentSong.id,
                            mediaType: mediaType,
                            playbackRate: playbackRate,
                            position: getPositionValue(currentTimestamp, useTicks),
                            submission: false,
                        },
                    },
                    {
                        onSuccess: () => {
                            logger.debug('Scrobbled a pause event', {
                                id: currentSong.id,
                            });
                        },
                    },
                );
            }

            // Send unpause event when status changes to playing (from paused)
            if (properties.status === PlayerStatus.PLAYING && prev.status === PlayerStatus.PAUSED) {
                sendScrobble.mutate(
                    {
                        apiClientProps: { serverId: currentSong._serverId || '' },
                        query: {
                            albumId: currentSong.albumId,
                            event: 'unpause',
                            id: currentSong.id,
                            mediaType: mediaType,
                            playbackRate: playbackRate,
                            position: getPositionValue(currentTimestamp, useTicks),
                            submission: false,
                        },
                    },
                    {
                        onSuccess: () => {
                            logger.debug('Scrobbled an unpause event', {
                                id: currentSong.id,
                            });
                        },
                    },
                );
            }

            // Send start event when resuming the same song that was stopped.
            if (
                properties.status === PlayerStatus.PLAYING &&
                prev.status === PlayerStatus.STOPPED &&
                stoppedSongIdRef.current === currentSong._uniqueId
            ) {
                stoppedSongIdRef.current = undefined;
                sendScrobble.mutate(
                    {
                        apiClientProps: { serverId: currentSong._serverId || '' },
                        query: {
                            albumId: currentSong.albumId,
                            event: 'start',
                            id: currentSong.id,
                            mediaType: mediaType,
                            playbackRate: playbackRate,
                            position: getPositionValue(currentTimestamp, useTicks),
                            submission: false,
                        },
                    },
                    {
                        onSuccess: () => {
                            logger.info('Scrobbled a start event', {
                                id: currentSong.id,
                            });
                        },
                    },
                );
            }

            // Send stop event when status changes to stopped (from an active state)
            if (
                properties.status === PlayerStatus.STOPPED &&
                prev.status !== PlayerStatus.STOPPED
            ) {
                stoppedSongIdRef.current = currentSong._uniqueId;
                sendScrobble.mutate(
                    {
                        apiClientProps: { serverId: currentSong._serverId || '' },
                        query: {
                            albumId: currentSong.albumId,
                            event: 'stop',
                            id: currentSong.id,
                            mediaType: mediaType,
                            playbackRate: playbackRate,
                            position: getPositionValue(currentTimestamp, useTicks),
                            submission: false,
                        },
                    },
                    {
                        onSuccess: () => {
                            logger.info('Scrobbled a stop event', {
                                id: currentSong.id,
                            });
                        },
                    },
                );
            }

            flushScrobbleDebug();
        },
        [isScrobbleEnabled, isPrivateModeEnabled, flushScrobbleDebug, sendScrobble, playbackRate],
    );

    const handleScrobbleFromRepeat = useCallback(() => {
        if (!isScrobbleEnabled || isPrivateModeEnabled) {
            return;
        }

        const currentSong = usePlayerStore.getState().getCurrentSong();
        const currentStatus = usePlayerStore.getState().player.status;
        const mediaType = currentSong?._itemType.includes('song') ? 'song' : 'podcast';

        if (currentStatus !== PlayerStatus.PLAYING || !currentSong?.id) {
            return;
        }

        isCurrentSongScrobbledRef.current = false;
        lastProgressEventRef.current = 0;
        previousTimestampRef.current = 0;
        stopPositionRef.current = 0;
        listenedMsRef.current = 0;
        lastListenSampleTimeRef.current = null;

        sendScrobble.mutate(
            {
                apiClientProps: { serverId: currentSong._serverId || '' },
                query: {
                    albumId: currentSong.albumId,
                    event: 'start',
                    id: currentSong.id,
                    mediaType: mediaType,
                    playbackRate: playbackRate,
                    position: 0,
                    submission: false,
                },
            },
            {
                onSuccess: () => {
                    logger.info('Scrobbled a start event', {
                        id: currentSong.id,
                        reason: 'from repeat',
                    });
                },
            },
        );
        flushScrobbleDebug();
    }, [isScrobbleEnabled, isPrivateModeEnabled, sendScrobble, playbackRate, flushScrobbleDebug]);

    // Update previous timestamp on progress for use in status change handler
    const handleProgressUpdate = useCallback(
        (properties: { timestamp: number }, prev: { timestamp: number }) => {
            // Preserve last playback position when the playhead resets to the start
            // (song change can fire after progress already reports 0 for the new track).
            if (
                properties.timestamp < SCROBBLE_TRACK_BEGIN_SEC &&
                prev.timestamp >= SCROBBLE_TRACK_BEGIN_SEC
            ) {
                stopPositionRef.current = prev.timestamp;
            } else {
                stopPositionRef.current = properties.timestamp;
            }

            previousTimestampRef.current = properties.timestamp;
            handleScrobbleFromProgress(properties, prev);
            flushScrobbleDebug();
        },
        [flushScrobbleDebug, handleScrobbleFromProgress],
    );

    useEffect(() => {
        registerScrobbleManualHandlers({
            forceSubmitScrobble: () => {
                if (!isScrobbleEnabled || isPrivateModeEnabled) {
                    return;
                }

                const song = usePlayerStore.getState().getCurrentSong();
                const mediaType = song?._itemType.includes('song') ? 'song' : 'podcast';
                const useTicks = song?._serverType === ServerType.JELLYFIN;

                if (!song?.id) {
                    return;
                }

                sendScrobble.mutate(
                    {
                        apiClientProps: { serverId: song._serverId || '' },
                        query: {
                            albumId: song.albumId,
                            id: song.id,
                            mediaType: mediaType,
                            playbackRate: playbackRate,
                            position: getPositionValue(song.duration ?? 0, useTicks),
                            submission: true,
                        },
                    },
                    {
                        onSuccess: () => {
                            logger.info('Scrobbled a submission event', {
                                id: song.id,
                                reason: 'forced from UI',
                            });
                            sendProgressAfterSubmission(song);
                        },
                    },
                );

                isCurrentSongScrobbledRef.current = true;
                flushScrobbleDebug();
            },
            resetListenedState: () => {
                if (!isScrobbleEnabled || isPrivateModeEnabled) {
                    return;
                }

                const song = usePlayerStore.getState().getCurrentSong();
                if (!song?.id) {
                    return;
                }

                listenedMsRef.current = 0;
                isCurrentSongScrobbledRef.current = false;
                lastProgressEventRef.current = 0;
                lastListenSampleTimeRef.current = useTimestampStoreBase.getState().timestamp;
                flushScrobbleDebug();
            },
        });

        return () => registerScrobbleManualHandlers(null);
    }, [
        flushScrobbleDebug,
        isPrivateModeEnabled,
        isScrobbleEnabled,
        playbackRate,
        sendProgressAfterSubmission,
        sendScrobble,
    ]);

    usePlayerEvents(
        {
            onCurrentSongChange: handleScrobbleFromSongChange,
            onPlayerProgress: handleProgressUpdate,
            onPlayerRepeated: handleScrobbleFromRepeat,
            onPlayerSeekToTimestamp: handleScrobbleFromSeek,
            onPlayerStatus: handleScrobbleFromStatus,
        },
        [
            handleScrobbleFromSongChange,
            handleProgressUpdate,
            handleScrobbleFromRepeat,
            handleScrobbleFromSeek,
            handleScrobbleFromStatus,
        ],
    );
};

const ScrobbleHookInner = () => {
    useScrobble();
    return null;
};

export const ScrobbleHook = () => {
    const isScrobbleEnabled = useSettingsStore((state) => state.playback.scrobble.enabled);
    const privateMode = useAppStore((state) => state.privateMode);

    if (!isScrobbleEnabled || privateMode) {
        return null;
    }

    return React.createElement(ScrobbleHookInner);
};
