import { AxiosHeaders } from 'axios';
import isElectron from 'is-electron';
import orderBy from 'lodash/orderBy';
import shuffle from 'lodash/shuffle';
import semverCoerce from 'semver/functions/coerce';
import semverGte from 'semver/functions/gte';
import { z } from 'zod';

import {
    Album,
    AlbumArtist,
    AlbumArtistListSort,
    AlbumListSort,
    ArtistListSort,
    InternetRadioStation,
    LibraryItem,
    RadioListSort,
    ServerListItem,
    Song,
    SongListSort,
    SortOrder,
} from '/@/shared/types/domain-types';
import { ServerFeature } from '/@/shared/types/features-types';

// Since ts-rest client returns a strict response type, we need to add the headers to the body object
export const resultWithHeaders = <ItemType extends z.ZodTypeAny>(itemSchema: ItemType) => {
    return z.object({
        data: itemSchema,
        headers: z.instanceof(AxiosHeaders),
    });
};

export const resultSubsonicBaseResponse = <ItemType extends z.ZodRawShape>(
    itemSchema: ItemType,
) => {
    return z.object({
        'subsonic-response': z
            .object({
                status: z.string(),
                version: z.string(),
            })
            .extend(itemSchema),
    });
};

export const hasFeature = (server: null | ServerListItem, feature: ServerFeature): boolean => {
    if (!server || !server.features) {
        return false;
    }

    return (server.features[feature]?.length || 0) > 0;
};

export const hasFeatureWithVersion = (
    server: null | ServerListItem,
    feature: ServerFeature,
    version: number,
): boolean => {
    if (!server || !server.features) {
        return false;
    }

    return (server.features[feature] ?? []).includes(version);
};

export type VersionInfo = ReadonlyArray<
    [string, Partial<Record<ServerFeature, readonly number[]>>]
>;

/**
 * Returns the available server features given the version string.
 * @param versionInfo a list, in DECREASING VERSION order, of the features supported by the server.
 *  The first version match will automatically consider the rest matched.
 * @example
 * ```
 * // The CORRECT way to order
 * const VERSION_INFO: VersionInfo = [
 *   ['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
 *   ['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
 * ];
 * // INCORRECT way to order
 * const VERSION_INFO: VersionInfo = [
 *   ['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
 *   ['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
 * ];
 *  ```
 * @param version the version string (SemVer)
 * @returns a Record containing the matched features (if any) and their versions
 */
export const getFeatures = (
    versionInfo: VersionInfo,
    version: string,
): Partial<Record<ServerFeature, number[]>> => {
    const cleanVersion = semverCoerce(version);
    const features: Partial<Record<ServerFeature, number[]>> = {};
    let matched = cleanVersion === null;

    for (const [version, supportedFeatures] of versionInfo) {
        if (!matched) {
            matched = semverGte(cleanVersion!, version);
        }

        if (matched) {
            for (const [feature, feat] of Object.entries(supportedFeatures)) {
                if (feature in features) {
                    features[feature].push(...feat);
                } else {
                    features[feature] = [...feat];
                }
            }
        }
    }

    return features;
};

export const getClientType = (): string => {
    if (isElectron()) {
        return 'Desktop Client';
    }
    const agent = navigator.userAgent;
    switch (true) {
        case agent.toLowerCase().indexOf('edge') > -1:
            return 'Microsoft Edge';
        case agent.toLowerCase().indexOf('edg/') > -1:
            return 'Edge Chromium'; // Match also / to avoid matching for the older Edge
        case agent.toLowerCase().indexOf('opr') > -1:
            return 'Opera';
        case agent.toLowerCase().indexOf('chrome') > -1:
            return 'Chrome';
        case agent.toLowerCase().indexOf('trident') > -1:
            return 'Internet Explorer';
        case agent.toLowerCase().indexOf('firefox') > -1:
            return 'Firefox';
        case agent.toLowerCase().indexOf('safari') > -1:
            return 'Safari';
        default:
            return 'PC';
    }
};

export const SEPARATOR_STRING = ' • ';

export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: SortOrder) => {
    let results: Song[] = songs;

    const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';

    switch (sortBy) {
        case SongListSort.ALBUM:
            results = orderBy(
                results,
                [(v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
                [order, order, order],
            );
            break;

        case SongListSort.ALBUM_ARTIST:
            results = orderBy(
                results,
                [
                    (v) => v.albumArtists[0]?.name.toLowerCase(),
                    (v) => v.album?.toLowerCase(),
                    'discNumber',
                    'trackNumber',
                ],
                [order, order, order, order],
            );
            break;

        case SongListSort.ARTIST:
            results = orderBy(
                results,
                [(v) => v.artistName?.toLowerCase(), 'discNumber', 'trackNumber'],
                [order, order, order, order],
            );
            break;

        case SongListSort.BPM:
            results = orderBy(
                results,
                ['bpm', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
                [order, order, order, order],
            );
            break;

        case SongListSort.CHANNELS:
            results = orderBy(
                results,
                ['channels', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
                [order, order, order, order],
            );
            break;

        case SongListSort.COMMENT:
            results = orderBy(
                results,
                ['comment', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
                [order, order, order, order],
            );
            break;

        case SongListSort.DURATION:
            results = orderBy(
                results,
                ['duration', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
                [order, order, order, order],
            );
            break;

        case SongListSort.FAVORITED:
            results = orderBy(
                results,
                [
                    'userFavorite',
                    (v) => v.name.toLowerCase(),
                    (v) => v.album?.toLowerCase(),
                    'discNumber',
                    'trackNumber',
                ],
                [order, order, order, order, order],
            );
            break;

        case SongListSort.GENRE:
            results = orderBy(
                results,
                [
                    (v) => v.genres?.[0]?.name.toLowerCase(),
                    (v) => v.album?.toLowerCase(),
                    'discNumber',
                    'trackNumber',
                ],
                [order, order, order, order],
            );
            break;

        case SongListSort.ID:
            results = [...results];

            if (order === 'desc') {
                results.reverse();
            }
            break;

        case SongListSort.NAME:
            results = orderBy(
                results,
                [(v) => v.name.toLowerCase(), (v) => v.album?.toLowerCase()],
                [order, order],
            );
            break;

        case SongListSort.PLAY_COUNT:
            results = orderBy(
                results,
                ['playCount', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
                [order, order, order, order],
            );
            break;

        case SongListSort.RANDOM:
            results = shuffle(results);
            break;

        case SongListSort.RATING:
            results = orderBy(
                results,
                [
                    'userRating',
                    (v) => v.name.toLowerCase(),
                    (v) => v.album?.toLowerCase(),
                    'discNumber',
                    'trackNumber',
                ],
                [order, order, order, order, order],
            );
            break;

        case SongListSort.RECENTLY_ADDED:
            results = orderBy(
                results,
                [
                    (v) => {
                        const x = v.createdAt;
                        if (x == null) return null;
                        const d = new Date(x);
                        return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
                    },
                    (v) => v.album?.toLowerCase(),
                    'discNumber',
                    'trackNumber',
                ],
                [order, order, order, order],
            );
            break;

        case SongListSort.RECENTLY_PLAYED:
            results = orderBy(
                results,
                ['lastPlayedAt', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
                [order, order, order, order],
            );
            break;

        case SongListSort.RELEASE_DATE:
            results = orderBy(
                results,
                ['releaseDate', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
                [order, order, order, order],
            );
            break;

        case SongListSort.RELEASE_YEAR:
            results = orderBy(
                results,
                ['releaseYear', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
                [order, order, order, order],
            );
            break;

        case SongListSort.SORT_NAME:
            results = orderBy(results, [(v) => v.sortName ?? v.name], [order]);
            break;

        case SongListSort.YEAR:
            results = orderBy(
                results,
                ['year', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
                [order, order, order, order],
            );
            break;

        default:
            break;
    }

    return results;
};

export const sortSongsByFetchedOrder = (
    songs: Song[],
    fetchedIds: string[],
    itemType: LibraryItem,
): Song[] => {
    // For folders, songs are already in the correct order
    if (itemType === LibraryItem.FOLDER) {
        return songs;
    }

    // Group songs by the fetched ID they belong to
    const songsByFetchedId = new Map<string, Song[]>();
    const fetchedIdsSet = new Set(fetchedIds);

    for (const song of songs) {
        let matchedId: string | undefined;

        switch (itemType) {
            case LibraryItem.ALBUM:
                matchedId = fetchedIdsSet.has(song.albumId) ? song.albumId : undefined;
                break;
            case LibraryItem.ALBUM_ARTIST:
                matchedId = song.albumArtists.find((a) => fetchedIdsSet.has(a.id))?.id;
                break;
            case LibraryItem.ARTIST:
                matchedId = song.artists.find((a) => fetchedIdsSet.has(a.id))?.id;
                break;
            case LibraryItem.GENRE:
                matchedId = song.genres.find((a) => fetchedIdsSet.has(a.id))?.id;
                break;
            case LibraryItem.PLAYLIST:
                // For playlists, we might need to track which playlist each song came from
                // This is a simplified approach - you may need to adjust based on your data structure
                matchedId =
                    song.playlistItemId && fetchedIdsSet.has(song.playlistItemId)
                        ? song.playlistItemId
                        : undefined;
                break;
            default:
                break;
        }

        if (matchedId) {
            if (!songsByFetchedId.has(matchedId)) {
                songsByFetchedId.set(matchedId, []);
            }
            songsByFetchedId.get(matchedId)!.push(song);
        }
    }

    // Sort each group by discNumber and trackNumber
    // Skip sorting for ALBUM_ARTIST as songs are already sorted by the API
    for (const [fetchedId, groupSongs] of songsByFetchedId.entries()) {
        if (itemType === LibraryItem.ALBUM_ARTIST) {
            continue;
        }
        const sortedGroup = orderBy(groupSongs, ['discNumber', 'trackNumber'], ['asc', 'asc']);
        songsByFetchedId.set(fetchedId, sortedGroup);
    }

    // Combine groups in the order of fetchedIds
    const result: Song[] = [];
    for (const fetchedId of fetchedIds) {
        const groupSongs = songsByFetchedId.get(fetchedId);
        if (groupSongs) {
            result.push(...groupSongs);
        }
    }

    // Add any songs that didn't match any fetched ID at the end
    const matchedIds = new Set(result.map((s) => s.id));
    const unmatchedSongs = songs.filter((s) => !matchedIds.has(s.id));
    if (unmatchedSongs.length > 0) {
        // Skip sorting for ALBUM_ARTIST as songs are already sorted by the API
        if (itemType === LibraryItem.ALBUM_ARTIST) {
            result.push(...unmatchedSongs);
        } else {
            const sortedUnmatched = orderBy(
                unmatchedSongs,
                ['discNumber', 'trackNumber'],
                ['asc', 'asc'],
            );
            result.push(...sortedUnmatched);
        }
    }

    return result;
};

export const sortAlbumArtistList = (
    artists: AlbumArtist[],
    sortBy: AlbumArtistListSort | ArtistListSort,
    sortOrder: SortOrder,
) => {
    const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';

    let results = artists;

    switch (sortBy) {
        case AlbumArtistListSort.ALBUM_COUNT:
            results = orderBy(artists, ['albumCount', (v) => v.name.toLowerCase()], [order, 'asc']);
            break;

        case AlbumArtistListSort.FAVORITED:
            results = orderBy(artists, ['starred'], [order]);
            break;

        case AlbumArtistListSort.NAME:
            results = orderBy(artists, [(v) => v.name.toLowerCase()], [order]);
            break;

        case AlbumArtistListSort.RATING:
            results = orderBy(artists, ['userRating'], [order]);
            break;

        default:
            break;
    }

    return results;
};

export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder: SortOrder) => {
    let results = albums;

    const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';

    switch (sortBy) {
        case AlbumListSort.ALBUM_ARTIST:
            results = orderBy(
                results,
                ['albumArtist', (v) => v.name.toLowerCase()],
                [order, 'asc'],
            );
            break;
        case AlbumListSort.DURATION:
            results = orderBy(results, ['duration'], [order]);
            break;
        case AlbumListSort.FAVORITED:
            results = orderBy(results, ['starred'], [order]);
            break;
        case AlbumListSort.ID:
            results = sortOrder === SortOrder.DESC ? [...results].reverse() : results;
            break;
        case AlbumListSort.NAME:
            results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
            break;
        case AlbumListSort.PLAY_COUNT:
            results = orderBy(results, ['playCount'], [order]);
            break;
        case AlbumListSort.RANDOM:
            results = shuffle(results);
            break;
        case AlbumListSort.RATING:
            results = orderBy(results, ['userRating'], [order]);
            break;
        case AlbumListSort.RECENTLY_ADDED:
            results = orderBy(results, ['createdAt'], [order]);
            break;
        case AlbumListSort.RECENTLY_PLAYED:
            results = orderBy(results, ['lastPlayedAt'], [order]);
            break;
        case AlbumListSort.RELEASE_DATE:
            results = orderBy(
                results,
                [
                    (v) => {
                        if (v.originalDate) {
                            return new Date(v.originalDate).getTime();
                        }

                        // Fallback to the first day of the original year
                        if (v.originalYear) {
                            return new Date(v.originalYear, 0, 1).getTime();
                        }
                        return 0;
                    },
                    (v) => {
                        if (v.releaseDate) {
                            return new Date(v.releaseDate).getTime();
                        }

                        // Fallback to the first day of the release year
                        if (v.releaseYear) {
                            return new Date(v.releaseYear, 0, 1).getTime();
                        }
                        return 0;
                    },
                    (v) => v.name.toLowerCase(),
                ],
                [order, order, 'asc'],
            );
            break;
        case AlbumListSort.SONG_COUNT:
            results = orderBy(results, ['songCount'], [order]);
            break;
        case AlbumListSort.SORT_NAME:
            results = orderBy(results, [(v) => v.sortName ?? v.name], [order]);
            break;
        case AlbumListSort.YEAR:
            results = orderBy(results, ['releaseYear'], [order]);
            break;
        default:
            break;
    }

    return results;
};

export const sortRadioList = (
    stations: InternetRadioStation[],
    sortBy: RadioListSort,
    sortOrder: SortOrder,
) => {
    let results = stations;

    const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';

    switch (sortBy) {
        case RadioListSort.ID:
            results = [...results];
            if (order === 'desc') {
                results.reverse();
            }
            break;
        case RadioListSort.NAME:
            results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
            break;
        default:
            break;
    }

    return results;
};

export const replacePathPrefix = (path: string, replacePrefix?: string, addPrefix?: string) => {
    let newPath = path;
    if (replacePrefix && newPath.startsWith(replacePrefix)) {
        newPath = newPath.slice(replacePrefix.length);
    }

    return addPrefix ? addPrefix + newPath : newPath;
};
