import { __, _n, sprintf } from '@wordpress/i18n';

import './styles/product-editor-recording.scss';

export type SourceRecordingData = {
	mode: 'source';
	buttonLabel: string;
	recordUrl: string;
	ajaxUrl: string;
	nonce: string;
	nonces?: RecordingActionNonces;
	sourceProductId: number;
};

export type ShadowRecordingData = {
	mode: 'shadow';
	ajaxUrl: string;
	nonce: string;
	nonces?: RecordingActionNonces;
	sessionUuid: string;
	sourceProductId: number;
	sourceTitle: string;
	sourceEditUrl: string;
	recordingsUrl?: string;
	shadowProductId: number;
	variationMap: Record< string, number >;
	defaultDate: string;
	defaultTime: string;
	timezone: string;
	startedFromDashboard?: boolean;
};

type ProductEditorRecordingData = SourceRecordingData | ShadowRecordingData;

type RecordingAction =
	| 'start'
	| 'save'
	| 'cancel'
	| 'finalize'
	| 'schedule'
	| 'pending_term';

type RecordingActionNonces = Partial< Record< RecordingAction, string > >;

export type FinalizeResponse = {
	launchUuid: string;
	launchId: number;
	versionId: number;
	selectedChangeCount: number;
	defaultDate: string;
	defaultTime: string;
	timezone: string;
	launchesUrl: string;
	sourceEditUrl: string;
	unsupportedMetadataAcknowledged?: boolean;
};

type UnsupportedMetadataWarning = {
	code?: string;
	metaKey?: string;
	sourceReference?: string;
	message?: string;
};

type UnsupportedMetadataAcknowledgementData = {
	code?: string;
	message?: string;
	requiresUnsupportedMetadataAcknowledgement?: boolean;
	unsupportedMetadataWarnings?: UnsupportedMetadataWarning[];
	selectedChangeCount?: number;
};

type ScheduleResponse = {
	actionId: number;
	versionId: number;
	scheduledAtUtc: string;
	launchesUrl: string;
};

type StartResponse = {
	sessionUuid: string;
	shadowId: number;
	shadowEditUrl: string;
	nonce?: string;
	nonces?: RecordingActionNonces;
	variationMap: Record< string, number >;
	sourceTitle: string;
	sourceEditUrl: string;
	defaultDate: string;
	defaultTime: string;
	timezone: string;
	recordingsUrl?: string;
};

type SaveResponse = {
	shadowProductId: number;
	shadowTitle: string;
};

type CancelResponse = {
	sourceEditUrl: string;
	message?: string;
};

type PendingTermResponse = {
	token: string;
	taxonomy: string;
	name: string;
	slug: string;
};

type AjaxResponse< T > = {
	success: boolean;
	data?: T &
		UnsupportedMetadataAcknowledgementData & {
			message?: string;
		};
};

type AjaxRequestError = Error & {
	data?: UnsupportedMetadataAcknowledgementData;
	status?: number;
};

type ScheduleState = {
	busy: boolean;
	scheduled: boolean;
};

type RecordingBannerActions = {
	onSaveReview: () => Promise< void >;
	onSaveDraft: () => Promise< void >;
};

declare global {
	interface Window {
		scheduledChangesProductEditorRecording?: ProductEditorRecordingData;
	}
}

const data = window.scheduledChangesProductEditorRecording;

function nonceFor(
	config: SourceRecordingData | ShadowRecordingData,
	action: RecordingAction
): string {
	return config.nonces?.[ action ] || config.nonce;
}

if ( data?.mode === 'source' ) {
	insertRecordButton( data );
}

if ( data?.mode === 'shadow' ) {
	setupShadowRecording( data );

	if ( data.startedFromDashboard ) {
		showStartedFromDashboardOverlay();
	}
}

export function insertRecordButton( config: SourceRecordingData ) {
	if ( document.querySelector( '.scheduled-changes-record-title-action' ) ) {
		return;
	}

	const button = document.createElement( 'a' );
	button.className =
		'page-title-action scheduled-changes-record-title-action';
	button.href = '#scheduled-changes-record';
	button.textContent =
		config.buttonLabel ||
		__( 'Record', 'jjs-post-scheduler' );
	button.addEventListener( 'click', async ( event ) => {
		event.preventDefault();

		if ( button.getAttribute( 'aria-disabled' ) === 'true' ) {
			return;
		}

		button.setAttribute( 'aria-disabled', 'true' );
		const startOverlay = showRecordingStartOverlay();

		try {
			await startOverlay.countdown;
			startOverlay.setStarting();
			button.textContent = __(
				'Starting recording',
				'jjs-post-scheduler'
			);
			setRecordingStatus(
				__(
					'Preparing a safe copy.',
					'jjs-post-scheduler'
				)
			);
			const started = await startRecording( config );

			button.textContent = __(
				'Safe copy open',
				'jjs-post-scheduler'
			);
			setRecordingStatus(
				__(
					'Safe copy is ready. The live item is not being changed. Save recording when you are done.',
					'jjs-post-scheduler'
				)
			);
			setupShadowRecording( {
				mode: 'shadow',
				ajaxUrl: config.ajaxUrl,
				nonce: started.nonce || config.nonce,
				nonces: started.nonces || config.nonces,
				sessionUuid: started.sessionUuid,
				sourceProductId: config.sourceProductId,
				sourceTitle: started.sourceTitle,
				sourceEditUrl: started.sourceEditUrl || window.location.href,
				recordingsUrl: started.recordingsUrl,
				shadowProductId: started.shadowId,
				variationMap: started.variationMap || {},
				defaultDate: started.defaultDate,
				defaultTime: started.defaultTime,
				timezone: started.timezone,
			} );
			preserveShadowEditorUrl( started.shadowEditUrl );
			startOverlay.remove();
		} catch ( error ) {
			startOverlay.remove();
			button.removeAttribute( 'aria-disabled' );
			button.textContent =
				config.buttonLabel ||
				__( 'Record', 'jjs-post-scheduler' );
			setRecordingStatus( errorMessage( error ), true );
		}
	} );

	const addNew = document.querySelector(
		'.wrap .page-title-action[href*="post-new.php?post_type=product"]'
	);

	if ( addNew ) {
		addNew.insertAdjacentElement( 'afterend', button );
		return;
	}

	const heading = document.querySelector( '.wrap h1' );
	heading?.appendChild( button );
}

export function showRecordingStartOverlay( seconds = 5 ): {
	countdown: Promise< void >;
	setStarting: () => void;
	remove: () => void;
} {
	document
		.querySelector( '.scheduled-changes-recording-start-overlay' )
		?.remove();

	const overlay = document.createElement( 'div' );
	overlay.className = 'scheduled-changes-recording-start-overlay';
	overlay.setAttribute( 'role', 'dialog' );
	overlay.setAttribute( 'aria-modal', 'true' );
	overlay.setAttribute(
		'aria-labelledby',
		'scheduled-changes-recording-start-overlay-title'
	);

	const panel = document.createElement( 'div' );
	panel.className = 'scheduled-changes-recording-start-overlay__panel';
	panel.tabIndex = -1;

	const eyebrow = document.createElement( 'span' );
	eyebrow.className = 'scheduled-changes-recording-start-overlay__eyebrow';
	eyebrow.textContent = __(
		'Scheduled Changes',
		'jjs-post-scheduler'
	);

	const title = document.createElement( 'h2' );
	title.id = 'scheduled-changes-recording-start-overlay-title';
	title.textContent = __(
		'Preparing your safe copy',
		'jjs-post-scheduler'
	);

	const message = document.createElement( 'p' );
	message.textContent = __(
		'You are about to edit a scheduled-change copy. The live item is not being changed.',
		'jjs-post-scheduler'
	);

	const countdown = document.createElement( 'p' );
	countdown.className =
		'scheduled-changes-recording-start-overlay__countdown';
	countdown.setAttribute( 'aria-live', 'polite' );

	const countdownNumber = document.createElement( 'strong' );
	const countdownLabel = document.createElement( 'span' );
	countdown.append( countdownLabel, ' ', countdownNumber );
	panel.append( eyebrow, title, message, countdown );
	overlay.appendChild( panel );
	document.body.appendChild( overlay );
	document.body.classList.add(
		'scheduled-changes-recording-start-overlay-open'
	);
	const releaseFocusTrap = trapModalFocus( overlay, panel );

	let remaining = Math.max( 0, Math.floor( seconds ) );
	let intervalId: number | undefined;
	let removed = false;
	let resolveCountdown = () => undefined;
	const countdownPromise = new Promise< void >( ( resolve ) => {
		resolveCountdown = resolve;
	} );
	const renderCountdown = () => {
		countdownLabel.textContent = __(
			'Starting in',
			'jjs-post-scheduler'
		);
		countdownNumber.textContent = sprintf(
			/* translators: %d: number of seconds before recording starts. */
			_n(
				'%d second',
				'%d seconds',
				remaining,
				'jjs-post-scheduler'
			),
			remaining
		);
	};

	renderCountdown();

	if ( remaining === 0 ) {
		resolveCountdown();
	} else {
		intervalId = window.setInterval( () => {
			remaining -= 1;
			renderCountdown();

			if ( remaining <= 0 ) {
				window.clearInterval( intervalId );
				intervalId = undefined;
				resolveCountdown();
			}
		}, 1000 );
	}

	return {
		countdown: countdownPromise,
		setStarting: () => {
			countdownLabel.textContent = __(
				'Starting recording…',
				'jjs-post-scheduler'
			);
			countdownNumber.textContent = '';
		},
		remove: () => {
			if ( removed ) {
				return;
			}

			removed = true;

			if ( intervalId !== undefined ) {
				window.clearInterval( intervalId );
			}

			releaseFocusTrap();
			overlay.remove();
			document.body.classList.remove(
				'scheduled-changes-recording-start-overlay-open'
			);
		},
	};
}

function preserveShadowEditorUrl( shadowEditUrl: string ) {
	if (
		! shadowEditUrl ||
		! window.history?.replaceState ||
		window.location.href === shadowEditUrl
	) {
		return;
	}

	window.history.replaceState( null, document.title, shadowEditUrl );
}

function showStartedFromDashboardOverlay() {
	const startOverlay = showRecordingStartOverlay();

	void startOverlay.countdown.then( () => {
		startOverlay.setStarting();
		window.setTimeout( () => {
			startOverlay.remove();
			removeStartedFlagFromUrl();
		}, 450 );
	} );
}

function removeStartedFlagFromUrl() {
	if ( ! window.history?.replaceState ) {
		return;
	}

	const url = new URL( window.location.href );
	url.searchParams.delete( 'scheduled_changes_recording_started' );
	window.history.replaceState( null, document.title, url.toString() );
}

async function startRecording(
	config: SourceRecordingData
): Promise< StartResponse > {
	return ajaxPost< StartResponse >( config.ajaxUrl, {
		action: 'scheduled_changes_start_product_editor_recording',
		nonce: nonceFor( config, 'start' ),
		source_product_id: String( config.sourceProductId ),
	} );
}

export function setupShadowRecording( config: ShadowRecordingData ) {
	const form = document.getElementById( 'post' ) as HTMLFormElement | null;

	if ( ! form || form.dataset.scheduledChangesRecordingActive === '1' ) {
		return;
	}

	form.dataset.scheduledChangesRecordingActive = '1';
	document.body.classList.add( 'scheduled-changes-recording-active' );
	activateShadowEditorContext( form, config );
	const removeLeaveWarning = installLeaveWarning();
	const scheduleState: ScheduleState = {
		busy: false,
		scheduled: false,
	};

	let busy = false;
	let finalized: FinalizeResponse | null = null;

	const openReview = async ( acknowledgeUnsupportedMetadata = false ) => {
		setRecordingStatus(
			__(
				'Preparing your change review.',
				'jjs-post-scheduler'
			)
		);
		finalized = await finalizeRecording(
			config,
			acknowledgeUnsupportedMetadata
		);
		busy = false;
		showScheduleModal(
			config,
			finalized,
			removeLeaveWarning,
			scheduleState
		);
	};

	const saveAndReview = async ( event: SubmitEvent | null = null ) => {
		if ( finalized ) {
			showScheduleModal(
				config,
				finalized,
				removeLeaveWarning,
				scheduleState
			);
			return;
		}

		if ( busy ) {
			return;
		}

		busy = true;

		try {
			setRecordingStatus(
				__(
					'Saving safe copy. The live item is not being changed.',
					'jjs-post-scheduler'
				)
			);
			await saveShadowProduct( form, event, config );
			setRecordingStatus(
				__(
					'Safe copy saved. Review the changes and choose when they should go live.',
					'jjs-post-scheduler'
				)
			);
			await openReview();
		} catch ( error ) {
			busy = false;
			if ( isUnsupportedMetadataAcknowledgementError( error ) ) {
				showUnsupportedMetadataAcknowledgement(
					error.data || {},
					async () => {
						if ( busy || finalized ) {
							return;
						}

						busy = true;

						try {
							await openReview( true );
						} catch ( retryError ) {
							busy = false;
							setRecordingStatus(
								recordingFailureMessage( retryError ),
								true
							);
						}
					}
				);
				return;
			}

			setRecordingStatus( recordingFailureMessage( error ), true );
		}
	};

	const saveDraft = async ( event: SubmitEvent | null = null ) => {
		if ( busy ) {
			return;
		}

		busy = true;

		try {
			setRecordingStatus(
				__(
					'Saving safe copy. The live item is not being changed.',
					'jjs-post-scheduler'
				)
			);
			await saveShadowProduct( form, event, config );
			busy = false;
			setRecordingStatus(
				__(
					'Safe copy saved. The live item was not changed. Keep editing, or use Save recording when you are ready.',
					'jjs-post-scheduler'
				)
			);
		} catch ( error ) {
			busy = false;
			setRecordingStatus( recordingFailureMessage( error ), true );
		}
	};

	addRecordingRail( config, removeLeaveWarning, {
		onSaveReview: () => saveAndReview(),
		onSaveDraft: () => saveDraft(),
	} );
	addRecordingBanner( config, removeLeaveWarning, {
		onSaveReview: () => saveAndReview(),
		onSaveDraft: () => saveDraft(),
	} );
	if ( config.startedFromDashboard === false ) {
		setRecordingStatus( resumedRecordingMessage( config ) );
	}
	guardUnsupportedReviewActions();
	setupPendingTermInterception( config );
	renamePublishButtons();
	suspendAutosave();

	form.addEventListener(
		'submit',
		( event ) => {
			event.preventDefault();

			if ( isSaveDraftSubmitter( event.submitter ) ) {
				void saveDraft( event );
				return;
			}

			void saveAndReview( event );
		},
		true
	);
}

export function guardUnsupportedReviewActions() {
	const reviews = document.getElementById( 'commentsdiv' );
	const reviewBody = reviews?.querySelector( '.inside' );

	if (
		! reviews ||
		! reviewBody ||
		reviews.dataset.scheduledChangesRecordingGuarded
	) {
		return;
	}

	reviews.dataset.scheduledChangesRecordingGuarded = '1';
	const notice = document.createElement( 'p' );
	notice.className =
		'notice notice-warning inline scheduled-changes-recording-review-warning';
	notice.textContent = __(
		'Review and comment moderation is unavailable while recording. Finish or abandon the recording before adding, editing, replying to, approving, or deleting reviews.',
		'jjs-post-scheduler'
	);
	reviewBody.prepend( notice );

	const blockReviewAction = ( event: Event ) => {
		const target = event.target;

		if (
			! ( target instanceof Element ) ||
			! target.closest(
				'#add-new-comment, #the-comment-list, #trash-undo-holder, #spam-undo-holder'
			)
		) {
			return;
		}

		event.preventDefault();
		event.stopImmediatePropagation();
		setRecordingStatus(
			__(
				'Reviews and comments cannot be changed during a product recording.',
				'jjs-post-scheduler'
			),
			true
		);
	};

	reviewBody.addEventListener( 'click', blockReviewAction, true );
	reviewBody.addEventListener( 'dblclick', blockReviewAction, true );
}

export function setupPendingTermInterception( config: ShadowRecordingData ) {
	injectPendingTermSessionInputs( config );

	if ( document.body.dataset.scheduledChangesPendingTermsActive === '1' ) {
		return;
	}

	document.body.dataset.scheduledChangesPendingTermsActive = '1';
	document.addEventListener(
		'click',
		async ( event ) => {
			const target = event.target;

			if ( ! ( target instanceof Element ) ) {
				return;
			}

			const button = target.closest<
				HTMLButtonElement | HTMLInputElement
			>( '.tagadd, [id$="-add-submit"]' );

			if ( ! button ) {
				return;
			}

			const request = pendingTermRequestFromButton( button );

			if ( ! request ) {
				return;
			}

			event.preventDefault();
			event.stopImmediatePropagation();
			button.disabled = true;

			try {
				for ( const name of request.names ) {
					const term = await createPendingTerm(
						config,
						request.taxonomy,
						name,
						request.parent
					);
					addPendingTermToEditor( term, request.container );
				}

				request.clear();
				setRecordingStatus(
					__(
						'Term will be created when the recorded changes go live.',
						'jjs-post-scheduler'
					)
				);
			} catch ( error ) {
				setRecordingStatus( errorMessage( error ), true );
			} finally {
				button.disabled = false;
			}
		},
		true
	);
}

type PendingTermRequest = {
	taxonomy: string;
	names: string[];
	parent: string;
	container: HTMLElement;
	clear: () => void;
};

function pendingTermRequestFromButton(
	button: HTMLButtonElement | HTMLInputElement
): PendingTermRequest | null {
	const taxonomyFromSubmit = button.id.match( /^(.+)-add-submit$/ )?.[ 1 ];

	if ( taxonomyFromSubmit ) {
		const container =
			( button.closest( '.categorydiv' ) as HTMLElement | null ) ||
			( button.closest( '.postbox' ) as HTMLElement | null );
		const input = document.getElementById(
			`new${ taxonomyFromSubmit }`
		) as HTMLInputElement | null;
		const parent = document.getElementById(
			`new${ taxonomyFromSubmit }_parent`
		) as HTMLSelectElement | null;
		const name = input?.value.trim() || '';

		if ( ! container || ! input || ! name ) {
			return null;
		}

		return {
			taxonomy: taxonomyFromSubmit,
			names: [ name ],
			parent: parent?.value || '',
			container,
			clear: () => {
				input.value = '';
			},
		};
	}

	const container = button.closest(
		'.tagsdiv, .postbox'
	) as HTMLElement | null;
	const input = container?.querySelector< HTMLInputElement >(
		'input[id^="new-tag-"], input[name^="newtag["]'
	);
	const taxonomy =
		input?.id.match( /^new-tag-(.+)$/ )?.[ 1 ] ||
		input?.name.match( /^newtag\[(.+)\]$/ )?.[ 1 ] ||
		'';
	const names = ( input?.value || '' )
		.split( ',' )
		.map( ( name ) => name.trim() )
		.filter( Boolean );

	if ( ! container || ! input || ! taxonomy || names.length === 0 ) {
		return null;
	}

	return {
		taxonomy,
		names,
		parent: '',
		container,
		clear: () => {
			input.value = '';
		},
	};
}

async function createPendingTerm(
	config: ShadowRecordingData,
	taxonomy: string,
	name: string,
	parent: string
): Promise< PendingTermResponse > {
	return ajaxPost< PendingTermResponse >( config.ajaxUrl, {
		action: 'scheduled_changes_recording_create_pending_term',
		nonce: nonceFor( config, 'pending_term' ),
		session_uuid: config.sessionUuid,
		taxonomy,
		term_name: name,
		parent,
	} );
}

function addPendingTermToEditor(
	term: PendingTermResponse,
	container: HTMLElement
) {
	const checklist = document.getElementById( `${ term.taxonomy }checklist` );

	if ( checklist ) {
		const item = document.createElement( 'li' );
		item.id = `${ term.taxonomy }-${ term.token }`;
		item.dataset.scheduledChangesPendingTerm = term.token;

		const label = document.createElement( 'label' );
		const checkbox = document.createElement( 'input' );
		checkbox.type = 'checkbox';
		checkbox.name = `tax_input[${ term.taxonomy }][]`;
		checkbox.value = term.token;
		checkbox.checked = true;
		label.append( checkbox, ` ${ term.name }` );
		item.appendChild( label );
		checklist.prepend( item );

		return;
	}

	const tagList =
		container.querySelector< HTMLElement >( '.tagchecklist' ) || container;
	const item = document.createElement( 'span' );
	item.dataset.scheduledChangesPendingTerm = term.token;
	item.textContent = term.name;

	const hidden = document.createElement( 'input' );
	hidden.type = 'hidden';
	hidden.name = `tax_input[${ term.taxonomy }][]`;
	hidden.value = term.token;
	item.appendChild( hidden );
	tagList.appendChild( item );
}

export function installLeaveWarning(): () => void {
	const warn = ( event: BeforeUnloadEvent ) => {
		event.preventDefault();
		event.returnValue = '';
	};

	window.addEventListener( 'beforeunload', warn );

	return () => window.removeEventListener( 'beforeunload', warn );
}

function activateShadowEditorContext(
	form: HTMLFormElement,
	config: ShadowRecordingData
) {
	for ( const name of [ 'post_ID', 'ID', 'post' ] ) {
		const input = form.elements.namedItem( name );

		if ( input instanceof HTMLInputElement ) {
			input.value = String( config.shadowProductId );
		}
	}

	const maybeWindow = window as Window & {
		woocommerce_admin_meta_boxes?: {
			post_id?: number | string;
		};
		woocommerce_admin_meta_boxes_variations?: {
			post_id?: number | string;
		};
	};

	if ( maybeWindow.woocommerce_admin_meta_boxes ) {
		maybeWindow.woocommerce_admin_meta_boxes.post_id =
			config.shadowProductId;
	}

	if ( maybeWindow.woocommerce_admin_meta_boxes_variations ) {
		maybeWindow.woocommerce_admin_meta_boxes_variations.post_id =
			config.shadowProductId;
	}

	for ( const [ sourceId, shadowId ] of Object.entries(
		config.variationMap || {}
	) ) {
		remapVariationEditorId( sourceId, String( shadowId ) );
	}
}

function injectPendingTermSessionInputs( config: ShadowRecordingData ) {
	document
		.querySelectorAll< HTMLElement >( '.category-add, .ajaxtag' )
		.forEach( ( container ) => {
			setHiddenInput(
				container,
				'scheduled_changes_recording_session_uuid',
				config.sessionUuid
			);
			setHiddenInput(
				container,
				'scheduled_changes_recording_shadow_id',
				String( config.shadowProductId )
			);
		} );
}

function setHiddenInput( container: HTMLElement, name: string, value: string ) {
	let input = container.querySelector< HTMLInputElement >(
		`input[type="hidden"][name="${ cssEscape( name ) }"]`
	);

	if ( ! input ) {
		input = document.createElement( 'input' );
		input.type = 'hidden';
		input.name = name;
		container.appendChild( input );
	}

	input.value = value;
}

function remapVariationEditorId( sourceId: string, shadowId: string ) {
	document
		.querySelectorAll< HTMLInputElement >(
			'.woocommerce_variation input.variable_post_id'
		)
		.forEach( ( input ) => {
			if ( input.value === sourceId ) {
				input.value = shadowId;
			}
		} );

	document
		.querySelectorAll< HTMLElement >(
			`.woocommerce_variation [rel="${ cssEscape( sourceId ) }"]`
		)
		.forEach( ( element ) => element.setAttribute( 'rel', shadowId ) );

	document
		.querySelectorAll<
			HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
		>( '.woocommerce_variation [name]' )
		.forEach( ( input ) => {
			const name = input.getAttribute( 'name' );

			if ( name?.includes( `[${ sourceId }]` ) ) {
				input.setAttribute(
					'name',
					name.replaceAll( `[${ sourceId }]`, `[${ shadowId }]` )
				);
			}
		} );
}

function cssEscape( value: string ): string {
	const css = window.CSS as typeof CSS | undefined;

	return css?.escape
		? css.escape( value )
		: value.replace( /["\\]/g, '\\$&' );
}

function addRecordingBanner(
	config: ShadowRecordingData,
	onCancel: () => void,
	actionsConfig: RecordingBannerActions
) {
	if (
		document.querySelector( '.scheduled-changes-recording-editor-banner' )
	) {
		return;
	}

	const banner = document.createElement( 'div' );
	banner.className =
		'notice notice-info scheduled-changes-recording-editor-banner';
	banner.setAttribute( 'role', 'region' );
	banner.setAttribute(
		'aria-label',
		__( 'Scheduled-change copy', 'jjs-post-scheduler' )
	);

	const content = document.createElement( 'div' );
	content.className = 'scheduled-changes-recording-editor-banner__content';

	const promise = document.createElement( 'p' );
	const strong = document.createElement( 'strong' );
	strong.textContent = __(
		'You are editing a scheduled-change copy. The live item is not being changed.',
		'jjs-post-scheduler'
	);
	promise.appendChild( strong );

	const guidance = document.createElement( 'p' );
	guidance.textContent = __(
		'Save this copy when you are ready, then review what changed and choose when it should go live.',
		'jjs-post-scheduler'
	);

	const links = document.createElement( 'p' );
	links.className = 'scheduled-changes-recording-editor-banner__links';

	if ( config.sourceEditUrl ) {
		const source = document.createElement( 'a' );
		source.href = config.sourceEditUrl;
		source.textContent = __(
			'View source item',
			'jjs-post-scheduler'
		);
		links.appendChild( source );
	}

	if ( config.recordingsUrl ) {
		if ( links.childNodes.length > 0 ) {
			links.append( document.createTextNode( ' · ' ) );
		}

		const dashboard = document.createElement( 'a' );
		dashboard.href = config.recordingsUrl;
		dashboard.textContent = __(
			'Open recording dashboard',
			'jjs-post-scheduler'
		);
		links.appendChild( dashboard );
	}

	if ( ! config.sourceEditUrl || ! config.recordingsUrl ) {
		const warning = document.createElement( 'p' );
		warning.className =
			'scheduled-changes-recording-editor-banner__warning';
		warning.textContent = __(
			'Some safe-copy details are unavailable. Open Record a Change or System Check before saving if this looks unexpected.',
			'jjs-post-scheduler'
		);
		content.append( promise, guidance, links, warning );
	} else {
		content.append( promise, guidance, links );
	}

	const actions = document.createElement( 'p' );
	actions.className = 'scheduled-changes-recording-editor-banner__actions';

	const saveReview = document.createElement( 'button' );
	saveReview.type = 'button';
	saveReview.className = 'button button-primary';
	saveReview.textContent = __(
		'Save recording',
		'jjs-post-scheduler'
	);
	saveReview.addEventListener( 'click', async () => {
		if ( saveReview.disabled ) {
			return;
		}

		saveReview.disabled = true;
		saveDraft.disabled = true;

		try {
			await actionsConfig.onSaveReview();
		} finally {
			saveReview.disabled = false;
			saveDraft.disabled = false;
		}
	} );

	const saveDraft = document.createElement( 'button' );
	saveDraft.type = 'button';
	saveDraft.className = 'button';
	saveDraft.textContent = __(
		'Save draft copy',
		'jjs-post-scheduler'
	);
	saveDraft.addEventListener( 'click', async () => {
		if ( saveDraft.disabled ) {
			return;
		}

		saveReview.disabled = true;
		saveDraft.disabled = true;

		try {
			await actionsConfig.onSaveDraft();
		} finally {
			saveReview.disabled = false;
			saveDraft.disabled = false;
		}
	} );

	const cancel = document.createElement( 'button' );
	cancel.type = 'button';
	cancel.className = 'button';
	cancel.textContent = __(
		'Cancel recording',
		'jjs-post-scheduler'
	);
	cancel.addEventListener( 'click', async () => {
		if ( cancel.disabled ) {
			return;
		}

		const confirmed = await confirmCancelRecording();

		if ( ! confirmed ) {
			return;
		}

		cancel.disabled = true;
		setRecordingStatus(
			__( 'Canceling recording.', 'jjs-post-scheduler' )
		);

		try {
			const canceled = await cancelRecording( config );
			onCancel();
			setRecordingStatus(
				canceled.message ||
					__(
						'Recording canceled. Returning to the source item.',
						'jjs-post-scheduler'
					)
			);
			window.location.assign(
				canceled.sourceEditUrl || config.sourceEditUrl
			);
		} catch ( error ) {
			cancel.disabled = false;
			setRecordingStatus( cancelFailureMessage( error ), true );
		}
	} );

	actions.append( saveReview, saveDraft, cancel );
	banner.append( content, actions );

	const wrap = document.querySelector( '.wrap' );
	const heading = wrap?.querySelector( 'h1' );

	if ( heading ) {
		heading.insertAdjacentElement( 'afterend', banner );
	} else {
		wrap?.prepend( banner );
	}

	if ( config.sourceTitle ) {
		setRecordingStatus(
			sprintf(
				/* translators: %s: source product title. */
				__(
					'Safe copy open for %s. The live item is not being changed.',
					'jjs-post-scheduler'
				),
				config.sourceTitle
			)
		);
	}
}

function addRecordingRail(
	config: ShadowRecordingData,
	onCancel: () => void,
	actionsConfig: RecordingBannerActions
) {
	document
		.querySelector( '.scheduled-changes-recording-editor-rail' )
		?.remove();
	document
		.querySelectorAll< HTMLElement >(
			'.scheduled-changes-recording-editor-status'
		)
		.forEach( ( status ) => {
			if (
				! status.closest( '.scheduled-changes-recording-editor-rail' )
			) {
				status.remove();
			}
		} );

	const rail = document.createElement( 'section' );
	rail.className = 'scheduled-changes-recording-editor-rail';
	rail.setAttribute( 'role', 'region' );
	rail.setAttribute(
		'aria-label',
		__( 'Recording controls', 'jjs-post-scheduler' )
	);

	const railInner = document.createElement( 'div' );
	railInner.className = 'scheduled-changes-recording-editor-rail__inner';

	const status = document.createElement( 'div' );
	status.className =
		'scheduled-changes-recording-editor-status scheduled-changes-recording-editor-rail__status is-info';
	status.setAttribute( 'role', 'status' );
	status.setAttribute( 'aria-live', 'polite' );
	status.setAttribute( 'aria-atomic', 'true' );

	const statusMessage = document.createElement( 'p' );
	statusMessage.textContent = config.sourceTitle
		? sprintf(
				/* translators: %s: source product title. */
				__(
					'Safe copy open for %s. The live item is not being changed.',
					'jjs-post-scheduler'
				),
				config.sourceTitle
		  )
		: __(
				'Safe copy open. The live item is not being changed.',
				'jjs-post-scheduler'
		  );
	status.appendChild( statusMessage );

	const actions = document.createElement( 'div' );
	actions.className = 'scheduled-changes-recording-editor-rail__actions';

	const save = document.createElement( 'button' );
	save.type = 'button';
	save.className = 'btn btn-primary scheduled-changes-recording-save-control';
	save.textContent = __(
		'Save recording',
		'jjs-post-scheduler'
	);
	save.addEventListener( 'click', async () => {
		if ( save.disabled ) {
			return;
		}

		save.disabled = true;

		try {
			await actionsConfig.onSaveReview();
		} finally {
			save.disabled = false;
		}
	} );

	const cancel = document.createElement( 'button' );
	cancel.type = 'button';
	cancel.className = 'btn btn-danger';
	cancel.textContent = __(
		'Cancel recording',
		'jjs-post-scheduler'
	);
	cancel.addEventListener( 'click', async () => {
		if ( cancel.disabled ) {
			return;
		}

		const confirmed = await confirmCancelRecording();

		if ( ! confirmed ) {
			return;
		}

		cancel.disabled = true;
		setRecordingStatus(
			__( 'Canceling recording.', 'jjs-post-scheduler' )
		);

		try {
			const canceled = await cancelRecording( config );
			onCancel();
			setRecordingStatus(
				canceled.message ||
					__(
						'Recording canceled. Returning to the source item.',
						'jjs-post-scheduler'
					)
			);
			window.location.assign(
				canceled.sourceEditUrl || config.sourceEditUrl
			);
		} catch ( error ) {
			cancel.disabled = false;
			setRecordingStatus( cancelFailureMessage( error ), true );
		}
	} );

	actions.append( save, cancel );
	railInner.append( status, actions );
	rail.appendChild( railInner );
	document.body.appendChild( rail );
}

function confirmCancelRecording(): Promise< boolean > {
	return new Promise( ( resolve ) => {
		document
			.querySelector( '.scheduled-changes-recording-confirm' )
			?.remove();

		const modal = document.createElement( 'div' );
		modal.className = 'scheduled-changes-recording-confirm';
		modal.setAttribute( 'role', 'dialog' );
		modal.setAttribute( 'aria-modal', 'true' );
		modal.setAttribute(
			'aria-labelledby',
			'scheduled-changes-recording-confirm-title'
		);

		const panel = document.createElement( 'div' );
		panel.className = 'scheduled-changes-recording-confirm__panel';

		const title = document.createElement( 'h2' );
		title.id = 'scheduled-changes-recording-confirm-title';
		title.textContent = __(
			'Cancel this safe copy?',
			'jjs-post-scheduler'
		);

		const message = document.createElement( 'p' );
		message.textContent = __(
			'This removes the safe copy. The live item will not be changed.',
			'jjs-post-scheduler'
		);

		const actions = document.createElement( 'div' );
		actions.className = 'scheduled-changes-recording-confirm__actions';

		const keep = document.createElement( 'button' );
		keep.type = 'button';
		keep.className = 'button';
		keep.textContent = __(
			'Keep editing',
			'jjs-post-scheduler'
		);

		const confirm = document.createElement( 'button' );
		confirm.type = 'button';
		confirm.className = 'button button-primary';
		confirm.textContent = __(
			'Yes, cancel recording',
			'jjs-post-scheduler'
		);

		let resolved = false;
		const finish = ( confirmed: boolean ) => {
			if ( resolved ) {
				return;
			}

			resolved = true;
			document.removeEventListener( 'keydown', onKeyDown );
			releaseFocusTrap();
			modal.remove();
			resolve( confirmed );
		};
		const onKeyDown = ( event: KeyboardEvent ) => {
			if ( event.key === 'Escape' ) {
				finish( false );
			}
		};

		keep.addEventListener( 'click', () => finish( false ) );
		confirm.addEventListener( 'click', () => finish( true ) );
		modal.addEventListener( 'click', ( event ) => {
			if ( event.target === modal ) {
				finish( false );
			}
		} );
		document.addEventListener( 'keydown', onKeyDown );

		actions.append( keep, confirm );
		panel.append( title, message, actions );
		modal.appendChild( panel );
		document.body.appendChild( modal );
		const releaseFocusTrap = trapModalFocus( modal, confirm );
	} );
}

function renamePublishButtons() {
	const publish = document.getElementById(
		'publish'
	) as HTMLInputElement | null;
	const saveDraft = document.getElementById(
		'save-post'
	) as HTMLInputElement | null;

	if ( publish ) {
		publish.dataset.scheduledChangesRecordingAction = 'review';
		publish.value = __(
			'Save recording',
			'jjs-post-scheduler'
		);
	}

	if ( saveDraft ) {
		saveDraft.dataset.scheduledChangesRecordingAction = 'review';
		saveDraft.value = __(
			'Save recording',
			'jjs-post-scheduler'
		);
	}
}

function isSaveDraftSubmitter(
	submitter: SubmitEvent[ 'submitter' ]
): boolean {
	if (
		! (
			submitter instanceof HTMLButtonElement ||
			submitter instanceof HTMLInputElement
		)
	) {
		return false;
	}

	return submitter.dataset.scheduledChangesRecordingAction === 'draft';
}

async function saveShadowProduct(
	form: HTMLFormElement,
	event: SubmitEvent | null,
	config: ShadowRecordingData
) {
	syncEditors();

	const formData = new FormData( form );
	const submitter = event?.submitter as
		| HTMLButtonElement
		| HTMLInputElement
		| null;

	if ( submitter?.name && ! formData.has( submitter.name ) ) {
		formData.append( submitter.name, submitter.value || '1' );
	}

	formData.set( 'action', 'scheduled_changes_save_product_editor_recording' );
	formData.set( 'nonce', nonceFor( config, 'save' ) );
	formData.set( 'session_uuid', config.sessionUuid );

	await ajaxFormPost< SaveResponse >( config.ajaxUrl, formData );
}

async function cancelRecording(
	config: ShadowRecordingData
): Promise< CancelResponse > {
	return ajaxPost< CancelResponse >( config.ajaxUrl, {
		action: 'scheduled_changes_cancel_product_editor_recording',
		nonce: nonceFor( config, 'cancel' ),
		session_uuid: config.sessionUuid,
	} );
}

function syncEditors() {
	const maybeWindow = window as Window & {
		tinyMCE?: {
			triggerSave?: () => void;
		};
	};

	maybeWindow.tinyMCE?.triggerSave?.();
}

function suspendAutosave() {
	const maybeWindow = window as Window & {
		wp?: {
			autosave?: {
				server?: {
					suspend?: () => void;
				};
			};
		};
	};

	maybeWindow.wp?.autosave?.server?.suspend?.();
}

async function ajaxFormPost< T >(
	url: string,
	formData: FormData
): Promise< T > {
	const response = await fetch( url, {
		method: 'POST',
		body: formData,
		credentials: 'same-origin',
	} );
	const payload = ( await response.json() ) as AjaxResponse< T >;

	if ( ! response.ok || ! payload.success || ! payload.data ) {
		throw new Error(
			payload.data?.message ||
				__(
					'Could not save the safe copy. The live item was not changed. Refresh this editor and try again if the action expired.',
					'jjs-post-scheduler'
				)
		);
	}

	return payload.data;
}

async function finalizeRecording(
	config: ShadowRecordingData,
	acknowledgeUnsupportedMetadata = false
): Promise< FinalizeResponse > {
	const fields: Record< string, string > = {
		action: 'scheduled_changes_finalize_product_editor_recording',
		nonce: nonceFor( config, 'finalize' ),
		session_uuid: config.sessionUuid,
	};

	if ( acknowledgeUnsupportedMetadata ) {
		fields.acknowledge_unsupported_metadata = '1';
	}

	return ajaxPost< FinalizeResponse >( config.ajaxUrl, fields );
}

async function scheduleRecording(
	config: ShadowRecordingData,
	finalized: FinalizeResponse,
	date: string,
	time: string
): Promise< ScheduleResponse > {
	return ajaxPost< ScheduleResponse >( config.ajaxUrl, {
		action: 'scheduled_changes_schedule_product_editor_recording',
		nonce: nonceFor( config, 'schedule' ),
		session_uuid: config.sessionUuid,
		version_id: String( finalized.versionId ),
		go_live_date: date,
		go_live_time: time,
	} );
}

async function ajaxPost< T >(
	url: string,
	fields: Record< string, string >
): Promise< T > {
	const response = await fetch( url, {
		method: 'POST',
		body: new URLSearchParams( fields ),
		credentials: 'same-origin',
		headers: {
			'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
		},
	} );
	const payload = ( await response.json() ) as AjaxResponse< T >;

	if ( ! response.ok || ! payload.success || ! payload.data ) {
		const error = new Error(
			payload.data?.message ||
				__(
					'Scheduled Changes request failed.',
					'jjs-post-scheduler'
				)
		) as AjaxRequestError;
		error.data = payload.data;
		error.status = response.status;
		throw error;
	}

	return payload.data;
}

export function showScheduleModal(
	config: ShadowRecordingData,
	finalized: FinalizeResponse,
	onScheduled: () => void = () => undefined,
	scheduleState: ScheduleState = { busy: false, scheduled: false }
) {
	let modal = document.querySelector(
		'.scheduled-changes-recording-modal'
	) as RecordingModalElement | null;

	if ( modal ) {
		if (
			modal.dataset.scheduledChangesVersionId ===
			String( finalized.versionId )
		) {
			const existingDate = modal.querySelector(
				'#scheduled-changes-recording-go-live-date'
			) as HTMLInputElement | null;
			existingDate?.focus();
			return;
		}

		modal.scheduledChangesReleaseFocusTrap?.();
		modal.remove();
	}

	modal = document.createElement( 'div' );
	modal.className = 'scheduled-changes-recording-modal';
	modal.dataset.scheduledChangesVersionId = String( finalized.versionId );
	modal.setAttribute( 'role', 'dialog' );
	modal.setAttribute( 'aria-modal', 'true' );
	modal.setAttribute(
		'aria-labelledby',
		'scheduled-changes-recording-modal-title'
	);
	modal.setAttribute(
		'aria-describedby',
		'scheduled-changes-recording-modal-summary'
	);

	const panel = document.createElement( 'div' );
	panel.className = 'scheduled-changes-recording-modal__panel';

	const title = document.createElement( 'h2' );
	title.id = 'scheduled-changes-recording-modal-title';
	title.textContent = __(
		'Review and choose a go-live time',
		'jjs-post-scheduler'
	);

	const summary = document.createElement( 'p' );
	summary.id = 'scheduled-changes-recording-modal-summary';
	summary.className = 'scheduled-changes-recording-modal__summary';
	summary.textContent = sprintf(
		/* translators: 1: selected change count, 2: timezone name. */
		_n(
			'%1$d change from your safe copy is ready to schedule. The live item has not changed. Times use %2$s.',
			'%1$d changes from your safe copy are ready to schedule. The live item has not changed. Times use %2$s.',
			finalized.selectedChangeCount,
			'jjs-post-scheduler'
		),
		finalized.selectedChangeCount,
		finalized.timezone || config.timezone
	);

	if ( finalized.unsupportedMetadataAcknowledged ) {
		summary.textContent +=
			' ' +
			__(
				'Unsupported changes were acknowledged and excluded.',
				'jjs-post-scheduler'
			);
	}

	const form = document.createElement( 'form' );
	form.className = 'scheduled-changes-recording-modal__form';

	const dateLabel = labeledInput(
		__( 'Go-live date', 'jjs-post-scheduler' ),
		'date',
		'scheduled-changes-recording-go-live-date',
		finalized.defaultDate || config.defaultDate
	);
	const timeLabel = labeledInput(
		__( 'Go-live time', 'jjs-post-scheduler' ),
		'time',
		'scheduled-changes-recording-go-live-time',
		finalized.defaultTime || config.defaultTime
	);
	const actions = document.createElement( 'div' );
	actions.className = 'scheduled-changes-recording-modal__actions';

	const submit = document.createElement( 'button' );
	submit.type = 'submit';
	submit.className = 'button button-primary';
	submit.textContent = __(
		'Schedule change',
		'jjs-post-scheduler'
	);

	const cancel = document.createElement( 'button' );
	cancel.type = 'button';
	cancel.className = 'button';
	cancel.textContent = __(
		'Cancel recording',
		'jjs-post-scheduler'
	);

	const status = document.createElement( 'p' );
	status.className = 'scheduled-changes-recording-modal__status';
	status.setAttribute( 'role', 'status' );
	status.setAttribute( 'aria-live', 'polite' );
	status.setAttribute( 'aria-atomic', 'true' );

	actions.append( submit, cancel );
	form.append( dateLabel, timeLabel, actions, status );
	panel.append( title, summary, form );
	modal.appendChild( panel );
	document.body.appendChild( modal );

	const dateInput = form.querySelector(
		'#scheduled-changes-recording-go-live-date'
	) as HTMLInputElement | null;
	const timeInput = form.querySelector(
		'#scheduled-changes-recording-go-live-time'
	) as HTMLInputElement | null;

	modal.scheduledChangesReleaseFocusTrap = trapModalFocus(
		modal,
		dateInput || panel
	);

	cancel.addEventListener( 'click', async () => {
		if ( scheduleState.busy || scheduleState.scheduled ) {
			return;
		}

		const confirmed = await confirmCancelRecording();

		if ( ! confirmed ) {
			return;
		}

		scheduleState.busy = true;
		submit.disabled = true;
		cancel.disabled = true;
		status.classList.remove( 'is-error' );
		status.textContent = __(
			'Canceling recording.',
			'jjs-post-scheduler'
		);

		try {
			const canceled = await cancelRecording( config );
			onScheduled();
			setRecordingStatus(
				canceled.message ||
					__(
						'Recording canceled. Returning to the source item.',
						'jjs-post-scheduler'
					)
			);
			window.location.assign(
				canceled.sourceEditUrl || config.sourceEditUrl
			);
		} catch ( error ) {
			scheduleState.busy = false;
			submit.disabled = false;
			cancel.disabled = false;
			status.textContent = cancelFailureMessage( error );
			status.classList.add( 'is-error' );
		}
	} );

	form.addEventListener( 'submit', async ( event ) => {
		event.preventDefault();

		if ( scheduleState.busy || scheduleState.scheduled ) {
			return;
		}

		if ( ! dateInput?.value || ! timeInput?.value ) {
			status.textContent = __(
				'Choose a date and time.',
				'jjs-post-scheduler'
			);
			status.classList.add( 'is-error' );
			return;
		}

		if (
			isPastSchedule(
				dateInput.value,
				timeInput.value,
				finalized.timezone || config.timezone
			)
		) {
			status.textContent = __(
				'Choose a future date and time.',
				'jjs-post-scheduler'
			);
			status.classList.add( 'is-error' );
			return;
		}

		scheduleState.busy = true;
		submit.disabled = true;
		cancel.disabled = true;
		status.classList.remove( 'is-error' );
		status.textContent = __(
			'Scheduling this change.',
			'jjs-post-scheduler'
		);

		try {
			await scheduleRecording(
				config,
				finalized,
				dateInput.value,
				timeInput.value
			);
			scheduleState.busy = false;
			scheduleState.scheduled = true;
			onScheduled();
			showScheduledResult(
				panel,
				finalized.launchesUrl,
				finalized.sourceEditUrl || config.sourceEditUrl,
				dateInput.value,
				timeInput.value,
				finalized.timezone || config.timezone
			);
			setRecordingStatus(
				__( 'Changes saved.', 'jjs-post-scheduler' )
			);
		} catch ( error ) {
			scheduleState.busy = false;
			submit.disabled = false;
			cancel.disabled = false;
			status.textContent = scheduleFailureMessage( error );
			status.classList.add( 'is-error' );
		}
	} );
}

export function isPastSchedule(
	date: string,
	time: string,
	timezone: string,
	now = new Date()
): boolean {
	if (
		! /^\d{4}-\d{2}-\d{2}$/.test( date ) ||
		! /^\d{2}:\d{2}$/.test( time )
	) {
		return true;
	}

	const formatter = new Intl.DateTimeFormat( 'en-CA', {
		timeZone: timezone || 'UTC',
		year: 'numeric',
		month: '2-digit',
		day: '2-digit',
		hour: '2-digit',
		minute: '2-digit',
		hourCycle: 'h23',
	} );
	const parts = Object.fromEntries(
		formatter
			.formatToParts( now )
			.filter( ( part ) => part.type !== 'literal' )
			.map( ( part ) => [ part.type, part.value ] )
	);
	const current = `${ parts.year }-${ parts.month }-${ parts.day }T${ parts.hour }:${ parts.minute }`;

	return `${ date }T${ time }` <= current;
}

function labeledInput(
	labelText: string,
	type: string,
	id: string,
	value: string
) {
	const label = document.createElement( 'label' );
	label.className = 'scheduled-changes-recording-modal__field';

	const span = document.createElement( 'span' );
	span.textContent = labelText;

	const input = document.createElement( 'input' );
	input.id = id;
	input.type = type;
	input.required = true;
	input.value = value;

	label.append( span, input );

	return label;
}

function showScheduledResult(
	panel: HTMLElement,
	launchesUrl: string,
	sourceEditUrl: string,
	date: string,
	time: string,
	timezone: string
) {
	panel.innerHTML = '';

	const title = document.createElement( 'h2' );
	title.textContent = __(
		'Changes saved',
		'jjs-post-scheduler'
	);

	const message = document.createElement( 'p' );
	message.textContent = sprintf(
		/* translators: 1: go-live date, 2: go-live time, 3: timezone name. */
		__(
			'Your changes will go live on %1$s at %2$s. Times use %3$s.',
			'jjs-post-scheduler'
		),
		date,
		time,
		timezone
	);

	const actions = document.createElement( 'p' );
	actions.className = 'scheduled-changes-recording-modal__actions';

	const launches = document.createElement( 'a' );
	launches.className = 'button button-primary';
	launches.href = launchesUrl;
	launches.textContent = __(
		'View scheduled changes',
		'jjs-post-scheduler'
	);

	const source = document.createElement( 'a' );
	source.className = 'button';
	source.href = sourceEditUrl;
	source.textContent = __(
		'Return to source item',
		'jjs-post-scheduler'
	);

	actions.append( launches, source );
	panel.append( title, message, actions );
	launches.focus();
}

type RecordingModalElement = HTMLElement & {
	scheduledChangesReleaseFocusTrap?: () => void;
};

function trapModalFocus(
	modal: HTMLElement,
	initialFocus: HTMLElement
): () => void {
	const ownerDocument = modal.ownerDocument;
	const previousFocus =
		ownerDocument.activeElement instanceof HTMLElement
			? ownerDocument.activeElement
			: null;
	const focusInitial = () => initialFocus.focus();
	const onKeyDown = ( event: KeyboardEvent ) => {
		if ( event.key !== 'Tab' ) {
			return;
		}

		if (
			ownerDocument.activeElement instanceof HTMLElement &&
			! modal.contains( ownerDocument.activeElement )
		) {
			return;
		}

		const focusable = focusableElements( modal );

		if ( focusable.length === 0 ) {
			event.preventDefault();
			focusInitial();
			return;
		}

		const currentIndex = focusable.indexOf(
			ownerDocument.activeElement as HTMLElement
		);
		let nextIndex = currentIndex + 1;

		if ( event.shiftKey ) {
			nextIndex =
				currentIndex <= 0 ? focusable.length - 1 : currentIndex - 1;
		} else if (
			currentIndex === -1 ||
			currentIndex === focusable.length - 1
		) {
			nextIndex = 0;
		}

		event.preventDefault();
		focusable[ nextIndex ].focus();
	};

	ownerDocument.addEventListener( 'keydown', onKeyDown, true );
	focusInitial();

	return () => {
		ownerDocument.removeEventListener( 'keydown', onKeyDown, true );

		if ( previousFocus?.isConnected ) {
			previousFocus.focus();
		}
	};
}

function focusableElements( container: HTMLElement ): HTMLElement[] {
	return Array.from(
		container.querySelectorAll< HTMLElement >(
			'a[href], button, input, select, textarea, [tabindex]'
		)
	).filter( ( element ) => {
		if (
			element.hasAttribute( 'hidden' ) ||
			element.getAttribute( 'aria-hidden' ) === 'true' ||
			element.getAttribute( 'tabindex' ) === '-1'
		) {
			return false;
		}

		if (
			element instanceof HTMLButtonElement ||
			element instanceof HTMLInputElement ||
			element instanceof HTMLSelectElement ||
			element instanceof HTMLTextAreaElement
		) {
			return ! element.disabled;
		}

		return true;
	} );
}

function recordingStatusElement(): HTMLElement {
	let status = document.querySelector(
		'.scheduled-changes-recording-editor-rail .scheduled-changes-recording-editor-status'
	) as HTMLElement | null;

	if ( status ) {
		return status;
	}

	status = document.querySelector(
		'.scheduled-changes-recording-editor-status'
	) as HTMLElement | null;

	if ( ! status ) {
		status = document.createElement( 'div' );
		status.className = 'notice scheduled-changes-recording-editor-status';
		status.setAttribute( 'role', 'status' );
		status.setAttribute( 'aria-live', 'polite' );
		status.setAttribute( 'aria-atomic', 'true' );

		const wrap = document.querySelector( '.wrap' );
		const heading = wrap?.querySelector( 'h1' );

		if ( heading ) {
			heading.insertAdjacentElement( 'afterend', status );
		} else {
			wrap?.prepend( status );
		}
	}

	return status;
}

function setRecordingStatusTone(
	status: HTMLElement,
	tone: 'info' | 'warning' | 'error'
) {
	const isRailStatus = Boolean(
		status.closest( '.scheduled-changes-recording-editor-rail' )
	);

	status.classList.remove(
		'notice-info',
		'notice-warning',
		'notice-error',
		'is-info',
		'is-warning',
		'is-error'
	);

	status.classList.add( isRailStatus ? `is-${ tone }` : `notice-${ tone }` );
}

function setRecordingStatus( message: string, isError = false ) {
	const status = recordingStatusElement();

	status.classList.remove( 'notice-warning' );
	setRecordingStatusTone( status, isError ? 'error' : 'info' );
	status.innerHTML = '';

	const paragraph = document.createElement( 'p' );
	paragraph.textContent = message;
	status.appendChild( paragraph );
}

function showUnsupportedMetadataAcknowledgement(
	acknowledgementData: UnsupportedMetadataAcknowledgementData,
	onAcknowledge: () => Promise< void >
) {
	const status = recordingStatusElement();

	setRecordingStatusTone( status, 'warning' );
	status.innerHTML = '';

	const message = document.createElement( 'p' );
	message.textContent = __(
		'Some third-party product data changed and cannot be scheduled safely yet.',
		'jjs-post-scheduler'
	);
	status.appendChild( message );

	const selected = Number( acknowledgementData.selectedChangeCount || 0 );
	const supported = document.createElement( 'p' );
	supported.textContent =
		selected > 0
			? sprintf(
					/* translators: %d: supported selected change count. */
					_n(
						'%d supported change can still be scheduled.',
						'%d supported changes can still be scheduled.',
						selected,
						'jjs-post-scheduler'
					),
					selected
			  )
			: __(
					'No supported changes are selected, so there is nothing safe to schedule yet.',
					'jjs-post-scheduler'
			  );
	status.appendChild( supported );

	const warnings = acknowledgementData.unsupportedMetadataWarnings || [];

	if ( warnings.length > 0 ) {
		const list = document.createElement( 'ul' );
		list.className =
			'scheduled-changes-recording-unsupported-metadata-list';

		warnings.slice( 0, 6 ).forEach( ( warning ) => {
			const item = document.createElement( 'li' );
			item.textContent = unsupportedMetadataWarningLabel( warning );
			list.appendChild( item );
		} );

		status.appendChild( list );
	}

	if ( selected <= 0 ) {
		return;
	}

	const action = document.createElement( 'button' );
	action.type = 'button';
	action.className = 'button button-primary';
	action.textContent = __(
		'Schedule supported changes only',
		'jjs-post-scheduler'
	);
	action.addEventListener( 'click', async () => {
		action.disabled = true;
		await onAcknowledge();
	} );
	status.appendChild( action );
}

function unsupportedMetadataWarningLabel(
	warning: UnsupportedMetadataWarning
): string {
	const field = warning.metaKey
		? __( 'A third-party field', 'jjs-post-scheduler' )
		: __( 'An unsupported field', 'jjs-post-scheduler' );
	const reference = warning.sourceReference
		? __( 'on this item', 'jjs-post-scheduler' )
		: __( 'in this safe copy', 'jjs-post-scheduler' );

	return sprintf(
		/* translators: 1: friendly field label, 2: friendly item reference. */
		__(
			'%1$s %2$s changed but is not supported yet.',
			'jjs-post-scheduler'
		),
		field,
		reference
	);
}

function isUnsupportedMetadataAcknowledgementError(
	error: unknown
): error is AjaxRequestError {
	return (
		error instanceof Error &&
		true ===
			( ( error as AjaxRequestError ).data
				?.requiresUnsupportedMetadataAcknowledgement || false )
	);
}

function errorMessage( error: unknown ): string {
	if ( error instanceof Error ) {
		return error.message;
	}

	return __(
		'Scheduled Changes could not complete the recording action.',
		'jjs-post-scheduler'
	);
}

function recordingFailureMessage( error: unknown ): string {
	return sprintf(
		/* translators: %s: specific save or review error returned by the server. */
		__(
			'Could not save the safe copy. The live item was not changed. Refresh this editor and try again if the action expired, or use Cancel recording to remove the safe copy. Error: %s',
			'jjs-post-scheduler'
		),
		errorMessage( error )
	);
}

function cancelFailureMessage( error: unknown ): string {
	return sprintf(
		/* translators: %s: specific cancel error returned by the server. */
		__(
			'Recording could not be canceled. The safe copy is still active, and the live item was not changed. Try canceling again, or return to the source item without saving this copy. Error: %s',
			'jjs-post-scheduler'
		),
		errorMessage( error )
	);
}

function resumedRecordingMessage( config: ShadowRecordingData ): string {
	return sprintf(
		/* translators: %s: source product title. */
		__(
			'You are editing a scheduled-change copy of %s. The live item is not being changed. Save recording when you are ready.',
			'jjs-post-scheduler'
		),
		config.sourceTitle ||
			__( 'this product', 'jjs-post-scheduler' )
	);
}

function scheduleFailureMessage( error: unknown ): string {
	return sprintf(
		/* translators: %s: specific scheduling error returned by the server. */
		__(
			'Safe copy saved, but the scheduled change was not created. The live item was not changed. Check the date and time, then try scheduling again. Error: %s',
			'jjs-post-scheduler'
		),
		errorMessage( error )
	);
}
