/**
 * WrenchPress visit-request form: progressive enhancement only.
 *
 * The form works fully without this script: the server stays authoritative for
 * validation and anti-abuse, and nothing here moves validation authority into
 * the client. This adds convenience and accessibility behaviour:
 *  - refreshes the submit-time-trap timestamp on load so a cached page does not
 *    fail the trap,
 *  - client-side required and email-on-blur validation,
 *  - a NAMED error message per field, mirrored into the form's live region,
 *  - focus on the status region after a server rejection,
 *  - disables the submit button on submit to prevent double submission.
 *
 * Copy comes from wp_localize_script (window.wrenchpressVisitForm). This file
 * has no build step, so it cannot call the JS i18n functions, and hardcoding
 * English here would put the copy beyond the reach of translators. When the
 * localized object is absent the script degrades to its previous behaviour
 * (focus the field, say nothing) rather than announcing an untranslated string.
 */
( function () {
	'use strict';

	var strings = window.wrenchpressVisitForm || {};

	function enhance( form ) {
		/*
		 * Take ownership of client-side validation, but ONLY when this script is
		 * actually running.
		 *
		 * The fields carry the `required` attribute, so the browser's own
		 * constraint validation refuses to submit and the `submit` event never
		 * fires at all. That means the failure path below could never run and
		 * could never produce a message: the browser silently preempted it and
		 * showed a native bubble instead. Native bubbles are transient, are not
		 * reliably exposed to assistive tech, and cannot be pointed at by
		 * aria-describedby, which is the whole SC 3.3.1 defect.
		 *
		 * Setting novalidate HERE, from JS, rather than in the server-rendered
		 * markup, is what keeps this progressive enhancement: a visitor without
		 * JavaScript never gets this attribute and keeps the browser's native
		 * validation, while the server stays authoritative for both.
		 */
		form.setAttribute( 'novalidate', 'novalidate' );

		refreshTimestamp( form );
		wireEmailBlur( form );
		wireErrorClearing( form );
		focusStatusRegion( form );

		var recaptcha = recaptchaConfig( form );
		var tokenReady = false;

		form.addEventListener( 'submit', function ( event ) {
			var invalid = firstInvalidField( form );

			if ( invalid ) {
				event.preventDefault();
				reportInvalid( form, invalid );
				return;
			}

			clearStatus( form );

			// reCAPTCHA v3 has no visible widget: fetch a fresh token on submit
			// (tokens expire after about 2 minutes), write it into the hidden
			// field, then resubmit. Turnstile populates its own field.
			if ( recaptcha && ! tokenReady ) {
				event.preventDefault();

				if ( typeof window.grecaptcha === 'undefined' ) {
					// Provider script did not load; let the server reject
					// generically rather than submit an empty token.
					return;
				}

				window.grecaptcha.ready( function () {
					window.grecaptcha
						.execute( recaptcha.siteKey, { action: 'submit' } )
						.then( function ( token ) {
							recaptcha.field.value = token;
							tokenReady = true;
							disableSubmit( form );
							form.submit();
						} );
				} );

				return;
			}

			disableSubmit( form );
		} );
	}

	/**
	 * Move focus to the status region on load when the server left a message.
	 *
	 * This is the fix for the Post/Redirect/Get case, and it is counter to the
	 * instinct. A live region announces on CHANGE, not on presence. After the
	 * redirect the rejection message is already in the parsed DOM, so it never
	 * changes and role="alert" announces precisely nothing: the visitor is
	 * silently dumped back on a form with no idea why. Adding more ARIA does not
	 * fix that. Focusing the region does, and it orients a sighted keyboard user
	 * at the same time. The region carries tabindex="-1" so it is
	 * programmatically focusable without joining the tab order.
	 *
	 * Guarded on there being something to read, so a fresh form never yanks
	 * focus away from the top of the page.
	 */
	function focusStatusRegion( form ) {
		var status = statusRegion( form );

		if ( status && status.textContent.trim() ) {
			status.focus();
		}
	}

	function statusRegion( form ) {
		return (
			form.querySelector( '#egcrm_form_status' ) ||
			form.querySelector( '.egcrm-form__status' )
		);
	}

	function errorElement( field ) {
		if ( ! field.id ) {
			return null;
		}

		return document.getElementById( field.id + '_error' );
	}

	function fieldLabel( field ) {
		return field.getAttribute( 'data-egcrm-label' ) || '';
	}

	/**
	 * Build the message for an invalid field.
	 *
	 * Returns an empty string when the localized copy is unavailable, so the
	 * caller falls back to focus-only behaviour rather than inventing English.
	 */
	function messageFor( field ) {
		if (
			field.type === 'email' &&
			field.value &&
			! isValidEmail( field.value )
		) {
			return strings.invalidEmail || '';
		}

		var label = fieldLabel( field );

		if ( ! strings.required || ! label ) {
			return '';
		}

		return strings.required.replace( '%s', label );
	}

	/**
	 * Announce and name a failed field.
	 *
	 * The message goes into TWO places on purpose:
	 *  - the field's own error element, which aria-describedby already points
	 *    at, so the message is read out when focus lands on the field;
	 *  - the form's live region, whose content CHANGES here (unlike the PRG
	 *    case), which is exactly what makes it announce.
	 */
	function reportInvalid( form, field ) {
		var message = messageFor( field );

		field.setAttribute( 'aria-invalid', 'true' );
		setFieldError( field, message );
		setStatus( form, message, field );
		field.focus();
	}

	/**
	 * Write the per-field message.
	 *
	 * textContent, never innerHTML: the message is assembled from a label and a
	 * localized template, and innerHTML would turn either into a script sink.
	 */
	function setFieldError( field, message ) {
		var box = errorElement( field );

		if ( box ) {
			box.textContent = message;
		}
	}

	function clearFieldError( field ) {
		field.removeAttribute( 'aria-invalid' );
		setFieldError( field, '' );
	}

	function setStatus( form, message, field ) {
		var status = statusRegion( form );

		if ( ! status ) {
			return;
		}

		clearStatus( form );

		if ( ! message ) {
			return;
		}

		var notice = document.createElement( 'div' );
		notice.className = 'egcrm-form__notice egcrm-form__notice--error';
		notice.textContent = message;
		status.appendChild( notice );

		// Remember which field owns the message so it can be retracted when
		// that field is corrected. A stale error that contradicts the field's
		// current state is worse than no error at all.
		status.setAttribute( 'data-egcrm-owner', field && field.id ? field.id : '' );
	}

	function clearStatus( form ) {
		var status = statusRegion( form );

		if ( ! status ) {
			return;
		}

		while ( status.firstChild ) {
			status.removeChild( status.firstChild );
		}

		status.removeAttribute( 'data-egcrm-owner' );
	}

	/**
	 * Retract a message as soon as the field it refers to becomes valid.
	 */
	function wireErrorClearing( form ) {
		var fields = form.querySelectorAll(
			'[required], [aria-required="true"], input[type="email"]'
		);

		for ( var i = 0; i < fields.length; i++ ) {
			( function ( field ) {
				var handler = function () {
					if ( ! isFieldValid( field ) ) {
						return;
					}

					clearFieldError( field );

					var status = statusRegion( form );

					if (
						status &&
						status.getAttribute( 'data-egcrm-owner' ) === field.id
					) {
						clearStatus( form );
					}
				};

				field.addEventListener( 'input', handler );
				field.addEventListener( 'change', handler );
			} )( fields[ i ] );
		}
	}

	function isFieldValid( field ) {
		if ( field.type === 'checkbox' ) {
			return field.checked;
		}

		if ( ! field.value || ! field.value.trim() ) {
			// An empty optional email is fine; an empty required field is not.
			return ! field.hasAttribute( 'required' );
		}

		if ( field.type === 'email' ) {
			return isValidEmail( field.value );
		}

		return true;
	}

	function recaptchaConfig( form ) {
		var box = form.querySelector( '[data-egcrm-captcha="1"]' );

		if ( ! box || box.getAttribute( 'data-egcrm-provider' ) !== 'recaptcha_v3' ) {
			return null;
		}

		var siteKey = box.getAttribute( 'data-egcrm-sitekey' );
		var field = box.querySelector( '.egcrm-form__captcha-token' );

		if ( ! siteKey || ! field ) {
			return null;
		}

		return { siteKey: siteKey, field: field };
	}

	function refreshTimestamp( form ) {
		var ts = form.querySelector( '[data-egcrm-ts]' );

		if ( ts ) {
			ts.value = String( Math.floor( Date.now() / 1000 ) );
		}
	}

	function wireEmailBlur( form ) {
		var email = form.querySelector( 'input[type="email"]' );

		if ( ! email ) {
			return;
		}

		email.addEventListener( 'blur', function () {
			if ( email.value && ! isValidEmail( email.value ) ) {
				// Previously this set aria-invalid and produced no message, so a
				// screen reader user heard the label again and nothing else.
				email.setAttribute( 'aria-invalid', 'true' );
				setFieldError( email, strings.invalidEmail || '' );
			} else {
				clearFieldError( email );
			}
		} );
	}

	function firstInvalidField( form ) {
		var required = form.querySelectorAll( '[required], [aria-required="true"]' );

		for ( var i = 0; i < required.length; i++ ) {
			var field = required[ i ];

			if ( field.type === 'checkbox' ) {
				if ( ! field.checked ) {
					return field;
				}
				continue;
			}

			if ( ! field.value || ! field.value.trim() ) {
				return field;
			}
		}

		var email = form.querySelector( 'input[type="email"]' );

		if ( email && email.value && ! isValidEmail( email.value ) ) {
			return email;
		}

		return null;
	}

	function isValidEmail( value ) {
		return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( value );
	}

	function disableSubmit( form ) {
		var submit = form.querySelector( 'button[type="submit"]' );

		if ( submit ) {
			submit.setAttribute( 'disabled', 'disabled' );
		}
	}

	function init() {
		var forms = document.querySelectorAll( '.egcrm-form' );

		for ( var i = 0; i < forms.length; i++ ) {
			enhance( forms[ i ] );
		}
	}

	if ( document.readyState === 'loading' ) {
		document.addEventListener( 'DOMContentLoaded', init );
	} else {
		init();
	}
} )();
