const STORAGE_KEY = "boostai-finetune-helper-state-v3";
const DATASET_STORAGE_KEY = "boostai-finetune-helper-dataset-v2";
const DRAFT_STORAGE_KEY = "boostai-finetune-helper-drafts-v1";
const ACTIVE_DRAFT_STORAGE_KEY = "boostai-finetune-helper-active-draft-v1";
const TRAIN_VAL_SPLIT = 0.2;

const fixedFieldIds = [
	"assignmentId",
	"studentId",
	"assignmentTitle",
	"instructions",
	"passThreshold",
	"topic",
	"difficulty",
	"questionCount",
	"generatorSeed",
	"assignmentSummary",
	"recommendedNextStep",
];

const fixedElements = Object.fromEntries(fixedFieldIds.map((id) => [id, document.getElementById(id)]));

const questionsContainer = document.getElementById("questions-container");
const questionSummary = document.getElementById("question-summary");
const aiConfigStatus = document.getElementById("ai-config-status");
const questionGeneratorStatus = document.getElementById("question-generator-status");
const collabStatus = document.getElementById("collab-status");
const presenceStatus = document.getElementById("presence-status");
const saveStatus = document.getElementById("save-status");
const recordPreview = document.getElementById("recordPreview");
const trainingPreview = document.getElementById("trainingPreview");
const datasetStatus = document.getElementById("datasetStatus");
const datasetEmpty = document.getElementById("datasetEmpty");
const datasetList = document.getElementById("datasetList");
const draftStatus = document.getElementById("draft-status");
const draftSearch = document.getElementById("draft-search");
const draftSort = document.getElementById("draft-sort");
const draftList = document.getElementById("draft-list");
const toast = document.getElementById("toast");

const generateAssignmentButton = document.getElementById("generate-assignment");
const addQuestionButton = document.getElementById("add-question");
const generateStudentsButton = document.getElementById("generate-students");
const generateTeacherButton = document.getElementById("generate-teacher");
const saveExampleButton = document.getElementById("save-example");
const exportDatasetButton = document.getElementById("export-dataset");
const exportSplitButton = document.getElementById("export-split");
const filterAllButton = document.getElementById("filter-all");
const filterAttentionButton = document.getElementById("filter-attention");
const filterUnlabeledButton = document.getElementById("filter-unlabeled");
const expandAllQuestionsButton = document.getElementById("expand-all-questions");
const collapseAllQuestionsButton = document.getElementById("collapse-all-questions");
const newDraftButton = document.getElementById("new-draft");
const duplicateDraftButton = document.getElementById("duplicate-draft");
const renameDraftButton = document.getElementById("rename-draft");
const deleteDraftButton = document.getElementById("delete-draft");
const DEFAULT_WORKSPACE_ID = "shared";

let state = createSampleState();
let savedExamples = [];
let activeExampleId = null;
let drafts = [];
let activeDraftId = null;
let toastTimeout = null;
let uiState = {
	questionFilter: "all",
	collapsedQuestions: new Set(),
	draftFilter: "",
	draftSort: "updated",
};
let collaboration = {
	clientId: null,
	ws: null,
	workspaceId: resolveWorkspaceId(),
	connected: false,
	ready: false,
	presenceCount: 1,
	serverVersion: 0,
	reconnectTimer: null,
	publishTimer: null,
	applyingRemoteState: false,
	lastSentStateHash: "",
};

initialize();

async function initialize() {
	bindEvents();
	hydrateDrafts();
	hydrateDataset();
	renderAll();
	renderCollaborationStatus();
	connectCollaboration();
	await loadConfig();
}

function bindEvents() {
	for (const [field, element] of Object.entries(fixedElements)) {
		element.addEventListener("input", () => {
			state[field] = element.value;
			persistState();
			renderPreviews();
			if (field === "questionCount") renderQuestionSummary();
		});
	}

	draftSearch.addEventListener("input", () => {
		uiState.draftFilter = draftSearch.value.trim().toLowerCase();
		renderDrafts();
	});

	draftSort.addEventListener("change", () => {
		uiState.draftSort = draftSort.value;
		renderDrafts();
	});

	document.getElementById("load-sample").addEventListener("click", () => {
		activeExampleId = null;
		state = createSampleState();
		renderAll();
		showToast("Loaded sample assignment.", "success");
	});

	document.getElementById("clear-form").addEventListener("click", () => {
		activeExampleId = null;
		state = createEmptyState();
		renderAll();
		showToast("Cleared assignment workspace.", "success");
	});

	newDraftButton.addEventListener("click", () => {
		createDraftFromCurrentState({
			state: createEmptyState(),
			label: "New assignment draft",
			successMessage: "Created a fresh assignment draft.",
		});
	});

	duplicateDraftButton.addEventListener("click", () => {
		createDraftFromCurrentState({
			state,
			label: `${getActiveDraftTitle()} copy`,
			successMessage: "Duplicated the current assignment draft.",
		});
	});

	renameDraftButton.addEventListener("click", () => {
		const current = getActiveDraft();
		if (!current) return;
		const nextLabel = window.prompt("Rename the current assignment draft", current.label || getDraftTitle(current));
		if (nextLabel === null) return;
		current.label = nextLabel.trim();
		persistDrafts();
		renderDrafts();
		showToast("Updated draft name.", "success");
	});

	deleteDraftButton.addEventListener("click", () => {
		deleteActiveDraft();
	});

	addQuestionButton.addEventListener("click", () => {
		state.questions.push(createBlankQuestion(state.questions.length + 1));
		state.questionCount = String(state.questions.length);
		expandQuestionAtIndex(state.questions.length - 1);
		renderAll();
		showToast("Added blank question.", "success");
	});

	filterAllButton.addEventListener("click", () => {
		uiState.questionFilter = "all";
		renderQuestions();
		renderQuestionSummary();
	});

	filterAttentionButton.addEventListener("click", () => {
		uiState.questionFilter = "attention";
		renderQuestions();
		renderQuestionSummary();
	});

	filterUnlabeledButton.addEventListener("click", () => {
		uiState.questionFilter = "unlabeled";
		renderQuestions();
		renderQuestionSummary();
	});

	expandAllQuestionsButton.addEventListener("click", () => {
		uiState.collapsedQuestions.clear();
		renderQuestions();
	});

	collapseAllQuestionsButton.addEventListener("click", () => {
		uiState.collapsedQuestions = new Set(state.questions.map((question, index) => getQuestionUiKey(question, index)));
		renderQuestions();
	});

	generateAssignmentButton.addEventListener("click", async () => {
		await runAssignmentGeneration();
	});

	generateStudentsButton.addEventListener("click", async () => {
		await runAssignmentDraft({
			button: generateStudentsButton,
			path: "/api/assignment/student-draft",
			successMessage: "Student submission drafted.",
			apply(result) {
				const byId = new Map((result.questions || []).map((question) => [String(question.questionId), question]));
				state.questions = state.questions.map((question) => {
					const drafted = byId.get(question.questionId) || byId.get(String(question.questionId));
					if (!drafted) return question;
					return {
						...question,
						studentAnswer: drafted.answerText || "",
						workingSteps: drafted.workingSteps || "",
						solveMode: drafted.solveMode || question.solveMode || "show_work",
					};
				});
			},
		});
	});

	generateTeacherButton.addEventListener("click", async () => {
		await runAssignmentDraft({
			button: generateTeacherButton,
			path: "/api/assignment/teacher-draft",
			successMessage: "Teacher review package drafted.",
			apply(result) {
				const byId = new Map((result.questions || []).map((question) => [String(question.questionId), question]));
				state.questions = state.questions.map((question) => {
					const drafted = byId.get(question.questionId) || byId.get(String(question.questionId));
					if (!drafted) return question;
					return {
						...question,
						aiFeedback: drafted.aiFeedback || "",
						understandingScore: formatScore(drafted.understandingScore),
						confidence: formatScore(drafted.confidence),
						needsAttention: formatBoolean(drafted.needsAttention),
						issueReason: drafted.issueReason || "",
					};
				});
				state.assignmentSummary = result.assignmentSummary || "";
				state.recommendedNextStep = result.recommendedNextStep || "";
			},
		});
	});

	saveExampleButton.addEventListener("click", () => {
		try {
			saveCurrentExample();
		} catch (error) {
			showToast(error instanceof Error ? error.message : "Could not save example.", "error");
		}
	});

	document.getElementById("copy-record").addEventListener("click", async () => {
		await copyPreview(recordPreview.textContent, "Labeled record copied.");
	});

	document.getElementById("copy-training").addEventListener("click", async () => {
		await copyPreview(trainingPreview.textContent, "Fine-tune example copied.");
	});

	exportDatasetButton.addEventListener("click", () => {
		try {
			exportDatasetJsonl();
		} catch (error) {
			showToast(error instanceof Error ? error.message : "Export failed.", "error");
		}
	});

	exportSplitButton.addEventListener("click", () => {
		try {
			exportTrainValidationSplit();
		} catch (error) {
			showToast(error instanceof Error ? error.message : "Split export failed.", "error");
		}
	});

	questionsContainer.addEventListener("input", (event) => {
		const target = event.target;
		if (!(target instanceof HTMLElement)) return;
		const index = Number(target.dataset.questionIndex);
		const field = target.dataset.questionField;
		if (!Number.isInteger(index) || !field || !state.questions[index]) return;

		state.questions[index][field] = target.value;
		persistState();
		renderPreviews();
		renderQuestionSummary();
	});

	questionsContainer.addEventListener("click", (event) => {
		const target = event.target;
		if (!(target instanceof HTMLElement)) return;
		const actionTarget = target.closest("[data-action]");
		if (!(actionTarget instanceof HTMLElement)) return;

		const action = actionTarget.dataset.action;
		const index = Number(actionTarget.dataset.questionIndex);
		if (!Number.isInteger(index) || !state.questions[index]) return;

		if (action === "remove-question") {
			uiState.collapsedQuestions.delete(getQuestionUiKey(state.questions[index], index));
			state.questions.splice(index, 1);
			reindexQuestions();
			renderAll();
			showToast("Removed question.", "success");
			return;
		}

		if (action === "duplicate-question") {
			const duplicate = normalizeQuestion(state.questions[index], index + 1);
			duplicate.position = index + 2;
			state.questions.splice(index + 1, 0, duplicate);
			reindexQuestions();
			expandQuestionAtIndex(index + 1);
			renderAll();
			showToast("Duplicated question.", "success");
			return;
		}

		if (action === "move-question-up" && index > 0) {
			[state.questions[index - 1], state.questions[index]] = [state.questions[index], state.questions[index - 1]];
			reindexQuestions();
			renderAll();
			showToast("Moved question up.", "success");
			return;
		}

		if (action === "move-question-down" && index < state.questions.length - 1) {
			[state.questions[index], state.questions[index + 1]] = [state.questions[index + 1], state.questions[index]];
			reindexQuestions();
			renderAll();
			showToast("Moved question down.", "success");
			return;
		}

		if (action === "toggle-question") {
			toggleQuestionCollapsed(index);
			renderQuestions();
		}
	});

	draftList.addEventListener("click", (event) => {
		const target = event.target;
		if (!(target instanceof HTMLElement)) return;
		const actionTarget = target.closest("[data-draft-action]");
		if (!(actionTarget instanceof HTMLElement)) return;

		const action = actionTarget.dataset.draftAction;
		const draftId = actionTarget.dataset.draftId;
		if (!draftId) return;

		if (action === "open-draft") {
			loadDraft(draftId);
		}
	});
	}

function hydrateDrafts() {
	const rawDrafts = localStorage.getItem(DRAFT_STORAGE_KEY);
	const legacyState = localStorage.getItem(STORAGE_KEY);

	if (rawDrafts) {
		try {
			const parsed = JSON.parse(rawDrafts);
			drafts = Array.isArray(parsed) ? parsed.map(normalizeDraft).filter(Boolean) : [];
		} catch {
			drafts = [];
		}
	}

	if (!drafts.length && legacyState) {
		try {
			drafts = [createDraftRecord(normalizeState(JSON.parse(legacyState)), { id: crypto.randomUUID() })];
		} catch {
			drafts = [];
		}
	}

	if (!drafts.length) {
		drafts = [createDraftRecord(createSampleState(), { id: crypto.randomUUID(), label: "Sample assignment" })];
	}

	const storedActiveDraftId = localStorage.getItem(ACTIVE_DRAFT_STORAGE_KEY);
	activeDraftId = drafts.some((draft) => draft.id === storedActiveDraftId) ? storedActiveDraftId : drafts[0].id;
	state = normalizeState(getActiveDraft()?.state || createSampleState());
}

function hydrateDataset() {
	const raw = localStorage.getItem(DATASET_STORAGE_KEY);
	if (!raw) {
		savedExamples = [];
		return;
	}

	try {
		const parsed = JSON.parse(raw);
		savedExamples = Array.isArray(parsed) ? parsed.filter(isSavedExampleShape) : [];
	} catch {
		savedExamples = [];
	}
}

function renderAll() {
	renderFixedFields();
	draftSearch.value = uiState.draftFilter;
	draftSort.value = uiState.draftSort;
	renderQuestions();
	renderQuestionSummary();
	renderQuestionToolbar();
	persistState();
	renderPreviews();
	renderDrafts();
	renderDataset();
}

function renderFixedFields() {
	for (const [field, element] of Object.entries(fixedElements)) {
		element.value = state[field] || "";
	}
}

function renderQuestions() {
	questionsContainer.innerHTML = "";
	const visibleQuestions = state.questions.filter((question, index) => matchesQuestionFilter(question, index));

	if (!visibleQuestions.length) {
		const empty = document.createElement("div");
		empty.className = "empty-state question-empty-state";
		empty.textContent =
			uiState.questionFilter === "all"
				? "No questions yet. Generate an assignment or add a blank question."
				: "No questions match the current filter.";
		questionsContainer.appendChild(empty);
		return;
	}

	for (const question of visibleQuestions) {
		const index = Math.max(0, (question.position || 1) - 1);
		const collapsed = isQuestionCollapsed(index);
		const statusPills = buildQuestionStatusPills(question);
		const article = document.createElement("article");
		article.className = `question-card${collapsed ? " is-collapsed" : ""}`;
		article.innerHTML = `
			<div class="question-card-header">
				<div class="question-card-heading">
					<div class="question-heading-topline">
						<h3>Question ${question.position || index + 1}</h3>
						<button type="button" class="button-secondary button-compact" data-action="toggle-question" data-question-index="${index}">${collapsed ? "Expand" : "Collapse"}</button>
					</div>
					<p>${escapeHtml(question.title || summarizeText(question.prompt, 100) || "Untitled question")}</p>
					<div class="question-status-row">${statusPills}</div>
				</div>
				<div class="hero-actions question-card-actions">
					<span class="dataset-pill">ID ${escapeHtml(question.questionId || "?")}</span>
					<button type="button" class="button-secondary button-compact" data-action="move-question-up" data-question-index="${index}" ${index === 0 ? "disabled" : ""}>Up</button>
					<button type="button" class="button-secondary button-compact" data-action="move-question-down" data-question-index="${index}" ${index === state.questions.length - 1 ? "disabled" : ""}>Down</button>
					<button type="button" class="button-secondary button-compact" data-action="duplicate-question" data-question-index="${index}">Duplicate</button>
					<button type="button" class="button-danger" data-action="remove-question" data-question-index="${index}">Remove</button>
				</div>
			</div>

			<div class="question-card-body" ${collapsed ? "hidden" : ""}>
			<div class="form-grid three-up">
				<label>
					<span>Question ID</span>
					<input type="number" min="1" step="1" value="${escapeAttribute(question.questionId)}" data-question-index="${index}" data-question-field="questionId" />
				</label>
				<label>
					<span>Title</span>
					<input type="text" value="${escapeAttribute(question.title)}" data-question-index="${index}" data-question-field="title" />
				</label>
				<label>
					<span>Subject</span>
					<input type="text" value="${escapeAttribute(question.subject)}" data-question-index="${index}" data-question-field="subject" />
				</label>
			</div>

			<div class="form-grid three-up">
				<label>
					<span>Source</span>
					<input type="text" value="${escapeAttribute(question.source)}" data-question-index="${index}" data-question-field="source" />
				</label>
				<label>
					<span>Difficulty</span>
					<input type="text" value="${escapeAttribute(question.difficulty)}" data-question-index="${index}" data-question-field="difficulty" />
				</label>
				<label>
					<span>Solve mode</span>
					<select data-question-index="${index}" data-question-field="solveMode">
						${renderSelectOptions(["show_work", "mental_math", "calculator", "unknown"], question.solveMode || "show_work")}
					</select>
				</label>
			</div>

			<label>
				<span>Tags</span>
				<input type="text" value="${escapeAttribute(question.tags)}" placeholder="fractions, easy, rng_generated" data-question-index="${index}" data-question-field="tags" />
			</label>

			<label>
				<span>Prompt</span>
				<textarea rows="4" data-question-index="${index}" data-question-field="prompt">${escapeHtml(question.prompt)}</textarea>
			</label>

			<div class="form-grid two-up">
				<label>
					<span>Correct answer</span>
					<textarea rows="5" data-question-index="${index}" data-question-field="correctAnswer">${escapeHtml(question.correctAnswer)}</textarea>
				</label>
				<label>
					<span>Worked solution</span>
					<textarea rows="5" data-question-index="${index}" data-question-field="workedSolution">${escapeHtml(question.workedSolution)}</textarea>
				</label>
			</div>

			<div class="subpanel">
				<h4>Student submission</h4>
				<div class="form-grid two-up">
					<label>
						<span>Answer text</span>
						<textarea rows="5" data-question-index="${index}" data-question-field="studentAnswer">${escapeHtml(question.studentAnswer)}</textarea>
					</label>
					<label>
						<span>Working steps</span>
						<textarea rows="5" data-question-index="${index}" data-question-field="workingSteps">${escapeHtml(question.workingSteps)}</textarea>
					</label>
				</div>
			</div>

			<div class="subpanel">
				<h4>Teacher review</h4>
				<label>
					<span>AI feedback</span>
					<textarea rows="5" data-question-index="${index}" data-question-field="aiFeedback">${escapeHtml(question.aiFeedback)}</textarea>
				</label>
				<div class="form-grid three-up">
					<label>
						<span>Understanding score</span>
						<input type="number" min="0" max="1" step="0.01" value="${escapeAttribute(question.understandingScore)}" data-question-index="${index}" data-question-field="understandingScore" />
					</label>
					<label>
						<span>Confidence</span>
						<input type="number" min="0" max="1" step="0.01" value="${escapeAttribute(question.confidence)}" data-question-index="${index}" data-question-field="confidence" />
					</label>
					<label>
						<span>Needs attention</span>
						<select data-question-index="${index}" data-question-field="needsAttention">
							<option value="" ${question.needsAttention ? "" : "selected"}>Select…</option>
							<option value="true" ${question.needsAttention === "true" ? "selected" : ""}>true</option>
							<option value="false" ${question.needsAttention === "false" ? "selected" : ""}>false</option>
						</select>
					</label>
				</div>
				<label>
					<span>Issue reason</span>
					<textarea rows="4" data-question-index="${index}" data-question-field="issueReason">${escapeHtml(question.issueReason)}</textarea>
				</label>
			</div>
			</div>
		`;

		questionsContainer.appendChild(article);
	}
}

function renderQuestionSummary() {
	const total = state.questions.length;
	const reviewed = state.questions.filter((question) => question.aiFeedback && question.issueReason).length;
	const studentDrafted = state.questions.filter((question) => question.studentAnswer || question.workingSteps).length;
	const needsAttention = state.questions.filter((question) => parseBoolean(question.needsAttention) === true).length;
	const visible = state.questions.filter((question, index) => matchesQuestionFilter(question, index)).length;
	questionSummary.textContent = total
		? `${total} question${total === 1 ? "" : "s"} • ${studentDrafted} with student work • ${reviewed} labeled • ${needsAttention} need attention${visible !== total ? ` • showing ${visible}` : ""}`
		: "No questions yet.";
}

function renderQuestionToolbar() {
	const filters = {
		all: filterAllButton,
		attention: filterAttentionButton,
		unlabeled: filterUnlabeledButton,
	};

	for (const [mode, button] of Object.entries(filters)) {
		button.classList.toggle("is-active", uiState.questionFilter === mode);
	}
}

function persistState() {
	syncCurrentStateIntoActiveDraft();
	localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
	persistDrafts();
	saveStatus.textContent = `Autosaved draft at ${new Date().toLocaleTimeString()}`;
	if (!collaboration.applyingRemoteState) {
		scheduleWorkspacePublish();
	}
}

function persistDrafts() {
	localStorage.setItem(DRAFT_STORAGE_KEY, JSON.stringify(drafts));
	if (activeDraftId) {
		localStorage.setItem(ACTIVE_DRAFT_STORAGE_KEY, activeDraftId);
	}
}

function persistDataset() {
	localStorage.setItem(DATASET_STORAGE_KEY, JSON.stringify(savedExamples));
}

function renderPreviews() {
	const derived = buildDerivedExample(state);
	recordPreview.textContent = JSON.stringify(derived.record, null, 2);
	trainingPreview.textContent = JSON.stringify(derived.trainingExample, null, 2);
	updateSaveButtonLabel();
}

function renderDataset() {
	const count = savedExamples.length;
	datasetStatus.textContent =
		count === 0
			? "No saved examples yet."
			: `${count} saved assignment example${count === 1 ? "" : "s"} ready for export.`;
	datasetEmpty.hidden = count > 0;
	datasetList.innerHTML = "";

	for (const example of savedExamples) {
		const article = document.createElement("article");
		article.className = `dataset-item${example.id === activeExampleId ? " active" : ""}`;

		const header = document.createElement("div");
		header.className = "dataset-item-header";

		const summary = document.createElement("div");
		const title = document.createElement("h3");
		title.className = "dataset-item-title";
		title.textContent = example.title;
		summary.appendChild(title);

		const meta = document.createElement("p");
		meta.className = "dataset-item-meta";
		meta.textContent = [
			example.assignmentId || "No assignment ID",
			example.studentId || "No student ID",
			example.savedAt ? `Saved ${new Date(example.savedAt).toLocaleString()}` : null,
		]
			.filter(Boolean)
			.join(" • ");
		summary.appendChild(meta);

		const actions = document.createElement("div");
		actions.className = "dataset-item-actions";

		const loadButton = document.createElement("button");
		loadButton.type = "button";
		loadButton.className = "button-secondary";
		loadButton.textContent = example.id === activeExampleId ? "Loaded" : "Load";
		loadButton.addEventListener("click", () => loadSavedExample(example.id));
		actions.appendChild(loadButton);

		const deleteButton = document.createElement("button");
		deleteButton.type = "button";
		deleteButton.className = "button-danger";
		deleteButton.textContent = "Delete";
		deleteButton.addEventListener("click", () => deleteSavedExample(example.id));
		actions.appendChild(deleteButton);

		header.append(summary, actions);
		article.appendChild(header);

		const pills = document.createElement("div");
		pills.className = "dataset-item-tags";
		for (const label of buildDatasetPills(example)) {
			const pill = document.createElement("span");
			pill.className = "dataset-pill";
			pill.textContent = label;
			pills.appendChild(pill);
		}
		article.appendChild(pills);

		datasetList.appendChild(article);
	}

	updateSaveButtonLabel();
}

function renderDrafts() {
	const visibleDrafts = drafts.filter(matchesDraftFilter).sort(compareDraftsForList);
	const count = drafts.length;
	const activeTitle = getActiveDraftTitle();
	draftStatus.textContent = `${count} local assignment${count === 1 ? "" : "s"}${activeTitle ? ` • Editing ${activeTitle}` : ""}`;
	draftList.innerHTML = "";

	if (!visibleDrafts.length) {
		const empty = document.createElement("div");
		empty.className = "empty-state draft-empty-state";
		empty.textContent = drafts.length ? "No drafts match this search." : "Create a new assignment draft to start working on more than one assignment.";
		draftList.appendChild(empty);
		return;
	}

	for (const draft of visibleDrafts) {
		const article = document.createElement("article");
		article.className = `draft-item${draft.id === activeDraftId ? " active" : ""}`;
		article.setAttribute("role", "option");
		article.setAttribute("aria-selected", draft.id === activeDraftId ? "true" : "false");
		article.innerHTML = `
			<div class="draft-item-main" data-draft-action="open-draft" data-draft-id="${escapeAttribute(draft.id)}">
				<div class="draft-item-row">
					<h3 class="draft-item-title">${escapeHtml(getDraftTitle(draft))}</h3>
					<p class="draft-item-updated">${escapeHtml(formatDraftUpdatedAt(draft.updatedAt))}</p>
				</div>
				<p class="draft-item-meta">${escapeHtml(buildDraftMeta(draft))}</p>
			</div>
			<div class="draft-item-actions">
				<button type="button" class="button-secondary button-compact" data-draft-action="open-draft" data-draft-id="${escapeAttribute(draft.id)}">${draft.id === activeDraftId ? "Editing" : "Open"}</button>
			</div>
		`;

		const pills = document.createElement("div");
		pills.className = "dataset-item-tags";
		for (const label of buildDraftPills(draft)) {
			const pill = document.createElement("span");
			pill.className = "dataset-pill";
			pill.textContent = label;
			pills.appendChild(pill);
		}
		article.appendChild(pills);
		draftList.appendChild(article);
	}
}

function updateSaveButtonLabel() {
	saveExampleButton.textContent = activeExampleId ? "Update saved assignment example" : "Save assignment example";
}

function getActiveDraft() {
	return drafts.find((draft) => draft.id === activeDraftId) || null;
}

function getActiveDraftTitle() {
	return getDraftTitle(getActiveDraft());
}

function getDraftTitle(draft) {
	if (!draft) return "Untitled assignment";
	if (draft.label) return draft.label;
	if (draft.state?.assignmentTitle) return draft.state.assignmentTitle;
	if (draft.assignmentId) return draft.assignmentId;
	if (draft.topic) return `${draft.topic} draft`;
	return "Untitled assignment";
}

function buildDraftMeta(draft) {
	return [
		draft.assignmentId || "No assignment ID",
		draft.studentId || "No student ID",
		draft.topic ? `Topic ${draft.topic}` : null,
		draft.questionCount ? `${draft.questionCount} question${draft.questionCount === 1 ? "" : "s"}` : null,
	]
		.filter(Boolean)
		.join(" • ");
}

function buildDraftPills(draft) {
	const pills = [];
	if (draft.topic) pills.push(`topic ${draft.topic}`);
	if (draft.difficulty) pills.push(draft.difficulty);
	if (draft.questionCount) pills.push(`${draft.questionCount} questions`);
	if (draft.id === activeDraftId) pills.push("active editor");
	return pills;
}

function matchesDraftFilter(draft) {
	if (!uiState.draftFilter) return true;
	const haystack = [
		getDraftTitle(draft),
		draft.assignmentId,
		draft.studentId,
		draft.topic,
		draft.difficulty,
	]
		.filter(Boolean)
		.join(" ")
		.toLowerCase();
	return haystack.includes(uiState.draftFilter);
}

function compareDraftsForList(left, right) {
	const mode = uiState.draftSort || "updated";
	if (mode === "title") {
		return getDraftTitle(left).localeCompare(getDraftTitle(right));
	}

	if (mode === "topic") {
		const topicCompare = (left.topic || "zzzz").localeCompare(right.topic || "zzzz");
		if (topicCompare !== 0) return topicCompare;
		return getDraftTitle(left).localeCompare(getDraftTitle(right));
	}

	if (mode === "questions") {
		const questionCompare = (right.questionCount || 0) - (left.questionCount || 0);
		if (questionCompare !== 0) return questionCompare;
		return getDraftTitle(left).localeCompare(getDraftTitle(right));
	}

	return (Date.parse(right.updatedAt || 0) || 0) - (Date.parse(left.updatedAt || 0) || 0);
}

function formatDraftUpdatedAt(value) {
	if (!value) return "No edits yet";
	const timestamp = Date.parse(value);
	if (Number.isNaN(timestamp)) return "Updated recently";
	const diffMinutes = Math.max(0, Math.round((Date.now() - timestamp) / 60000));
	if (diffMinutes < 1) return "Updated just now";
	if (diffMinutes < 60) return `Updated ${diffMinutes}m ago`;
	const diffHours = Math.round(diffMinutes / 60);
	if (diffHours < 24) return `Updated ${diffHours}h ago`;
	const diffDays = Math.round(diffHours / 24);
	if (diffDays < 7) return `Updated ${diffDays}d ago`;
	return `Updated ${new Date(timestamp).toLocaleDateString()}`;
}

function normalizeDraft(raw) {
	if (!raw || typeof raw !== "object") return null;
	const normalizedState = normalizeState(raw.state || raw);
	return createDraftRecord(normalizedState, {
		id: typeof raw.id === "string" && raw.id ? raw.id : crypto.randomUUID(),
		label: typeof raw.label === "string" ? raw.label.trim() : "",
		createdAt: typeof raw.createdAt === "string" ? raw.createdAt : null,
		updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : null,
	});
}

function createDraftRecord(nextState, overrides = {}) {
	const normalizedState = normalizeState(nextState);
	const now = new Date().toISOString();
	return {
		id: overrides.id || crypto.randomUUID(),
		label: typeof overrides.label === "string" ? overrides.label.trim() : "",
		createdAt: overrides.createdAt || now,
		updatedAt: overrides.updatedAt || now,
		assignmentId: normalizedState.assignmentId,
		studentId: normalizedState.studentId,
		topic: normalizedState.topic,
		difficulty: normalizedState.difficulty,
		questionCount: normalizedState.questions.length,
		state: normalizedState,
	};
}

function syncCurrentStateIntoActiveDraft() {
	const current = getActiveDraft();
	if (!current) return;
	const normalizedState = normalizeState(state);
	const nextRecord = createDraftRecord(normalizedState, {
		id: current.id,
		label: current.label,
		createdAt: current.createdAt,
	});
	const index = drafts.findIndex((draft) => draft.id === current.id);
	if (index >= 0) {
		drafts.splice(index, 1, nextRecord);
	}
}

function createDraftFromCurrentState({ state: nextState, label = "", successMessage }) {
	const draft = createDraftRecord(nextState, { label });
	drafts.unshift(draft);
	activeDraftId = draft.id;
	activeExampleId = null;
	state = normalizeState(draft.state);
	primeQuestionCollapsing(state.questions.length, { preserveExisting: false });
	renderAll();
	if (successMessage) showToast(successMessage, "success");
}

function loadDraft(id) {
	const draft = drafts.find((item) => item.id === id);
	if (!draft) {
		showToast("Draft not found.", "error");
		return;
	}

	activeDraftId = id;
	activeExampleId = null;
	state = normalizeState(draft.state || createEmptyState());
	primeQuestionCollapsing(state.questions.length, { preserveExisting: false });
	renderAll();
	showToast("Loaded assignment draft into the editor.", "success");
}

function deleteActiveDraft() {
	if (!activeDraftId) return;
	const current = getActiveDraft();
	if (!current) return;

	if (drafts.length === 1) {
		drafts = [createDraftRecord(createEmptyState(), { label: "New assignment draft" })];
		activeDraftId = drafts[0].id;
		state = normalizeState(drafts[0].state);
		activeExampleId = null;
		primeQuestionCollapsing(state.questions.length, { preserveExisting: false });
		renderAll();
		showToast("Reset the final remaining draft to a blank assignment.", "success");
		return;
	}

	const currentIndex = drafts.findIndex((draft) => draft.id === activeDraftId);
	drafts = drafts.filter((draft) => draft.id !== activeDraftId);
	const fallback = drafts[Math.max(0, Math.min(currentIndex, drafts.length - 1))];
	activeDraftId = fallback.id;
	state = normalizeState(fallback.state);
	activeExampleId = null;
	primeQuestionCollapsing(state.questions.length, { preserveExisting: false });
	renderAll();
	showToast(`Deleted “${getDraftTitle(current)}”.`, "success");
}

async function loadConfig() {
	try {
		const response = await fetch("/api/config");
		const data = await response.json();
		aiConfigStatus.textContent = data.hasAiConfig
			? `Connected to ${data.model} via ${data.endpoint}`
			: "Missing local AI env config";

		questionGeneratorStatus.textContent = data.hasQuestionGeneratorConfig
			? `Ready via ${data.backendUrl}`
			: "Missing backend generator env config";
	} catch {
		aiConfigStatus.textContent = "Could not load local config";
		questionGeneratorStatus.textContent = "Could not load local config";
	}
}

function connectCollaboration() {
	clearTimeout(collaboration.reconnectTimer);
	const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
	const url = new URL(`${protocol}//${window.location.host}/ws`);
	url.searchParams.set("workspace", collaboration.workspaceId);

	collaboration.connected = false;
	collaboration.ready = false;
	renderCollaborationStatus();

	const ws = new WebSocket(url);
	collaboration.ws = ws;

	ws.addEventListener("open", () => {
		renderCollaborationStatus();
	});

	ws.addEventListener("message", (event) => {
		handleCollaborationMessage(event.data);
	});

	ws.addEventListener("close", () => {
		if (collaboration.ws === ws) {
			collaboration.connected = false;
			collaboration.ready = false;
			renderCollaborationStatus();
			collaboration.reconnectTimer = setTimeout(() => {
				connectCollaboration();
			}, 1500);
		}
	});

	ws.addEventListener("error", () => {});
}

function handleCollaborationMessage(raw) {
	let message;
	try {
		message = JSON.parse(raw);
	} catch {
		return;
	}

	if (message.type === "workspace:init") {
		collaboration.clientId = message.clientId || null;
		collaboration.connected = true;
		collaboration.ready = true;
		collaboration.serverVersion = Number(message.version || 0);
		collaboration.presenceCount = Number(message.presenceCount || 1);
		renderCollaborationStatus();

		if (message.state && typeof message.state === "object") {
			applyRemoteWorkspaceState(message.state);
		} else {
			publishWorkspaceNow({ force: true });
		}
		return;
	}

	if (message.type === "workspace:presence") {
		collaboration.connected = true;
		collaboration.presenceCount = Number(message.presenceCount || 1);
		renderCollaborationStatus();
		return;
	}

	if (message.type === "workspace:snapshot") {
		collaboration.connected = true;
		collaboration.ready = true;
		collaboration.serverVersion = Number(message.version || collaboration.serverVersion || 0);
		collaboration.presenceCount = Number(message.presenceCount || collaboration.presenceCount || 1);
		renderCollaborationStatus();

		if (message.actorClientId === collaboration.clientId) {
			return;
		}

		if (message.state && typeof message.state === "object") {
			applyRemoteWorkspaceState(message.state);
		}
		return;
	}

	if (message.type === "workspace:error" && message.message) {
		showToast(message.message, "error");
	}
}

function applyRemoteWorkspaceState(nextState) {
	const normalized = normalizeState(nextState);
	const nextHash = JSON.stringify(normalized);
	const currentHash = JSON.stringify(normalizeState(state));
	collaboration.lastSentStateHash = nextHash;

	if (nextHash === currentHash) {
		return;
	}

	collaboration.applyingRemoteState = true;
	activeExampleId = null;
	state = normalized;
	renderAll();
	collaboration.applyingRemoteState = false;
	saveStatus.textContent = `Synced shared workspace at ${new Date().toLocaleTimeString()}`;
}

function scheduleWorkspacePublish() {
	if (!collaboration.ready) return;
	clearTimeout(collaboration.publishTimer);
	collaboration.publishTimer = setTimeout(() => {
		publishWorkspaceNow();
	}, 150);
}

function publishWorkspaceNow(options = {}) {
	if (!collaboration.ready) return false;
	if (!collaboration.ws || collaboration.ws.readyState !== WebSocket.OPEN) return false;

	const normalized = normalizeState(state);
	const stateHash = JSON.stringify(normalized);
	if (!options.force && stateHash === collaboration.lastSentStateHash) {
		return false;
	}

	collaboration.lastSentStateHash = stateHash;
	collaboration.ws.send(
		JSON.stringify({
			type: "workspace:update",
			workspaceId: collaboration.workspaceId,
			state: normalized,
		}),
	);
	return true;
}

function renderCollaborationStatus() {
	if (collabStatus) {
		collabStatus.textContent = collaboration.connected && collaboration.ready
			? `${collaboration.workspaceId} (live)`
			: `${collaboration.workspaceId} (reconnecting…)`;
	}

	if (presenceStatus) {
		const count = Math.max(1, Number(collaboration.presenceCount || 1));
		presenceStatus.textContent = `${count} connected`;
	}
}

function resolveWorkspaceId() {
	const params = new URLSearchParams(window.location.search);
	const raw = String(params.get("workspace") || DEFAULT_WORKSPACE_ID).trim();
	if (!raw) return DEFAULT_WORKSPACE_ID;
	return /^[a-zA-Z0-9._-]{1,64}$/.test(raw) ? raw : DEFAULT_WORKSPACE_ID;
}

async function runAssignmentGeneration() {
	const topic = state.topic.trim();
	const difficulty = state.difficulty.trim();
	const count = clampInteger(state.questionCount) || 4;

	if (!topic) {
		showToast("Pick a topic before generating an assignment.", "error");
		return;
	}

	if (!difficulty) {
		showToast("Pick a difficulty before generating an assignment.", "error");
		return;
	}

	generateAssignmentButton.disabled = true;
	const originalLabel = generateAssignmentButton.textContent;
	generateAssignmentButton.textContent = "Generating…";

	try {
		const response = await fetch("/api/assignment/generate", {
			method: "POST",
			headers: { "content-type": "application/json" },
			body: JSON.stringify({ topic, difficulty, count }),
		});

		const payload = await response.json();
		if (!response.ok) {
			throw new Error(payload.message || "Assignment generation failed.");
		}

		if (!Array.isArray(payload.questions) || payload.questions.length === 0) {
			throw new Error("Assignment generator returned no questions.");
		}

		state.questions = payload.questions.map((question, index) => normalizeQuestion(question, index));
		primeQuestionCollapsing(state.questions.length);
		state.questionCount = String(state.questions.length);
		state.generatorSeed = payload.seed ? String(payload.seed) : "";
		if (!state.assignmentId) state.assignmentId = buildSuggestedAssignmentId(state.topic, state.difficulty);
		if (!state.assignmentTitle) state.assignmentTitle = buildSuggestedAssignmentTitle(state.topic, state.difficulty, state.questions.length);
		if (!state.instructions) state.instructions = "Show your working for each question.";
		if (!state.studentId) state.studentId = "student-001";
		if (!state.passThreshold) state.passThreshold = "0.70";

		renderAll();
		showToast("Assignment context filled from backend generator.", "success");
	} catch (error) {
		showToast(error instanceof Error ? error.message : "Assignment generation failed.", "error");
	} finally {
		generateAssignmentButton.disabled = false;
		generateAssignmentButton.textContent = originalLabel;
	}
}

async function runAssignmentDraft({ button, path, apply, successMessage }) {
	button.disabled = true;
	const originalLabel = button.textContent;
	button.textContent = "Working…";

	try {
		const response = await fetch(path, {
			method: "POST",
			headers: { "content-type": "application/json" },
			body: JSON.stringify(buildAssignmentPayload(state)),
		});

		const payload = await response.json();
		if (!response.ok) {
			throw new Error(payload.message || "AI request failed.");
		}

		apply(payload);
		renderAll();
		showToast(successMessage, "success");
	} catch (error) {
		showToast(error instanceof Error ? error.message : "AI request failed.", "error");
	} finally {
		button.disabled = false;
		button.textContent = originalLabel;
	}
}

function saveCurrentExample() {
	const normalized = normalizeState(state);
	const derived = buildDerivedExample(normalized);
	const errors = validateStateForDataset(normalized, derived.record.teacherReview);
	if (errors.length > 0) {
		throw new Error(`Save blocked: ${errors[0]}`);
	}

	const timestamp = new Date().toISOString();
	const example = {
		id: activeExampleId || crypto.randomUUID(),
		savedAt: timestamp,
		title: deriveTitle(normalized),
		assignmentId: normalized.assignmentId,
		studentId: normalized.studentId,
		questionCount: normalized.questions.length,
		topic: normalized.topic,
		difficulty: normalized.difficulty,
		state: normalized,
		record: derived.record,
		trainingExample: derived.trainingExample,
	};

	const existingIndex = savedExamples.findIndex((item) => item.id === example.id);
	if (existingIndex >= 0) {
		savedExamples.splice(existingIndex, 1, example);
	} else {
		savedExamples.unshift(example);
	}

	activeExampleId = example.id;
	persistDataset();
	renderDataset();
	showToast(existingIndex >= 0 ? "Saved example updated." : "Saved assignment example added to local dataset.", "success");
}

function loadSavedExample(id) {
	const example = savedExamples.find((item) => item.id === id);
	if (!example) {
		showToast("Saved example not found.", "error");
		return;
	}

	activeExampleId = id;
	state = normalizeState(example.state || createEmptyState());
	primeQuestionCollapsing(state.questions.length, { preserveExisting: false });
	renderAll();
	showToast("Loaded saved assignment example into the workspace.", "success");
}

function deleteSavedExample(id) {
	const example = savedExamples.find((item) => item.id === id);
	if (!example) {
		showToast("Saved example not found.", "error");
		return;
	}

	savedExamples = savedExamples.filter((item) => item.id !== id);
	if (activeExampleId === id) {
		activeExampleId = null;
	}
	persistDataset();
	renderDataset();
	showToast(`Deleted “${example.title}”.`, "success");
}

function exportDatasetJsonl() {
	const prepared = prepareSavedExamplesForExport();
	const jsonl = prepared.map((entry) => JSON.stringify(entry.trainingExample)).join("\n");
	downloadTextFile("dataset.jsonl", `${jsonl}\n`);
	showToast(`Exported ${prepared.length} example${prepared.length === 1 ? "" : "s"} to dataset.jsonl.`, "success");
}

function exportTrainValidationSplit() {
	const prepared = prepareSavedExamplesForExport();
	if (prepared.length < 2) {
		throw new Error("Need at least 2 saved examples before exporting a train/val split.");
	}

	const ordered = [...prepared].sort((left, right) => {
		const leftTime = left.savedAt ? Date.parse(left.savedAt) : 0;
		const rightTime = right.savedAt ? Date.parse(right.savedAt) : 0;
		return leftTime - rightTime;
	});

	const validationCount = Math.max(1, Math.round(ordered.length * TRAIN_VAL_SPLIT));
	const splitIndex = Math.max(1, ordered.length - validationCount);
	const train = ordered.slice(0, splitIndex);
	const validation = ordered.slice(splitIndex);

	downloadTextFile("train.jsonl", `${train.map((entry) => JSON.stringify(entry.trainingExample)).join("\n")}\n`);
	downloadTextFile("val.jsonl", `${validation.map((entry) => JSON.stringify(entry.trainingExample)).join("\n")}\n`);

	showToast(
		`Exported ${train.length} train and ${validation.length} validation example${validation.length === 1 ? "" : "s"}.`,
		"success",
	);
}

function prepareSavedExamplesForExport() {
	if (savedExamples.length === 0) {
		throw new Error("No saved examples yet. Save at least one example first.");
	}

	const invalidExamples = [];
	const prepared = savedExamples.map((example) => {
		const normalized = normalizeState(example.state || createEmptyState());
		const derived = buildDerivedExample(normalized);
		const errors = validateStateForDataset(normalized, derived.record.teacherReview);
		if (errors.length > 0) {
			invalidExamples.push(`${example.title}: ${errors[0]}`);
		}
		return {
			...example,
			record: derived.record,
			trainingExample: derived.trainingExample,
		};
	});

	if (invalidExamples.length > 0) {
		throw new Error(`Fix saved examples before export. First issue: ${invalidExamples[0]}`);
	}

	return prepared;
}

function buildDerivedExample(currentState) {
	const normalized = normalizeState(currentState);
	const assignmentQuestions = normalized.questions.map((question, index) => ({
		questionId: clampInteger(question.questionId),
		position: index + 1,
		title: question.title || `Question ${index + 1}`,
		prompt: question.prompt,
		correctAnswer: question.correctAnswer,
		tags: splitTags(question.tags),
		workedSolution: splitLines(question.workedSolution),
		subject: question.subject || "Mathematics",
		source: question.source || "rng_generated",
		difficulty: question.difficulty || normalized.difficulty,
	}));

	const studentSubmissionQuestions = normalized.questions.map((question, index) => ({
		questionId: clampInteger(question.questionId),
		position: index + 1,
		answerText: question.studentAnswer,
		workingSteps: question.workingSteps,
		solveMode: question.solveMode || "show_work",
	}));

	const teacherReviewQuestions = normalized.questions.map((question) => ({
		questionId: clampInteger(question.questionId),
		aiFeedback: question.aiFeedback,
		understandingScore: clampScore(question.understandingScore),
		confidence: clampScore(question.confidence),
		needsAttention: parseBoolean(question.needsAttention),
		issueReason: question.issueReason,
	}));

	const record = {
		version: "assignment-review-v1",
		metadata: {
			createdAt: new Date().toISOString(),
			topic: normalized.topic,
			difficulty: normalized.difficulty,
			questionCount: normalized.questions.length,
		},
		assignment: {
			assignmentId: normalized.assignmentId,
			assignmentTitle: normalized.assignmentTitle,
			instructions: normalized.instructions,
			passThreshold: clampScore(normalized.passThreshold),
			questions: assignmentQuestions,
		},
		studentSubmission: {
			studentId: normalized.studentId,
			questions: studentSubmissionQuestions,
		},
		teacherReview: {
			questions: teacherReviewQuestions,
			assignmentSummary: normalized.assignmentSummary,
			recommendedNextStep: normalized.recommendedNextStep,
		},
	};

	const trainingExample = {
		messages: [
			{
				role: "system",
				content:
					"You are an expert teacher reviewer. Review the full assignment in one pass. Return a JSON object with question-level teacher review labels for every question plus assignmentSummary and recommendedNextStep.",
			},
			{
				role: "user",
				content: JSON.stringify(
					{
						assignment: record.assignment,
						studentSubmission: record.studentSubmission,
					},
					null,
					2,
				),
			},
			{
				role: "assistant",
				content: JSON.stringify(record.teacherReview, null, 2),
			},
		],
		metadata: {
			version: "assignment-review-v1",
			assignmentId: normalized.assignmentId,
			studentId: normalized.studentId,
			questionCount: normalized.questions.length,
			topic: normalized.topic,
			difficulty: normalized.difficulty,
		},
	};

	return { record, trainingExample };
}

function validateStateForDataset(currentState, teacherReview) {
	const errors = [];
	if (!currentState.assignmentId) errors.push("Assignment ID is required.");
	if (!currentState.studentId) errors.push("Student ID is required.");
	if (!currentState.assignmentTitle) errors.push("Assignment title is required.");
	if (currentState.questions.length === 0) errors.push("At least one question is required.");
	if (!currentState.assignmentSummary) errors.push("Assignment summary is required.");
	if (!currentState.recommendedNextStep) errors.push("Recommended next step is required.");

	for (const [index, question] of currentState.questions.entries()) {
		const label = `Question ${index + 1}`;
		if (!clampInteger(question.questionId)) errors.push(`${label} needs a valid question ID.`);
		if (!question.prompt) errors.push(`${label} prompt is required.`);
		if (!question.correctAnswer) errors.push(`${label} correct answer is required.`);
		if (!question.workedSolution) errors.push(`${label} worked solution is required.`);
		if (!question.studentAnswer && !question.workingSteps) errors.push(`${label} needs student work.`);
		if (!question.aiFeedback) errors.push(`${label} AI feedback is required.`);
		if (teacherReview.questions[index]?.understandingScore === null) errors.push(`${label} understanding score must be between 0.00 and 1.00.`);
		if (teacherReview.questions[index]?.confidence === null) errors.push(`${label} confidence must be between 0.00 and 1.00.`);
		if (teacherReview.questions[index]?.needsAttention === null) errors.push(`${label} needsAttention must be true or false.`);
		if (!question.issueReason) errors.push(`${label} issue reason is required.`);
	}

	return errors;
}

function deriveTitle(currentState) {
	const count = currentState.questions.length;
	const topic = currentState.topic || "assignment";
	return `${currentState.assignmentTitle || "Untitled assignment"} • ${count} question${count === 1 ? "" : "s"} • ${topic}`;
}

function buildDatasetPills(example) {
	const pills = [];
	if (example.topic) pills.push(`topic ${example.topic}`);
	if (example.difficulty) pills.push(example.difficulty);
	if (example.questionCount) pills.push(`${example.questionCount} questions`);
	const reviewed = example.state?.questions?.filter((question) => question.aiFeedback).length || 0;
	if (reviewed) pills.push(`${reviewed} labeled`);
	return pills;
}

function buildAssignmentPayload(currentState) {
	const normalized = normalizeState(currentState);
	return {
		assignmentId: normalized.assignmentId,
		studentId: normalized.studentId,
		assignmentTitle: normalized.assignmentTitle,
		instructions: normalized.instructions,
		passThreshold: clampScore(normalized.passThreshold),
		topic: normalized.topic,
		difficulty: normalized.difficulty,
		questions: normalized.questions.map((question, index) => ({
			questionId: clampInteger(question.questionId),
			position: index + 1,
			title: question.title,
			prompt: question.prompt,
			subject: question.subject,
			source: question.source,
			difficulty: question.difficulty,
			correctAnswer: question.correctAnswer,
			workedSolution: question.workedSolution,
			tags: splitTags(question.tags),
			studentAnswer: question.studentAnswer,
			workingSteps: question.workingSteps,
			solveMode: question.solveMode,
			aiFeedback: question.aiFeedback,
			understandingScore: clampScore(question.understandingScore),
			confidence: clampScore(question.confidence),
			needsAttention: parseBoolean(question.needsAttention),
			issueReason: question.issueReason,
		})),
	};
}

function normalizeState(raw = {}) {
	const next = createEmptyState();
	for (const key of fixedFieldIds) {
		next[key] = typeof raw[key] === "string" ? raw[key] : next[key];
	}

	next.questions = Array.isArray(raw.questions) && raw.questions.length
		? raw.questions.map((question, index) => normalizeQuestion(question, index))
		: [];

	if (!next.questions.length && raw.questionPrompt) {
		next.questions = [normalizeLegacyQuestion(raw)];
	}

	next.questionCount = String(next.questions.length || clampInteger(raw.questionCount) || 0);
	return next;
}

function normalizeLegacyQuestion(raw) {
	return normalizeQuestion(
		{
			questionId: raw.questionId,
			title: raw.subject || "Legacy question",
			prompt: raw.questionPrompt,
			subject: raw.subject || "Mathematics",
			source: "legacy_helper",
			difficulty: raw.difficulty || "",
			correctAnswer: raw.correctAnswer,
			workedSolution: raw.correctReasoning,
			tags: raw.tags,
			studentAnswer: raw.studentAnswer,
			workingSteps: raw.studentReasoning,
			solveMode: "show_work",
			aiFeedback: raw.aiFeedback,
			understandingScore: raw.understandingScore,
			confidence: raw.confidence,
			needsAttention: raw.needsAttention,
			issueReason: raw.issueReason,
		},
		0,
	);
}

function normalizeQuestion(question = {}, index = 0) {
	return {
		questionId: stringValue(question.questionId),
		position: index + 1,
		title: stringValue(question.title),
		prompt: stringValue(question.prompt),
		subject: stringValue(question.subject) || "Mathematics",
		source: stringValue(question.source) || "rng_generated",
		difficulty: stringValue(question.difficulty),
		correctAnswer: stringValue(question.correctAnswer),
		workedSolution: Array.isArray(question.workedSolution)
			? question.workedSolution.join("\n")
			: stringValue(question.workedSolution),
		tags: Array.isArray(question.tags) ? question.tags.join(", ") : stringValue(question.tags),
		studentAnswer: stringValue(question.studentAnswer),
		workingSteps: stringValue(question.workingSteps),
		solveMode: stringValue(question.solveMode) || "show_work",
		aiFeedback: stringValue(question.aiFeedback),
		understandingScore: stringValue(question.understandingScore),
		confidence: stringValue(question.confidence),
		needsAttention: stringValue(question.needsAttention),
		issueReason: stringValue(question.issueReason),
	};
}

function getQuestionUiKey(question, index) {
	return `${stringValue(question.questionId) || `idx-${index + 1}`}`;
}

function isQuestionCollapsed(index) {
	return uiState.collapsedQuestions.has(getQuestionUiKey(state.questions[index], index));
}

function toggleQuestionCollapsed(index) {
	const key = getQuestionUiKey(state.questions[index], index);
	if (uiState.collapsedQuestions.has(key)) {
		uiState.collapsedQuestions.delete(key);
	} else {
		uiState.collapsedQuestions.add(key);
	}
}

function expandQuestionAtIndex(index) {
	if (!state.questions[index]) return;
	uiState.collapsedQuestions.delete(getQuestionUiKey(state.questions[index], index));
}

function primeQuestionCollapsing(questionCount, options = {}) {
	const preserveExisting = options.preserveExisting ?? true;
	if (!preserveExisting) {
		uiState.collapsedQuestions.clear();
	}

	if (questionCount <= 2) {
		uiState.collapsedQuestions.clear();
		return;
	}

	uiState.collapsedQuestions = new Set(
		state.questions
			.map((question, index) => ({ key: getQuestionUiKey(question, index), index }))
			.filter(({ index }) => index > 0)
			.map(({ key }) => key),
	);
	uiState.collapsedQuestions.delete(getQuestionUiKey(state.questions[0], 0));
	}

function matchesQuestionFilter(question) {
	if (uiState.questionFilter === "attention") {
		return parseBoolean(question.needsAttention) === true;
	}

	if (uiState.questionFilter === "unlabeled") {
		return !question.aiFeedback || !question.issueReason;
	}

	return true;
}

function hasStudentWork(question) {
	return Boolean(question.studentAnswer || question.workingSteps);
}

function isQuestionReviewed(question) {
	return Boolean(question.aiFeedback && question.issueReason);
}

function buildQuestionStatusPills(question) {
	const pills = [];
	pills.push(`<span class="dataset-pill ${hasStudentWork(question) ? "pill-success" : "pill-muted"}">${hasStudentWork(question) ? "Student drafted" : "No student work"}</span>`);
	pills.push(`<span class="dataset-pill ${isQuestionReviewed(question) ? "pill-success" : "pill-muted"}">${isQuestionReviewed(question) ? "Teacher labeled" : "Needs labels"}</span>`);
	if (parseBoolean(question.needsAttention) === true) {
		pills.push('<span class="dataset-pill pill-warning">Needs attention</span>');
	}
	if (question.understandingScore) {
		pills.push(`<span class="dataset-pill">Understanding ${escapeHtml(question.understandingScore)}</span>`);
	}
	if (question.confidence) {
		pills.push(`<span class="dataset-pill">Confidence ${escapeHtml(question.confidence)}</span>`);
	}
	return pills.join("");
}

function reindexQuestions() {
	state.questions = state.questions.map((question, index) => ({
		...question,
		position: index + 1,
	}));
	state.questionCount = String(state.questions.length);
}

function createEmptyState() {
	return {
		assignmentId: "",
		studentId: "",
		assignmentTitle: "",
		instructions: "",
		passThreshold: "0.70",
		topic: "",
		difficulty: "",
		questionCount: "0",
		generatorSeed: "",
		assignmentSummary: "",
		recommendedNextStep: "",
		questions: [],
	};
}

function createBlankQuestion(position) {
	return normalizeQuestion({
		questionId: "",
		title: `Question ${position}`,
		prompt: "",
		subject: "Mathematics",
		source: "manual",
		difficulty: state.difficulty || "",
		correctAnswer: "",
		workedSolution: "",
		tags: "",
		studentAnswer: "",
		workingSteps: "",
		solveMode: "show_work",
		aiFeedback: "",
		understandingScore: "",
		confidence: "",
		needsAttention: "",
		issueReason: "",
	}, position - 1);
}

function createSampleState() {
	return normalizeState({
		assignmentId: "assignment-fractions-01",
		studentId: "student-17",
		assignmentTitle: "Fractions review",
		instructions: "Show all working and explain your reasoning when you can.",
		passThreshold: "0.70",
		topic: "fractions",
		difficulty: "medium",
		questionCount: "2",
		generatorSeed: "sample-seed",
		assignmentSummary:
			"The student shows a workable idea for finding a fraction of an amount, but the assignment reveals inconsistent understanding when simplifying and converting between equivalent forms.",
		recommendedNextStep:
			"Reinforce fraction simplification and equivalent-fraction checks with one worked example before asking the student to retry a similar mixed set.",
		questions: [
			{
				questionId: "401",
				title: "Find a fraction of an amount",
				prompt: "Find 3/4 of 20.",
				subject: "Mathematics",
				source: "rng_generated",
				difficulty: "medium",
				correctAnswer: "15",
				workedSolution: "First find 1/4 of 20 by dividing 20 by 4 to get 5. Then multiply 5 by 3 to get 15.",
				tags: "fractions, fraction_of_amount, rng_generated",
				studentAnswer: "15",
				workingSteps: "I did 20 divided by 4 which is 5, then 5 times 3 is 15.",
				solveMode: "show_work",
				aiFeedback:
					"The student uses the correct two-step method and explains the quarter-then-multiply structure clearly, showing secure understanding for this question.",
				understandingScore: "0.90",
				confidence: "0.95",
				needsAttention: "false",
				issueReason: "The response is correct and the working shows sound understanding of finding a fraction of an amount.",
			},
			{
				questionId: "402",
				title: "Simplify a fraction",
				prompt: "Simplify 6/8.",
				subject: "Mathematics",
				source: "rng_generated",
				difficulty: "medium",
				correctAnswer: "3/4",
				workedSolution: "Divide numerator and denominator by their greatest common factor, 2. 6 ÷ 2 = 3 and 8 ÷ 2 = 4, so 6/8 = 3/4.",
				tags: "fractions, simplify, rng_generated",
				studentAnswer: "2/4",
				workingSteps: "I cut both numbers in half, so 6 became 2 and 8 became 4.",
				solveMode: "show_work",
				aiFeedback:
					"The student knows the fraction should be reduced but does not preserve the equivalence correctly. Their numerator change suggests shaky control of the simplification step rather than secure understanding.",
				understandingScore: "0.42",
				confidence: "0.89",
				needsAttention: "true",
				issueReason:
					"The student attempted to simplify but changed 6 to 2 instead of 3, so the reduced fraction is not equivalent to the original.",
			},
		],
	});
}

function buildSuggestedAssignmentId(topic, difficulty) {
	return `assignment-${topic || "topic"}-${difficulty || "level"}-${Date.now()}`;
}

function buildSuggestedAssignmentTitle(topic, difficulty, count) {
	const topicLabel = topic ? topic.replace(/_/g, " ") : "Mixed maths";
	return `${capitalize(topicLabel)} ${difficulty || ""} review • ${count} question${count === 1 ? "" : "s"}`.trim();
}

function capitalize(text) {
	return text ? text.charAt(0).toUpperCase() + text.slice(1) : "";
}

function splitTags(tags) {
	return String(tags || "")
		.split(",")
		.map((tag) => tag.trim())
		.filter(Boolean);
}

function splitLines(text) {
	return String(text || "")
		.split(/\n+/)
		.map((line) => line.trim())
		.filter(Boolean);
}

function clampScore(raw) {
	const parsed = Number(raw);
	if (!Number.isFinite(parsed)) return null;
	return Math.max(0, Math.min(1, Number(parsed.toFixed(2))));
}

function formatScore(value) {
	if (typeof value !== "number" || Number.isNaN(value)) return "";
	return Math.max(0, Math.min(1, value)).toFixed(2);
}

function clampInteger(raw) {
	const parsed = Number.parseInt(raw, 10);
	if (!Number.isInteger(parsed) || parsed < 1) return null;
	return parsed;
}

function parseBoolean(raw) {
	if (raw === true || raw === "true") return true;
	if (raw === false || raw === "false") return false;
	return null;
}

function formatBoolean(value) {
	if (value === true) return "true";
	if (value === false) return "false";
	return "";
}

function stringValue(value) {
	if (value === null || value === undefined) return "";
	return String(value).trim();
}

function summarizeText(text, limit) {
	if (!text) return "";
	const normalized = text.replace(/\s+/g, " ").trim();
	if (normalized.length <= limit) return normalized;
	return `${normalized.slice(0, Math.max(0, limit - 1)).trimEnd()}…`;
}

function renderSelectOptions(options, selected) {
	return options
		.map((option) => `<option value="${escapeAttribute(option)}" ${option === selected ? "selected" : ""}>${escapeHtml(option)}</option>`)
		.join("");
}

function escapeHtml(value) {
	return String(value || "")
		.replaceAll("&", "&amp;")
		.replaceAll("<", "&lt;")
		.replaceAll(">", "&gt;")
		.replaceAll('"', "&quot;");
}

function escapeAttribute(value) {
	return escapeHtml(value).replaceAll("'", "&#39;");
}

function isSavedExampleShape(value) {
	return Boolean(value && typeof value === "object" && typeof value.id === "string" && value.id);
}

async function copyPreview(text, successMessage) {
	try {
		await navigator.clipboard.writeText(text);
		showToast(successMessage, "success");
	} catch {
		showToast("Clipboard copy failed. You can still select and copy manually.", "error");
	}
}

function downloadTextFile(filename, content) {
	const blob = new Blob([content], { type: "application/x-ndjson;charset=utf-8" });
	const url = URL.createObjectURL(blob);
	const link = document.createElement("a");
	link.href = url;
	link.download = filename;
	document.body.appendChild(link);
	link.click();
	link.remove();
	URL.revokeObjectURL(url);
}

function showToast(message, tone) {
	toast.textContent = message;
	toast.className = `toast ${tone}`;
	clearTimeout(toastTimeout);
	toastTimeout = setTimeout(() => {
		toast.className = "toast hidden";
	}, 3200);
}
