import { createId } from "@paralleldrive/cuid2"
import type { ExamNode } from "../components/assembly/selected-question-list"
/**
* Normalize raw exam structure data into typed `ExamNode[]`.
*
* - Validates each node's shape at runtime (type guard pattern, no `as`).
* - Ensures every node has a unique id (generates one if missing or duplicate).
* - Recursively normalizes group children.
* - Returns `[]` for non-array input.
*
* Used by the exam build page to convert persisted `exam.structure` (unknown
* JSON from DB) into a typed tree before passing to ``.
*/
export function normalizeStructure(nodes: unknown): ExamNode[] {
const seen = new Set()
const isRecord = (v: unknown): v is Record =>
typeof v === "object" && v !== null
const normalize = (raw: unknown[]): ExamNode[] => {
return raw
.map((n): ExamNode | null => {
if (!isRecord(n)) return null
const type = n.type
if (type !== "group" && type !== "question") return null
let id = typeof n.id === "string" && n.id.length > 0 ? n.id : createId()
while (seen.has(id)) id = createId()
seen.add(id)
if (type === "group") {
return {
id,
type: "group",
title: typeof n.title === "string" ? n.title : undefined,
children: normalize(Array.isArray(n.children) ? n.children : []),
} satisfies ExamNode
}
if (typeof n.questionId !== "string" || n.questionId.length === 0) return null
return {
id,
type: "question",
questionId: n.questionId,
score: typeof n.score === "number" ? n.score : undefined,
} satisfies ExamNode
})
.filter((n): n is ExamNode => n !== null)
}
if (!Array.isArray(nodes)) return []
return normalize(nodes)
}