import { Exercise, FillBlanksExercise, FillBlanksMCOption, MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap, Shuffles, UserSolution } from "@/interfaces/exam";

export default function shuffleExamExercise(
    shuffle: boolean | undefined,
    exercise: Exercise,
    showSolutions: boolean,
    userSolutions: UserSolution[],
    shuffles: Shuffles[],
    setShuffles: (maps: Shuffles[]) => void
): Exercise {
    if (!shuffle) {
        return exercise;
    }
    const userSolution = userSolutions.find((x) => x.exercise === exercise.id)!;

    if (exercise.type === "multipleChoice") {
        return shuffleMultipleChoice(exercise, userSolution, shuffles, setShuffles, showSolutions);
    } else if (exercise.type === "fillBlanks") {
        return shuffleFillBlanks(exercise, userSolution, shuffles, setShuffles, showSolutions);
    }

    return exercise;
}

function shuffleMultipleChoice(
    exercise: MultipleChoiceExercise,
    userSolution: UserSolution,
    shuffles: Shuffles[],
    setShuffles: (maps: Shuffles[]) => void,
    showSolutions: boolean,
): MultipleChoiceExercise {

    if (typeof userSolution.shuffleMaps === "undefined" || (userSolution.shuffleMaps && userSolution.shuffleMaps.length === 0) && !showSolutions) {
        const newShuffleMaps: ShuffleMap[] = [];
        exercise.questions = exercise.questions.map(shuffleQuestion(newShuffleMaps));
        userSolution!.shuffleMaps = newShuffleMaps;
        setShuffles([...shuffles.filter((x) => x.exerciseID !== exercise.id), {exerciseID: exercise.id, shuffles: newShuffleMaps}]);
    } else {
        exercise.questions = exercise.questions.map(retrieveShuffledQuestion(userSolution.shuffleMaps));
    }

    return exercise;
}

function shuffleQuestion(newShuffleMaps: ShuffleMap[]) {
    return (question: MultipleChoiceQuestion): MultipleChoiceQuestion => {
        const options = [...question.options];
        const shuffledOptions = fisherYatesShuffle(options);

        const optionMapping: Record<string, string> = {};
        const newOptions = shuffledOptions.map((option, index) => {
            const newId = String.fromCharCode(65 + index);
            optionMapping[option.id] = newId;
            return { ...option, id: newId };
        });

        newShuffleMaps.push({ questionID: question.id, map: optionMapping });

        return { ...question, options: newOptions, shuffleMap: optionMapping };
    };
}

function retrieveShuffledQuestion(shuffleMaps: ShuffleMap[]) {
    return (question: MultipleChoiceQuestion): MultipleChoiceQuestion => {
        const questionShuffleMap = shuffleMaps.find(map => map.questionID === question.id);
        if (questionShuffleMap) {
            const shuffledOptions = Object.entries(questionShuffleMap.map)
                .sort(([, a], [, b]) => a.localeCompare(b))
                .map(([originalId, newId]) => {
                    const originalOption = question.options.find(opt => opt.id === originalId);
                    return { ...originalOption, id: newId };
                });

            return { ...question, options: shuffledOptions, shuffleMap: questionShuffleMap.map };
        }
        return question;
    };
}
function shuffleFillBlanks(
    exercise: FillBlanksExercise,
    userSolution: UserSolution,
    shuffles: Shuffles[],
    setShuffles: (maps: Shuffles[]) => void,
    showSolutions: boolean
): FillBlanksExercise {
    if (typeof userSolution.shuffleMaps === "undefined" || (userSolution.shuffleMaps && userSolution.shuffleMaps.length === 0) && !showSolutions) {
        const newShuffleMaps: ShuffleMap[] = [];
        exercise.words = exercise.words.map(shuffleWord(newShuffleMaps));
        userSolution.shuffleMaps = newShuffleMaps;
        setShuffles([...shuffles.filter((x) => x.exerciseID !== exercise.id), {exerciseID: exercise.id, shuffles: newShuffleMaps}]);
    } else {
        exercise.words = exercise.words.map(retrieveShuffledWord(userSolution.shuffleMaps!));
    }

    return exercise;
}

function shuffleWord(newShuffleMaps: ShuffleMap[]) {
    return (word: string | { letter: string; word: string } | FillBlanksMCOption): typeof word => {
        if (typeof word === 'object' && 'options' in word) {
            const options = word.options;
            const originalKeys = Object.keys(options);
            const shuffledKeys = fisherYatesShuffle(originalKeys);

            const newOptions = shuffledKeys.reduce<typeof options>((acc, key, index) => {
                acc[key as keyof typeof options] = options[originalKeys[index] as keyof typeof options];
                return acc;
            }, {} as typeof options);

            const optionMapping = originalKeys.reduce<Record<string, string>>((acc, key, index) => {
                acc[key] = shuffledKeys[index];
                return acc;
            }, {});

            newShuffleMaps.push({ questionID: word.id, map: optionMapping });

            return { ...word, options: newOptions };
        }
        return word;
    };
}

function retrieveShuffledWord(shuffleMaps: ShuffleMap[]) {
    return (word: string | { letter: string; word: string } | FillBlanksMCOption): typeof word => {
        if (typeof word === 'object' && 'options' in word) {
            const shuffleMap = shuffleMaps.find(map => map.questionID === word.id);
            if (shuffleMap) {
                const options = word.options;
                const shuffledOptions = Object.keys(options).reduce<typeof options>((acc, key) => {
                    const shuffledKey = shuffleMap.map[key as keyof typeof options];
                    acc[shuffledKey as keyof typeof options] = options[key as keyof typeof options];
                    return acc;
                }, {} as typeof options);

                return { ...word, options: shuffledOptions };
            }
        }
        return word;
    };
}

function fisherYatesShuffle<T>(array: T[]): T[] {
    const shuffled = [...array];
    for (let i = shuffled.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
    }
    return shuffled;
}

const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
    return Array.isArray(words) && words.every(
        word => word && typeof word === 'object' && 'id' in word && 'options' in word
    );
}