(function () {
	'use strict';

	const app = document.getElementById('pufferpdf-app');
	let settings = null;

	if (app) {
		try {
			settings = JSON.parse(app.dataset.pufferpdfSettings || '');
		} catch (error) {
			settings = null;
		}
	}

	if (!settings || !app) {
		return;
	}

	const allowedBlocks = new Set([
		'core/paragraph',
		'core/heading',
		'core/list',
		'core/list-item',
		'core/table',
		'core/code',
		'core/quote',
		'core/separator',
	]);
	const maxBlockCount = 5000;
	const maxBlockDepth = 8;

	const elements = {
		fileInput: document.getElementById('pufferpdf-file'),
		dropzone: document.getElementById('pufferpdf-dropzone'),
		chooseMedia: document.getElementById('pufferpdf-choose-media'),
		fileSummary: document.getElementById('pufferpdf-file-summary'),
		fileName: document.getElementById('pufferpdf-file-name'),
		fileSize: document.getElementById('pufferpdf-file-size'),
		removeFile: document.getElementById('pufferpdf-remove-file'),
		profile: document.getElementById('pufferpdf-profile'),
		pageMarkers: document.getElementById('pufferpdf-page-markers'),
		passwordWrap: document.getElementById('pufferpdf-password-wrap'),
		password: document.getElementById('pufferpdf-password'),
		convert: document.getElementById('pufferpdf-convert'),
		cancel: document.getElementById('pufferpdf-cancel'),
		fileError: document.getElementById('pufferpdf-file-error'),
		emptyState: document.getElementById('pufferpdf-empty-state'),
		processingState: document.getElementById('pufferpdf-processing-state'),
		resultState: document.getElementById('pufferpdf-result-state'),
		type: document.getElementById('pufferpdf-type'),
		pages: document.getElementById('pufferpdf-pages'),
		time: document.getElementById('pufferpdf-time'),
		confidence: document.getElementById('pufferpdf-confidence'),
		ocrNotice: document.getElementById('pufferpdf-ocr-notice'),
		postTitle: document.getElementById('pufferpdf-post-title'),
		previewTab: document.getElementById('pufferpdf-preview-tab'),
		markdownTab: document.getElementById('pufferpdf-markdown-tab'),
		outlineTab: document.getElementById('pufferpdf-outline-tab'),
		previewPanel: document.getElementById('pufferpdf-preview-panel'),
		markdownPanel: document.getElementById('pufferpdf-markdown-panel'),
		outlinePanel: document.getElementById('pufferpdf-outline-panel'),
		outlineContent: document.getElementById('pufferpdf-outline-content'),
		markdown: document.getElementById('pufferpdf-markdown'),
		incompleteWrap: document.getElementById('pufferpdf-incomplete-wrap'),
		incompleteAck: document.getElementById('pufferpdf-incomplete-ack'),
		incompleteLabel: document.getElementById('pufferpdf-incomplete-label'),
		copy: document.getElementById('pufferpdf-copy'),
		download: document.getElementById('pufferpdf-download'),
		import: document.getElementById('pufferpdf-import'),
		openPublishing: document.getElementById('pufferpdf-open-publishing'),
		importHelp: document.getElementById('pufferpdf-import-help'),
		publishingSourceHelp: document.getElementById('pufferpdf-publishing-source-help'),
		importStatus: document.getElementById('pufferpdf-import-status'),
		publishing: document.getElementById('pufferpdf-publishing'),
		publishingTitle: document.getElementById('pufferpdf-publishing-title'),
		planForm: document.getElementById('pufferpdf-plan-form'),
		outputSingle: document.getElementById('pufferpdf-output-single'),
		outputSplit: document.getElementById('pufferpdf-output-split'),
		splitLevelWrap: document.getElementById('pufferpdf-split-level-wrap'),
		splitLevel: document.getElementById('pufferpdf-split-level'),
		postType: document.getElementById('pufferpdf-post-type'),
		postStatus: document.getElementById('pufferpdf-post-status'),
		parentWrap: document.getElementById('pufferpdf-parent-wrap'),
		parentControl: document.getElementById('pufferpdf-parent-control'),
		authorWrap: document.getElementById('pufferpdf-author-wrap'),
		authorControl: document.getElementById('pufferpdf-author-control'),
		taxonomyControls: document.getElementById('pufferpdf-taxonomy-controls'),
		createToc: document.getElementById('pufferpdf-create-toc'),
		createTocWrap: document.getElementById('pufferpdf-create-toc-wrap'),
		tocHelp: document.getElementById('pufferpdf-toc-help'),
		addCitations: document.getElementById('pufferpdf-add-citations'),
		createCompanion: document.getElementById('pufferpdf-create-companion'),
		companionWrap: document.getElementById('pufferpdf-companion-wrap'),
		companionHelp: document.getElementById('pufferpdf-companion-help'),
		previewPlan: document.getElementById('pufferpdf-preview-plan'),
		planStatus: document.getElementById('pufferpdf-plan-status'),
		planPreview: document.getElementById('pufferpdf-plan-preview'),
		planPreviewTitle: document.getElementById('pufferpdf-plan-preview-title'),
		planSummary: document.getElementById('pufferpdf-plan-summary'),
		planState: document.getElementById('pufferpdf-plan-state'),
		planBody: document.getElementById('pufferpdf-plan-body'),
		planParentHeading: document.getElementById('pufferpdf-plan-parent-heading'),
		createRun: document.getElementById('pufferpdf-create-run'),
		createStatus: document.getElementById('pufferpdf-create-status'),
		creationReport: document.getElementById('pufferpdf-creation-report'),
		reportTitle: document.getElementById('pufferpdf-report-title'),
		reportSummary: document.getElementById('pufferpdf-report-summary'),
		reportBody: document.getElementById('pufferpdf-report-body'),
		reviewUndo: document.getElementById('pufferpdf-review-undo'),
		undoPanel: document.getElementById('pufferpdf-undo-panel'),
		undoDetails: document.getElementById('pufferpdf-undo-details'),
		undoConfirmWrap: document.getElementById('pufferpdf-undo-confirm-wrap'),
		undoConfirm: document.getElementById('pufferpdf-undo-confirm'),
		undoConfirmLabel: document.getElementById('pufferpdf-undo-confirm-label'),
		confirmUndo: document.getElementById('pufferpdf-confirm-undo'),
		undoStatus: document.getElementById('pufferpdf-undo-status'),
		refreshInventory: document.getElementById('pufferpdf-refresh-inventory'),
		inventoryRegion: document.getElementById('pufferpdf-inventory-region'),
		inventoryBody: document.getElementById('pufferpdf-inventory-body'),
		inventoryPagination: document.getElementById('pufferpdf-inventory-pagination'),
		inventoryPageSummary: document.getElementById('pufferpdf-inventory-page-summary'),
		inventoryPrevious: document.getElementById('pufferpdf-inventory-previous'),
		inventoryNext: document.getElementById('pufferpdf-inventory-next'),
		inventoryStatus: document.getElementById('pufferpdf-inventory-status'),
		inventoryError: document.getElementById('pufferpdf-inventory-error'),
	};
	const conversionErrorCodes = new Set([
		'encrypted',
		'malformed',
		'limit',
		'engine',
		'security',
	]);
	const conversionErrorMessages = {
		encrypted: 'This PDF is password-protected. Enter its password and try again.',
		malformed: 'This PDF appears damaged or incomplete. Re-export it and try again.',
		limit: 'This PDF exceeds the safe conversion limits. Try a smaller document.',
		engine: 'PufferPDF could not process this PDF. Reload the page and try again.',
		security: 'PufferPDF could not start the PDF parser. A Content Security Policy may be blocking the same-origin module Worker or WebAssembly.',
	};

	const state = {
		file: null,
		result: null,
		engineVersion: '',
		serializedContent: '',
		activeTask: null,
		busy: false,
		importing: false,
		inventory: [],
		inventoryLoading: false,
		inventoryPage: 1,
		inventoryPerPage: 20,
		inventoryRequestId: 0,
		inventoryTotal: 0,
		inventoryTotalPages: 0,
		sourceLoading: false,
		mediaFrame: null,
		capabilityError: '',
		publishing: {
			options: null,
			model: null,
			mode: 'single',
			splitLevel: 0,
			destination: {
				postType: '',
				status: 'draft',
				parentId: 0,
				authorId: 0,
				terms: {},
			},
			extras: {
				toc: false,
				citations: false,
				companion: false,
			},
			overrides: [],
			dirty: true,
			planning: false,
			creating: false,
			plan: null,
			requestPayload: null,
			createRequestId: '',
			report: null,
			undoPreview: null,
			undoRequestId: '',
			reversing: false,
		},
			source: {
				attachmentId: 0,
				currentHash: '',
				serverVerified: false,
			latestRecord: null,
			options: {
				profile: 'fidelity',
				pageDividers: true,
			},
			selectionInvalidated: false,
			targetModifiedGmt: '',
			verificationState: '',
		},
	};

	function formatBytes(bytes) {
		if (bytes < 1024) {
			return bytes + ' B';
		}

		const units = ['KB', 'MB', 'GB'];
		let value = bytes / 1024;
		let unitIndex = 0;

		while (value >= 1024 && unitIndex < units.length - 1) {
			value /= 1024;
			unitIndex += 1;
		}

		return value.toFixed(value >= 10 ? 1 : 2) + ' ' + units[unitIndex];
	}

	function formatPdfType(pdfType) {
		return String(pdfType || 'Unknown').replace(/([a-z])([A-Z])/g, '$1 $2');
	}

	function createConversionError(code) {
		const safeCode = conversionErrorCodes.has(code) ? code : 'engine';
		const error = new Error(conversionErrorMessages[safeCode]);
		error.code = safeCode;
		return error;
	}

	function clearPdfPassword() {
		elements.password.value = '';
		elements.password.removeAttribute('aria-invalid');
		elements.passwordWrap.hidden = true;
	}

	function showPdfPassword(wasRejected) {
		elements.passwordWrap.hidden = false;
		if (wasRejected) {
			elements.password.setAttribute('aria-invalid', 'true');
		} else {
			elements.password.removeAttribute('aria-invalid');
		}
		window.setTimeout(() => elements.password.focus(), 0);
	}

	function publishingSourceNeedsVerification(source) {
		return Boolean(
			Number(source && source.attachmentId) &&
				!Boolean(source && source.serverVerified)
		);
	}

	function baseName(fileName) {
		return String(fileName || 'Imported PDF')
			.replace(/\.pdf$/i, '')
			.replace(/[-_]+/g, ' ')
			.replace(/\s+/g, ' ')
			.trim();
	}

	function downloadName(fileName) {
		const clean = baseName(fileName)
			.toLowerCase()
			.replace(/[^a-z0-9]+/g, '-')
			.replace(/^-|-$/g, '');

		return (clean || 'pufferpdf-conversion') + '.md';
	}

	function attachmentId(item) {
		return Math.max(0, Number(item && item.id) || 0);
	}

	function latestRecord(item) {
		if (!item || typeof item !== 'object') {
			return null;
		}

		const record = item.latestRecord;
		return record && typeof record === 'object' ? record : null;
	}

	function recordTarget(record) {
		const target = record && record.target;
		return target && typeof target === 'object' ? target : {};
	}

	function recordOptions(record) {
		const options = record && record.options;
		return {
			profile:
				options && options.profile === 'compact' ? 'compact' : 'fidelity',
			pageDividers:
				!options || typeof options.pageDividers !== 'boolean'
					? true
					: options.pageDividers,
		};
	}

	function recordHash(record) {
		return String((record && record.sourceHash) || '').toLowerCase();
	}

	function targetModifiedGmt(record) {
		const target = recordTarget(record);
		return String(target.modifiedGmt || '');
	}

	function targetName(record) {
		const target = recordTarget(record);
		return String(target.title || 'the existing draft');
	}

	function canReprocess(record) {
		const target = recordTarget(record);
		let editUrl;

		try {
			editUrl = new URL(String(target.editUrl || ''), window.location.origin);
		} catch (error) {
			return false;
		}

		return Boolean(
			record &&
				Number(record.id) > 0 &&
				Number(target.id) > 0 &&
				String(target.status) === 'draft' &&
				String(target.modifiedGmt || '') &&
				['http:', 'https:'].includes(editUrl.protocol) &&
				editUrl.origin === window.location.origin
		);
	}

	function updateSourceOptions() {
		state.source.options = {
			profile: elements.profile.value,
			pageDividers: elements.pageMarkers.checked,
		};
	}

	function resetSourceContext() {
		state.source = {
			attachmentId: 0,
			currentHash: '',
			serverVerified: false,
			latestRecord: null,
			options: {
				profile: elements.profile.value,
				pageDividers: elements.pageMarkers.checked,
			},
			selectionInvalidated: false,
			targetModifiedGmt: '',
			verificationState: '',
		};
	}

	function applySourceContext(context) {
		const record = context.latestRecord || null;
		const options = record ? recordOptions(record) : {
			profile: elements.profile.value,
			pageDividers: elements.pageMarkers.checked,
		};

		state.source = {
			attachmentId: Number(context.attachmentId) || 0,
			currentHash: String(context.currentHash || '').toLowerCase(),
			serverVerified: Boolean(context.serverVerified),
			latestRecord: record,
			options,
			selectionInvalidated: false,
			targetModifiedGmt: targetModifiedGmt(record),
			verificationState: [
				'never',
				'current',
				'changed',
				'unverified',
			].includes(String(context.verificationState || '').toLowerCase())
				? String(context.verificationState).toLowerCase()
				: '',
		};
		elements.profile.value = options.profile;
		elements.pageMarkers.checked = options.pageDividers;
	}

	async function sha256(buffer) {
		if (
			!window.crypto ||
			!window.crypto.subtle ||
			typeof window.crypto.subtle.digest !== 'function'
		) {
			throw new Error(
				'SHA-256 is unavailable in this browser context. Open this WordPress site over HTTPS to verify the PDF safely.'
			);
		}

		const digest = new Uint8Array(
			await window.crypto.subtle.digest('SHA-256', buffer)
		);
		return Array.from(digest, (byte) => byte.toString(16).padStart(2, '0')).join(
			''
		);
	}

	function selectedItemState(item) {
		const record = latestRecord(item);
		const selected = attachmentId(item) === state.source.attachmentId;
		const itemState = String((item && item.state) || '').toLowerCase();

		if (selected && itemState === 'changed') {
			return 'changed';
		}

		if (selected && state.source.verificationState === 'unverified') {
			return 'unverified';
		}

		if (selected && state.source.currentHash) {
			const previousHash = recordHash(record);
			if (!record || !previousHash) {
				return record ? 'unverified' : 'never';
			}
			return previousHash === state.source.currentHash ? 'current' : 'changed';
		}

		if (['never', 'current', 'changed', 'unverified'].includes(itemState)) {
			return itemState;
		}

		return record ? 'unverified' : 'never';
	}

	function stateLabel(value) {
		return {
			never: 'Never converted',
			current: 'Current',
			changed: 'Source changed',
			unverified: 'Not verified',
		}[value] || 'Not verified';
	}

	function formatDate(value) {
		if (!value) {
			return '—';
		}

		const raw = String(value);
		const date = new Date(
			/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}$/.test(raw)
				? raw.replace(' ', 'T') + 'Z'
				: raw
		);
		return Number.isNaN(date.getTime())
			? '—'
			: new Intl.DateTimeFormat(undefined, {
					dateStyle: 'medium',
					timeStyle: 'short',
				}).format(date);
	}

	function browserCapabilityError() {
		const missing = [];

		if (typeof window.Worker !== 'function') {
			missing.push('Web Workers');
		}
		if (typeof window.WebAssembly !== 'object') {
			missing.push('WebAssembly');
		}
		if (!window.crypto || !window.crypto.subtle) {
			missing.push('Web Crypto');
		}
		if (typeof window.File !== 'function' || typeof window.Blob !== 'function') {
			missing.push('File API');
		}
		if (
			typeof window.fetch !== 'function' ||
			typeof window.ReadableStream !== 'function'
		) {
			missing.push('streaming fetch');
		}

		if (!missing.length) {
			return '';
		}

		return (
			'PDF conversion is unavailable because this browser or site context lacks ' +
			missing.join(', ') +
			'. Use a current desktop browser and an HTTPS wp-admin; localhost is also supported.'
		);
	}

	function setFileError(message) {
		elements.fileError.textContent = message || '';
		elements.fileError.hidden = !message;
	}

	function setImportStatus(message, type, link) {
		elements.importStatus.replaceChildren();
		elements.importStatus.className = 'pufferpdf__message';
		elements.importStatus.setAttribute('role', type === 'error' ? 'alert' : 'status');
		elements.importStatus.setAttribute(
			'aria-live',
			type === 'error' ? 'assertive' : 'polite'
		);
		elements.importStatus.setAttribute('aria-atomic', 'true');

		if (!message) {
			elements.importStatus.hidden = true;
			return;
		}

		elements.importStatus.classList.add(
			type === 'error'
				? 'pufferpdf__message--error'
				: 'pufferpdf__message--success'
		);
		elements.importStatus.append(document.createTextNode(message));

		if (link && link.url) {
			elements.importStatus.append(document.createTextNode(' '));
			const anchor = document.createElement('a');
			anchor.href = link.url;
			anchor.textContent = link.label;
			elements.importStatus.append(anchor);
		}

		elements.importStatus.hidden = false;
	}

	function setInventoryError(message) {
		elements.inventoryError.textContent = message || '';
		elements.inventoryError.hidden = !message;
	}

	function inventorySummary(record) {
		const summary = record && record.summary;
		return summary && typeof summary === 'object' ? summary : {};
	}

	function appendInventoryCell(row, text, className) {
		const cell = document.createElement('td');
		if (className) {
			cell.className = className;
		}
		cell.textContent = text;
		row.append(cell);
		return cell;
	}

	function inventoryPageLabel() {
		if (!state.inventoryTotal || !state.inventory.length) {
			return '';
		}

		const first = (state.inventoryPage - 1) * state.inventoryPerPage + 1;
		const last = Math.min(
			first + state.inventory.length - 1,
			state.inventoryTotal
		);

		return (
			first +
			'–' +
			last +
			' of ' +
			state.inventoryTotal +
			' · Page ' +
			state.inventoryPage +
			' of ' +
			state.inventoryTotalPages
		);
	}

	function inventoryIsLocked() {
		return (
			Boolean(state.capabilityError) ||
			state.inventoryLoading ||
			state.busy ||
			state.importing ||
			publishingIsBusy() ||
			state.sourceLoading
		);
	}

	function renderInventoryPagination() {
		const locked = inventoryIsLocked();
		const hasMultiplePages = state.inventoryTotalPages > 1;

		elements.inventoryPagination.hidden = !hasMultiplePages;
		elements.inventoryPageSummary.textContent = inventoryPageLabel();
		elements.inventoryPrevious.disabled = locked || state.inventoryPage <= 1;
		elements.inventoryNext.disabled =
			locked ||
			!hasMultiplePages ||
			state.inventoryPage >= state.inventoryTotalPages;
	}

	function renderInventory() {
		renderInventoryPagination();
		elements.inventoryBody.replaceChildren();

		if (state.inventoryLoading) {
			const row = document.createElement('tr');
			appendInventoryCell(row, 'Loading Media Library PDFs…').colSpan = 6;
			elements.inventoryBody.append(row);
			return;
		}

		if (!state.inventory.length) {
			const row = document.createElement('tr');
			const cell = appendInventoryCell(
				row,
				'No PDF attachments are currently available in the Media Library.',
				'pufferpdf__inventory-empty'
			);
			cell.colSpan = 6;
			elements.inventoryBody.append(row);
			return;
		}

		state.inventory.forEach((item) => {
			const id = attachmentId(item);
			const record = latestRecord(item);
			const summary = inventorySummary(record);
			const target = recordTarget(record);
			const row = document.createElement('tr');

			if (id === state.source.attachmentId) {
				row.classList.add('is-selected');
			}

			const documentCell = document.createElement('td');
			const documentName = document.createElement('strong');
			documentName.textContent = String(item.title || item.filename || 'Untitled PDF');
			const documentMeta = document.createElement('span');
			documentMeta.textContent = [
				String(item.filename || ''),
				Number(item.bytes) > 0 ? formatBytes(Number(item.bytes)) : '',
			].filter(Boolean).join(' · ');
			documentCell.append(documentName, documentMeta);
			row.append(documentCell);

			const itemState = selectedItemState(item);
			const stateCell = document.createElement('td');
			const badge = document.createElement('span');
			badge.className = 'pufferpdf__state pufferpdf__state--' + itemState;
			badge.textContent = stateLabel(itemState);
			stateCell.append(badge);
			row.append(stateCell);

			const resultParts = [];
			if (summary.pdfType) {
				resultParts.push(formatPdfType(summary.pdfType));
			}
			if (Number(summary.pageCount) > 0) {
				resultParts.push(Number(summary.pageCount) + ' pages');
			}
			const ocrPages = normalizePages(
				summary.pagesNeedingOcr,
				summary.pageCount
			);
			if (ocrPages.length) {
				resultParts.push('OCR ' + ocrPages.join(', '));
			} else if (record) {
				resultParts.push('No OCR');
			}
			appendInventoryCell(row, resultParts.join(' · ') || '—');

			appendInventoryCell(
				row,
				formatDate(record && record.convertedAtGmt)
			);

			const targetCell = document.createElement('td');
			if (canReprocess(record)) {
				const targetLink = document.createElement('a');
				targetLink.href = safeUrl(target.editUrl);
				targetLink.textContent = String(target.title || 'Draft #' + target.id);
				targetCell.append(targetLink);
			} else if (record) {
				targetCell.textContent = 'Unavailable';
				targetCell.title =
					'PufferPDF only reprocesses an existing editable draft with a verified modification time.';
			} else {
				targetCell.textContent = '—';
			}
			if (record && target.status) {
				const targetStatus = document.createElement('span');
				targetStatus.textContent = String(target.status);
				targetCell.append(targetStatus);
			}
			row.append(targetCell);

			const actionCell = document.createElement('td');
			actionCell.className = 'pufferpdf__inventory-action';
			const action = document.createElement('button');
			action.type = 'button';
			action.className = 'button button-small';
			action.dataset.attachmentId = String(id);
			action.textContent = record
				? canReprocess(record)
					? 'Reprocess'
					: 'Review PDF'
				: 'Use PDF';
			action.disabled =
				Boolean(state.capabilityError) ||
				state.busy ||
				state.importing ||
				publishingIsBusy() ||
				state.sourceLoading ||
				!id;
			if (record && !canReprocess(record)) {
				action.title =
					'The PDF can be converted and reviewed, but its previous target cannot be updated safely.';
			}
			action.setAttribute(
				'aria-label',
				(record && !canReprocess(record)
					? 'Review '
					: record
						? 'Reprocess '
						: 'Use ') +
					String(item.title || item.filename || 'this PDF')
			);
			actionCell.append(action);
			row.append(actionCell);

			elements.inventoryBody.append(row);
		});
	}

	function normalizeInventoryPage(data, requestedPage) {
		if (!Array.isArray(data.items)) {
			throw new Error('WordPress returned an invalid PDF inventory.');
		}

		const items = data.items.filter((item) => attachmentId(item) > 0);
		const responsePage = Number(data.page);
		const responsePerPage = Number(data.perPage);
		const responseTotal = Number(data.total);
		const responseTotalPages = Number(data.totalPages);
		const page =
			Number.isInteger(responsePage) && responsePage > 0
				? responsePage
				: requestedPage;
		const perPage =
			Number.isInteger(responsePerPage) && responsePerPage > 0
				? Math.min(50, responsePerPage)
				: state.inventoryPerPage;
		const total =
			Number.isInteger(responseTotal) && responseTotal >= 0
				? responseTotal
				: items.length;
		const totalPages = total
			? Number.isInteger(responseTotalPages) && responseTotalPages > 0
				? responseTotalPages
				: Math.ceil(total / perPage)
			: 0;

		return { items, page, perPage, total, totalPages };
	}

	async function fetchInventoryPage(page) {
		let url;
		try {
			url = new URL(settings.attachmentsUrl, window.location.origin);
			url.searchParams.set('page', String(page));
			url.searchParams.set('perPage', String(state.inventoryPerPage));
		} catch (error) {
			throw new Error('The Media Library inventory endpoint is unavailable.');
		}

		const response = await fetch(url.href, {
			credentials: 'same-origin',
			headers: {
				'X-WP-Nonce': settings.nonce,
			},
		});
		const data = await response.json().catch(() => ({}));

		if (!response.ok) {
			throw new Error(data.message || 'WordPress could not load the PDF inventory.');
		}

		return normalizeInventoryPage(data, page);
	}

	async function loadInventory(verifySelected, requestedPage) {
		if (!settings.attachmentsUrl) {
			setInventoryError('The Media Library inventory endpoint is unavailable.');
			return;
		}

		const page =
			Number.isInteger(Number(requestedPage)) && Number(requestedPage) > 0
				? Number(requestedPage)
				: state.inventoryPage;
		const requestId = ++state.inventoryRequestId;
		let loadFailed = false;
		state.inventoryLoading = true;
		elements.refreshInventory.disabled = true;
		elements.chooseMedia.disabled = true;
		elements.inventoryRegion.setAttribute('aria-busy', 'true');
		elements.inventoryStatus.textContent = 'Loading Media Library PDFs.';
		setInventoryError('');
		renderInventory();

		try {
			let inventoryPage = await fetchInventoryPage(page);
			if (requestId !== state.inventoryRequestId) {
				return;
			}

			if (
				inventoryPage.totalPages > 0 &&
				inventoryPage.page > inventoryPage.totalPages
			) {
				inventoryPage = await fetchInventoryPage(inventoryPage.totalPages);
				if (requestId !== state.inventoryRequestId) {
					return;
				}
			}

			state.inventory = inventoryPage.items;
			state.inventoryPage = inventoryPage.totalPages ? inventoryPage.page : 1;
			state.inventoryPerPage = inventoryPage.perPage;
			state.inventoryTotal = inventoryPage.total;
			state.inventoryTotalPages = inventoryPage.totalPages;

			if (
				verifySelected !== false &&
				state.source.attachmentId &&
				state.source.currentHash
			) {
				try {
					const freshItem = await resolveMediaItem(state.source.attachmentId);
					if (requestId !== state.inventoryRequestId) {
						return;
					}
					const freshRecord = latestRecord(freshItem);
					const selectedRecord = state.source.latestRecord;
					const freshHash = String(freshItem.currentHash || '').toLowerCase();
					const sourceChanged =
						/^[a-f0-9]{64}$/.test(freshHash) &&
						freshHash !== state.source.currentHash;
					const recordChanged =
						(Number(freshRecord && freshRecord.id) || 0) !==
						(Number(selectedRecord && selectedRecord.id) || 0);
					const freshTargetVersion = targetModifiedGmt(freshRecord);
					const targetChanged =
						freshTargetVersion !== state.source.targetModifiedGmt;

					state.source.selectionInvalidated =
						sourceChanged || recordChanged || targetChanged;
					state.source.serverVerified =
						/^[a-f0-9]{64}$/.test(freshHash) &&
						freshHash === state.source.currentHash;
					state.source.verificationState = String(
						freshItem.state || 'unverified'
					).toLowerCase();

					const freshIndex = state.inventory.findIndex(
						(item) => attachmentId(item) === state.source.attachmentId
					);
					if (freshIndex >= 0) {
						state.inventory[freshIndex] = freshItem;
					}

					if (state.file) {
						elements.fileSize.textContent =
							'Media Library · ' +
							formatBytes(state.file.size) +
							' · ' +
							(state.source.selectionInvalidated
								? 'Refresh required'
								: stateLabel(selectedItemState(freshItem)));
					}

					if (state.source.selectionInvalidated) {
						setFileError(
							'This Media Library PDF or its linked draft changed after selection. Select it again before converting or importing.'
						);
					}
				} catch (selectionError) {
					if (requestId !== state.inventoryRequestId) {
						return;
					}
					state.source.selectionInvalidated = true;
					state.source.serverVerified = false;
					setFileError(
						selectionError.message ||
							'The selected Media Library PDF could not be reverified.'
					);
				}
			}
		} catch (error) {
			if (requestId !== state.inventoryRequestId) {
				return;
			}
			loadFailed = true;
			setInventoryError(
				error.message || 'WordPress could not load the PDF inventory.'
			);
		} finally {
			if (requestId !== state.inventoryRequestId) {
				return;
			}
			state.inventoryLoading = false;
			elements.inventoryRegion.setAttribute('aria-busy', 'false');
			elements.inventoryStatus.textContent = loadFailed
				? 'The Media Library PDF inventory could not be loaded.'
				: state.inventoryTotal
					? 'Showing ' + inventoryPageLabel().replace(' · ', ', ') + '.'
					: 'No Media Library PDFs were found.';
			elements.refreshInventory.disabled =
				Boolean(state.capabilityError) ||
				state.busy ||
				state.importing ||
				publishingIsBusy() ||
				state.sourceLoading;
			elements.chooseMedia.disabled =
				Boolean(state.capabilityError) ||
				state.busy ||
				state.importing ||
				publishingIsBusy() ||
				state.sourceLoading;
			elements.convert.disabled =
				state.busy ||
				state.sourceLoading ||
				state.source.selectionInvalidated ||
				!state.file;
			updateImportAvailability();
			renderInventory();
		}
	}

	async function resolveMediaItem(id) {
		let url;
		try {
			url = new URL(settings.attachmentsUrl, window.location.origin);
			url.searchParams.set('attachmentId', String(id));
		} catch (error) {
			throw new Error('The Media Library inventory endpoint is unavailable.');
		}

		const response = await fetch(url.href, {
			credentials: 'same-origin',
			headers: {
				'X-WP-Nonce': settings.nonce,
			},
		});
		const data = await response.json().catch(() => ({}));

		if (!response.ok) {
			throw new Error(
				data.message || 'WordPress could not resolve this PDF attachment.'
			);
		}
		if (!Array.isArray(data.items)) {
			throw new Error('WordPress returned an invalid PDF attachment record.');
		}

		const item = data.items.find(
			(candidate) => attachmentId(candidate) === Number(id)
		);
		if (!item) {
			throw new Error(
				'This PDF attachment is no longer available. Refresh the inventory and choose another document.'
			);
		}

		return item;
	}

	function mediaAccessError(isOffloaded, status) {
		if (isOffloaded) {
			return new Error(
				'WordPress can list this PDF, but this browser cannot read its bytes from the offloaded media host. Allow CORS access from ' +
					window.location.origin +
					' over HTTPS, or serve the attachment through a same-origin URL. You can also download it and use the file picker for an independent conversion that is not linked to this Media Library record. PufferPDF did not upload or duplicate the file.'
			);
		}

		return new Error(
			'WordPress can list this PDF, but its file URL could not be read' +
				(status ? ' (HTTP ' + status + ')' : '') +
				'. Check that the attachment still exists and is accessible to this administrator. PufferPDF did not upload or duplicate it.'
		);
	}

	async function readPdfResponse(response, maxBytes) {
		if (!response.body || typeof response.body.getReader !== 'function') {
			throw new Error(
				'This browser cannot enforce the PDF size limit while downloading this attachment. Download it first, then use the file picker for an independent conversion that is not linked to this Media Library record.'
			);
		}

		const reader = response.body.getReader();
		const chunks = [];
		let receivedBytes = 0;

		try {
			while (true) {
				const chunk = await reader.read();
				if (chunk.done) {
					break;
				}

				receivedBytes += chunk.value.byteLength;
				if (receivedBytes > maxBytes) {
					await reader.cancel();
					throw new Error(
						'This PDF exceeds the ' +
							formatBytes(maxBytes) +
							' browser conversion limit.'
					);
				}

				chunks.push(chunk.value);
			}
		} finally {
			reader.releaseLock();
		}

		return new Blob(chunks, {
			type: response.headers.get('Content-Type') || 'application/pdf',
		});
	}

	async function fetchMediaFile(item) {
		const urlValue = String(item.url || '').trim();
		const knownBytes = Number(item.bytes) || 0;
		if (!urlValue) {
			throw new Error('This Media Library item does not have a valid file URL.');
		}
		if (knownBytes > Number(settings.maxBytes)) {
			throw new Error(
				'This PDF is ' +
					formatBytes(knownBytes) +
					'. The current limit is ' +
					formatBytes(Number(settings.maxBytes)) +
					'.'
			);
		}
		let url;

		try {
			url = new URL(urlValue, window.location.origin);
		} catch (error) {
			throw new Error('This Media Library item does not have a valid file URL.');
		}

		if (!['http:', 'https:'].includes(url.protocol)) {
			throw new Error('This Media Library PDF does not use a readable HTTP URL.');
		}

		const isOffloaded = url.origin !== window.location.origin;
		let response;
		try {
			response = await fetch(url.href, {
				credentials: isOffloaded ? 'omit' : 'same-origin',
				mode: 'cors',
				cache: 'no-store',
				referrerPolicy: 'no-referrer',
			});
		} catch (error) {
			throw mediaAccessError(isOffloaded, 0);
		}

		if (!response.ok) {
			throw mediaAccessError(isOffloaded, response.status);
		}

		const maxBytes = Number(settings.maxBytes);
		const contentLength = Number(response.headers.get('Content-Length')) || 0;
		if (contentLength > maxBytes) {
			throw new Error(
				'This PDF is ' +
					formatBytes(contentLength) +
					'. The current limit is ' +
					formatBytes(maxBytes) +
					'.'
			);
		}

		const blob = await readPdfResponse(response, maxBytes);
		const fileName = String(item.filename || baseName(item.title) + '.pdf');
		const file = new File([blob], fileName, {
			type: blob.type || 'application/pdf',
			lastModified: Date.now(),
		});
		await validateFile(file);

		const buffer = await file.arrayBuffer();
		return {
			file,
			hash: await sha256(buffer),
		};
	}

	async function selectMediaItem(item) {
		if (
			state.capabilityError ||
			state.busy ||
			state.importing ||
			publishingIsBusy() ||
			state.sourceLoading
		) {
			return;
		}

		setSourceLoading(true);
		setFileError('');

		try {
			const resolvedItem = await resolveMediaItem(attachmentId(item));
			const loaded = await fetchMediaFile(resolvedItem);
			const authoritativeHash = String(
				resolvedItem.currentHash || ''
			).toLowerCase();
			const serverVerified = /^[a-f0-9]{64}$/.test(authoritativeHash);
			if (
				serverVerified &&
				loaded.hash !== authoritativeHash
			) {
				throw new Error(
					'WordPress and this browser received different bytes for this PDF. Purge any browser or CDN cache, refresh the inventory, and select it again.'
				);
			}
			await selectFile(loaded.file, {
				attachmentId: attachmentId(resolvedItem),
				currentHash: loaded.hash,
				serverVerified,
				latestRecord: latestRecord(resolvedItem),
				verificationState: resolvedItem.state,
			});
			const knownIndex = state.inventory.findIndex(
				(inventoryItem) =>
					attachmentId(inventoryItem) === attachmentId(resolvedItem)
			);
			if (knownIndex >= 0) {
				state.inventory[knownIndex] = resolvedItem;
			}
			elements.fileInput.value = '';
			elements.fileSize.textContent =
				'Media Library · ' +
				formatBytes(loaded.file.size) +
				' · ' +
				stateLabel(selectedItemState(resolvedItem));
		} catch (error) {
			setFileError(error.message || 'This Media Library PDF could not be read.');
		} finally {
			setSourceLoading(false);
		}
	}

	function openMediaLibrary() {
		if (!window.wp || typeof window.wp.media !== 'function') {
			setFileError('The WordPress Media Library picker is unavailable.');
			return;
		}

		if (!state.mediaFrame) {
			state.mediaFrame = window.wp.media({
				title: 'Choose an existing PDF',
				button: { text: 'Use this PDF' },
				library: { type: 'application/pdf' },
				multiple: false,
			});

			state.mediaFrame.on('select', () => {
				const selection = state.mediaFrame.state().get('selection').first();
				if (!selection) {
					return;
				}

				const selected = selection.toJSON();
				const id = Number(selected.id) || 0;
				const mime = String(selected.mime || selected.type || '').toLowerCase();
				const filename = String(selected.filename || '');

				if (!id) {
					setFileError('WordPress did not return a valid PDF attachment ID.');
					return;
				}
				if (mime && mime !== 'application/pdf' && selected.subtype !== 'pdf') {
					setFileError('Choose a PDF attachment from the Media Library.');
					return;
				}
				if (!/\.pdf$/i.test(filename)) {
					setFileError('The selected Media Library item is not a PDF.');
					return;
				}

				selectMediaItem({ id });
			});
		}

		state.mediaFrame.open();
	}

	async function validateFile(file) {
		if (!(file instanceof File)) {
			throw new Error('Choose a PDF file to continue.');
		}

		if (!/\.pdf$/i.test(file.name)) {
			throw new Error('This file does not have a .pdf extension.');
		}

		if (file.size === 0) {
			throw new Error('This PDF is empty.');
		}

		if (file.size > Number(settings.maxBytes)) {
			throw new Error(
				'This PDF is ' +
					formatBytes(file.size) +
					'. The current limit is ' +
					formatBytes(Number(settings.maxBytes)) +
					'.'
			);
		}

		const signature = new Uint8Array(await file.slice(0, 5).arrayBuffer());
		const header = String.fromCharCode.apply(null, signature);

		if (header !== '%PDF-') {
			throw new Error('The selected file does not contain a valid PDF header.');
		}
	}

	function clearResult() {
		state.result = null;
		state.engineVersion = '';
		state.serializedContent = '';
		clearPdfPassword();
		elements.markdown.value = '';
		elements.previewPanel.replaceChildren();
		elements.outlineContent.replaceChildren();
		elements.incompleteAck.checked = false;
		elements.incompleteWrap.hidden = true;
		elements.resultState.hidden = true;
		elements.processingState.hidden = true;
		elements.emptyState.hidden = false;
		setImportStatus('');
		resetPublishing();
		setTab('preview');
		updateImportAvailability();
	}

	async function selectFile(file, sourceContext) {
		if (state.capabilityError) {
			setFileError(state.capabilityError);
			return;
		}

		setFileError('');

		try {
			await validateFile(file);
		} catch (error) {
			state.file = null;
			resetSourceContext();
			elements.fileInput.value = '';
			elements.fileSummary.hidden = true;
			elements.convert.disabled = true;
			setFileError(error.message);
			clearResult();
			renderInventory();
			return;
		}

		state.file = file;
		if (sourceContext) {
			applySourceContext(sourceContext);
		} else {
			resetSourceContext();
		}
		elements.fileName.textContent = file.name;
		elements.fileSize.textContent =
			(state.source.attachmentId ? 'Media Library · ' : '') + formatBytes(file.size);
		elements.fileSummary.hidden = false;
		elements.convert.disabled = false;
		clearResult();
		renderInventory();
	}

	function removeFile() {
		cancelActiveTask();
		state.file = null;
		resetSourceContext();
		elements.fileInput.value = '';
		elements.fileSummary.hidden = true;
		elements.convert.disabled = true;
		setFileError('');
		clearResult();
		renderInventory();
	}

	function setBusy(isBusy) {
		state.busy = isBusy;
		const locked = Boolean(state.capabilityError) || isBusy || state.sourceLoading;
		elements.convert.disabled =
			locked || state.source.selectionInvalidated || !state.file;
		elements.removeFile.disabled = locked;
		elements.fileInput.disabled = locked;
		elements.chooseMedia.disabled = locked;
		elements.refreshInventory.disabled = locked || state.inventoryLoading;
		elements.profile.disabled = locked;
		elements.pageMarkers.disabled = locked;
		elements.password.disabled = locked;
		elements.dropzone.setAttribute('aria-disabled', String(locked));
		updateImportAvailability();
		renderInventory();

		if (isBusy) {
			elements.emptyState.hidden = true;
			elements.resultState.hidden = true;
			elements.processingState.hidden = false;
		}
	}

	function setSourceLoading(isLoading) {
		state.sourceLoading = isLoading;
		const locked = Boolean(state.capabilityError) || isLoading || state.busy;
		elements.convert.disabled =
			locked || state.source.selectionInvalidated || !state.file;
		elements.removeFile.disabled = locked;
		elements.fileInput.disabled = locked;
		elements.chooseMedia.disabled = locked;
		elements.refreshInventory.disabled = locked || state.inventoryLoading;
		elements.profile.disabled = locked;
		elements.pageMarkers.disabled = locked;
		elements.password.disabled = locked;
		elements.dropzone.setAttribute('aria-disabled', String(locked));
		updateImportAvailability();
		renderInventory();
	}

	function processInWorker(buffer, options) {
		return new Promise((resolve, reject) => {
			let worker;

			try {
				worker = new Worker(settings.workerUrl, {
					type: 'module',
					name: 'pufferpdf-parser',
				});
			} catch (error) {
				reject(createConversionError('security'));
				return;
			}
			let settled = false;

			const finish = (callback, value) => {
				if (settled) {
					return;
				}

				settled = true;
				window.clearTimeout(timeoutId);
				worker.terminate();
				state.activeTask = null;
				callback(value);
			};

			const timeoutId = window.setTimeout(() => {
				finish(reject, createConversionError('limit'));
			}, 120000);

			state.activeTask = {
				cancel: function () {
					const error = new Error('Conversion cancelled.');
					error.name = 'AbortError';
					finish(reject, error);
				},
			};

			worker.addEventListener('message', (event) => {
				const message = event.data || {};

				if (message.type === 'result') {
					finish(resolve, message);
					return;
				}

				if (message.type === 'error') {
					finish(reject, createConversionError(message.code));
				}
			});

			worker.addEventListener('error', () => {
				finish(reject, createConversionError('engine'));
			});

			try {
				worker.postMessage(
					{
						type: 'process',
						buffer,
						options,
					},
					[buffer]
				);
			} catch (error) {
				finish(reject, createConversionError('engine'));
			}
		});
	}

	function cancelActiveTask() {
		if (state.activeTask) {
			state.activeTask.cancel();
		}
	}

	async function convertFile() {
		if (
			state.capabilityError ||
			!state.file ||
			state.busy ||
			state.importing ||
			publishingIsBusy() ||
			state.sourceLoading
		) {
			return;
		}

		setFileError('');
		setImportStatus('');
		setBusy(true);
		const password = elements.password.value;

		try {
			const buffer = await state.file.arrayBuffer();
			if (!state.source.attachmentId || !state.source.currentHash) {
				state.source.currentHash = await sha256(buffer);
			}
			updateSourceOptions();
			renderInventory();
			const processOptions = {
				profile: state.source.options.profile,
				includePageMarkers: true,
				includeImages: false,
			};
			if (password) {
				processOptions.password = password;
			}
			const message = await processInWorker(buffer, processOptions);

			state.result = message.result || {};
			state.engineVersion = message.engineVersion || '';
			displayResult();
			clearPdfPassword();
		} catch (error) {
			if (error.name === 'AbortError') {
				elements.processingState.hidden = true;
				elements.emptyState.hidden = false;
				setFileError('Conversion cancelled.');
			} else if (error.code === 'encrypted') {
				elements.processingState.hidden = true;
				elements.emptyState.hidden = false;
				showPdfPassword(Boolean(password));
				setFileError(
					password
						? 'That password could not unlock this PDF. Check it and try again.'
						: conversionErrorMessages.encrypted
				);
			} else {
				elements.processingState.hidden = true;
				elements.emptyState.hidden = false;
				clearPdfPassword();
				setFileError(error.message || conversionErrorMessages.engine);
			}
		} finally {
			setBusy(false);
		}
	}

	function displayResult() {
		const result = state.result;
		const retainedMarkdown = String(result.markdown || '').trim();
		const markdown = state.source.options.pageDividers
			? retainedMarkdown
			: retainedMarkdown
					.split(/\r?\n/)
					.filter((line) => !/^\s*<!--\s*Page\s+\d+\s*-->\s*$/i.test(line))
					.join('\n')
					.trim();
		const model = buildConversionModel(
			retainedMarkdown,
			state.source.options.pageDividers
		);
		const previewRoot = model.root;
		let blockConversionError = null;

		elements.type.textContent = formatPdfType(result.pdfType);
		elements.pages.textContent = String(result.pageCount || 0);
		elements.time.textContent =
			Math.round(Number(result.processingTimeMs) || 0) + ' ms';
		elements.time.title = 'Parser time after the WebAssembly engine initialized';
		elements.confidence.textContent =
			Math.round((Number(result.confidence) || 0) * 100) + '%';
		elements.markdown.value = markdown;
		elements.previewPanel.replaceChildren(
			...Array.from(previewRoot.childNodes).map((node) => node.cloneNode(true))
		);

		try {
			state.serializedContent = serializePreview(previewRoot);
			const topLevelBlocks = window.wp.blocks
				.parse(state.serializedContent)
				.filter((block) => block && block.name);
			if (topLevelBlocks.length !== model.blockPages.length) {
				throw new Error(
					'WordPress changed the top-level document structure while preparing the publishing model.'
				);
			}
			state.publishing.model = {
				blockPages: model.blockPages.slice(),
				outline: model.outline.slice(),
			};
		} catch (error) {
			state.serializedContent = '';
			state.publishing.model = null;
			blockConversionError = error;
		}
		const existingTarget = recordTarget(state.source.latestRecord);
		elements.postTitle.value = String(
			existingTarget.title || result.title || baseName(state.file.name)
		);
		elements.postTitle.readOnly = Boolean(state.source.latestRecord);
		elements.processingState.hidden = true;
		elements.emptyState.hidden = true;
		elements.resultState.hidden = false;
		setTab('preview');
		renderOutline();
		renderSplitLevels();
		updateInspectionNotice();
		updateImportAvailability();

		if (blockConversionError) {
			setImportStatus(
				'The PDF was converted, but WordPress could not prepare the draft blocks: ' +
					(blockConversionError.message || 'Unknown block conversion error.'),
				'error'
			);
		}
	}

	function updateInspectionNotice() {
		const result = state.result || {};
		const pages = normalizePages(result.pagesNeedingOcr, result.pageCount);
		const parts = [];
		const layout = result.layout || {};
		const isBlocked = ['Scanned', 'ImageBased'].includes(result.pdfType);
		const isIncomplete =
			result.pdfType === 'Mixed' || pages.length > 0 || result.hasEncodingIssues;

		elements.ocrNotice.replaceChildren();
		elements.ocrNotice.className = 'pufferpdf__notice';

		if (isBlocked) {
			elements.ocrNotice.classList.add('pufferpdf__notice--error');
			parts.push('This document needs OCR before its content can be imported.');
		} else if (isIncomplete) {
			elements.ocrNotice.classList.add('pufferpdf__notice--warning');
			parts.push('The available text was extracted, but the draft may be incomplete.');
		} else {
			elements.ocrNotice.classList.add('pufferpdf__notice--success');
			parts.push('All detected pages contain native text. No OCR routing is needed.');
		}

		if (pages.length) {
			parts.push('Pages needing OCR: ' + pages.join(', ') + '.');
		}

		if (result.hasEncodingIssues) {
			parts.push('The parser detected a font encoding issue.');
		}

		const tablePages = normalizePages(layout.pagesWithTables, result.pageCount);
		const columnPages = normalizePages(layout.pagesWithColumns, result.pageCount);

		if (tablePages.length || columnPages.length) {
			const structures = [];
			if (tablePages.length) {
				structures.push(tablePages.length + ' page(s) with tables');
			}
			if (columnPages.length) {
				structures.push(columnPages.length + ' page(s) with columns');
			}
			parts.push('Detected ' + structures.join(' and ') + '.');
		}

		const icon = document.createElement('span');
		icon.className = 'dashicons ' +
			(isBlocked
				? 'dashicons-dismiss'
				: isIncomplete
					? 'dashicons-warning'
					: 'dashicons-yes-alt');
		icon.setAttribute('aria-hidden', 'true');

		const copy = document.createElement('p');
		copy.textContent = parts.join(' ');
		elements.ocrNotice.append(icon, copy);

		elements.incompleteAck.checked = false;
		elements.incompleteWrap.hidden = !isIncomplete || isBlocked;
		if (!elements.incompleteWrap.hidden) {
			elements.incompleteLabel.textContent = pages.length
				? 'I understand that pages ' +
					pages.join(', ') +
					' may be missing from this draft.'
				: 'I understand that this draft may contain encoding errors.';
		}
	}

	function normalizePages(pages, pageCount) {
		const maximum = Number(pageCount) || Number.MAX_SAFE_INTEGER;
		return Array.from(
			new Set(
				(Array.isArray(pages) ? pages : [])
					.map(Number)
					.filter((page) => Number.isInteger(page) && page >= 1 && page <= maximum)
			)
		).sort((a, b) => a - b);
	}

	function serializePreview(previewRoot) {
		if (!previewRoot.childNodes.length) {
			return '';
		}

		ensureCoreBlocksRegistered();

		const blocks = window.wp.blocks.rawHandler({ HTML: previewRoot.innerHTML });
		validateBlockTree(blocks);

		const serialized = window.wp.blocks.serialize(blocks);
		const reparsed = window.wp.blocks.parse(serialized);
		validateBlockTree(reparsed, true);

		return serialized;
	}

	function validateBlockTree(blocks, requireValid = false) {
		if (!Array.isArray(blocks)) {
			throw new Error('WordPress returned an invalid block conversion result.');
		}

		const unsupported = [];
		const queue = blocks.map((block) => ({ block, depth: 0 }));
		let blockCount = 0;

		while (queue.length) {
			const current = queue.shift();
			const block = current.block;
			if (!block || typeof block !== 'object') {
				throw new Error('WordPress returned an incomplete block conversion result.');
			}

			blockCount += 1;
			if (blockCount > maxBlockCount || current.depth > maxBlockDepth) {
				throw new Error('The converted document contains too many nested blocks.');
			}
			if (!allowedBlocks.has(block.name)) {
				unsupported.push(block.name || 'unknown');
			}
			if (requireValid && block.isValid === false) {
				throw new Error('WordPress could not validate the serialized draft blocks.');
			}
			if (Array.isArray(block.innerBlocks)) {
				queue.push(
					...block.innerBlocks.map((innerBlock) => ({
						block: innerBlock,
						depth: current.depth + 1,
					}))
				);
			}
		}

		if (unsupported.length) {
			throw new Error(
				'The preview produced unsupported blocks: ' +
					Array.from(new Set(unsupported)).join(', ')
			);
		}
	}

	function ensureCoreBlocksRegistered() {
		if (
			!window.wp ||
			!window.wp.blocks ||
			typeof window.wp.blocks.getBlockType !== 'function' ||
			typeof window.wp.blocks.rawHandler !== 'function' ||
			typeof window.wp.blocks.serialize !== 'function' ||
			typeof window.wp.blocks.parse !== 'function' ||
			!window.wp.blockLibrary ||
			typeof window.wp.blockLibrary.registerCoreBlocks !== 'function'
		) {
			throw new Error('The WordPress block converter is unavailable.');
		}

		const required = Array.from(allowedBlocks);
		let missing = required.filter(
			(blockName) => !window.wp.blocks.getBlockType(blockName)
		);

		if (missing.length === required.length) {
			window.wp.blockLibrary.registerCoreBlocks();
			missing = required.filter(
				(blockName) => !window.wp.blocks.getBlockType(blockName)
			);
		}

		if (missing.length) {
			throw new Error(
				'The required WordPress blocks are unavailable: ' + missing.join(', ')
			);
		}
	}

	function buildConversionModel(markdown, includePageDividers = true) {
		const root = document.createElement('div');
		const lines = String(markdown || '').replace(/\r\n?/g, '\n').split('\n');
		const blockPages = [];
		const outline = [];
		let index = 0;
		let currentPage = 1;
		let lastContentPage = 1;

		const appendBlock = (node, sourcePage = currentPage, contributesSource = true) => {
			root.append(node);
			blockPages.push(sourcePage);
			if (contributesSource) {
				lastContentPage = sourcePage;
			}
		};

		while (index < lines.length) {
			const line = lines[index];

			if (!line.trim()) {
				index += 1;
				continue;
			}

			const fence = parseFenceStart(line);
			if (fence) {
				const code = [];
				index += 1;
				while (index < lines.length && !isFenceEnd(lines[index], fence)) {
					code.push(lines[index]);
					index += 1;
				}
				if (index < lines.length) {
					index += 1;
				}
				const pre = document.createElement('pre');
				const codeElement = document.createElement('code');
				if (fence.language) {
					codeElement.className = 'language-' + fence.language;
				}
				codeElement.textContent = code.join('\n');
				pre.append(codeElement);
				appendBlock(pre);
				continue;
			}

			const pageMarker = line.match(/^\s*<!--\s*Page\s+(\d+)\s*-->\s*$/i);
			if (pageMarker) {
				const page = Number(pageMarker[1]);
				if (Number.isInteger(page) && page >= 1) {
					if (includePageDividers && page > 1 && page !== currentPage && root.childNodes.length) {
						appendBlock(document.createElement('hr'), lastContentPage, false);
					}
					currentPage = page;
				}
				index += 1;
				continue;
			}

			const heading = line.match(/^\s*(#{1,6})\s+(.+)$/);
			if (heading) {
				const level = heading[1].length;
				const headingElement = document.createElement('h' + level);
				appendInline(headingElement, heading[2].trim());
				appendBlock(headingElement);
				outline.push({
					blockIndex: blockPages.length - 1,
					level,
					page: currentPage,
					title: String(headingElement.textContent || '').replace(/\s+/g, ' ').trim(),
				});
				index += 1;
				continue;
			}

			if (/^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/.test(line)) {
				appendBlock(document.createElement('hr'));
				index += 1;
				continue;
			}

			const table = parseTableAt(lines, index);
			if (table) {
				const rendered = renderTable(table);
				appendBlock(rendered.element);
				index = rendered.nextIndex;
				continue;
			}

			if (parseListItem(line)) {
				const rendered = renderList(lines, index);
				rendered.elements.forEach((list) => appendBlock(list));
				index = rendered.nextIndex;
				continue;
			}

			if (/^\s*>/.test(line)) {
				const quoteLines = [];
				while (index < lines.length && /^\s*>/.test(lines[index])) {
					quoteLines.push(lines[index].replace(/^\s*>\s?/, ''));
					index += 1;
				}
				const quote = document.createElement('blockquote');
				const paragraph = document.createElement('p');
				appendInline(paragraph, quoteLines.join(' ').trim());
				quote.append(paragraph);
				appendBlock(quote);
				continue;
			}

			const paragraphLines = [line.trim()];
			index += 1;
			while (
				index < lines.length &&
				lines[index].trim() &&
				!startsBlock(lines, index)
			) {
				paragraphLines.push(lines[index].trim());
				index += 1;
			}

			const paragraph = document.createElement('p');
			appendParagraphInline(paragraph, paragraphLines.join(' '));
			appendBlock(paragraph);
		}

		return { blockPages, outline, root };
	}

	function startsBlock(lines, index) {
		const line = lines[index] || '';
		return (
			Boolean(parseFenceStart(line)) ||
			/^\s*#{1,6}\s+/.test(line) ||
			/^\s*<!--\s*Page\s+\d+\s*-->\s*$/i.test(line) ||
			/^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/.test(line) ||
			Boolean(parseListItem(line)) ||
			/^\s*>/.test(line) ||
			Boolean(parseTableAt(lines, index))
		);
	}

	function parseFenceStart(line) {
		const match = String(line || '').match(/^ {0,3}(`{3,}|~{3,})([^\r\n]*)$/);
		if (!match) {
			return null;
		}

		const language = match[2].trim().match(/^([\w-]+)/);
		return {
			character: match[1][0],
			length: match[1].length,
			language: language ? language[1] : '',
		};
	}

	function isFenceEnd(line, fence) {
		const match = String(line || '').match(/^ {0,3}(`+|~+)[ \t]*$/);
		return Boolean(
			match &&
			match[1][0] === fence.character &&
			match[1].length >= fence.length
		);
	}

	function indentationWidth(value) {
		let width = 0;
		for (const character of String(value || '')) {
			width += character === '\t' ? 4 - (width % 4) : 1;
		}
		return width;
	}

	function parseListItem(line) {
		const match = String(line || '').match(/^([ \t]*)([-+*]|(\d+)[.)])[ \t]+(.*)$/);
		if (!match) {
			return null;
		}

		return {
			content: match[4],
			indent: indentationWidth(match[1]),
			ordered: Boolean(match[3]),
			start: match[3] ? Math.max(1, Number(match[3]) || 1) : 1,
		};
	}

	function renderList(lines, startIndex) {
		const elements = [];
		const stack = [];
		const maxListLevels = Math.floor((maxBlockDepth + 1) / 2);
		let index = startIndex;

		const pushList = (item, parent) => {
			if (stack.length >= maxListLevels) {
				throw new Error(
					'The converted document contains a list nested beyond the supported depth.'
				);
			}

			const list = document.createElement(item.ordered ? 'ol' : 'ul');
			if (item.ordered && item.start !== 1) {
				list.setAttribute('start', String(item.start));
			}

			if (parent) {
				if (!parent.lastItem) {
					throw new Error('The converted Markdown contains an invalid nested list.');
				}
				parent.lastItem.append(list);
			} else {
				elements.push(list);
			}

			const entry = {
				indent: item.indent,
				lastItem: null,
				list,
				ordered: item.ordered,
			};
			stack.push(entry);
			return entry;
		};

		while (index < lines.length) {
			const item = parseListItem(lines[index]);
			if (!item) {
				break;
			}

			while (stack.length && item.indent < stack[stack.length - 1].indent) {
				stack.pop();
			}

			let current = stack[stack.length - 1] || null;
			if (!current) {
				current = pushList(item, null);
			} else if (item.indent > current.indent) {
				current = pushList(item, current);
			} else if (item.ordered !== current.ordered) {
				stack.pop();
				current = pushList(item, stack[stack.length - 1] || null);
			}

			const listItem = document.createElement('li');
			appendInline(listItem, item.content.trim());
			current.list.append(listItem);
			current.lastItem = listItem;
			index += 1;
		}

		return { elements, nextIndex: index };
	}

	function parseTableRow(line) {
		const value = String(line || '').trim();
		const cells = [];
		let cell = '';
		let startsWithDelimiter = false;
		let endsWithDelimiter = false;

		for (let index = 0; index < value.length; index += 1) {
			const character = value[index];
			if (character === '\\') {
				const nextCharacter = value[index + 1];
				if (nextCharacter === '\\' || nextCharacter === '|') {
					cell += nextCharacter;
					index += 1;
				} else {
					cell += character;
				}
				endsWithDelimiter = false;
				continue;
			}

			if (character !== '|') {
				cell += character;
				endsWithDelimiter = false;
				continue;
			}

			if (index === 0) {
				startsWithDelimiter = true;
			}
			cells.push(cell.trim());
			cell = '';
			endsWithDelimiter = index === value.length - 1;
		}

		cells.push(cell.trim());
		if (startsWithDelimiter) {
			cells.shift();
		}
		if (endsWithDelimiter) {
			cells.pop();
		}

		return cells;
	}

	function parseTableAt(lines, index) {
		if (index + 1 >= lines.length || !String(lines[index + 1] || '').includes('|')) {
			return null;
		}

		const separators = parseTableRow(lines[index + 1]);
		if (
			separators.length < 2 ||
			!separators.every((cell) => /^:?-{3,}:?$/.test(cell))
		) {
			return null;
		}

		const headers = parseTableRow(lines[index]);
		if (headers.length !== separators.length) {
			throw new Error(
				'The converted Markdown contains an inconsistent table header. No cells were imported.'
			);
		}

		const rows = [];
		let nextIndex = index + 2;
		while (
			nextIndex < lines.length &&
			lines[nextIndex].trim() &&
			lines[nextIndex].includes('|')
		) {
			const row = parseTableRow(lines[nextIndex]);
			if (row.length !== headers.length) {
				throw new Error(
					'The converted Markdown contains an inconsistent table row. No cells were imported.'
				);
			}
			rows.push(row);
			nextIndex += 1;
		}

		return { headers, rows, nextIndex };
	}

	function renderTable(tableDefinition) {
		const table = document.createElement('table');
		const head = document.createElement('thead');
		const body = document.createElement('tbody');
		const headRow = document.createElement('tr');

		tableDefinition.headers.forEach((value) => {
			const cell = document.createElement('th');
			appendInline(cell, value);
			headRow.append(cell);
		});
		head.append(headRow);
		table.append(head, body);

		tableDefinition.rows.forEach((values) => {
			const row = document.createElement('tr');
			tableDefinition.headers.forEach((unused, cellIndex) => {
				const cell = document.createElement('td');
				appendInline(cell, values[cellIndex]);
				row.append(cell);
			});
			body.append(row);
		});

		return { element: table, nextIndex: tableDefinition.nextIndex };
	}

	function paragraphDirection(value) {
		for (const character of String(value || '')) {
			if (/[\u0590-\u08ff\ufb1d-\ufdff\ufe70-\ufefc]/.test(character)) {
				return 'rtl';
			}
			if (/\p{Letter}/u.test(character)) {
				return 'ltr';
			}
		}

		return '';
	}

	function appendParagraphInline(parent, value) {
		appendInline(parent, value);
		if (paragraphDirection(parent.textContent) === 'rtl') {
			parent.setAttribute('dir', 'rtl');
		}
	}

	function unescapeMarkdownLinkLabel(value) {
		return String(value || '').replace(/\\([\\\[\]])/g, '$1');
	}

	function appendInline(parent, value, allowLinks = true) {
		let remaining = String(value || '');

		while (remaining) {
			const token = findInlineToken(remaining, allowLinks);

			if (!token) {
				parent.append(document.createTextNode(remaining));
				break;
			}

			if (token.index > 0) {
				parent.append(document.createTextNode(remaining.slice(0, token.index)));
			}

			if (token.type === 'code') {
				const code = document.createElement('code');
				code.textContent = token.match[1];
				parent.append(code);
			} else if (token.type === 'link') {
				const href = safeUrl(token.match[2]);
				if (href) {
					const anchor = document.createElement('a');
					anchor.href = href;
					appendInline(
						anchor,
						unescapeMarkdownLinkLabel(token.match[1]),
						false
					);
					parent.append(anchor);
				} else {
					parent.append(document.createTextNode(token.match[0]));
				}
			} else {
				const element = document.createElement(token.type === 'strong' ? 'strong' : 'em');
				appendInline(element, token.match[1], allowLinks);
				parent.append(element);
			}

			remaining = remaining.slice(token.index + token.match[0].length);
		}
	}

	function findInlineToken(value, allowLinks = true) {
		const patterns = [
			...(allowLinks
				? [
						{
							type: 'link',
							regex: /\[((?:\\[\\\[\]]|[^\\\]])+)\]\(([^)\s]+)\)/,
						},
					]
				: []),
			{ type: 'code', regex: /`([^`\n]+)`/ },
			{ type: 'strong', regex: /\*\*([^*\n]+)\*\*/ },
			{ type: 'em', regex: /\*([^*\n]+)\*/ },
		];
		let earliest = null;

		patterns.forEach((pattern, priority) => {
			const match = pattern.regex.exec(value);
			if (
				match &&
				(!earliest ||
					match.index < earliest.index ||
					(match.index === earliest.index && priority < earliest.priority))
			) {
				earliest = {
					type: pattern.type,
					match,
					index: match.index,
					priority,
				};
			}
		});

		return earliest;
	}

	function safeUrl(value) {
		try {
			const url = new URL(value, window.location.origin);
			if (['http:', 'https:', 'mailto:'].includes(url.protocol)) {
				return url.href;
			}
		} catch (error) {
			return '';
		}

		return '';
	}

	function setTab(tabName) {
		const tabs = [
			{ name: 'preview', panel: elements.previewPanel, tab: elements.previewTab },
			{ name: 'markdown', panel: elements.markdownPanel, tab: elements.markdownTab },
			{ name: 'outline', panel: elements.outlinePanel, tab: elements.outlineTab },
		];
		const selectedName = tabs.some((entry) => entry.name === tabName)
			? tabName
			: 'preview';

		tabs.forEach((entry) => {
			const selected = entry.name === selectedName;
			entry.tab.setAttribute('aria-selected', String(selected));
			entry.tab.tabIndex = selected ? 0 : -1;
			entry.panel.hidden = !selected;
		});
	}

	function setLiveMessage(element, message, type) {
		element.textContent = message || '';
		element.className = 'pufferpdf__inline-status';
		element.setAttribute('role', type === 'error' ? 'alert' : 'status');
		element.setAttribute('aria-live', type === 'error' ? 'assertive' : 'polite');
		element.setAttribute('aria-atomic', 'true');
		if (message && type) {
			element.classList.add('pufferpdf__inline-status--' + type);
		}
	}

	function toggleClass(element, className, enabled) {
		if (enabled) {
			element.classList.add(className);
		} else {
			element.classList.remove(className);
		}
	}

	function publishingIsBusy() {
		return Boolean(
			state.publishing.planning ||
				state.publishing.creating ||
				state.publishing.reversing
		);
	}

	function resetPublishing() {
		state.publishing.model = null;
		state.publishing.mode = 'single';
		state.publishing.splitLevel = 0;
		state.publishing.overrides = [];
		state.publishing.dirty = true;
		state.publishing.planning = false;
		state.publishing.creating = false;
		state.publishing.plan = null;
		state.publishing.requestPayload = null;
		state.publishing.createRequestId = '';
		state.publishing.report = null;
		state.publishing.undoPreview = null;
		state.publishing.undoRequestId = '';
		state.publishing.reversing = false;
		state.publishing.extras = {
			companion: false,
			citations: false,
			toc: false,
		};
		elements.outputSingle.checked = true;
		elements.outputSplit.checked = false;
		elements.splitLevelWrap.hidden = true;
		elements.splitLevel.replaceChildren();
		elements.createToc.checked = false;
		elements.addCitations.checked = false;
		elements.createCompanion.checked = false;
		elements.undoConfirm.checked = false;
		elements.publishing.hidden = true;
		elements.planPreview.hidden = true;
		elements.creationReport.hidden = true;
		elements.undoPanel.hidden = true;
		elements.planBody.replaceChildren();
		elements.reportBody.replaceChildren();
		elements.undoDetails.replaceChildren();
		setLiveMessage(elements.planStatus, '');
		setLiveMessage(elements.createStatus, '');
		setLiveMessage(elements.undoStatus, '');
	}

	function renderOutline() {
		const model = state.publishing.model;
		elements.outlineContent.replaceChildren();

		if (!model || !model.outline.length) {
			const empty = document.createElement('p');
			empty.className = 'pufferpdf__outline-empty';
			empty.textContent = 'No Markdown headings were detected in this document.';
			elements.outlineContent.append(empty);
			return;
		}

		const list = document.createElement('ol');
		list.className = 'pufferpdf__outline-list';
		model.outline.forEach((heading) => {
			const item = document.createElement('li');
			item.style.setProperty('--pufferpdf-outline-depth', String(heading.level - 1));
			const level = document.createElement('span');
			level.className = 'pufferpdf__outline-level';
			level.textContent = 'H' + heading.level;
			const title = document.createElement('span');
			title.className = 'pufferpdf__outline-title';
			title.dir = 'auto';
			title.textContent = heading.title || 'Untitled heading';
			const page = document.createElement('span');
			page.className = 'pufferpdf__outline-page';
			page.textContent = 'Page ' + heading.page;
			item.append(level, title, page);
			list.append(item);
		});
		elements.outlineContent.append(list);
	}

	function splitItemCount(model, level) {
		const boundaries = model && Array.isArray(model.outline)
			? model.outline.filter(
				(heading) => heading.level === level && heading.title
			)
			: [];
		if (!boundaries.length) {
			return 0;
		}
		const blockCount = model && Array.isArray(model.blockPages)
			? model.blockPages.length
			: 0;
		const hasEmptySection = boundaries.some((heading, index) => {
			const start = Number(heading.blockIndex) + 1;
			const end = index + 1 < boundaries.length
				? Number(boundaries[index + 1].blockIndex)
				: blockCount;
			return !Number.isInteger(start) || start < 1 || end <= start || end > blockCount;
		});
		if (hasEmptySection) {
			return 0;
		}
		return boundaries.length + (Number(boundaries[0].blockIndex) > 0 ? 1 : 0);
	}

	function splitLevelIsAvailable(model, level) {
		const count = splitItemCount(model, level);
		return count >= 2 && count <= 50;
	}

	function renderSplitLevels() {
		const current = Number(elements.splitLevel.value) || 0;

		elements.splitLevel.replaceChildren();
		let firstAvailable = 0;
		[1, 2, 3].forEach((level) => {
			const count = splitItemCount(state.publishing.model, level);
			const option = document.createElement('option');
			option.value = String(level);
			option.textContent =
				'Heading ' + level + ' (' + count + ' item' + (count === 1 ? '' : 's') + ')';
			option.disabled = !splitLevelIsAvailable(state.publishing.model, level);
			if (!firstAvailable && !option.disabled) {
				firstAvailable = level;
			}
			elements.splitLevel.append(option);
		});

		elements.outputSplit.disabled = !firstAvailable;
		if (!firstAvailable) {
			elements.outputSingle.checked = true;
			elements.outputSplit.checked = false;
			elements.splitLevelWrap.hidden = true;
			state.publishing.mode = 'single';
			state.publishing.splitLevel = 0;
			return;
		}

		const selected = splitLevelIsAvailable(state.publishing.model, current)
			? current
			: firstAvailable;
		elements.splitLevel.value = String(selected);
		state.publishing.splitLevel = selected;
	}

	function hasAvailableSplitLevel() {
		return [1, 2, 3].some(
			(level) => splitLevelIsAvailable(state.publishing.model, level)
		);
	}

	function optionValue(item, fallback) {
		if (item && typeof item === 'object') {
			return String(
				item.value ?? item.slug ?? item.postType ?? item.id ?? fallback ?? ''
			);
		}
		return String(item ?? fallback ?? '');
	}

	function optionLabel(item, fallback) {
		if (item && typeof item === 'object') {
			return String(item.label ?? item.name ?? item.title ?? fallback ?? '');
		}
		return String(item ?? fallback ?? '');
	}

	function appendSelectOption(select, item, fallbackValue, fallbackLabel) {
		const option = document.createElement('option');
		option.value = optionValue(item, fallbackValue);
		option.textContent = optionLabel(item, fallbackLabel || option.value);
		select.append(option);
		return option;
	}

	function destinationTypes() {
		const options = state.publishing.options || {};
		const candidates = options.postTypes || options.destinations || options.items;
		return Array.isArray(candidates) ? candidates : [];
	}

	function selectedDestinationType() {
		const selected = String(elements.postType.value || '');
		return destinationTypes().find(
			(item) => optionValue(item) === selected
		) || null;
	}

	function renderTaxonomyControls(postType) {
		elements.taxonomyControls.replaceChildren();
		const taxonomies = postType && Array.isArray(postType.taxonomies)
			? postType.taxonomies
			: [];

		taxonomies.forEach((taxonomy) => {
			const terms = Array.isArray(taxonomy.terms) ? taxonomy.terms : [];
			if (!terms.length) {
				return;
			}
			const slug = String(taxonomy.slug || taxonomy.value || taxonomy.name || '');
			if (!slug) {
				return;
			}
			const label = document.createElement('label');
			label.className = 'pufferpdf__compact-field';
			const copy = document.createElement('span');
			copy.textContent = optionLabel(taxonomy, slug);
			const select = document.createElement('select');
			select.multiple = true;
			select.dataset.taxonomy = slug;
			select.setAttribute('aria-label', optionLabel(taxonomy, slug));
			terms.forEach((term) => appendSelectOption(select, term));
			select.addEventListener('change', () => invalidatePublishingPlan());
			label.append(copy, select);
			elements.taxonomyControls.append(label);
		});

		elements.taxonomyControls.hidden = !elements.taxonomyControls.childNodes.length;
	}

	function renderDestinationFields() {
		const types = destinationTypes();
		const options = state.publishing.options || {};
		const previousType =
			state.publishing.destination.postType ||
			elements.postType.value ||
			options.defaultPostType;
		elements.postType.replaceChildren();

		types.forEach((postType) => appendSelectOption(elements.postType, postType));
		const validPrevious = types.some(
			(postType) => optionValue(postType) === String(previousType || '')
		);
		if (validPrevious) {
			elements.postType.value = String(previousType);
		} else if (types.length) {
			elements.postType.value = optionValue(types[0]);
		}
		elements.postType.disabled = !types.length;

		const selected = selectedDestinationType();
		state.publishing.destination.postType = elements.postType.value;

		const statuses = selected && Array.isArray(selected.statuses)
			? selected.statuses
			: [];
		const previousStatus = state.publishing.destination.status || 'draft';
		elements.postStatus.replaceChildren();
		statuses.forEach((status) => {
			const normalized = typeof status === 'string'
				? {
						value: status,
						label: status.charAt(0).toUpperCase() + status.slice(1),
					}
				: status;
			appendSelectOption(elements.postStatus, normalized);
		});
		if (statuses.some((status) => optionValue(status) === previousStatus)) {
			elements.postStatus.value = previousStatus;
		} else if (statuses.length) {
			elements.postStatus.value = optionValue(statuses[0]);
		}
		elements.postStatus.disabled = !statuses.length;
		state.publishing.destination.status = elements.postStatus.value;

		const parents = selected && Array.isArray(selected.parents)
			? selected.parents
			: [];
		elements.parentControl.replaceChildren();
		appendSelectOption(elements.parentControl, { value: 0, label: 'No existing parent' });
		parents.forEach((parent) => appendSelectOption(elements.parentControl, parent));
		elements.parentWrap.hidden = !Boolean(selected && selected.hierarchical);
		if (!elements.parentWrap.hidden) {
			const parentId = String(state.publishing.destination.parentId || 0);
			if (Array.from(elements.parentControl.options).some((option) => option.value === parentId)) {
				elements.parentControl.value = parentId;
			}
		} else {
			elements.parentControl.value = '0';
		}

		const authors = selected && Array.isArray(selected.authors)
			? selected.authors
			: Array.isArray(options.authors)
				? options.authors
				: [];
		elements.authorControl.replaceChildren();
		authors.forEach((author) => appendSelectOption(elements.authorControl, author));
		elements.authorWrap.hidden = authors.length < 2 && !Boolean(selected && selected.canAssignAuthor);
		if (authors.length) {
			const authorId = String(
				state.publishing.destination.authorId ||
				selected.defaultAuthorId ||
				options.currentUserId ||
				optionValue(authors[0])
			);
			if (Array.from(elements.authorControl.options).some((option) => option.value === authorId)) {
				elements.authorControl.value = authorId;
			}
		}

		renderTaxonomyControls(selected);
		updateTocAvailability();
		updateCompanionAvailability();
		updatePublishingAvailability();
	}

	function updateTocAvailability() {
		const selected = selectedDestinationType();
		const unavailable = Boolean(selected && selected.viewable === false);
		const locked = state.busy || state.importing || state.sourceLoading || publishingIsBusy();
		elements.createToc.disabled = unavailable || locked;
		if (unavailable) {
			elements.createToc.checked = false;
		}
		toggleClass(elements.createTocWrap, 'is-disabled', unavailable || locked);
		elements.tocHelp.hidden = !unavailable;
	}

	function updateCompanionAvailability() {
		const options = state.publishing.options || {};
		const companionPolicy = options.companion || {};
		const selected = selectedDestinationType();
		const allowed = Boolean(
			state.source.attachmentId &&
				companionPolicy.allowed !== false &&
				(!selected || selected.companionAllowed !== false)
		);
		const locked = state.busy || state.importing || state.sourceLoading || publishingIsBusy();
		elements.createCompanion.disabled = !allowed || locked;
		if (!allowed) {
			elements.createCompanion.checked = false;
		}
		toggleClass(elements.companionWrap, 'is-disabled', !allowed || locked);
		elements.companionHelp.textContent = allowed
			? 'The companion will retain a direct link to the existing Media Library PDF.'
			: 'Choose a Media Library PDF to retain the original download on a companion page.';
	}

	async function requestJson(url, options = {}) {
		if (!url) {
			throw new Error('The Phase 3 publishing endpoint is unavailable.');
		}
		let endpoint;
		try {
			endpoint = new URL(String(url), window.location.origin);
		} catch (error) {
			throw new Error('The Phase 3 publishing endpoint is invalid.');
		}
		if (
			!['http:', 'https:'].includes(endpoint.protocol) ||
			endpoint.origin !== window.location.origin
		) {
			throw new Error('The Phase 3 publishing endpoint must use this WordPress site.');
		}
		const headers = { 'X-WP-Nonce': settings.nonce };
		if (options.body !== undefined) {
			headers['Content-Type'] = 'application/json';
		}
		const response = await fetch(endpoint.href, {
			method: options.method || 'GET',
			credentials: 'same-origin',
			headers,
			body: options.body === undefined ? undefined : JSON.stringify(options.body),
		});
		const data = await response.json().catch(() => ({}));
		if (!response.ok) {
			const error = new Error(data.message || 'WordPress could not complete the publishing request.');
			error.status = response.status;
			error.data = data;
			throw error;
		}
		return data;
	}

	async function loadPublishingOptions() {
		if (state.publishing.options || publishingIsBusy()) {
			renderDestinationFields();
			return;
		}
		state.publishing.planning = true;
		setLiveMessage(elements.planStatus, 'Loading available destinations…');
		updatePublishingAvailability();
		try {
			state.publishing.options = await requestJson(settings.destinationsUrl);
			if (!destinationTypes().length) {
				throw new Error('No capability-approved WordPress destinations are available.');
			}
			renderDestinationFields();
			setLiveMessage(elements.planStatus, '');
		} catch (error) {
			setLiveMessage(elements.planStatus, error.message, 'error');
		} finally {
			state.publishing.planning = false;
			updatePublishingAvailability();
		}
	}

	async function openPublishing() {
		if (elements.openPublishing.disabled) {
			return;
		}
		elements.publishing.hidden = false;
		renderSplitLevels();
		updateCompanionAvailability();
		await loadPublishingOptions();
		elements.publishingTitle.focus();
		elements.publishing.scrollIntoView({ block: 'start' });
	}

	function collectTerms() {
		const terms = {};
		elements.taxonomyControls.querySelectorAll('select[data-taxonomy]').forEach((select) => {
			terms[select.dataset.taxonomy] = Array.from(select.selectedOptions)
				.map((option) => Number(option.value) || 0)
				.filter(Boolean);
		});
		return terms;
	}

	function collectPlanOverrides() {
		if (!elements.planBody.childNodes.length) {
			return state.publishing.overrides.slice();
		}
		return Array.from(elements.planBody.querySelectorAll('tr[data-plan-index]')).map((row) => {
			const title = row.querySelector('[data-plan-field="title"]');
			const slug = row.querySelector('[data-plan-field="slug"]');
			const parent = row.querySelector('[data-plan-field="parent"]');
			const approved = row.querySelector('[data-plan-field="approved"]');
			return {
				index: Number(row.dataset.planIndex),
				approved: approved ? approved.checked : true,
				title: title ? title.value.trim() : '',
				slug: slug ? slug.value.trim() : '',
				parentIndex:
					parent && parent.value !== '' ? Number(parent.value) : null,
			};
		});
	}

	function filterPlanOverrides(overrides, policy) {
		if (policy === 'clear') {
			return [];
		}
		if (policy === 'withoutParents') {
			return overrides.map((override) => {
				const safe = { ...override };
				delete safe.parentIndex;
				return safe;
			});
		}
		return overrides;
	}

	function invalidatePublishingPlan(message, overridePolicy = 'preserve') {
		if (state.publishing.plan) {
			state.publishing.overrides = filterPlanOverrides(
				collectPlanOverrides(),
				overridePolicy
			);
		} else if (overridePolicy !== 'preserve') {
			state.publishing.overrides = filterPlanOverrides(
				state.publishing.overrides,
				overridePolicy
			);
		}
		state.publishing.dirty = true;
		state.publishing.requestPayload = null;
		state.publishing.createRequestId = '';
		elements.planState.className = 'pufferpdf__state pufferpdf__state--changed';
		elements.planState.textContent = 'Preview required';
		if (message !== false) {
			setLiveMessage(
				elements.planStatus,
				typeof message === 'string'
					? message
					: 'The publishing choices changed. Preview the plan again before creating content.',
				'warning'
			);
		}
		updatePublishingAvailability();
	}

	function collectPublishingPayload() {
		state.publishing.mode = elements.outputSplit.checked ? 'heading' : 'single';
		state.publishing.splitLevel = state.publishing.mode === 'heading'
			? Number(elements.splitLevel.value) || 0
			: 0;
		state.publishing.destination = {
			postType: elements.postType.value,
			status: elements.postStatus.value,
			parentId: elements.parentWrap.hidden ? 0 : Number(elements.parentControl.value) || 0,
			authorId: Number(elements.authorControl.value) || 0,
			terms: collectTerms(),
		};
		state.publishing.extras = {
			toc: elements.createToc.checked,
			citations: elements.addCitations.checked,
			companion: elements.createCompanion.checked,
		};
		const destination = { ...state.publishing.destination };
		if (!destination.authorId) {
			delete destination.authorId;
		}

		return {
			schemaVersion: 1,
			source: {
				filename: state.file ? state.file.name : '',
				attachmentId: state.source.attachmentId || 0,
				sourceHash: state.source.currentHash,
				engineVersion: state.engineVersion,
				pdfType: state.result ? state.result.pdfType : '',
				pageCount: Number(state.result && state.result.pageCount) || 0,
				pagesNeedingOcr: normalizePages(
					state.result && state.result.pagesNeedingOcr,
					state.result && state.result.pageCount
				),
				hasEncodingIssues: Boolean(state.result && state.result.hasEncodingIssues),
				profile: state.source.options.profile,
				pageDividers: state.source.options.pageDividers,
				acknowledgeIncomplete: elements.incompleteAck.checked,
			},
			document: {
				title: elements.postTitle.value.trim(),
				content: state.serializedContent,
				blockPages: state.publishing.model
					? state.publishing.model.blockPages.slice()
					: [],
			},
			split: {
				mode: state.publishing.mode,
				level: state.publishing.splitLevel,
			},
			destination,
			overrides: state.publishing.overrides.slice(),
			artifacts: { ...state.publishing.extras },
		};
	}

	function planItems(plan) {
		const items = plan && (plan.items || plan.outputs);
		return Array.isArray(items) ? items : [];
	}

	function planHash(plan) {
		return String((plan && (plan.planHash || plan.hash)) || '');
	}

	function itemCollisions(plan, item, index) {
		const direct = Array.isArray(item.collisions)
			? item.collisions
			: Array.isArray(item.errors)
				? item.errors
				: [];
		const global = Array.isArray(plan.collisions)
			? plan.collisions.filter(
				(collision) =>
					Number(collision.index ?? collision.itemIndex) === index
			)
			: [];
		return direct.concat(global);
	}

	function planHasCollisions(plan) {
		if (Array.isArray(plan && plan.collisions) && plan.collisions.length) {
			return true;
		}
		return planItems(plan).some((item, position) => {
			const index = Number.isInteger(Number(item.index)) ? Number(item.index) : position;
			return itemCollisions(plan, item, index).length > 0;
		});
	}

	function planValidationMessages(plan) {
		const errors = Array.isArray(plan && plan.errors) ? plan.errors : [];
		return errors.map((error) => String(error.message || error.code || error)).filter(Boolean);
	}

	function currentDestinationIsHierarchical() {
		const destination = selectedDestinationType();
		return Boolean(destination && destination.hierarchical);
	}

	function itemKindLabel(kind) {
		return {
			companion: 'HTML companion page',
			document: 'Complete document',
			overview: 'Document overview',
			section: 'Heading section',
		}[kind] || 'WordPress content';
	}

	function formatSourcePages(item) {
		const sourcePages = Array.isArray(item && item.sourcePages)
			? item.sourcePages
				.map((page) => Number(page))
				.filter((page) => Number.isInteger(page) && page > 0)
			: [];
		const pages = Array.from(new Set(sourcePages)).sort((left, right) => left - right);

		if (!pages.length) {
			return 'Source pages unavailable';
		}

		const ranges = [];
		let rangeStart = pages[0];
		let rangeEnd = pages[0];
		pages.slice(1).forEach((page) => {
			if (page === rangeEnd + 1) {
				rangeEnd = page;
				return;
			}
			ranges.push(rangeStart === rangeEnd ? String(rangeStart) : rangeStart + '–' + rangeEnd);
			rangeStart = page;
			rangeEnd = page;
		});
		ranges.push(rangeStart === rangeEnd ? String(rangeStart) : rangeStart + '–' + rangeEnd);

		return (pages.length === 1 ? 'Page ' : 'Pages ') + ranges.join(', ');
	}

	function renderPublishingPlan(plan) {
		const items = planItems(plan);
		const hierarchical = currentDestinationIsHierarchical();
		const existingParentLabel = Number(elements.parentControl.value)
			? elements.parentControl.options[elements.parentControl.selectedIndex].textContent
			: 'No parent';
		elements.planBody.replaceChildren();
		elements.planParentHeading.hidden = !hierarchical;

		let approvedCount = 0;
		let firstInvalid = null;
		items.forEach((item, position) => {
			const index = Number.isInteger(Number(item.index)) ? Number(item.index) : position;
			const kind = String(item.kind || 'content');
			const collisions = itemCollisions(plan, item, index);
			const row = document.createElement('tr');
			row.dataset.planIndex = String(index);
			row.dataset.planKind = kind;
			if (collisions.length) {
				row.classList.add('pufferpdf__plan-row--invalid');
			}

			const approveCell = document.createElement('td');
			approveCell.className = 'pufferpdf__plan-approve';
			const approve = document.createElement('input');
			approve.type = 'checkbox';
			approve.checked = kind === 'companion' ? true : item.approved !== false;
			approve.disabled = kind === 'companion';
			approve.dataset.planField = 'approved';
			approve.setAttribute('aria-label', 'Approve ' + String(item.title || 'item ' + (position + 1)));
			if (approve.checked) {
				approvedCount += 1;
			}
			approve.addEventListener('change', () => {
				row.classList.toggle('pufferpdf__plan-row--excluded', !approve.checked);
				invalidatePublishingPlan();
			});
			approveCell.append(approve);

			const titleCell = document.createElement('td');
			const titleLabel = document.createElement('label');
			titleLabel.className = 'screen-reader-text';
			titleLabel.htmlFor = 'pufferpdf-plan-title-' + index;
			titleLabel.textContent = 'Title for item ' + (position + 1);
			const title = document.createElement('input');
			title.type = 'text';
			title.id = titleLabel.htmlFor;
			title.className = 'regular-text pufferpdf__plan-input';
			title.maxLength = 200;
			title.dir = 'auto';
			title.value = String(item.title || '');
			title.dataset.planField = 'title';
			const kindLabel = document.createElement('span');
			kindLabel.className = 'pufferpdf__plan-meta';
			kindLabel.textContent = itemKindLabel(kind);
			title.addEventListener('input', () => invalidatePublishingPlan());
			titleCell.append(titleLabel, title, kindLabel);

			const slugCell = document.createElement('td');
			const slugLabel = document.createElement('label');
			slugLabel.className = 'screen-reader-text';
			slugLabel.htmlFor = 'pufferpdf-plan-slug-' + index;
			slugLabel.textContent = 'Slug for item ' + (position + 1);
			const slug = document.createElement('input');
			slug.type = 'text';
			slug.id = slugLabel.htmlFor;
			slug.className = 'regular-text pufferpdf__plan-input pufferpdf__plan-input--slug';
			slug.maxLength = 200;
			slug.value = String(item.slug || '');
			slug.dataset.planField = 'slug';
			slug.addEventListener('input', () => invalidatePublishingPlan());
			slugCell.append(slugLabel, slug);

			const sourceCell = document.createElement('td');
			sourceCell.textContent = formatSourcePages(item);

			const parentCell = document.createElement('td');
			parentCell.hidden = !hierarchical;
			if (hierarchical) {
				if (kind === 'companion') {
					parentCell.textContent = 'Root page';
				} else {
					const parent = document.createElement('select');
					parent.dataset.planField = 'parent';
					parent.setAttribute('aria-label', 'Parent for ' + String(item.title || 'item ' + (position + 1)));
					appendSelectOption(parent, { value: '', label: existingParentLabel });
					items.slice(0, position).forEach((candidate, candidatePosition) => {
						if (String(candidate.kind || 'content') === 'companion') {
							return;
						}
						const candidateIndex = Number.isInteger(Number(candidate.index))
							? Number(candidate.index)
							: candidatePosition;
						appendSelectOption(parent, {
							value: candidateIndex,
							label: String(candidate.title || 'Item ' + (candidatePosition + 1)),
						});
					});
					if (item.parentIndex !== null && item.parentIndex !== undefined) {
						parent.value = String(item.parentIndex);
					}
					parent.addEventListener('change', () => invalidatePublishingPlan());
					parentCell.append(parent);
				}
			}

			const validationCell = document.createElement('td');
			if (collisions.length) {
				const list = document.createElement('ul');
				list.className = 'pufferpdf__collision-list';
				list.id = 'pufferpdf-plan-errors-' + index;
				collisions.forEach((collision) => {
					const problem = document.createElement('li');
					problem.textContent = String(collision.message || collision.code || 'Resolve this collision.');
					list.append(problem);
					if (collision.field === 'slug') {
						slug.setAttribute('aria-invalid', 'true');
						slug.setAttribute('aria-describedby', list.id);
						firstInvalid = firstInvalid || slug;
					} else {
						title.setAttribute('aria-invalid', 'true');
						title.setAttribute('aria-describedby', list.id);
						firstInvalid = firstInvalid || title;
					}
				});
				validationCell.append(list);
			} else {
				const ready = document.createElement('span');
				ready.className = 'pufferpdf__state pufferpdf__state--current';
				ready.textContent = 'Ready';
				validationCell.append(ready);
			}

			row.classList.toggle('pufferpdf__plan-row--excluded', !approve.checked);
			row.append(approveCell, titleCell, slugCell, sourceCell, parentCell, validationCell);
			elements.planBody.append(row);
		});

		const collisions = planHasCollisions(plan);
		const validationMessages = planValidationMessages(plan);
		const blockedByServer = plan.canCreate === false || validationMessages.length > 0;
		const canCreate =
			!blockedByServer &&
			!collisions &&
			approvedCount > 0 &&
			Boolean(planHash(plan));
		state.publishing.dirty = !canCreate;
		state.publishing.overrides = collectPlanOverrides();
		elements.planSummary.textContent =
			approvedCount + ' approved item' + (approvedCount === 1 ? '' : 's') +
			(collisions
				? ' · collisions must be resolved'
				: blockedByServer
					? ' · validation changes required'
					: ' · ready to create');
		elements.planState.className =
			'pufferpdf__state pufferpdf__state--' +
			(collisions || blockedByServer ? 'changed' : 'current');
		elements.planState.textContent = collisions || blockedByServer ? 'Needs changes' : 'Previewed';
		elements.planPreview.hidden = false;
		updatePublishingAvailability();
		elements.planPreviewTitle.focus();
		if (firstInvalid) {
			window.setTimeout(() => firstInvalid.focus(), 0);
		}
	}

	async function previewPublishingPlan(event) {
		if (event) {
			event.preventDefault();
		}
		if (elements.previewPlan.disabled) {
			return;
		}
		state.publishing.overrides = state.publishing.plan
			? collectPlanOverrides()
			: state.publishing.overrides;
		const payload = collectPublishingPayload();
		state.publishing.planning = true;
		setLiveMessage(elements.planStatus, 'Validating titles, slugs, destinations, and source ranges…');
		setLiveMessage(elements.createStatus, '');
		elements.planPreview.setAttribute('aria-busy', 'true');
		updatePublishingAvailability();
		try {
			const plan = await requestJson(settings.plansUrl, {
				method: 'POST',
				body: payload,
			});
			if (!planHash(plan) || !planItems(plan).length) {
				throw new Error('WordPress returned an incomplete publishing plan.');
			}
			state.publishing.plan = plan;
			state.publishing.requestPayload = payload;
			state.publishing.createRequestId = '';
			state.publishing.dirty = false;
			state.publishing.report = null;
			elements.creationReport.hidden = true;
			renderPublishingPlan(plan);
			const blockedPlan =
				planHasCollisions(plan) ||
				plan.canCreate === false ||
				planValidationMessages(plan).length > 0;
			setLiveMessage(
				elements.planStatus,
				blockedPlan
					? planValidationMessages(plan)[0] || 'Resolve every highlighted title or slug, then preview again.'
					: 'Plan previewed. No WordPress content has been written.',
				blockedPlan ? 'warning' : 'success'
			);
		} catch (error) {
			state.publishing.dirty = true;
			state.publishing.requestPayload = null;
			setLiveMessage(elements.planStatus, error.message, 'error');
		} finally {
			state.publishing.planning = false;
			elements.planPreview.removeAttribute('aria-busy');
			updatePublishingAvailability();
			renderInventory();
		}
	}

	function createRequestId() {
		if (window.crypto && typeof window.crypto.randomUUID === 'function') {
			return window.crypto.randomUUID();
		}
		return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);
	}

	function publicationObjects(report) {
		const objects = report && (report.objects || report.items);
		return Array.isArray(objects) ? objects : [];
	}

	function publicationId(report) {
		return Number(report && (report.publicationId || report.id || report.batchId)) || 0;
	}

	function publicationUndoUrl(report) {
		const explicit = String(
			(report && (report.undoUrl || report.links?.undo)) || ''
		);
		if (explicit) {
			return explicit;
		}
		const id = publicationId(report);
		return id && settings.publicationsUrl
			? String(settings.publicationsUrl).replace(/\/$/, '') + '/' + id + '/undo'
			: '';
	}

	function renderCreationReport(report) {
		const objects = publicationObjects(report);
		elements.reportBody.replaceChildren();
		objects.forEach((object) => {
			const row = document.createElement('tr');
			const objectCell = document.createElement('td');
			const title = document.createElement('strong');
			title.dir = 'auto';
			title.textContent = String(object.title || 'Untitled object');
			const meta = document.createElement('span');
			meta.textContent = optionLabel(object.postType, 'WordPress object') +
				(object.id ? ' #' + object.id : '');
			objectCell.append(title, meta);
			const roleCell = document.createElement('td');
			roleCell.textContent = itemKindLabel(String(object.kind || object.role || ''));
			const statusCell = document.createElement('td');
			statusCell.textContent = optionLabel(object.status, '—');
			const actionCell = document.createElement('td');
			const editUrl = safeUrl(object.editUrl || object.viewUrl || object.url);
			if (editUrl) {
				const link = document.createElement('a');
				link.href = editUrl;
				link.textContent = object.editUrl ? 'Edit' : 'View';
				actionCell.append(link);
			} else {
				actionCell.textContent = '—';
			}
			row.append(objectCell, roleCell, statusCell, actionCell);
			elements.reportBody.append(row);
		});

		elements.reportSummary.textContent = report.unchanged
			? 'This request matched an existing publishing run; no duplicate objects were created.'
			: objects.length + ' WordPress object' + (objects.length === 1 ? ' was' : 's were') + ' created.';
		elements.creationReport.hidden = false;
		elements.undoPanel.hidden = true;
		elements.reviewUndo.disabled = !publicationUndoUrl(report) || Boolean(report.undone);
		elements.reportTitle.focus();
	}

	async function createPublishingRun() {
		if (elements.createRun.disabled || !state.publishing.requestPayload) {
			return;
		}
		state.publishing.creating = true;
		setLiveMessage(elements.createStatus, 'Creating the exact previewed publishing plan…');
		updatePublishingAvailability();
		const payload = {
			...state.publishing.requestPayload,
			expectedPlanHash: planHash(state.publishing.plan),
			confirm: true,
			requestId:
				state.publishing.createRequestId ||
				(state.publishing.createRequestId = createRequestId()),
		};
		const createUrl = String(
			state.publishing.plan.createUrl || settings.publicationsUrl || ''
		);
		try {
			const report = await requestJson(createUrl, {
				method: 'POST',
				body: payload,
			});
			if (!publicationId(report) || !publicationObjects(report).length) {
				throw new Error('WordPress created content but returned an incomplete creation report.');
			}
			state.publishing.report = report;
			renderCreationReport(report);
			setLiveMessage(
				elements.createStatus,
				report.unchanged
					? 'The publishing run already existed; nothing was duplicated.'
					: 'Publishing run created successfully.',
				'success'
			);
			if (state.source.attachmentId) {
				await loadInventory(false);
			}
		} catch (error) {
			if (error.status === 409) {
				invalidatePublishingPlan(false);
			}
			setLiveMessage(elements.createStatus, error.message, 'error');
		} finally {
			state.publishing.creating = false;
			updatePublishingAvailability();
			renderInventory();
		}
	}

	function renderUndoPreview(preview) {
		const blockers = Array.isArray(preview.blockers) ? preview.blockers : [];
		const previewObjects = publicationObjects(preview);
		const objects = previewObjects.length
			? previewObjects
			: publicationObjects(state.publishing.report);
		elements.undoDetails.replaceChildren();
		if (blockers.length) {
			const intro = document.createElement('p');
			intro.textContent = 'This publishing run cannot be undone because created content changed:';
			const list = document.createElement('ul');
			blockers.forEach((blocker) => {
				const item = document.createElement('li');
				item.textContent = String(blocker.message || blocker.title || blocker.code || 'A created object changed.');
				list.append(item);
			});
			elements.undoDetails.append(intro, list);
		} else {
			const copy = document.createElement('p');
			copy.textContent =
				objects.length + ' created object' + (objects.length === 1 ? '' : 's') +
				' can be moved to Trash together. ' +
				(state.source.attachmentId
					? 'The original PDF will remain in the Media Library.'
					: 'The PDF will remain unchanged.');
			elements.undoDetails.append(copy);
		}

		const canUndo = preview.canUndo !== false && !blockers.length;
		elements.undoConfirmWrap.hidden = !canUndo;
		elements.undoConfirm.checked = false;
		elements.undoConfirmLabel.textContent =
			'Move all ' + objects.length + ' created object' + (objects.length === 1 ? '' : 's') +
			' to Trash. Keep the original PDF.';
		elements.confirmUndo.disabled = true;
	}

	async function reviewUndo() {
		const report = state.publishing.report;
		const url = publicationUndoUrl(report);
		if (!url || state.publishing.reversing) {
			return;
		}
		state.publishing.reversing = true;
		elements.undoPanel.hidden = false;
		setLiveMessage(elements.undoStatus, 'Checking whether every created object is unchanged…');
		updatePublishingAvailability();
		try {
			const preview = await requestJson(url, {
				method: 'POST',
				body: {
					preview: true,
					expectedPlanHash: planHash(state.publishing.plan),
				},
			});
			state.publishing.undoPreview = preview;
			state.publishing.undoRequestId = '';
			renderUndoPreview(preview);
			setLiveMessage(
				elements.undoStatus,
				preview.canUndo === false || (preview.blockers || []).length
					? 'Undo is blocked to protect content changed after creation.'
					: 'Undo is available after explicit confirmation.',
				preview.canUndo === false || (preview.blockers || []).length ? 'error' : 'success'
			);
		} catch (error) {
			setLiveMessage(elements.undoStatus, error.message, 'error');
		} finally {
			state.publishing.reversing = false;
			updatePublishingAvailability();
			renderInventory();
		}
	}

	async function undoPublishingRun() {
		if (elements.confirmUndo.disabled || !state.publishing.undoPreview) {
			return;
		}
		const report = state.publishing.report;
		state.publishing.reversing = true;
		setLiveMessage(elements.undoStatus, 'Moving the complete publishing run to Trash…');
		updatePublishingAvailability();
		try {
			const result = await requestJson(publicationUndoUrl(report), {
				method: 'POST',
				body: {
					preview: false,
					confirm: true,
					expectedPlanHash: planHash(state.publishing.plan),
					requestId:
						state.publishing.undoRequestId ||
						(state.publishing.undoRequestId = createRequestId()),
				},
			});
			if (!result.undone) {
				throw new Error('WordPress did not confirm that the publishing run was undone.');
			}
			state.publishing.report.undone = true;
			state.publishing.undoPreview = null;
			state.publishing.undoRequestId = '';
			elements.reviewUndo.disabled = true;
			elements.undoConfirmWrap.hidden = true;
			elements.undoConfirm.checked = false;
			elements.confirmUndo.disabled = true;
			setLiveMessage(
				elements.undoStatus,
				state.source.attachmentId
					? 'All created objects were moved to Trash. The original Media Library PDF was retained.'
					: 'All created objects were moved to Trash. The PDF was unchanged.',
				'success'
			);
			if (state.source.attachmentId) {
				await loadInventory(false);
			}
		} catch (error) {
			setLiveMessage(elements.undoStatus, error.message, 'error');
		} finally {
			state.publishing.reversing = false;
			updatePublishingAvailability();
			renderInventory();
		}
	}

	function updatePublishingAvailability() {
		const result = state.result || {};
		const blocked = ['Scanned', 'ImageBased'].includes(result.pdfType);
		const incomplete =
			result.pdfType === 'Mixed' ||
			normalizePages(result.pagesNeedingOcr, result.pageCount).length > 0 ||
			Boolean(result.hasEncodingIssues);
		const reprocessing = Boolean(
			state.source.attachmentId &&
				state.source.latestRecord &&
				Number(state.source.latestRecord.id)
		);
		const sourceNeedsVerification = publishingSourceNeedsVerification(state.source);
		const locked =
			Boolean(state.capabilityError) ||
			state.busy ||
			state.importing ||
			state.sourceLoading ||
			publishingIsBusy();
		const hasReviewedDocument = Boolean(
			state.file &&
				state.serializedContent &&
				state.publishing.model &&
				elements.postTitle.value.trim()
		);

		elements.convert.disabled =
			locked || state.source.selectionInvalidated || !state.file;
		elements.removeFile.disabled = locked;
		elements.fileInput.disabled = locked;
		elements.chooseMedia.disabled = locked || state.inventoryLoading;
		elements.refreshInventory.disabled = locked || state.inventoryLoading;
		elements.profile.disabled = locked;
		elements.pageMarkers.disabled = locked;
		elements.dropzone.setAttribute('aria-disabled', String(locked));

		elements.openPublishing.hidden = false;
		elements.publishingSourceHelp.hidden = !sourceNeedsVerification;
		if (sourceNeedsVerification) {
			elements.openPublishing.setAttribute('aria-describedby', 'pufferpdf-publishing-source-help');
			elements.previewPlan.setAttribute('aria-describedby', 'pufferpdf-publishing-source-help');
		} else {
			elements.openPublishing.removeAttribute('aria-describedby');
			elements.previewPlan.removeAttribute('aria-describedby');
		}
		elements.openPublishing.disabled =
			locked ||
			state.source.selectionInvalidated ||
			sourceNeedsVerification ||
			blocked ||
			!hasReviewedDocument ||
			(incomplete && !elements.incompleteAck.checked);
		toggleClass(elements.import, 'button-primary', reprocessing);
		toggleClass(elements.openPublishing, 'button-primary', !reprocessing);

		const planControls = [
			elements.outputSingle,
			elements.outputSplit,
			elements.splitLevel,
			elements.postType,
			elements.postStatus,
			elements.parentControl,
				elements.authorControl,
				elements.addCitations,
		];
		planControls.forEach((control) => {
			control.disabled = locked;
		});
		if (!state.publishing.options) {
			elements.postType.disabled = true;
			elements.postStatus.disabled = true;
		}
		if (!hasAvailableSplitLevel()) {
			elements.outputSplit.disabled = true;
		}
		elements.splitLevel.disabled = locked || !elements.outputSplit.checked;
		updateTocAvailability();
		updateCompanionAvailability();
		if (typeof elements.taxonomyControls.querySelectorAll === 'function') {
			elements.taxonomyControls.querySelectorAll('select').forEach((select) => {
				select.disabled = locked;
			});
		}

		elements.previewPlan.disabled =
			locked ||
			state.source.selectionInvalidated ||
			sourceNeedsVerification ||
			!hasReviewedDocument ||
			!state.publishing.options ||
			!elements.postType.value ||
			!elements.postStatus.value ||
			(elements.outputSplit.checked && !Number(elements.splitLevel.value));

		const approvedCount = state.publishing.plan
			? planItems(state.publishing.plan).filter((item, position) => {
				const override = state.publishing.overrides.find(
					(candidate) => candidate.index === (Number(item.index) || position)
				);
				return String(item.kind || '') === 'companion' || !override || override.approved;
			}).length
			: 0;
		elements.createRun.textContent =
			'Create ' + approvedCount + ' approved ' +
			(elements.postStatus.value === 'pending' ? 'submission' : 'draft') +
			(approvedCount === 1 ? '' : 's');
		elements.createRun.disabled =
			locked ||
			state.source.selectionInvalidated ||
			sourceNeedsVerification ||
			state.publishing.dirty ||
			!state.publishing.plan ||
			!state.publishing.requestPayload ||
			!planHash(state.publishing.plan) ||
			planHasCollisions(state.publishing.plan) ||
			approvedCount < 1;
		elements.confirmUndo.disabled =
			locked ||
			Boolean(state.publishing.report && state.publishing.report.undone) ||
			elements.undoConfirmWrap.hidden ||
			!state.publishing.undoPreview ||
			state.publishing.undoPreview.canUndo === false ||
			!elements.undoConfirm.checked;
		renderInventoryPagination();
	}

	function updateImportAvailability() {
		const result = state.result || {};
		const blocked = ['Scanned', 'ImageBased'].includes(result.pdfType);
		const incomplete =
			result.pdfType === 'Mixed' ||
			normalizePages(result.pagesNeedingOcr, result.pageCount).length > 0 ||
			Boolean(result.hasEncodingIssues);
		const hasTitle = elements.postTitle.value.trim().length > 0;
		const reprocessing = Boolean(
			state.source.attachmentId &&
				state.source.latestRecord &&
				Number(state.source.latestRecord.id)
		);
		const unsafeReprocessing = Boolean(
			reprocessing && !canReprocess(state.source.latestRecord)
		);
		const unavailableReason = unsafeReprocessing
			? 'This conversion target is not an editable draft with a verified version.'
			: state.source.selectionInvalidated
				? 'The selected source or target changed. Select this Media Library PDF again.'
				: '';

		elements.copy.disabled = !String(result.markdown || '').trim();
		elements.copy.disabled = elements.copy.disabled || state.sourceLoading;
		elements.download.disabled = elements.copy.disabled;
		elements.import.textContent = reprocessing
			? 'Update existing draft'
			: 'Create single draft';
		elements.import.title = unavailableReason;
		elements.importHelp.textContent = unavailableReason;
		elements.importHelp.hidden = !unavailableReason;
		if (unavailableReason) {
			elements.import.setAttribute('aria-describedby', 'pufferpdf-import-help');
		} else {
			elements.import.removeAttribute('aria-describedby');
		}
		elements.import.disabled =
			state.importing ||
			publishingIsBusy() ||
			state.sourceLoading ||
			state.source.selectionInvalidated ||
			unsafeReprocessing ||
			blocked ||
			!state.serializedContent ||
			!hasTitle ||
			(incomplete && !elements.incompleteAck.checked);
		updatePublishingAvailability();
	}

	async function copyMarkdown() {
		const markdown = elements.markdown.value;
		if (!markdown) {
			return;
		}

		try {
			await navigator.clipboard.writeText(markdown);
		} catch (error) {
			elements.markdown.hidden = false;
			elements.markdown.select();
			document.execCommand('copy');
		}

		const original = elements.copy.textContent;
		elements.copy.textContent = 'Copied';
		window.setTimeout(() => {
			elements.copy.textContent = original;
		}, 1600);
	}

	function downloadMarkdown() {
		const markdown = elements.markdown.value;
		if (!markdown || !state.file) {
			return;
		}

		const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' });
		const url = URL.createObjectURL(blob);
		const anchor = document.createElement('a');
		anchor.href = url;
		anchor.download = downloadName(state.file.name);
		document.body.append(anchor);
		anchor.click();
		anchor.remove();
		URL.revokeObjectURL(url);
	}

	async function importDraft() {
		if (elements.import.disabled || !state.result || !state.file) {
			return;
		}

		const record = state.source.latestRecord;
		const reprocessRecordId =
			state.source.attachmentId && record ? Number(record.id) || 0 : 0;

		if (reprocessRecordId && !canReprocess(record)) {
			setImportStatus(
				'PufferPDF will not overwrite this conversion target because it is no longer an editable draft or its version cannot be verified.',
				'error'
			);
			return;
		}
		if (reprocessRecordId && !state.source.targetModifiedGmt) {
			setImportStatus(
				'PufferPDF cannot safely reprocess this document because the target draft version is unavailable. Refresh the inventory and select it again.',
				'error'
			);
			return;
		}

		if (
			reprocessRecordId &&
			!window.confirm(
				'Update the existing draft “' +
					targetName(record) +
					'” with this reviewed conversion? WordPress revision history will be preserved.'
			)
		) {
			return;
		}

		state.importing = true;
		updateImportAvailability();
		setImportStatus(
			reprocessRecordId
				? 'Updating “' + targetName(record) + '”…'
				: 'Creating the draft…',
			'success'
		);

		try {
			const payload = {
				title: elements.postTitle.value.trim(),
				content: state.serializedContent,
				sourceFile: state.file.name,
				sourceHash: state.source.currentHash,
				profile: state.source.options.profile,
				pageDividers: state.source.options.pageDividers,
				engineVersion: state.engineVersion,
				pdfType: state.result.pdfType,
				pageCount: Number(state.result.pageCount) || 0,
				pagesNeedingOcr: normalizePages(
					state.result.pagesNeedingOcr,
					state.result.pageCount
				),
				hasEncodingIssues: Boolean(state.result.hasEncodingIssues),
				acknowledgeIncomplete: elements.incompleteAck.checked,
			};

			if (state.source.attachmentId) {
				payload.attachmentId = state.source.attachmentId;
			}
			if (reprocessRecordId) {
				payload.reprocessRecordId = reprocessRecordId;
				payload.expectedTargetModifiedGmt = state.source.targetModifiedGmt;
			}

			const response = await fetch(settings.importUrl, {
				method: 'POST',
				credentials: 'same-origin',
				headers: {
					'Content-Type': 'application/json',
					'X-WP-Nonce': settings.nonce,
				},
				body: JSON.stringify(payload),
			});
			const data = await response.json().catch(() => ({}));

			if (!response.ok) {
				throw new Error(
					data.message ||
						(reprocessRecordId
							? 'WordPress could not update the existing draft.'
							: 'WordPress could not create the draft.')
				);
			}

			setImportStatus(
				data.unchanged
					? 'The existing draft already matches this conversion.'
					: data.reprocessed
						? 'Draft updated successfully.'
						: 'Draft created successfully.',
				'success',
				{
				url: data.editUrl,
				label: 'Edit draft',
				}
			);

			if (state.source.attachmentId) {
				await loadInventory(false);
				try {
					const freshItem = await resolveMediaItem(state.source.attachmentId);
					const freshIndex = state.inventory.findIndex(
						(item) => attachmentId(item) === state.source.attachmentId
					);
					if (freshIndex >= 0) {
						state.inventory[freshIndex] = freshItem;
					}
					state.source.latestRecord = latestRecord(freshItem);
					state.source.selectionInvalidated = false;
					state.source.targetModifiedGmt = targetModifiedGmt(
						state.source.latestRecord
					);
					state.source.verificationState = String(
						freshItem.state || 'unverified'
					).toLowerCase();
					const freshHash = String(freshItem.currentHash || '').toLowerCase();
					state.source.serverVerified =
						/^[a-f0-9]{64}$/.test(freshHash) &&
						freshHash === state.source.currentHash;
				} catch (refreshError) {
					state.source.selectionInvalidated = true;
					state.source.serverVerified = false;
					setInventoryError(
						refreshError.message ||
							'The draft was saved, but its inventory row could not be refreshed.'
					);
				}
				renderInventory();
			}
		} catch (error) {
			setImportStatus(
				error.message ||
					(reprocessRecordId
						? 'WordPress could not update the existing draft.'
						: 'WordPress could not create the draft.'),
				'error'
			);
		} finally {
			state.importing = false;
			updateImportAvailability();
			renderInventory();
		}
	}

	elements.fileInput.addEventListener('change', () => {
		if (elements.fileInput.files && elements.fileInput.files[0]) {
			selectFile(elements.fileInput.files[0]);
		}
	});

	['dragenter', 'dragover'].forEach((eventName) => {
		elements.dropzone.addEventListener(eventName, (event) => {
			event.preventDefault();
			if (
				!state.capabilityError &&
				!state.busy &&
				!state.importing &&
				!publishingIsBusy() &&
				!state.sourceLoading
			) {
				elements.dropzone.classList.add('is-dragging');
			}
		});
	});

	['dragleave', 'drop'].forEach((eventName) => {
		elements.dropzone.addEventListener(eventName, (event) => {
			event.preventDefault();
			elements.dropzone.classList.remove('is-dragging');
		});
	});

	elements.dropzone.addEventListener('drop', (event) => {
		if (
			!state.capabilityError &&
			!state.busy &&
			!state.importing &&
			!publishingIsBusy() &&
			!state.sourceLoading &&
			event.dataTransfer.files[0]
		) {
			selectFile(event.dataTransfer.files[0]);
		}
	});

	elements.removeFile.addEventListener('click', removeFile);
	elements.chooseMedia.addEventListener('click', openMediaLibrary);
	elements.refreshInventory.addEventListener('click', () => {
		loadInventory(true, state.inventoryPage);
	});
	elements.inventoryPrevious.addEventListener('click', () => {
		if (!inventoryIsLocked() && state.inventoryPage > 1) {
			loadInventory(false, state.inventoryPage - 1);
		}
	});
	elements.inventoryNext.addEventListener('click', () => {
		if (
			!inventoryIsLocked() &&
			state.inventoryPage < state.inventoryTotalPages
		) {
			loadInventory(false, state.inventoryPage + 1);
		}
	});
	elements.inventoryBody.addEventListener('click', (event) => {
		const action = event.target.closest('button[data-attachment-id]');
		if (!action) {
			return;
		}

		const id = Number(action.dataset.attachmentId) || 0;
		const item = state.inventory.find(
			(inventoryItem) => attachmentId(inventoryItem) === id
		);
		if (item) {
			selectMediaItem(item);
		}
	});
	elements.convert.addEventListener('click', convertFile);
	elements.password.addEventListener('keydown', (event) => {
		if (event.key === 'Enter') {
			event.preventDefault();
			convertFile();
		}
	});
	elements.cancel.addEventListener('click', cancelActiveTask);
	elements.previewTab.addEventListener('click', () => setTab('preview'));
	elements.markdownTab.addEventListener('click', () => setTab('markdown'));
	elements.outlineTab.addEventListener('click', () => setTab('outline'));
	elements.copy.addEventListener('click', copyMarkdown);
	elements.download.addEventListener('click', downloadMarkdown);
	elements.import.addEventListener('click', importDraft);
	elements.openPublishing.addEventListener('click', openPublishing);
	elements.planForm.addEventListener('submit', previewPublishingPlan);
	elements.createRun.addEventListener('click', createPublishingRun);
	elements.reviewUndo.addEventListener('click', reviewUndo);
	elements.confirmUndo.addEventListener('click', undoPublishingRun);
	elements.incompleteAck.addEventListener('change', updateImportAvailability);
	elements.postTitle.addEventListener('input', () => {
		updateImportAvailability();
		if (!elements.publishing.hidden) {
			invalidatePublishingPlan(undefined, 'clear');
		}
	});
	[elements.outputSingle, elements.outputSplit].forEach((control) => {
		control.addEventListener('change', () => {
			state.publishing.mode = elements.outputSplit.checked ? 'heading' : 'single';
			elements.splitLevelWrap.hidden = state.publishing.mode !== 'heading';
			if (state.publishing.mode === 'heading') {
				renderSplitLevels();
			}
			invalidatePublishingPlan(undefined, 'clear');
		});
	});
	elements.splitLevel.addEventListener('change', () => {
		state.publishing.splitLevel = Number(elements.splitLevel.value) || 0;
		invalidatePublishingPlan(undefined, 'clear');
	});
	elements.postType.addEventListener('change', () => {
		state.publishing.destination.postType = elements.postType.value;
		state.publishing.destination.parentId = 0;
		state.publishing.destination.authorId = 0;
		renderDestinationFields();
		invalidatePublishingPlan(undefined, 'withoutParents');
	});
	elements.postStatus.addEventListener('change', () => {
		state.publishing.destination.status = elements.postStatus.value;
		invalidatePublishingPlan();
	});
	elements.parentControl.addEventListener('change', () => {
		state.publishing.destination.parentId = Number(elements.parentControl.value) || 0;
		invalidatePublishingPlan();
	});
	elements.authorControl.addEventListener('change', () => {
		state.publishing.destination.authorId = Number(elements.authorControl.value) || 0;
		invalidatePublishingPlan();
	});
	[elements.createToc, elements.addCitations].forEach((control) => {
		control.addEventListener('change', () => invalidatePublishingPlan());
	});
	elements.createCompanion.addEventListener('change', () => {
		invalidatePublishingPlan(undefined, 'clear');
	});
	elements.undoConfirm.addEventListener('change', updatePublishingAvailability);
	[elements.profile, elements.pageMarkers].forEach((control) => {
		control.addEventListener('change', () => {
			updateSourceOptions();
			if (state.result) {
				clearResult();
				setFileError('Conversion options changed. Convert the PDF again to continue.');
			}
		});
	});

	const resultTabs = [elements.previewTab, elements.markdownTab, elements.outlineTab];
	resultTabs.forEach((tab, index) => {
		tab.addEventListener('keydown', (event) => {
			if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
				return;
			}
			event.preventDefault();
			let nextIndex = index;
			if (event.key === 'Home') {
				nextIndex = 0;
			} else if (event.key === 'End') {
				nextIndex = resultTabs.length - 1;
			} else {
				const direction = event.key === 'ArrowRight' ? 1 : -1;
				nextIndex = (index + direction + resultTabs.length) % resultTabs.length;
			}
			const next = resultTabs[nextIndex];
			next.click();
			next.focus();
		});
	});

	window.addEventListener('pagehide', cancelActiveTask);
	resetSourceContext();
	clearResult();
	state.capabilityError = browserCapabilityError();
	if (state.capabilityError) {
		setFileError(state.capabilityError);
		updatePublishingAvailability();
	}
	loadInventory();
})();
