import CryptoJS from 'crypto-js';

import InvalidParameterError from '@/errors/types/invalid-parameter';
import type { Route } from '@/types';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';

import { getObjectFields, MIXCLOUD_CONFIG, TYPE_CONFIG, TYPE_NAMES } from './config';

export const route: Route = {
    path: '/:username/:type?',
    categories: ['multimedia'],
    example: '/mixcloud/dholbach/uploads',
    parameters: {
        username: 'Username, can be found in URL',
        type: 'Type, see below, uploads by default',
    },
    features: {
        requireConfig: false,
        requirePuppeteer: false,
        antiCrawler: false,
        supportBT: false,
        supportPodcast: true,
        supportScihub: false,
    },
    radar: [
        {
            source: ['mixcloud.com/:username/:type?'],
        },
        {
            source: ['www.mixcloud.com/:username/:type?'],
        },
    ],
    name: 'User',
    maintainers: ['Misaka13514'],
    handler,
    description: `| Shows   | Reposts | Favorites | History | Stream |
| ------- | ------- | --------- | ------- | ------ |
| uploads | reposts | favorites | listens | stream |`,
};

async function callApi(objectType: string, objectFields: string, username: string, slug?: string) {
    const lookupKey = objectType + 'Lookup';
    const headers = MIXCLOUD_CONFIG.headers;

    const lookupParams = slug ? `, slug: "${slug}"` : '';
    const query = /* GraphQL */ `{
    ${lookupKey}(lookup: {username: "${username}"${lookupParams}}) {
      ${objectFields}
    }
  }`;

    const response = await got({
        method: 'post',
        url: MIXCLOUD_CONFIG.graphqlURL,
        headers,
        json: { query },
    });

    return response.data.data[lookupKey];
}

// https://github.com/yt-dlp/yt-dlp/commits/master/yt_dlp/extractor/mixcloud.py
function decryptXorCipher(key: string, ciphertext: string): string {
    const decoded = CryptoJS.enc.Base64.parse(ciphertext).toString(CryptoJS.enc.Utf8);
    return [...decoded].map((ch, i) => String.fromCodePoint((ch.codePointAt(0) || 0) ^ (key.codePointAt(i % key.length) || 0))).join('');
}

function tryDecrypt(ciphertext: string | undefined): string {
    if (!ciphertext) {
        return '';
    }
    try {
        return decryptXorCipher(MIXCLOUD_CONFIG.decryptionKey, ciphertext);
    } catch {
        return ciphertext;
    }
}

function getCloudcast(node: any, type: string): any {
    if (type === 'playlist' || type === 'listens') {
        return node.cloudcast;
    }
    return node;
}

function getPlaylistTitle(displayName: string, type: string, playlistName?: string): string {
    if (type === 'playlist' && playlistName) {
        return `Mixcloud - ${displayName}'s Playlist: ${playlistName}`;
    }
    return `Mixcloud - ${displayName}'s ${TYPE_NAMES[type] || type}`;
}

function getPlaylistLink(username: string, type: string, playlistSlug?: string): string {
    const host = MIXCLOUD_CONFIG.host;
    if (type === 'playlist' && playlistSlug) {
        return `${host}/${username}/playlists/${playlistSlug}/`;
    }
    return `${host}/${username}/${type === 'uploads' ? '' : type + '/'}`;
}

export async function handler(ctx) {
    const username = ctx.req.param('username');
    const playlistSlug = ctx.req.param('playlist');
    const type = ctx.req.param('type') ?? (playlistSlug ? 'playlist' : 'uploads');

    if (!TYPE_CONFIG[type]) {
        throw new InvalidParameterError(`Invalid type: ${type}`);
    }

    const { objectType, objectFields } = getObjectFields(type);

    const data = await callApi(objectType, objectFields, username, playlistSlug);

    if (!data) {
        throw new Error(`${type === 'playlist' ? 'Playlist' : 'User'} not found`);
    }

    const isPlaylist = type === 'playlist';
    const displayName = isPlaylist ? username : data.displayName;
    const description = isPlaylist ? data.description : data.biog;
    const picture = data.picture;
    const image = picture && picture.urlRoot ? `${MIXCLOUD_CONFIG.imageBaseURL}${picture.urlRoot}` : '';

    const itemsData = data[TYPE_CONFIG[type]];
    const edges = itemsData?.edges || [];

    const items = edges
        .map((edge: any) => {
            const cloudcast = getCloudcast(edge.node, type);

            if (!cloudcast) {
                return null;
            }

            const streamInfo = cloudcast.streamInfo || {};
            const enclosureUrl = tryDecrypt(streamInfo.url);
            const tags = cloudcast.tags?.map((t: any) => t.tag?.name).filter(Boolean) || [];

            let richDescription = cloudcast.description?.replaceAll('\n', '<br>') || '';

            if (cloudcast.featuringArtistList && cloudcast.featuringArtistList.length > 0) {
                const artists = cloudcast.featuringArtistList.slice(0, 5).join(', ');
                richDescription += richDescription ? '<br><br>' : '';
                richDescription += `<strong>Featured Artists:</strong> ${artists}`;
                if (cloudcast.featuringArtistList.length > 5) {
                    richDescription += ` and ${cloudcast.featuringArtistList.length - 5} more...`;
                }
            }

            const stats: string[] = [];
            if (cloudcast.plays) {
                stats.push(`${cloudcast.plays} plays`);
            }
            if (cloudcast.favorites?.totalCount) {
                stats.push(`${cloudcast.favorites.totalCount} favorites`);
            }
            if (cloudcast.reposts?.totalCount) {
                stats.push(`${cloudcast.reposts.totalCount} reposts`);
            }
            if (cloudcast.comments?.totalCount) {
                stats.push(`${cloudcast.comments.totalCount} comments`);
            }

            if (stats.length > 0) {
                richDescription += richDescription ? '<br><br>' : '';
                richDescription += `<em>${stats.join(' • ')}</em>`;
            }

            return {
                title: cloudcast.name,
                author: cloudcast.owner?.displayName || username,
                description: richDescription,
                pubDate: parseDate(cloudcast.publishDate),
                guid: Buffer.from(cloudcast.id, 'base64').toString(),
                link: `${MIXCLOUD_CONFIG.host}/${cloudcast.owner?.username || username}/${cloudcast.slug}`,
                itunes_item_image: cloudcast.picture?.url || '',
                itunes_duration: cloudcast.audioLength,
                enclosure_url: enclosureUrl,
                enclosure_type: 'audio/x-m4a',
                itunes_author: cloudcast.owner?.displayName || username,
                category: tags,
                comments: cloudcast.comments?.totalCount || 0,
                upvotes: cloudcast.favorites?.totalCount || 0,
                media: enclosureUrl
                    ? {
                          content: {
                              url: enclosureUrl,
                              type: 'audio/x-m4a',
                              duration: cloudcast.audioLength,
                          },
                          thumbnail: cloudcast.picture?.url
                              ? {
                                    url: cloudcast.picture.url,
                                }
                              : undefined,
                      }
                    : undefined,
            };
        })
        .filter(Boolean);

    const title = getPlaylistTitle(displayName, type, data.name);
    const link = getPlaylistLink(username, type, playlistSlug);

    return {
        title,
        description: description?.replaceAll('\n', '<br>') || '',
        itunes_author: displayName,
        image,
        link,
        item: items,
    };
}
