import type { ServerInferResponses } from '@ts-rest/core';

import dayjs from 'dayjs';
import { set } from 'idb-keyval';
import filter from 'lodash/filter';
import orderBy from 'lodash/orderBy';
import md5 from 'md5';
import { z } from 'zod';

import { contract, ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
import { mapStructuredLyric } from '/@/renderer/api/subsonic/subsonic-structured-lyrics';
import {
    getDefaultTranscodingProfiles,
    getDirectPlayProfiles,
} from '/@/renderer/features/player/components/audio-players';
import { randomString } from '/@/renderer/utils';
import { logger } from '/@/renderer/utils/logger';
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
import { ssNormalize } from '/@/shared/api/subsonic/subsonic-normalize';
import {
    AlbumListSortType,
    ssType,
    SubsonicExtensions,
} from '/@/shared/api/subsonic/subsonic-types';
import {
    hasFeature,
    hasFeatureWithVersion,
    sortAlbumArtistList,
    sortAlbumList,
    sortSongList,
} from '/@/shared/api/utils';
import {
    AlbumListSort,
    GenreListSort,
    ImageArgs,
    ImageRequest,
    InternalControllerEndpoint,
    LibraryItem,
    PlaylistListSort,
    ReplaceApiClientProps,
    ServerType,
    Song,
    SongListSort,
    SortOrder,
} from '/@/shared/types/domain-types';
import { ServerFeature, ServerFeatures } from '/@/shared/types/features-types';

const getSubsonicImageRequest = ({
    apiClientProps: { server },
    baseUrl,
    query,
}: ReplaceApiClientProps<ImageArgs>): ImageRequest | null => {
    const { id, size } = query;
    const imageSize = size;
    const url = baseUrl || getServerUrl(server);

    if (!url || !server?.credential) {
        return null;
    }

    // Check for default placeholder image ID
    if (id.match('2a96cbd8b46e442fc41c2b86b821562f')) {
        return null;
    }

    return {
        cacheKey: ['subsonic', server.id, baseUrl || '', id, imageSize || ''].join(':'),
        url:
            `${url}/rest/getCoverArt.view` +
            `?id=${id}` +
            `&${server.credential}` +
            '&v=1.13.0' +
            '&c=Feishin' +
            (imageSize ? `&size=${imageSize}` : ''),
    };
};

const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefined> = {
    [AlbumListSort.ALBUM_ARTIST]: AlbumListSortType.ALPHABETICAL_BY_ARTIST,
    [AlbumListSort.ARTIST]: undefined,
    [AlbumListSort.COMMUNITY_RATING]: undefined,
    [AlbumListSort.CRITIC_RATING]: undefined,
    [AlbumListSort.DURATION]: undefined,
    [AlbumListSort.EXPLICIT_STATUS]: undefined,
    [AlbumListSort.FAVORITED]: AlbumListSortType.STARRED,
    [AlbumListSort.ID]: undefined,
    [AlbumListSort.NAME]: AlbumListSortType.ALPHABETICAL_BY_NAME,
    [AlbumListSort.PLAY_COUNT]: AlbumListSortType.FREQUENT,
    [AlbumListSort.RANDOM]: AlbumListSortType.RANDOM,
    [AlbumListSort.RATING]: undefined,
    [AlbumListSort.RECENTLY_ADDED]: AlbumListSortType.NEWEST,
    [AlbumListSort.RECENTLY_PLAYED]: AlbumListSortType.RECENT,
    [AlbumListSort.RELEASE_DATE]: AlbumListSortType.BY_YEAR,
    [AlbumListSort.SONG_COUNT]: undefined,
    [AlbumListSort.SORT_NAME]: AlbumListSortType.ALPHABETICAL_BY_NAME,
    [AlbumListSort.YEAR]: AlbumListSortType.BY_YEAR,
};

const MAX_SUBSONIC_ITEMS = 500;
const SUBSONIC_FAST_BATCH_SIZE = MAX_SUBSONIC_ITEMS * 10;

// const TRANSCODE_DIRECT_PLAY_PROFILES = [
//     {
//         audioCodecs: ['mp3'],
//         containers: ['mp3'],
//         maxAudioChannels: 2,
//         protocols: ['http'],
//     },
//     {
//         audioCodecs: ['aac'],
//         containers: ['m4a', 'mp4'],
//         maxAudioChannels: 2,
//         protocols: ['http'],
//     },
//     {
//         audioCodecs: ['vorbis'],
//         containers: ['ogg'],
//         maxAudioChannels: 2,
//         protocols: ['http'],
//     },
//     {
//         audioCodecs: ['opus'],
//         containers: ['ogg', 'webm'],
//         maxAudioChannels: 2,
//         protocols: ['http'],
//     },
//     {
//         audioCodecs: ['pcm'],
//         containers: ['wav'],
//         maxAudioChannels: 2,
//         protocols: ['http'],
//     },
//     {
//         audioCodecs: ['flac'],
//         containers: ['flac'],
//         maxAudioChannels: 2,
//         protocols: ['http'],
//     },
// ];

// const TRANSCODE_UNSUPPORTED_DIRECT_PLAY_PROFILES = [
//     {
//       containers: ["m4a", "mp4"],
//       audioCodecs: ["alac"],
//       protocols: ["http"],
//       maxAudioChannels: 2
//     },
//     {
//       containers: ["m4a", "mp4"],
//       audioCodecs: ["ac3", "eac3"],
//       protocols: ["http"],
//       maxAudioChannels: 6
//     },
//     {
//       containers: ["ogg"],
//       audioCodecs: ["flac", "speex"],
//       protocols: ["http"],
//       maxAudioChannels: 2
//     },
//     {
//       containers: ["wav"],
//       audioCodecs: ["adpcm", "gsm", "aac", "mp3"],
//       protocols: ["http"],
//       maxAudioChannels: 2
//     },
//     {
//       containers: ["mkv"],
//       audioCodecs: ["aac", "mp3", "flac", "opus", "vorbis", "ac3", "eac3", "dts"],
//       protocols: ["http"],
//       maxAudioChannels: 8
//     },
//     {
//       containers: ["avi"],
//       audioCodecs: ["mp3", "ac3", "pcm", "aac"],
//       protocols: ["http"],
//       maxAudioChannels: 6
//     },
//     {
//       containers: ["asf", "wma"],
//       audioCodecs: ["wma", "pcm", "mp3"],
//       protocols: ["http"],
//       maxAudioChannels: 2
//     },
//     {
//       containers: ["caf"],
//       audioCodecs: ["pcm", "aac", "alac", "mp3"],
//       protocols: ["http"],
//       maxAudioChannels: 8
//     },
//     {
//       containers: ["3gp"],
//       audioCodecs: ["aac", "amr"],
//       protocols: ["http"],
//       maxAudioChannels: 2
//     },
//     {
//       containers: ["amr"],
//       audioCodecs: ["amr"],
//       protocols: ["http"],
//       maxAudioChannels: 1
//     },
//     {
//       containers: ["ape"],
//       audioCodecs: ["ape"],
//       protocols: ["http"],
//       maxAudioChannels: 2
//     },
//     {
//       containers: ["wv"],
//       audioCodecs: ["wavpack"],
//       protocols: ["http"],
//       maxAudioChannels: 2
//     },
//     {
//       containers: ["ac3"],
//       audioCodecs: ["ac3"],
//       protocols: ["http"],
//       maxAudioChannels: 6
//     },
//     {
//       containers: ["eac3"],
//       audioCodecs: ["eac3"],
//       protocols: ["http"],
//       maxAudioChannels: 8
//     },
//     {
//       containers: ["dts"],
//       audioCodecs: ["dts"],
//       protocols: ["http"],
//       maxAudioChannels: 8
//     }
//   ];

function appendTranscodeParams(url: string, format?: string, bitrate?: number) {
    let streamUrl = url;

    if (format) {
        streamUrl += `&format=${format}`;
    }
    if (bitrate !== undefined) {
        streamUrl += `&maxBitRate=${bitrate}`;
    }

    return streamUrl;
}

function buildGetTranscodeStreamUrl(
    server: null | undefined | { credential?: string; url?: string },
    args: {
        mediaId: string;
        mediaType: 'podcast' | 'song';
        offset: number;
        transcodeParams: string;
    },
): string {
    const params = new URLSearchParams({
        c: 'Feishin',
        mediaId: args.mediaId,
        mediaType: args.mediaType,
        offset: String(args.offset),
        transcodeParams: args.transcodeParams,
        v: '1.13.0',
    });

    return `${server?.url}/rest/getTranscodeStream.view?${params.toString()}&${server?.credential}`;
}

function sortAndPaginate<T>(
    items: T[],
    options: {
        limit?: number;
        sortBy?: any;
        sortFn?: (items: T[], sortBy: any, sortOrder: SortOrder) => T[];
        sortOrder?: SortOrder;
        startIndex?: number;
    },
): {
    items: T[];
    startIndex: number;
    totalRecordCount: number;
} {
    let sortedItems = items;

    if (options.sortFn && options.sortBy) {
        const sortOrder = options.sortOrder || SortOrder.ASC;
        sortedItems = options.sortFn(items, options.sortBy, sortOrder);
    }

    const totalCount = sortedItems.length;
    const startIndex = options.startIndex || 0;
    const limit = options.limit || totalCount;
    const paginatedItems = sortedItems.slice(startIndex, startIndex + limit);

    return {
        items: paginatedItems,
        startIndex: startIndex,
        totalRecordCount: totalCount,
    };
}

export const SubsonicController: InternalControllerEndpoint = {
    addToPlaylist: async ({ apiClientProps, body, query }) => {
        const res = await ssApiClient(apiClientProps).updatePlaylist({
            query: {
                playlistId: query.id,
                songIdToAdd: body.songId,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to add to playlist');
        }

        return null;
    },
    authenticate: async (url, body) => {
        let credential: string;
        let credentialParams: {
            p?: string;
            s?: string;
            t?: string;
            u: string;
        };

        const cleanServerUrl = `${url.replace(/\/$/, '')}/rest`;

        if (body.legacy) {
            credential = `u=${encodeURIComponent(body.username)}&p=${encodeURIComponent(body.password)}`;
            credentialParams = {
                p: body.password,
                u: body.username,
            };
        } else {
            const salt = randomString(12);
            const hash = md5(body.password + salt);

            credential = `u=${encodeURIComponent(body.username)}&s=${encodeURIComponent(salt)}&t=${encodeURIComponent(hash)}`;
            credentialParams = {
                s: salt,
                t: hash,
                u: body.username,
            };
        }

        const resp = await ssApiClient({ server: null, url: cleanServerUrl }).authenticate({
            query: {
                c: 'Feishin',
                f: 'json',
                username: body.username,
                v: '1.13.0',
                ...credentialParams,
            },
        });

        if (resp.status !== 200) {
            throw new Error('Failed to log in');
        }

        return {
            credential,
            isAdmin: Boolean(resp.body.user.adminRole),
            userId: resp.body.user.username,
            username: body.username,
        };
    },
    createFavorite: async (args) => {
        const { apiClientProps, query } = args;

        const res = await ssApiClient(apiClientProps).createFavorite({
            query: {
                albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
                artistId:
                    query.type === LibraryItem.ALBUM_ARTIST || query.type === LibraryItem.ARTIST
                        ? query.id
                        : undefined,
                id:
                    query.type === LibraryItem.SONG || query.type === LibraryItem.PLAYLIST_SONG
                        ? query.id
                        : undefined,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to create favorite');
        }

        return null;
    },
    createInternetRadioStation: async (args) => {
        const { apiClientProps, body } = args;

        const res = await ssApiClient(apiClientProps).createInternetRadioStation({
            query: {
                homepageUrl: body.homepageUrl,
                name: body.name,
                streamUrl: body.streamUrl,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to create internet radio station');
        }

        return null;
    },
    createPlaylist: async ({ apiClientProps, body }) => {
        const res = await ssApiClient(apiClientProps).createPlaylist({
            query: {
                name: body.name,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to create playlist');
        }

        return {
            id: res.body.playlist.id.toString(),
            name: res.body.playlist.name,
        };
    },
    deleteFavorite: async (args) => {
        const { apiClientProps, query } = args;

        const res = await ssApiClient(apiClientProps).removeFavorite({
            query: {
                albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
                artistId:
                    query.type === LibraryItem.ALBUM_ARTIST || query.type === LibraryItem.ARTIST
                        ? query.id
                        : undefined,
                id:
                    query.type === LibraryItem.SONG || query.type === LibraryItem.PLAYLIST_SONG
                        ? query.id
                        : undefined,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to delete favorite');
        }

        return null;
    },
    deleteInternetRadioStation: async (args) => {
        const { apiClientProps, query } = args;

        const res = await ssApiClient(apiClientProps).deleteInternetRadioStation({
            query: {
                id: query.id,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to delete internet radio station');
        }

        return null;
    },
    deletePlaylist: async (args) => {
        const { apiClientProps, query } = args;

        const res = await ssApiClient(apiClientProps).deletePlaylist({
            query: {
                id: query.id,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to delete playlist');
        }

        return null;
    },
    getAlbumArtistDetail: async (args) => {
        const { apiClientProps, query } = args;

        const res = await ssApiClient(apiClientProps).getArtist({
            query: {
                id: query.id,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to get album artist detail');
        }

        const artist = res.body.artist;

        return {
            ...ssNormalize.albumArtist(artist, apiClientProps.server),
            albums: artist.album?.map((album) => ssNormalize.album(album, apiClientProps.server)),
            similarArtists: null,
        };
    },
    getAlbumArtistInfo: async (args) => {
        const { apiClientProps, query } = args;

        const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({
            query: {
                id: query.id,
                ...(query.limit != null && { count: query.limit }),
            },
        });

        if (artistInfoRes.status !== 200) {
            return null;
        }

        const artistInfo = artistInfoRes.body.artistInfo;

        return {
            biography: artistInfo?.biography || null,
            similarArtists:
                artistInfo?.similarArtist?.map((artist) => ({
                    id: artist.id,
                    imageId: artist.coverArt ?? artist.id,
                    imageUrl: null,
                    name: artist.name,
                    userFavorite: Boolean(artist.starred) || false,
                    userRating: artist.userRating ?? null,
                })) ?? null,
        };
    },
    getAlbumArtistList: async (args) => {
        const { apiClientProps, query } = args;

        const res = await ssApiClient(apiClientProps).getArtists({
            query: {
                musicFolderId: getLibraryId(query.musicFolderId),
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to get album artist list');
        }

        const artists = (res.body.artists?.index || []).flatMap((index) => index.artist);

        let results = artists.map((artist) =>
            ssNormalize.albumArtist(artist, apiClientProps.server),
        );

        if (query.searchTerm) {
            const searchResults = filter(results, (artist) => {
                return artist.name.toLowerCase().includes(query.searchTerm!.toLowerCase());
            });

            results = searchResults;
        }

        if (query.favorite) {
            results = results.filter((artist) => artist.userFavorite);
        }

        return sortAndPaginate(results, {
            limit: query.limit,
            sortBy: query.sortBy,
            sortFn: query.sortBy ? sortAlbumArtistList : undefined,
            sortOrder: query.sortOrder,
            startIndex: query.startIndex,
        });
    },
    getAlbumArtistListCount: (args) =>
        SubsonicController.getAlbumArtistList({
            ...args,
            query: { ...args.query, startIndex: 0 },
        }).then((res) => res!.totalRecordCount!),
    getAlbumDetail: async (args) => {
        const { apiClientProps, query } = args;

        const res = await ssApiClient(apiClientProps).getAlbum({
            query: {
                id: query.id,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to get album detail');
        }

        return ssNormalize.album(res.body.album, apiClientProps.server);
    },
    getAlbumList: async (args) => {
        const { apiClientProps, query } = args;

        if (query.searchTerm) {
            const res = await ssApiClient(apiClientProps).search3({
                query: {
                    albumCount: query.limit,
                    albumOffset: query.startIndex,
                    artistCount: 0,
                    artistOffset: 0,
                    musicFolderId: getLibraryId(query.musicFolderId),
                    query: query.searchTerm || '',
                    songCount: 0,
                    songOffset: 0,
                },
            });

            if (res.status !== 200) {
                throw new Error('Failed to get album list');
            }

            const results =
                res.body.searchResult3?.album?.map((album) =>
                    ssNormalize.album(album, apiClientProps.server),
                ) || [];

            return {
                items: results,
                startIndex: query.startIndex,
                totalRecordCount: null,
            };
        }

        let type = ALBUM_LIST_SORT_MAPPING[query.sortBy] ?? AlbumListSortType.ALPHABETICAL_BY_NAME;

        if (query.artistIds) {
            const promises: Promise<ServerInferResponses<typeof contract.getArtist>>[] = [];

            for (const artistId of query.artistIds) {
                promises.push(
                    ssApiClient(apiClientProps).getArtist({
                        query: {
                            id: artistId,
                        },
                    }),
                );
            }

            const artistResult = await Promise.all(promises);

            const albums = artistResult.flatMap((artist) => {
                if (artist.status !== 200) {
                    return [];
                }

                return artist.body.artist.album ?? [];
            });

            const items = albums.map((album) => ssNormalize.album(album, apiClientProps.server));

            return {
                items: sortAlbumList(items, query.sortBy, query.sortOrder),
                startIndex: 0,
                totalRecordCount: albums.length,
            };
        }

        if (query.favorite) {
            const res = await ssApiClient(apiClientProps).getStarred({
                query: {
                    musicFolderId: getLibraryId(query.musicFolderId),
                },
            });

            if (res.status !== 200) {
                throw new Error('Failed to get album list');
            }

            const allResults =
                res.body.starred?.album?.map((album) =>
                    ssNormalize.album(album, apiClientProps.server),
                ) || [];

            return sortAndPaginate(allResults, {
                limit: query.limit,
                sortBy: query.sortBy,
                sortFn: sortAlbumList,
                sortOrder: query.sortOrder,
                startIndex: query.startIndex,
            });
        }

        if (query.genreIds?.length) {
            type = AlbumListSortType.BY_GENRE;
        }

        if (query.minYear || query.maxYear) {
            type = AlbumListSortType.BY_YEAR;
        }

        let fromYear: number | undefined;
        let toYear: number | undefined;

        if (query.minYear) {
            fromYear = query.minYear;
            toYear = dayjs().year();
        }

        if (query.maxYear) {
            toYear = query.maxYear;

            if (!query.minYear) {
                fromYear = 0;
            }
        }

        if (type === AlbumListSortType.BY_YEAR && !fromYear && !toYear) {
            if (query.sortOrder === SortOrder.ASC) {
                fromYear = 0;
                toYear = dayjs().year();
            } else {
                fromYear = dayjs().year();
                toYear = 0;
            }
        }

        const res = await ssApiClient(apiClientProps).getAlbumList2({
            query: {
                fromYear,
                genre: query.genreIds?.length ? query.genreIds[0] : undefined,
                musicFolderId: getLibraryId(query.musicFolderId),
                offset: query.startIndex,
                size: query.limit,
                toYear,
                type,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to get album list');
        }

        return {
            items:
                res.body.albumList2.album?.map((album) =>
                    ssNormalize.album(album, apiClientProps.server),
                ) || [],
            startIndex: query.startIndex,
            totalRecordCount: null,
        };
    },
    getAlbumListCount: async (args) => {
        const { apiClientProps, query } = args;

        if (query.searchTerm) {
            let fetchNextPage = true;
            let startIndex = 0;
            let totalRecordCount = 0;

            while (fetchNextPage) {
                const res = await ssApiClient(apiClientProps).search3({
                    query: {
                        albumCount: MAX_SUBSONIC_ITEMS,
                        albumOffset: startIndex,
                        artistCount: 0,
                        artistOffset: 0,
                        musicFolderId: getLibraryId(query.musicFolderId),
                        query: query.searchTerm || '',
                        songCount: 0,
                        songOffset: 0,
                    },
                });

                if (res.status !== 200) {
                    throw new Error('Failed to get album list count');
                }

                const albumCount = (res.body.searchResult3?.album || [])?.length;

                totalRecordCount += albumCount;
                startIndex += albumCount;

                fetchNextPage = albumCount === MAX_SUBSONIC_ITEMS;
            }

            return totalRecordCount;
        }

        if (query.artistIds) {
            const promises: Promise<ServerInferResponses<typeof contract.getArtist>>[] = [];

            for (const artistId of query.artistIds) {
                promises.push(
                    ssApiClient(apiClientProps).getArtist({
                        query: {
                            id: artistId,
                        },
                    }),
                );
            }

            const artistResult = await Promise.all(promises);

            const albums = artistResult.reduce((total: number, artist) => {
                if (artist.status !== 200) {
                    return 0;
                }

                const length = artist.body.artist.album?.length ?? 0;
                return length + total;
            }, 0);

            return albums;
        }

        if (query.favorite) {
            const res = await ssApiClient(apiClientProps).getStarred({
                query: {
                    musicFolderId: getLibraryId(query.musicFolderId),
                },
            });

            if (res.status !== 200) {
                throw new Error('Failed to get album list');
            }

            return (res.body.starred?.album || []).length || 0;
        }

        let type = AlbumListSortType.ALPHABETICAL_BY_NAME;

        let fetchNextPage = true;
        let startIndex = 0;
        let totalRecordCount = 0;

        if (query.genreIds?.length) {
            type = AlbumListSortType.BY_GENRE;
        }

        if (query.minYear || query.maxYear) {
            type = AlbumListSortType.BY_YEAR;
        }

        let fromYear: number | undefined;
        let toYear: number | undefined;

        if (query.minYear) {
            fromYear = query.minYear;
            toYear = dayjs().year();
        }

        if (query.maxYear) {
            toYear = query.maxYear;

            if (!query.minYear) {
                fromYear = 0;
            }
        }

        while (fetchNextPage) {
            const res = await ssApiClient(apiClientProps).getAlbumList2({
                query: {
                    fromYear,
                    genre: query.genreIds?.length ? query.genreIds[0] : undefined,
                    musicFolderId: getLibraryId(query.musicFolderId),
                    offset: startIndex,
                    size: MAX_SUBSONIC_ITEMS,
                    toYear,
                    type,
                },
            });

            const headers = res.headers;

            // Navidrome returns the total count in the header
            if (headers.get('x-total-count')) {
                fetchNextPage = false;
                totalRecordCount = Number(headers.get('x-total-count'));
                break;
            }

            if (res.status !== 200) {
                throw new Error('Failed to get album list count');
            }

            const albumCount = res.body.albumList2.album.length;

            totalRecordCount += albumCount;
            startIndex += albumCount;

            fetchNextPage = albumCount === MAX_SUBSONIC_ITEMS;
        }

        return totalRecordCount;
    },
    getAlbumRadio: async (args) => {
        const { apiClientProps, query } = args;

        const res = await ssApiClient(apiClientProps).getSimilarSongs({
            query: {
                count: query.count,
                id: query.albumId,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to get album radio songs');
        }

        if (!res.body.similarSongs?.song) {
            return [];
        }

        return res.body.similarSongs.song.map((song) =>
            ssNormalize.song(song, apiClientProps.server),
        );
    },
    getArtistList: async (args) => {
        const { apiClientProps, query } = args;

        const res = await ssApiClient(apiClientProps).getArtists({
            query: {
                musicFolderId: getLibraryId(query.musicFolderId),
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to get artist list');
        }

        let artists = (res.body.artists?.index || []).flatMap((index) => index.artist);
        if (query.role) {
            artists = artists.filter(
                (artist) => !artist.roles || artist.roles.includes(query.role!),
            );
        }

        let results = artists.map((artist) =>
            ssNormalize.albumArtist(artist, apiClientProps.server),
        );

        if (query.searchTerm) {
            const searchResults = filter(results, (artist) => {
                return artist.name.toLowerCase().includes(query.searchTerm!.toLowerCase());
            });

            results = searchResults;
        }

        return sortAndPaginate(results, {
            limit: query.limit,
            sortBy: query.sortBy,
            sortFn: query.sortBy ? sortAlbumArtistList : undefined,
            sortOrder: query.sortOrder,
            startIndex: query.startIndex,
        });
    },
    getArtistListCount: async (args) =>
        SubsonicController.getArtistList({
            ...args,
            query: { ...args.query, startIndex: 0 },
        }).then((res) => res!.totalRecordCount!),
    getArtistRadio: async (args) => {
        const { apiClientProps, query } = args;

        const res = await ssApiClient(apiClientProps).getSimilarSongs2({
            query: {
                count: query.count,
                id: query.artistId,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to get artist radio songs');
        }

        if (!res.body.similarSongs2?.song) {
            return [];
        }

        return res.body.similarSongs2.song.map((song) =>
            ssNormalize.song(song, apiClientProps.server),
        );
    },
    getDownloadUrl: (args) => {
        const { apiClientProps, query } = args;

        return (
            `${apiClientProps.server?.url}/rest/download.view` +
            `?id=${query.id}` +
            `&${apiClientProps.server?.credential}` +
            '&v=1.13.0' +
            '&c=Feishin'
        );
    },
    getFavoriteSongs: async (args) => {
        const { apiClientProps, query } = args;

        // if user selects 'rating'
        if (query.type === 'rating') {
            const res = await SubsonicController.getSongList({
                apiClientProps,
                query: {
                    artistIds: [query.artistId],
                    sortBy: SongListSort.RATING,
                    sortOrder: SortOrder.DESC,
                    startIndex: 0,
                },
            });

            const songsWithHighRating = orderBy(
                res.items.filter((song) => song.userRating !== null && song.userRating > 2),
                ['userRating', 'userFavorite', 'playCount', 'albumId', 'trackNumber'],
                ['desc', 'desc', 'desc', 'asc', 'asc'],
            );

            return {
                items: songsWithHighRating,
                startIndex: 0,
                totalRecordCount: res.totalRecordCount,
            };
        }

        // else if user selects 'favorites'
        const res = await SubsonicController.getSongList({
            apiClientProps,
            query: {
                artistIds: [query.artistId],
                sortBy: SongListSort.FAVORITED,
                sortOrder: SortOrder.DESC,
                startIndex: 0,
            },
        });
        const songsWithFavorite = orderBy(
            res.items.filter((song) => song.userFavorite),
            ['userFavorite', 'userRating', 'playCount', 'albumId', 'trackNumber'],
            ['desc', 'desc', 'desc', 'asc', 'asc'],
        );

        return {
            items: songsWithFavorite,
            startIndex: 0,
            totalRecordCount: res.totalRecordCount,
        };
    },
    getFolder: async ({ apiClientProps, query }) => {
        const sortOrder = (query.sortOrder?.toLowerCase() ?? 'asc') as 'asc' | 'desc';

        const isRootFolderId = query.id === '0';

        if (isRootFolderId) {
            const res = await ssApiClient(apiClientProps).getIndexes({
                query: {
                    musicFolderId: getLibraryId(query.musicFolderId),
                },
            });

            if (res.status !== 200) {
                throw new Error(`Failed to get folder list: ${JSON.stringify(res.body)}`);
            }

            let items =
                res.body.indexes?.index?.flatMap((idx) =>
                    idx.artist.map((artist) => ({
                        artist: artist.name,
                        coverArt: artist.coverArt,
                        id: artist.id.toString(),
                        isDir: true,
                        title: artist.name,
                    })),
                ) || [];

            if (query.searchTerm) {
                items = filter(items, (item) => {
                    return item.title.toLowerCase().includes(query.searchTerm!.toLowerCase());
                });
            }

            let folders = items.map((item) => ssNormalize.folder(item, apiClientProps.server));

            folders = orderBy(folders, [(v) => v.name.toLowerCase()], [sortOrder]);

            return {
                _itemType: LibraryItem.FOLDER,
                _serverId: apiClientProps.server?.id || 'unknown',
                _serverType: ServerType.SUBSONIC,
                children: {
                    folders,
                    songs: [],
                },
                id: query.id,
                name: '~',
                parentId: undefined,
            };
        }

        const directoryRes = await ssApiClient(apiClientProps).getMusicDirectory({
            query: {
                id: query.id,
            },
        });

        if (directoryRes.status !== 200) {
            throw new Error('Failed to get folder');
        }

        const folder = ssNormalize.folder(directoryRes.body.directory, apiClientProps.server);

        let filteredFolders = folder.children?.folders || [];
        let filteredSongs = folder.children?.songs || [];

        if (query.searchTerm) {
            const searchTermLower = query.searchTerm.toLowerCase();
            filteredFolders = filter(filteredFolders, (f) =>
                f.name.toLowerCase().includes(searchTermLower),
            );
            filteredSongs = filter(filteredSongs, (s) => {
                const name = s.name?.toLowerCase() || '';
                const album = s.album?.toLowerCase() || '';
                const artist = s.artistName?.toLowerCase() || '';
                return (
                    name.includes(searchTermLower) ||
                    album.includes(searchTermLower) ||
                    artist.includes(searchTermLower)
                );
            });
        }

        filteredFolders = orderBy(filteredFolders, [(v) => v.name.toLowerCase()], [sortOrder]);

        if (filteredSongs.length > 0) {
            filteredSongs = sortSongList(
                filteredSongs,
                query.sortBy || SongListSort.NAME,
                query.sortOrder || SortOrder.ASC,
            );
        }

        return {
            ...folder,
            children: {
                folders: filteredFolders,
                songs: filteredSongs,
            },
        };
    },
    getGenreList: async ({ apiClientProps, query }) => {
        const sortOrder = (query.sortOrder?.toLowerCase() ?? 'asc') as 'asc' | 'desc';

        const res = await ssApiClient(apiClientProps).getGenres({});

        if (res.status !== 200) {
            throw new Error('Failed to get genre list');
        }

        let results = res.body.genres?.genre || [];

        if (query.searchTerm) {
            const searchResults = filter(results, (genre) =>
                genre.value.toLowerCase().includes(query.searchTerm!.toLowerCase()),
            );

            results = searchResults;
        }

        switch (query.sortBy) {
            case GenreListSort.ALBUM_COUNT:
                results = orderBy(results, [(v) => v.albumCount], [sortOrder]);
                break;
            case GenreListSort.NAME:
                results = orderBy(results, [(v) => v.value.toLowerCase()], [sortOrder]);
                break;
            case GenreListSort.SONG_COUNT:
                results = orderBy(results, [(v) => v.songCount], [sortOrder]);
                break;
            default:
                break;
        }

        const genres = results.map((genre) => ssNormalize.genre(genre, apiClientProps.server));

        return sortAndPaginate(genres, {
            limit: query.limit,
            startIndex: query.startIndex,
        });
    },
    getImageRequest: getSubsonicImageRequest,
    getImageUrl: (args) => getSubsonicImageRequest(args)?.url || null,
    getInternetRadioStations: async (args) => {
        const { apiClientProps } = args;

        const res = await ssApiClient(apiClientProps).getInternetRadioStations();

        if (res.status !== 200) {
            throw new Error('Failed to get internet radio stations');
        }

        const stations = res.body.internetRadioStations?.internetRadioStation || [];

        return stations.map((station) => ssNormalize.internetRadioStation(station));
    },
    getMusicFolderList: async (args) => {
        const { apiClientProps } = args;

        const res = await ssApiClient(apiClientProps).getMusicFolderList({});

        if (res.status !== 200) {
            throw new Error('Failed to get music folder list');
        }

        return {
            items: res.body.musicFolders.musicFolder.map((folder) => ({
                id: folder.id.toString(),
                name: folder.name,
            })),
            startIndex: 0,
            totalRecordCount: res.body.musicFolders.musicFolder.length,
        };
    },

    getPlaylistDetail: async (args) => {
        const { apiClientProps, query } = args;

        const res = await ssApiClient(apiClientProps).getPlaylist({
            query: {
                id: query.id,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to get playlist detail');
        }

        return ssNormalize.playlist(res.body.playlist, apiClientProps.server);
    },
    getPlaylistList: async ({ apiClientProps, query }) => {
        const sortOrder = (query.sortOrder || SortOrder.ASC).toLowerCase() as 'asc' | 'desc';

        const res = await ssApiClient(apiClientProps).getPlaylists({});

        if (res.status !== 200) {
            throw new Error('Failed to get playlist list');
        }

        let results = res.body.playlists?.playlist || [];

        if (query.searchTerm) {
            const searchResults = filter(results, (playlist) => {
                return playlist.name.toLowerCase().includes(query.searchTerm!.toLowerCase());
            });

            results = searchResults;
        }

        switch (query.sortBy) {
            case PlaylistListSort.DURATION:
                results = orderBy(results, ['duration'], [sortOrder]);
                break;
            case PlaylistListSort.NAME:
                results = orderBy(results, [(v) => v.name?.toLowerCase()], [sortOrder]);
                break;
            case PlaylistListSort.OWNER:
                results = orderBy(results, [(v) => v.owner?.toLowerCase()], [sortOrder]);
                break;
            case PlaylistListSort.PUBLIC:
                results = orderBy(results, ['public'], [sortOrder]);
                break;
            case PlaylistListSort.SONG_COUNT:
                results = orderBy(results, ['songCount'], [sortOrder]);
                break;
            case PlaylistListSort.UPDATED_AT:
                results = orderBy(results, ['changed'], [sortOrder]);
                break;
            default:
                break;
        }

        const playlists = results.map((playlist) =>
            ssNormalize.playlist(playlist, apiClientProps.server),
        );

        return sortAndPaginate(playlists, {
            limit: query.limit,
            startIndex: query.startIndex,
        });
    },
    getPlaylistListCount: async ({ apiClientProps, query }) => {
        const res = await ssApiClient(apiClientProps).getPlaylists({});

        if (res.status !== 200) {
            throw new Error('Failed to get playlist list');
        }

        let results = res.body.playlists?.playlist || [];

        if (query.searchTerm) {
            const searchResults = filter(results, (playlist) => {
                return playlist.name.toLowerCase().includes(query.searchTerm!.toLowerCase());
            });

            results = searchResults;
        }

        return results.length;
    },
    getPlaylistSongIds: async (args) =>
        SubsonicController.getPlaylistSongList(args).then((result) => ({
            ...result,
            items: result.items.map((song) => song.id),
        })),
    getPlaylistSongList: async ({ apiClientProps, query }) => {
        const res = await ssApiClient(apiClientProps).getPlaylist({
            query: {
                id: query.id,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to get playlist song list');
        }

        const items =
            res.body.playlist.entry?.map((song, index) =>
                ssNormalize.song(song, apiClientProps.server, index),
            ) || [];

        return {
            items,
            startIndex: 0,
            totalRecordCount: items.length,
        };
    },
    getPlayQueue: async ({ apiClientProps }) => {
        if (hasFeature(apiClientProps.server, ServerFeature.SERVER_PLAY_QUEUE)) {
            const res = await ssApiClient(apiClientProps).getPlayQueueByIndex();

            if (res.status !== 200) {
                throw new Error('Failed to get play queue');
            }

            const { changed, changedBy, currentIndex, entry, position, username } =
                res.body.playQueueByIndex || {}; // if there is no queue saved, playQueueByIndex may be undefined from a bug in Navidrome

            return {
                changed: changed ?? '',
                changedBy: changedBy ?? '',
                currentIndex: currentIndex ?? 0,
                entry: entry?.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
                positionMs: position ?? 0,
                username: username ?? '',
            };
        } else {
            const res = await ssApiClient(apiClientProps).getPlayQueue();

            if (res.status !== 200) {
                throw new Error('Failed to get play queue');
            }

            const { changed, changedBy, current, entry, position, username } = res.body.playQueue;

            return {
                changed,
                changedBy,
                currentIndex: current ? entry.findIndex((item) => item.id === current) : 0,
                entry: entry?.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
                positionMs: position ?? 0,
                username,
            };
        }
    },
    getRandomSongList: async (args) => {
        const { apiClientProps, query } = args;

        const res = await ssApiClient(apiClientProps).getRandomSongList({
            query: {
                fromYear: query.minYear,
                genre: query.genre,
                musicFolderId: getLibraryId(query.musicFolderId),
                size: query.limit,
                toYear: query.maxYear,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to get random songs');
        }

        const results = res.body.randomSongs?.song || [];
        const normalizedResults = results.map((song) =>
            ssNormalize.song(song, apiClientProps.server),
        );

        return {
            items: normalizedResults,
            startIndex: 0,
            totalRecordCount: normalizedResults.length,
        };
    },
    getRoles: async (args) => {
        const { apiClientProps } = args;

        const res = await ssApiClient(apiClientProps).getArtists({});

        if (res.status !== 200) {
            throw new Error('Failed to get artist list');
        }

        const roles = new Set<string>();

        for (const index of res.body.artists?.index || []) {
            for (const artist of index.artist) {
                for (const role of artist.roles || []) {
                    roles.add(role);
                }
            }
        }

        const final: Array<string | { label: string; value: string }> = Array.from(roles).sort();
        // Always add 'all artist' filter, even if there are no other roles
        // This is relevant when switching from a server which has roles to one with
        // no roles.
        final.splice(0, 0, { label: 'all artists', value: '' });
        return final;
    },
    getScanStatus: async (args) => {
        const { apiClientProps } = args;

        const res = await ssApiClient(apiClientProps).getScanStatus({ query: {} });

        if (res.status !== 200) {
            throw new Error('Failed to get scan status');
        }

        return {
            count: res.body.scanStatus.count,
            folderCount: res.body.scanStatus.folderCount,
            lastScan: res.body.scanStatus.lastScan,
            scanning: res.body.scanStatus.scanning,
        };
    },
    getServerInfo: async (args) => {
        const { apiClientProps } = args;

        const ping = await ssApiClient(apiClientProps).ping();

        if (ping.status !== 200) {
            throw new Error('Failed to ping server');
        }

        const features: ServerFeatures = {};

        if (!ping.body.openSubsonic || !ping.body.serverVersion) {
            return { features, version: ping.body.version };
        }

        const res = await ssApiClient(apiClientProps).getServerInfo();

        if (res.status !== 200) {
            throw new Error('Failed to get server extensions');
        }

        const subsonicFeatures: Record<string, number[]> = {};
        if (Array.isArray(res.body.openSubsonicExtensions)) {
            for (const extension of res.body.openSubsonicExtensions) {
                subsonicFeatures[extension.name] = extension.versions;
            }
        }

        if (subsonicFeatures[SubsonicExtensions.TRANSCODING]) {
            features.osTranscodeDecision = [1];
        }

        if (subsonicFeatures[SubsonicExtensions.SONG_LYRICS]) {
            features.lyricsMultipleStructured = subsonicFeatures[SubsonicExtensions.SONG_LYRICS];
        }

        if (subsonicFeatures[SubsonicExtensions.FORM_POST]) {
            features.osFormPost = [1];
        }

        if (subsonicFeatures[SubsonicExtensions.INDEX_BASED_QUEUE]) {
            features.serverPlayQueue = [1];
        }

        if (subsonicFeatures[SubsonicExtensions.PLAYBACK_REPORT]) {
            features.reportPlayback = [1];
        }
        try {
            const jukeboxStatus = await ssApiClient(apiClientProps).jukeboxControl({
                query: { action: 'status' },
            });

            if (jukeboxStatus.status === 200 && !(jukeboxStatus.body as any)?.error) {
                features[ServerFeature.JUKEBOX] = [1];
            } else {
                logger.warn(
                    'Jukebox endpoint returned an error payload:',
                    (jukeboxStatus.body as any)?.error,
                );
            }
        } catch (error) {
            logger.warn('Jukebox is not supported by this server:', error);
        }

        return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion };
    },
    getSimilarSongs: async (args) => {
        const { apiClientProps, query } = args;

        const res = await ssApiClient(apiClientProps).getSimilarSongs({
            query: {
                count: query.count,
                id: query.songId,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to get similar songs');
        }

        if (!res.body.similarSongs?.song) {
            return [];
        }

        return res.body.similarSongs.song.reduce<Song[]>((acc, song) => {
            if (song.id !== query.songId) {
                acc.push(ssNormalize.song(song, apiClientProps.server));
            }

            return acc;
        }, []);
    },
    getSongDetail: async (args) => {
        const { apiClientProps, query } = args;

        const res = await ssApiClient(apiClientProps).getSong({
            query: {
                id: query.id,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to get song detail');
        }

        return ssNormalize.song(res.body.song, apiClientProps.server);
    },
    getSongList: async ({ apiClientProps, query }) => {
        const fromAlbumPromises: Promise<ServerInferResponses<typeof contract.getAlbum>>[] = [];
        const artistDetailPromises: Promise<ServerInferResponses<typeof contract.getArtist>>[] = [];

        if (query.searchTerm) {
            const res = await ssApiClient(apiClientProps).search3({
                query: {
                    albumCount: 0,
                    albumOffset: 0,
                    artistCount: 0,
                    artistOffset: 0,
                    musicFolderId: getLibraryId(query.musicFolderId),
                    query: query.searchTerm || '',
                    songCount: query.limit,
                    songOffset: query.startIndex,
                },
            });

            if (res.status !== 200) {
                throw new Error('Failed to get song list');
            }

            return {
                items:
                    res.body.searchResult3?.song?.map((song) =>
                        ssNormalize.song(song, apiClientProps.server),
                    ) || [],
                startIndex: query.startIndex,
                totalRecordCount: null,
            };
        }

        if (query.genreIds) {
            const res = await ssApiClient(apiClientProps).getSongsByGenre({
                query: {
                    count: query.limit,
                    genre: query.genreIds[0],
                    musicFolderId: getLibraryId(query.musicFolderId),
                    offset: query.startIndex,
                },
            });

            if (res.status !== 200) {
                throw new Error('Failed to get song list');
            }

            const results = res.body.songsByGenre?.song || [];

            return {
                items: results.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
                startIndex: 0,
                totalRecordCount: null,
            };
        }

        if (query.favorite) {
            const res = await ssApiClient(apiClientProps).getStarred({
                query: {
                    musicFolderId: getLibraryId(query.musicFolderId),
                },
            });

            if (res.status !== 200) {
                throw new Error('Failed to get song list');
            }

            let allResults =
                (res.body.starred?.song || []).map((song) =>
                    ssNormalize.song(song, apiClientProps.server),
                ) || [];

            const filterArtistIds = query.albumArtistIds || query.artistIds;

            if (filterArtistIds?.length) {
                const idSet = new Set(filterArtistIds);
                allResults = allResults.filter((song) =>
                    song.albumArtists?.some((aa) => idSet.has(aa.id)),
                );
            }

            return sortAndPaginate(allResults, {
                limit: query.limit,
                sortBy: query.sortBy,
                sortFn: sortSongList,
                sortOrder: query.sortOrder,
                startIndex: query.startIndex,
            });
        }

        const artistIds = query.albumArtistIds || query.artistIds;

        if (query.albumIds || artistIds) {
            if (query.albumIds) {
                for (const albumId of query.albumIds) {
                    fromAlbumPromises.push(
                        ssApiClient(apiClientProps).getAlbum({
                            query: {
                                id: albumId,
                            },
                        }),
                    );
                }
            }

            if (artistIds) {
                for (const artistId of artistIds) {
                    artistDetailPromises.push(
                        ssApiClient(apiClientProps).getArtist({
                            query: {
                                id: artistId,
                            },
                        }),
                    );
                }

                const artistResult = await Promise.all(artistDetailPromises);

                const albums = artistResult.flatMap((artist) => {
                    if (artist.status !== 200) {
                        return [];
                    }

                    return artist.body.artist.album ?? [];
                });

                const albumIds = albums.map((album) => album.id);

                for (const albumId of albumIds) {
                    fromAlbumPromises.push(
                        ssApiClient(apiClientProps).getAlbum({
                            query: {
                                id: albumId.toString(),
                            },
                        }),
                    );
                }
            }

            let results: z.infer<typeof ssType._response.song>[] = [];

            if (fromAlbumPromises) {
                const albumsResult = await Promise.all(fromAlbumPromises);

                results = albumsResult.flatMap((album) => {
                    if (album.status !== 200) {
                        return [];
                    }

                    return album.body.album.song;
                });
            }

            return {
                items: results.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
                startIndex: 0,
                totalRecordCount: results.length,
            };
        }

        const res = await ssApiClient(apiClientProps).search3({
            query: {
                albumCount: 0,
                albumOffset: 0,
                artistCount: 0,
                artistOffset: 0,
                musicFolderId: getLibraryId(query.musicFolderId),
                query: query.searchTerm || '',
                songCount: query.limit,
                songOffset: query.startIndex,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to get song list');
        }

        return {
            items:
                res.body.searchResult3?.song?.map((song) =>
                    ssNormalize.song(song, apiClientProps.server),
                ) || [],
            startIndex: 0,
            totalRecordCount: null,
        };
    },
    getSongListCount: async (args) => {
        const { apiClientProps, query } = args;

        let fetchNextPage = true;
        let startIndex = 0;

        let fetchNextSection = true;
        let sectionIndex = 0;

        if (query.searchTerm) {
            let fetchNextPage = true;
            let startIndex = 0;
            let totalRecordCount = 0;

            while (fetchNextPage) {
                const res = await ssApiClient(apiClientProps).search3({
                    query: {
                        albumCount: 0,
                        albumOffset: 0,
                        artistCount: 0,
                        artistOffset: 0,
                        musicFolderId: getLibraryId(query.musicFolderId),
                        query: query.searchTerm || '',
                        songCount: MAX_SUBSONIC_ITEMS,
                        songOffset: startIndex,
                    },
                });

                if (res.status !== 200) {
                    throw new Error('Failed to get song list count');
                }

                const songCount = (res.body.searchResult3?.song || []).length || 0;

                totalRecordCount += songCount;
                startIndex += songCount;

                fetchNextPage = songCount === MAX_SUBSONIC_ITEMS;
            }

            return totalRecordCount;
        }

        if (query.genreIds) {
            let totalRecordCount = 0;

            // Rather than just do `getSongsByGenre` by groups of 500, instead
            // jump the offset 10x, and then backtrack on the last chunk. This improves
            // performance for extremely large libraries
            while (fetchNextSection) {
                const res = await ssApiClient(apiClientProps).getSongsByGenre({
                    query: {
                        count: 1,
                        genre: query.genreIds[0],
                        musicFolderId: getLibraryId(query.musicFolderId),
                        offset: sectionIndex,
                    },
                });

                if (res.status !== 200) {
                    throw new Error('Failed to get song list count');
                }

                const numberOfResults = (res.body.songsByGenre?.song || []).length || 0;

                if (numberOfResults !== 1) {
                    fetchNextSection = false;
                    startIndex = sectionIndex === 0 ? 0 : sectionIndex - SUBSONIC_FAST_BATCH_SIZE;
                    break;
                } else {
                    sectionIndex += SUBSONIC_FAST_BATCH_SIZE;
                }
            }

            while (fetchNextPage) {
                const res = await ssApiClient(apiClientProps).getSongsByGenre({
                    query: {
                        count: MAX_SUBSONIC_ITEMS,
                        genre: query.genreIds[0],
                        musicFolderId: getLibraryId(query.musicFolderId),
                        offset: startIndex,
                    },
                });

                if (res.status !== 200) {
                    throw new Error('Failed to get song list count');
                }

                const numberOfResults = (res.body.songsByGenre?.song || []).length || 0;

                totalRecordCount = startIndex + numberOfResults;
                startIndex += numberOfResults;

                fetchNextPage = numberOfResults === MAX_SUBSONIC_ITEMS;
            }

            return totalRecordCount;
        }

        if (query.favorite) {
            const res = await ssApiClient(apiClientProps).getStarred({
                query: {
                    musicFolderId: getLibraryId(query.musicFolderId),
                },
            });

            if (res.status !== 200) {
                throw new Error('Failed to get song list');
            }

            return (res.body.starred?.song || []).length || 0;
        }

        const artistIds = query.albumArtistIds || query.artistIds;

        if (query.albumIds || artistIds) {
            const fromAlbumPromises: Promise<ServerInferResponses<typeof contract.getAlbum>>[] = [];
            const artistDetailPromises: Promise<ServerInferResponses<typeof contract.getArtist>>[] =
                [];

            if (query.albumIds) {
                for (const albumId of query.albumIds) {
                    fromAlbumPromises.push(
                        ssApiClient(apiClientProps).getAlbum({
                            query: {
                                id: albumId,
                            },
                        }),
                    );
                }
            }

            if (artistIds) {
                for (const artistId of artistIds) {
                    artistDetailPromises.push(
                        ssApiClient(apiClientProps).getArtist({
                            query: {
                                id: artistId,
                            },
                        }),
                    );
                }

                const artistResult = await Promise.all(artistDetailPromises);

                const albums = artistResult.flatMap((artist) => {
                    if (artist.status !== 200) {
                        return [];
                    }

                    return artist.body.artist.album ?? [];
                });

                const albumIds = albums.map((album) => album.id);

                for (const albumId of albumIds) {
                    fromAlbumPromises.push(
                        ssApiClient(apiClientProps).getAlbum({
                            query: {
                                id: albumId.toString(),
                            },
                        }),
                    );
                }
            }

            let results: z.infer<typeof ssType._response.song>[] = [];

            if (fromAlbumPromises.length > 0) {
                const albumsResult = await Promise.all(fromAlbumPromises);

                results = albumsResult.flatMap((album) => {
                    if (album.status !== 200) {
                        return [];
                    }

                    return album.body.album.song;
                });
            }

            return results.length;
        }

        let totalRecordCount = 0;

        // Rather than just do `search3` by groups of 500, instead
        // jump the offset 10x, and then backtrack on the last chunk. This improves
        // performance for extremely large libraries
        while (fetchNextSection) {
            const res = await ssApiClient(apiClientProps).search3({
                query: {
                    albumCount: 0,
                    albumOffset: 0,
                    artistCount: 0,
                    artistOffset: 0,
                    musicFolderId: getLibraryId(query.musicFolderId),
                    query: query.searchTerm || '',
                    songCount: 1,
                    songOffset: sectionIndex,
                },
            });

            if (res.status !== 200) {
                throw new Error('Failed to get song list count');
            }

            const numberOfResults = (res.body.searchResult3?.song || []).length || 0;

            if (numberOfResults !== 1) {
                fetchNextSection = false;
                startIndex = sectionIndex === 0 ? 0 : sectionIndex - SUBSONIC_FAST_BATCH_SIZE;
                break;
            } else {
                sectionIndex += SUBSONIC_FAST_BATCH_SIZE;
            }
        }

        while (fetchNextPage) {
            const res = await ssApiClient(apiClientProps).search3({
                query: {
                    albumCount: 0,
                    albumOffset: 0,
                    artistCount: 0,
                    artistOffset: 0,
                    musicFolderId: getLibraryId(query.musicFolderId),
                    query: query.searchTerm || '',
                    songCount: MAX_SUBSONIC_ITEMS,
                    songOffset: startIndex,
                },
            });

            if (res.status !== 200) {
                throw new Error('Failed to get song list count');
            }

            const numberOfResults = (res.body.searchResult3?.song || []).length || 0;

            totalRecordCount = startIndex + numberOfResults;
            startIndex += numberOfResults;

            fetchNextPage = numberOfResults === MAX_SUBSONIC_ITEMS;
        }

        return totalRecordCount;
    },
    getStreamUrl: async ({ apiClientProps, query }) => {
        const { server } = apiClientProps;
        const { bitrate, format, id, mediaType = 'song', skipAutoTranscode, transcode } = query;

        const streamUrl = `${server?.url}/rest/stream.view?id=${id}&v=1.13.0&c=Feishin&${server?.credential}`;

        // If transcoding is explicitly enabled, just return the direct transcoded stream URL
        if (transcode) {
            return appendTranscodeParams(streamUrl, format, bitrate);
        }

        // Used in cases where MPV is the default player, since mpv handles basically every audio format
        if (skipAutoTranscode) {
            return streamUrl;
        }

        // If the server supports transcoding decision, always use it to determine if we need to transcode
        if (hasFeature(server, ServerFeature.OS_TRANSCODE_DECISION)) {
            const maxTranscodingAudioBitrate = 0;

            const directPlayProfiles = getDirectPlayProfiles();
            const transcodingProfiles = getDefaultTranscodingProfiles();

            const transcodeDecision = await ssApiClient(apiClientProps).getTranscodeDecision({
                body: {
                    codecProfiles: [],
                    directPlayProfiles,
                    maxAudioBitrate: 0,
                    maxTranscodingAudioBitrate,
                    name: 'Feishin',
                    platform: navigator.userAgent,
                    transcodingProfiles,
                },
                query: {
                    mediaId: id,
                    mediaType,
                },
            });

            // If the server returns an error for transcodeDecision, fall back to direct stream so that we don't break the player
            if (transcodeDecision.status !== 200) {
                logger.error(
                    `Failed to get transcode decision for song ${id}, falling back to direct stream`,
                );
                return streamUrl;
            }

            const td = transcodeDecision.body.transcodeDecision;
            const requiresTranscoding = !td?.canDirectPlay;

            // If the server does not require transcoding, just return the direct stream URL
            if (!requiresTranscoding) {
                return streamUrl;
            }

            logger.info(`Song ${id} requires transcoding: ${[td.transcodeReason].join(', ')}`);

            // If the server does not return transcode params, manually create the transcode params
            if (!td.transcodeParams) {
                return appendTranscodeParams(streamUrl, format, bitrate);
            }

            const transcodeStreamUrl = buildGetTranscodeStreamUrl(server, {
                mediaId: String(id),
                mediaType: (mediaType ?? 'song') as 'podcast' | 'song',
                offset: 0,
                transcodeParams: td.transcodeParams,
            });

            return transcodeStreamUrl;
        }

        return streamUrl;
    },
    getStructuredLyrics: async (args) => {
        const { apiClientProps, query } = args;
        const server = apiClientProps.server;
        const supportsStructuredLyrics = hasFeatureWithVersion(
            server,
            ServerFeature.LYRICS_MULTIPLE_STRUCTURED,
            1,
        );

        const supportsEnhancedLyrics = hasFeatureWithVersion(
            server,
            ServerFeature.LYRICS_MULTIPLE_STRUCTURED,
            2,
        );

        if (!supportsStructuredLyrics && !supportsEnhancedLyrics) {
            return [];
        }

        const res = await ssApiClient(apiClientProps).getStructuredLyrics({
            query: {
                enhanced: supportsEnhancedLyrics ? true : undefined,
                id: query.songId,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to get structured lyrics');
        }

        const lyrics = res.body.lyricsList?.structuredLyrics;

        if (!lyrics) {
            return [];
        }

        const source = apiClientProps.server?.name || 'music server';

        return lyrics.map((lyric) => mapStructuredLyric(lyric, source));
    },
    getTopSongs: async (args) => {
        const { apiClientProps, query } = args;

        const type = query.type === 'personal' ? 'personal' : 'community';

        if (type === 'community') {
            const res = await ssApiClient(apiClientProps).getTopSongsList({
                query: {
                    artist: query.artist,
                    count: query.limit,
                },
            });

            if (res.status !== 200) {
                throw new Error('Failed to get top songs');
            }

            return {
                items: (res.body.topSongs?.song || []).map((song) =>
                    ssNormalize.song(song, apiClientProps.server),
                ),
                startIndex: 0,
                totalRecordCount: res.body.topSongs?.song?.length || 0,
            };
        }

        const res = await SubsonicController.getSongList({
            apiClientProps,
            query: {
                artistIds: [query.artistId],
                sortBy: SongListSort.PLAY_COUNT,
                sortOrder: SortOrder.DESC,
                startIndex: 0,
            },
        });

        const songsWithPlayCount = orderBy(
            res.items.filter((song) => song.playCount > 0),
            ['playCount', 'albumId', 'trackNumber'],
            ['desc', 'asc', 'asc'],
        );

        return {
            items: songsWithPlayCount,
            startIndex: 0,
            totalRecordCount: res.totalRecordCount,
        };
    },
    getUserInfo: async (args) => {
        const { apiClientProps, query } = args;

        const res = await ssApiClient(apiClientProps).getUser({
            query: {
                username: query.username,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to get user info');
        }

        return {
            id: res.body.user.username,
            isAdmin: Boolean(res.body.user.adminRole),
            name: res.body.user.username,
        };
    },
    jukeboxControl: async (args) => {
        const { apiClientProps, query } = args;

        const res = await ssApiClient(apiClientProps).jukeboxControl({
            query: {
                action: query.action,
                gain: query.gain,
                id: query.id,
                index: query.index,
                offset: query.offset,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to control jukebox');
        }

        return res.body;
    },
    refreshItems: async (args) => {
        const { apiClientProps } = args;

        const res = await ssApiClient(apiClientProps).startScan({ query: {} });

        if (res.status !== 200) {
            throw new Error('Failed to start scan');
        }

        return null;
    },
    removeFromPlaylist: async ({ apiClientProps, query }) => {
        const res = await ssApiClient(apiClientProps).updatePlaylist({
            query: {
                playlistId: query.id,
                songIndexToRemove: query.songId,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to add to playlist');
        }

        return null;
    },
    replacePlaylist: async (args) => {
        const { apiClientProps, body, query } = args;

        // 1. Fetch existing songs from the playlist
        const existingSongsRes = await ssApiClient(apiClientProps).getPlaylist({
            query: {
                id: query.id,
            },
        });

        if (existingSongsRes.status !== 200) {
            throw new Error('Failed to fetch existing playlist songs');
        }

        const existingSongs =
            existingSongsRes.body.playlist.entry?.map((song) =>
                ssNormalize.song(song, apiClientProps.server),
            ) || [];

        // 2. Get playlist detail to get the name
        const playlistDetailRes = await ssApiClient(apiClientProps).getPlaylist({
            query: {
                id: query.id,
            },
        });

        if (playlistDetailRes.status !== 200) {
            throw new Error('Failed to get playlist detail');
        }

        const playlist = ssNormalize.playlist(
            playlistDetailRes.body.playlist,
            apiClientProps.server,
        );

        // 3. Make a backup of the playlist ids and their order, along with the id of the playlist and name
        const backup = {
            id: query.id,
            name: playlist.name,
            songIds: existingSongs.map((song) => song.id),
            timestamp: Date.now(),
        };

        // Store backup in IndexedDB using idb-keyval
        const backupKey = `playlist-backup-${query.id}`;
        await set(backupKey, backup);

        // 4. Remove all songs from the playlist (Subsonic uses indices, not IDs)
        if (existingSongs.length > 0) {
            // Get indices of all songs (0-based)
            // Remove in reverse order to avoid index shifting issues
            const songIndices = existingSongs.map((_, index) => index).reverse();

            const removeRes = await ssApiClient(apiClientProps).updatePlaylist({
                query: {
                    playlistId: query.id,
                    songIndexToRemove: songIndices.map((index) => index.toString()),
                },
            });

            if (removeRes.status !== 200) {
                throw new Error('Failed to remove songs from playlist');
            }
        }

        // 5. Add the new song ids to the playlist
        if (body.songId.length > 0) {
            const addRes = await ssApiClient(apiClientProps).updatePlaylist({
                query: {
                    playlistId: query.id,
                    songIdToAdd: body.songId,
                },
            });

            if (addRes.status !== 200) {
                throw new Error('Failed to add songs to playlist');
            }
        }

        return null;
    },
    savePlayQueue: async ({ apiClientProps, query }) => {
        if (hasFeature(apiClientProps.server, ServerFeature.SERVER_PLAY_QUEUE)) {
            const res = await ssApiClient(apiClientProps).savePlayQueueByIndex({
                query: {
                    currentIndex:
                        query.currentIndex !== undefined && query.currentIndex < query.songs.length
                            ? Math.max(0, query.currentIndex)
                            : undefined,
                    id: query.songs,
                    position: query.positionMs,
                },
            });

            if (res.status !== 200) {
                throw new Error('Failed to save play queue');
            }
        } else {
            const res = await ssApiClient(apiClientProps).savePlayQueue({
                query: {
                    current:
                        query.currentIndex !== undefined && query.currentIndex < query.songs.length
                            ? query.songs[query.currentIndex]
                            : undefined,
                    id: query.songs,
                    position: query.positionMs,
                },
            });

            if (res.status !== 200) {
                throw new Error('Failed to save play queue');
            }
        }
    },
    scrobble: async (args) => {
        const { apiClientProps, query } = args;

        if (query.submission || query.event === 'start') {
            const res = await ssApiClient(apiClientProps).scrobble({
                query: {
                    id: query.id,
                    submission: query.submission,
                },
            });

            if (res.status !== 200) {
                throw new Error('Failed to scrobble');
            }

            if (query.submission) {
                return null;
            }
        }

        if (hasFeature(apiClientProps.server, ServerFeature.REPORT_PLAYBACK)) {
            const defaultParams = {
                ignoreScrobble: true,
                mediaId: query.id,
                mediaType: query.mediaType,
                playbackRate: query.playbackRate,
                positionMs: Math.round(query.position ?? 0),
            };

            const reportPlayback = async (state: 'paused' | 'playing' | 'starting' | 'stopped') => {
                const res = await ssApiClient(apiClientProps).reportPlayback({
                    query: {
                        ...defaultParams,
                        state,
                    },
                });

                if (res.status !== 200) {
                    throw new Error('Failed to report playback');
                }
            };

            switch (query.event) {
                case 'pause':
                    await reportPlayback('paused');
                    break;
                case 'start':
                    await reportPlayback('starting');
                    await reportPlayback('playing');
                    break;
                case 'stop':
                    await reportPlayback('stopped');
                    break;
                case 'unpause':
                    await reportPlayback('playing');
                    break;
                default:
                    break;
            }

            return null;
        }

        return null;
    },
    search: async (args) => {
        const { apiClientProps, query } = args;

        const res = await ssApiClient(apiClientProps).search3({
            query: {
                albumCount: query.albumLimit,
                albumOffset: query.albumStartIndex,
                artistCount: query.albumArtistLimit,
                artistOffset: query.albumArtistStartIndex,
                musicFolderId: getLibraryId(query.musicFolderId),
                query: query.query,
                songCount: query.songLimit,
                songOffset: query.songStartIndex,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to search');
        }

        return {
            albumArtists: (res.body.searchResult3?.artist || [])?.map((artist) =>
                ssNormalize.albumArtist(artist, apiClientProps.server),
            ),
            albums: (res.body.searchResult3?.album || []).map((album) =>
                ssNormalize.album(album, apiClientProps.server),
            ),
            songs: (res.body.searchResult3?.song || []).map((song) =>
                ssNormalize.song(song, apiClientProps.server),
            ),
        };
    },
    setPlaylistSongs: async (args) => {
        const { apiClientProps, body } = args;

        const res = await ssApiClient(apiClientProps).createPlaylist({
            query: {
                playlistId: body.id,
                songId: body.songIds,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to update playlist songs');
        }

        return null;
    },
    setRating: async (args) => {
        const { apiClientProps, query } = args;

        const itemIds = query.id;

        for (const id of itemIds) {
            await ssApiClient(apiClientProps).setRating({
                query: {
                    id,
                    rating: query.rating,
                },
            });
        }

        return null;
    },
    startLibraryScan: async (args) => {
        const { apiClientProps } = args;

        const res = await ssApiClient(apiClientProps).startScan({ query: {} });

        if (res.status !== 200) {
            throw new Error('Failed to start library scan');
        }

        return null;
    },
    updateInternetRadioStation: async (args) => {
        const { apiClientProps, body, query } = args;

        const res = await ssApiClient(apiClientProps).updateInternetRadioStation({
            query: {
                homepageUrl: body.homepageUrl,
                id: query.id,
                name: body.name,
                streamUrl: body.streamUrl,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to update internet radio station');
        }

        return null;
    },
    updatePlaylist: async (args) => {
        const { apiClientProps, body, query } = args;

        const res = await ssApiClient(apiClientProps).updatePlaylist({
            query: {
                comment: body.comment,
                name: body.name,
                playlistId: query.id,
                public: body.public,
            },
        });

        if (res.status !== 200) {
            throw new Error('Failed to add to playlist');
        }

        return null;
    },
};

function getLibraryId(musicFolderId?: string | string[]) {
    return Array.isArray(musicFolderId) ? musicFolderId[0] : musicFolderId;
}
