/*
 * Copyright (C) Contributors to the Suwayomi project
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

import path from 'path';
import fs from 'fs';
import readline from 'readline';
import { ControlledPromise } from '@/lib/ControlledPromise.ts';
import tokens from '../tokens.json';
import {
    ActionByTranslationUrlByUserUrl,
    ContributionsByLanguage,
    LanguageNameByTranslationUrl,
    UsernameByUserUrl,
    UserUrlsByTranslationUrl,
    WeblateChangeResult,
    WeblateLanguageStatistic,
} from '../weblate/Weblate.types.ts';
import {
    fetchWeblateChanges,
    fetchWeblateLanguageStats,
    getLanguageName,
    getUsername,
    getWeblateLanguageStatsFor,
    meetsTranslatedPercentThreshold,
    validateWeblateDates,
} from '../weblate/Weblate.utils.ts';
import {
    creditRelevantActions,
    TRANSLATED_PERCENT_THRESHOLD,
    TRANSLATION_CHANGELOG_YARG_OPTIONS_DEFAULT,
} from '../weblate/Weblate.constants.ts';

const extractContributionInfoFromChanges = (
    changes: WeblateChangeResult[],
): {
    userUrlsByTranslationUrl: UserUrlsByTranslationUrl;
    actionsByTranslationUrlByUserUrl: ActionByTranslationUrlByUserUrl;
    actions: Record<number, string>;
} => {
    const userUrlsByTranslationUrl: UserUrlsByTranslationUrl = {};
    const actionsByTranslationUrlByUserUrl: ActionByTranslationUrlByUserUrl = {};
    const actions: Record<number, string> = {};

    changes.forEach((change) => {
        // eslint-disable-next-line @typescript-eslint/naming-convention
        const { translation: translationUrl, action, action_name, user: userUrl } = change;
        if (userUrl === null) {
            return;
        }

        userUrlsByTranslationUrl[translationUrl] = [
            ...new Set([...(userUrlsByTranslationUrl[translationUrl] ?? []), userUrl]),
        ];
        actions[action] = action_name;
        actionsByTranslationUrlByUserUrl[userUrl] = {
            ...actionsByTranslationUrlByUserUrl[userUrl],
            [translationUrl]: {
                count: (actionsByTranslationUrlByUserUrl[userUrl]?.[translationUrl]?.count ?? 0) + 1,
                actions: {
                    ...actionsByTranslationUrlByUserUrl[userUrl]?.[translationUrl]?.actions,
                    [action]: (actionsByTranslationUrlByUserUrl[userUrl]?.[translationUrl]?.actions[action] ?? 0) + 1,
                },
            },
        };
    });

    return { userUrlsByTranslationUrl, actionsByTranslationUrlByUserUrl, actions };
};

const getUsernameByUserUrlMap = async (
    userUrlsByTranslationUrl: UserUrlsByTranslationUrl,
    authToken: string,
): Promise<Record<string, string>> =>
    Object.fromEntries(
        (await Promise.all(
            Object.values(userUrlsByTranslationUrl)
                .flat()
                .map(async (userUrl) => [userUrl, await getUsername(userUrl, authToken)]),
        )) satisfies [string, string][],
    );

const getLanguageNameByTranslationUrl = async (
    userUrlsByTranslationUrl: UserUrlsByTranslationUrl,
    authToken: string,
): Promise<LanguageNameByTranslationUrl> =>
    Object.fromEntries(
        (await Promise.all(
            Object.keys(userUrlsByTranslationUrl)
                .flat()
                .map(async (translationUrl) => [translationUrl, await getLanguageName(translationUrl, authToken)]),
        )) satisfies [string, string][],
    );

const getUserContributionByLanguage = (
    userUrlsByTranslationUrl: UserUrlsByTranslationUrl,
    languageNameByTranslationUrl: LanguageNameByTranslationUrl,
    usernameByUserUrl: UsernameByUserUrl,
    actionsByTranslationUrlByUserUrl: ActionByTranslationUrlByUserUrl,
) =>
    Object.fromEntries(
        Object.entries(userUrlsByTranslationUrl).map(([translationUrl, userUrls]) => {
            const languageName = languageNameByTranslationUrl[translationUrl];
            const contributors = userUrls
                .map((userUrl) => {
                    const username = usernameByUserUrl[userUrl];
                    const contributionCount = actionsByTranslationUrlByUserUrl[userUrl][translationUrl].count;

                    return { username, contributionCount };
                })
                .sort((userA, userB) => userB.contributionCount - userA.contributionCount);

            return [languageName, contributors];
        }),
    );

const doesLanguageOfChangeMeetTranslatePercentThreshold = (
    change: WeblateChangeResult,
    stats: WeblateLanguageStatistic[],
): boolean => {
    // format: 'https://hosted.weblate.org/api/translations/suwayomi/suwayomi-webui/ar/'
    const code = change.translation.split('/').slice(-2)[0];
    const langaugeStats = getWeblateLanguageStatsFor(code, stats);

    return meetsTranslatedPercentThreshold(langaugeStats.translated_percent);
};

const getContributorsForRange = async (url: string, authToken: string): Promise<ContributionsByLanguage> => {
    const weblateChanges = await fetchWeblateChanges(url, authToken);
    const weblateLanguageStats = await fetchWeblateLanguageStats();

    const validWeblateChanges = weblateChanges.filter((change) =>
        doesLanguageOfChangeMeetTranslatePercentThreshold(change, weblateLanguageStats),
    );

    const invalidWeblateChangesAmount = Math.abs(validWeblateChanges.length - weblateChanges.length);
    if (invalidWeblateChangesAmount > 0) {
        console.log(
            `Filtered out ${invalidWeblateChangesAmount} invalid change(s) due languages not meeting the translated percent threshold (${TRANSLATED_PERCENT_THRESHOLD})`,
        );
    }

    const { userUrlsByTranslationUrl, actionsByTranslationUrlByUserUrl } =
        extractContributionInfoFromChanges(validWeblateChanges);

    const usernameByUserUrl = await getUsernameByUserUrlMap(userUrlsByTranslationUrl, authToken);
    const languageNameByTranslationUrl = await getLanguageNameByTranslationUrl(userUrlsByTranslationUrl, authToken);

    return getUserContributionByLanguage(
        userUrlsByTranslationUrl,
        languageNameByTranslationUrl,
        usernameByUserUrl,
        actionsByTranslationUrlByUserUrl,
    );
};

const getLanguageLinesFromChangelog = (): Promise<string[]> => {
    const fileReadPromise = new ControlledPromise<string[]>();

    const changelogFile = path.resolve(import.meta.dirname, '../../../CHANGELOG.md');
    const fileStream = fs.createReadStream(changelogFile);
    const fileLineReader = readline.createInterface({
        input: fileStream,
        crlfDelay: Infinity, // Recognize all line breaks (CR LF) as a single line break
    });

    const translationRelevantLines: string[] = [];
    let isTranslationSection = false;
    fileLineReader.on('line', (line) => {
        if (line.toLowerCase() === '## Translations'.toLowerCase()) {
            isTranslationSection = true;
            return;
        }

        if (isTranslationSection && line.toLowerCase() === '## Full Changelog'.toLowerCase()) {
            isTranslationSection = false;
            return;
        }

        if (!isTranslationSection) {
            return;
        }

        if (!line.startsWith('-')) {
            return;
        }

        translationRelevantLines.push(line);
    });

    fileLineReader.on('close', () => {
        fileReadPromise.resolve(translationRelevantLines);
    });

    fileLineReader.on('error', (error) => {
        fileReadPromise.reject(error);
    });

    return fileReadPromise.promise;
};

const getKnownContributorsByLanguage = async (): Promise<Record<string, string[]>> => {
    const changelogLanguageLines = await getLanguageLinesFromChangelog();

    const contributorByLanguage: Record<string, string[]> = {};
    changelogLanguageLines.forEach((languageLine) => {
        const languageChangeRegex = /^- (.*) \(by (.*)\)$/g;
        const languageChangeRegexMatch = [...languageLine.matchAll(languageChangeRegex)][0];

        const language = languageChangeRegexMatch[1];
        const contributors = languageChangeRegexMatch[2].split(', ');

        const knownContributors = contributorByLanguage[language] ?? [];
        contributorByLanguage[language] = [...new Set([...knownContributors, ...contributors])];
    });

    return contributorByLanguage;
};

export const createTranslationChangelog = async (
    afterDate: string,
    beforeDate: string,
    requiredContributionCount: number = TRANSLATION_CHANGELOG_YARG_OPTIONS_DEFAULT.requiredContributionCount,
    keepKnownContributors: boolean = TRANSLATION_CHANGELOG_YARG_OPTIONS_DEFAULT.keepKnownContributors,
): Promise<string> => {
    validateWeblateDates(afterDate, beforeDate);

    const timestampAfter = `${afterDate}T00:00:00.000Z`;
    const timestampBefore = `${beforeDate}T00:00:00.000Z`;

    const url = `https://hosted.weblate.org/api/components/suwayomi/suwayomi-webui/changes/?timestamp_after=${timestampAfter}&timestamp_before=${timestampBefore}&${creditRelevantActions.map((action) => `action=${action}`).join('&')}`;

    const contributorsForRange = await getContributorsForRange(url, tokens.weblateToken);
    const knownContributorsByLanguage = await getKnownContributorsByLanguage();

    const contributionInfo: { language: string; contributors: string[] }[] = Object.entries(contributorsForRange).map(
        ([language, languageContributors]) => {
            const validContributors = languageContributors.filter(
                ({ username, contributionCount }) =>
                    (keepKnownContributors && knownContributorsByLanguage[language]?.includes(username)) ||
                    contributionCount >= requiredContributionCount,
            );
            const contributors = validContributors.length ? validContributors : [languageContributors[0]];

            return {
                language,
                contributors: contributors.map(({ username }) => username),
            };
        },
    );

    const languageByNewState = Object.groupBy(
        contributionInfo,
        ({ language }) => `${knownContributorsByLanguage[language] == null}`,
    );

    const newLanguages = languageByNewState.true ?? [];
    const updatedLanguages = languageByNewState.false ?? [];

    let changelog =
        '## Translations\n' +
        'Feel free to translate the project on [Weblate](https://hosted.weblate.org/projects/suwayomi/suwayomi-webui/)\n' +
        '\n' +
        'Thank you for your contribution to the translation of the project.\n\n';

    if (newLanguages.length) {
        changelog += '### Added\n';
        newLanguages.forEach(({ language, contributors }) => {
            changelog += `- ${language} (by ${contributors.join(', ')})\n`;
        });
        changelog += '\n';
    }

    if (updatedLanguages.length) {
        changelog += '### Updated\n';
        updatedLanguages.forEach(({ language, contributors }) => {
            changelog += `- ${language} (by ${contributors.join(', ')})\n`;
        });
    }

    return changelog;
};
