import { Exam } from "@/interfaces/exam";

const EXCLUDED_KEYS = new Set<string>(["_id", "id", "uuid", "isDiagnostic", "owners", "entities", "createdAt", "createdBy", "access", "requiresApproval", "exerciseID", "questionID", "sectionId", "userSolutions"]);

const PATH_LABELS: Record<string, string> = {
    access: "Access Type",
    parts: "Parts",
    exercises: "Exercises",
    userSolutions: "User Solutions",
    words: "Words",
    options: "Options",
    prompt: "Prompt",
    text: "Text",
    audio: "Audio",
    script: "Script",
    difficulty: "Difficulty",
    shuffle: "Shuffle",
    solutions: "Solutions",
    variant: "Variant",
    prefix: "Prefix",
    suffix: "Suffix",
    topic: "Topic",
    allowRepetition: "Allow Repetition",
    maxWords: "Max Words",
    minTimer: "Timer",
    section: "Section",
    module: "Module",
    type: "Type",
    intro: "Intro",
    category: "Category",
    context: "Context",
    instructions: "Instructions",
    name: "Name",
    gender: "Gender",
    voice: "Voice",
    enableNavigation: "Enable Navigation",
    limit: "Limit",
    instructorGender: "Instructor Gender",
    wordCounter: "Word Counter",
    attachment: "Attachment",
    first_title: "First Title",
    second_title: "Second Title",
    first_topic: "First Topic",
    second_topic: "Second Topic",
    questions: "Questions",
    sentences: "Sentences",
    sentence: "Sentence",
    solution: "Solution",
    passage: "Passage",
};

const ARRAY_ITEM_LABELS: Record<string, string> = {
    exercises: "Exercise",
    paths: "Path",
    difficulties: "Difficulty",
    solutions: "Solution",
    options: "Option",
    words: "Word",
    questions: "Question",
    userSolutions: "User Solution",
    sentences: "Sentence",
    parts: "Part",
};

export function generateExamDifferences(oldExam: Exam, newExam: Exam): string[] {
    const differences: string[] = [];
    compareObjects(oldExam, newExam, [], differences);
    return differences;
}

function isObject(val: any): val is Record<string, any> {
    return val !== null && typeof val === "object" && !Array.isArray(val);
}

function formatPrimitive(value: any): string {
    if (value === undefined) return "undefined";
    if (value === null) return "null";
    return JSON.stringify(value);
}

function pathToHumanReadable(pathSegments: Array<string | number>): string {
    const mapped = pathSegments.map((seg) => {
        if (typeof seg === "number") {
            return `#${seg + 1}`;
        }
        return PATH_LABELS[seg] ?? seg;
    });

    let result = "";
    for (let i = 0; i < mapped.length; i++) {
        result += mapped[i];
        if (mapped[i].startsWith("#") && i < mapped.length - 1) {
            result += " - ";
        } else if (i < mapped.length - 1) {
            result += " ";
        }
    }

    return result.trim();
}

function getArrayItemLabel(path: (string | number)[]): string {
    if (path.length === 0) return "item";
    const lastSegment = path[path.length - 1];
    if (typeof lastSegment === "string" && ARRAY_ITEM_LABELS[lastSegment]) {
        return ARRAY_ITEM_LABELS[lastSegment];
    }
    return "item";
}

function getIdentifier(item: any): string | number | undefined {
    if (item?.uuid !== undefined) return item.uuid;
    if (item?.id !== undefined) return item.id;
    return undefined;
}

function compareObjects(oldObj: any, newObj: any, path: (string | number)[], differences: string[]) {
    if (Array.isArray(oldObj) && Array.isArray(newObj)) {
        // Check if array elements are objects with an identifier (uuid or id).
        if (oldObj.length > 0 && typeof oldObj[0] === "object" && getIdentifier(oldObj[0]) !== undefined) {
            // Process removed items
            const newIds = new Set(newObj.map((item: any) => getIdentifier(item)));
            for (let i = 0; i < oldObj.length; i++) {
                const oldItem = oldObj[i];
                const identifier = getIdentifier(oldItem);
                if (identifier !== undefined && !newIds.has(identifier)) {
                    differences.push(`• Removed ${getArrayItemLabel(path)} #${i + 1} from ${pathToHumanReadable(path)}\n`);
                }
            }

            const oldIndexMap = new Map(oldObj.map((item: any, index: number) => [getIdentifier(item), index]));
            // Process items in the new array using their order.
            for (let i = 0; i < newObj.length; i++) {
                const newItem = newObj[i];
                const identifier = getIdentifier(newItem);
                if (identifier !== undefined) {
                    if (oldIndexMap.has(identifier)) {
                        const oldIndex = oldIndexMap.get(identifier)!;
                        const oldItem = oldObj[oldIndex];
                        compareObjects(oldItem, newItem, path.concat(`#${i + 1}`), differences);
                    } else {
                        differences.push(`• Added new ${getArrayItemLabel(path)} #${i + 1} at ${pathToHumanReadable(path)}\n`);
                    }
                } else {
                    // Fallback: if item does not have an identifier, compare by index.
                    compareObjects(oldObj[i], newItem, path.concat(`#${i + 1}`), differences);
                }
            }
        } else {
            // For arrays that are not identifier-based, compare element by element.
            const maxLength = Math.max(oldObj.length, newObj.length);
            for (let i = 0; i < maxLength; i++) {
                compareObjects(oldObj[i], newObj[i], path.concat(`#${i + 1}`), differences);
            }
        }
    } else if (isObject(oldObj) && isObject(newObj)) {
        // Compare objects by keys (ignoring excluded keys).
        const keys = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);
        for (const key of keys) {
            if (EXCLUDED_KEYS.has(key)) continue;
            compareObjects(oldObj[key], newObj[key], path.concat(key), differences);
        }
    } else {
        if (oldObj !== newObj) {
            differences.push(`• Changed ${pathToHumanReadable(path)} from:\n ${formatPrimitive(oldObj)}\n To:\n ${formatPrimitive(newObj)}\n`);
        }
    }
}
