import type { Api } from '@jellyfin/sdk/lib/api';
import type { MediaSegmentDto } from '@jellyfin/sdk/lib/generated-client/models/media-segment-dto';
import { MediaSegmentType } from '@jellyfin/sdk/lib/generated-client/models/media-segment-type';
import { getMediaSegmentApi } from '@jellyfin/sdk/lib/utils/api/media-segment-api';

import type { PlaybackManager } from 'components/playback/playbackmanager';
import { TICKS_PER_MILLISECOND, TICKS_PER_SECOND } from 'constants/time';
import { ServerConnections } from 'lib/jellyfin-apiclient';
import { currentSettings as userSettings } from 'scripts/settings/userSettings';
import type { PlayerState } from 'types/playbackStopInfo';
import type { Event } from 'utils/events';

import { getMediaSegmentAction } from './mediaSegmentSettings';
import { findCurrentSegment } from './mediaSegments';
import { PlaybackSubscriber } from './playbackSubscriber';
import { MediaSegmentAction } from '../constants/mediaSegmentAction';

class MediaSegmentManager extends PlaybackSubscriber {
    private hasSegments = false;
    private isLastSegmentIgnored = false;
    private lastSegmentIndex = 0;
    private lastTime = -1;
    private mediaSegmentTypeActions: Record<Partial<MediaSegmentType>, MediaSegmentAction> | undefined;
    private mediaSegments: MediaSegmentDto[] = [];

    private async fetchMediaSegments(api: Api, itemId: string, includeSegmentTypes: MediaSegmentType[]) {
        try {
            const { data: mediaSegments } = await getMediaSegmentApi(api)
                .getItemSegments({ itemId, includeSegmentTypes });
            this.mediaSegments = mediaSegments.Items || [];
        } catch (err) {
            console.error('[MediaSegmentManager] failed to fetch segments', err);
            this.mediaSegments = [];
        }
    }

    skipSegment(mediaSegment: MediaSegmentDto) {
        // Ignore segment if playback progress has passed the segment's start time
        if (mediaSegment.StartTicks !== undefined && this.lastTime > mediaSegment.StartTicks) {
            console.info('[MediaSegmentManager] ignoring skipping segment that has been seeked back into', mediaSegment);
            this.isLastSegmentIgnored = true;
        } else if (mediaSegment.EndTicks) {
            // If there is an end time, seek to it
            // Do not skip if duration < 1s to avoid slow stream changes
            if (mediaSegment.StartTicks && mediaSegment.EndTicks - mediaSegment.StartTicks < TICKS_PER_SECOND) {
                console.info('[MediaSegmentManager] ignoring skipping segment with duration <1s', mediaSegment);
                this.isLastSegmentIgnored = true;
                return;
            }
            console.debug('[MediaSegmentManager] skipping to %s ms', mediaSegment.EndTicks / TICKS_PER_MILLISECOND);
            this.playbackManager.seek(mediaSegment.EndTicks, this.player);
        } else {
            // If there is no end time, skip to the next track
            console.debug('[MediaSegmentManager] skipping to next item in queue');
            this.playbackManager.nextTrack(this.player);
        }
    }

    promptToSkip(mediaSegment: MediaSegmentDto) {
        if (mediaSegment.StartTicks && mediaSegment.EndTicks
            && mediaSegment.EndTicks - mediaSegment.StartTicks < TICKS_PER_SECOND * 3) {
            console.info('[MediaSegmentManager] ignoring segment prompt with duration <3s', mediaSegment);
            this.isLastSegmentIgnored = true;
            return;
        }
        this.playbackManager.promptToSkip(mediaSegment);
    }

    private performAction(mediaSegment: MediaSegmentDto) {
        if (!this.mediaSegmentTypeActions || !mediaSegment.Type || !this.mediaSegmentTypeActions[mediaSegment.Type]) {
            console.error('[MediaSegmentManager] segment type missing from action map', mediaSegment, this.mediaSegmentTypeActions);
            return;
        }

        const action = this.mediaSegmentTypeActions[mediaSegment.Type];
        if (action === MediaSegmentAction.Skip) {
            this.skipSegment(mediaSegment);
        } else if (action === MediaSegmentAction.AskToSkip) {
            this.promptToSkip(mediaSegment);
        }
    }

    onPlayerPlaybackStart(_e: Event, state: PlayerState) {
        this.isLastSegmentIgnored = false;
        this.lastSegmentIndex = 0;
        this.lastTime = -1;
        this.hasSegments = !!state.MediaSource?.HasSegments;

        const itemId = state.MediaSource?.Id;
        const serverId = state.NowPlayingItem?.ServerId || ServerConnections.currentApiClient()?.serverId();

        if (!this.hasSegments || !serverId || !itemId) return;

        // Get the user settings for media segment actions
        this.mediaSegmentTypeActions = Object.values(MediaSegmentType)
            .map(type => ({
                type,
                action: getMediaSegmentAction(userSettings, type)
            }))
            .filter(({ action }) => !!action && action !== MediaSegmentAction.None)
            .reduce((acc, { type, action }) => {
                if (action) acc[type] = action;
                return acc;
            }, {} as Record<Partial<MediaSegmentType>, MediaSegmentAction>);

        if (!Object.keys(this.mediaSegmentTypeActions).length) {
            console.info('[MediaSegmentManager] user has no media segment actions enabled');
            return;
        }

        const api = ServerConnections.getApi(serverId);
        if (!api) {
            console.error('[MediaSegmentManager] no api instance available for server', serverId);
            return;
        }

        void this.fetchMediaSegments(
            api,
            itemId,
            Object.keys(this.mediaSegmentTypeActions).map(t => t as keyof typeof MediaSegmentType));
    }

    onPlayerTimeUpdate() {
        if (this.hasSegments && this.mediaSegments.length) {
            const time = this.playbackManager.currentTime(this.player) * TICKS_PER_MILLISECOND;
            const currentSegmentDetails = findCurrentSegment(this.mediaSegments, time, this.lastSegmentIndex);
            if (
                // The current time falls within a segment
                currentSegmentDetails
                // and the last segment is not ignored or the segment index has changed
                && (!this.isLastSegmentIgnored || this.lastSegmentIndex !== currentSegmentDetails.index)
            ) {
                console.debug(
                    '[MediaSegmentManager] found %s segment at %s ms',
                    currentSegmentDetails.segment.Type,
                    time / TICKS_PER_MILLISECOND,
                    currentSegmentDetails);
                this.isLastSegmentIgnored = false;
                this.performAction(currentSegmentDetails.segment);
                this.lastSegmentIndex = currentSegmentDetails.index;
            }
            this.lastTime = time;
        }
    }
}

export const bindMediaSegmentManager = (playbackManager: PlaybackManager) => new MediaSegmentManager(playbackManager);
