/**
 * BitForgeNet Guest Tickets — public form behaviour.
 * Vanilla JS, no dependencies. The form works without it; this only adds polish.
 */
( function () {
	'use strict';

	function money( cents, config ) {
		var value = ( cents / 100 ).toFixed( config.decimals );
		var parts = value.split( '.' );
		parts[ 0 ] = parts[ 0 ].replace( /\B(?=(\d{3})+(?!\d))/g, config.thousandSeparator );
		var amount = parts.join( config.decimalSeparator );

		switch ( config.position ) {
			case 'prefix':
				return config.symbol + amount;
			case 'prefix_space':
				return config.symbol + ' ' + amount;
			case 'suffix':
				return amount + config.symbol;
			default:
				return amount + ' ' + config.symbol;
		}
	}

	function initForm( root ) {
		var form = root.querySelector( '.bfnet-form' );
		if ( ! form ) {
			return;
		}

		// Declared before anything reads it. It used to sit two hundred lines
		// further down, which was harmless only by accident.
		var config = window.BFNetConfig || {};
		config.symbol = config.symbol || '$';
		config.decimals = typeof config.decimals === 'number' ? config.decimals : 2;
		config.decimalSeparator = config.decimalSeparator || '.';
		config.thousandSeparator = typeof config.thousandSeparator === 'string' ? config.thousandSeparator : ' ';
		config.position = config.position || 'suffix_space';

		// admin-ajax is always at a known place; falling back to it means a
		// missing setting can never silence the discount check.
		if ( ! config.ajaxUrl ) {
			config.ajaxUrl = window.ajaxurl || ( window.location.origin + '/wp-admin/admin-ajax.php' );
		}

		var quantity = form.querySelector( '#bfnet-quantity' );
		var guests = form.querySelector( '#bfnet-guests' );
		var totalValue = form.querySelector( '[data-bfnet-payable]' );
		var promo = form.querySelector( '[data-bfnet-code]' );
		var promoField = form.querySelector( '[name="bfnet_discount_code"]' );
		var promoMsg = form.querySelector( '[data-bfnet-code-msg]' );
		var offLine = form.querySelector( '[data-bfnet-off]' );
		var offLabel = form.querySelector( '[data-bfnet-off-label]' );
		var offValue = form.querySelector( '[data-bfnet-off-value]' );
		var subtotal = 0;
		var discount = ( function () {
			// Carry over whatever the server already worked out, so the first
			// repaint does not wipe a discount off the screen.
			var line = form.querySelector( '[data-bfnet-off]' );
			if ( ! line || line.hidden ) { return 0; }
			var shown = form.querySelector( '[data-bfnet-off-value]' );
			var cents = shown ? String( shown.textContent ).replace( /[^0-9]/g, '' ) : '';
			return cents ? parseInt( cents, 10 ) : 0;
		} )();

		/**
		 * Repaint the money, taking any accepted discount off.
		 *
		 * The figure shown here is a courtesy. The server prices the booking
		 * again from the basket it actually receives, so a code edited in the
		 * page buys nothing.
		 */
		function paintTotal() {
			if ( ! totalValue ) { return; }

			var payable = Math.max( 0, subtotal - discount );
			totalValue.textContent = money( payable, config );

			if ( offLine ) {
				offLine.hidden = discount <= 0;
				if ( offValue ) { offValue.textContent = '-' + money( discount, config ); }
			}
		}

		function sayPromo( text, good ) {
			if ( ! promoMsg ) { return; }
			promoMsg.hidden = ! text;
			promoMsg.textContent = text || '';
			promoMsg.className = 'bfnet-promo__msg' + ( good ? ' is-good' : ( text ? ' is-bad' : '' ) );
		}

		function checkPromo() {
			if ( ! promo || ! promoField ) { return; }

			var code = promoField.value.trim();
			if ( ! code ) {
				discount = 0;
				sayPromo( '', false );
				paintTotal();
				return;
			}

			sayPromo( 'Checking…', false );

			var body = new window.FormData();
			body.append( 'action', 'bfnet_quote' );
			body.append( 'bfnet_quote', '1' );
			body.append( 'event', promo.getAttribute( 'data-event' ) );
			body.append( 'code', code );
			body.append( 'subtotal', String( subtotal ) );
			body.append( 'quantity', String( currentQuantity() ) );

			var held = 'Saved. This code is applied when you send the booking.';

			function ask( url ) {
				return window.fetch( url, { method: 'POST', credentials: 'same-origin', body: body } )
					.then( function ( r ) { return r.json(); } );
			}

			// The plugin's own route first, admin-ajax only as a spare. A site
			// that declines to dispatch an admin-ajax action answers with a bare
			// "0", which is indistinguishable from a genuine refusal — and that
			// is how a perfectly good code came to report itself broken.
			ask( config.quoteUrl || config.ajaxUrl )
				.catch( function () { return ask( config.ajaxUrl ); } )
				.then( function ( data ) {
					if ( data && data.ok ) {
						discount = parseInt( data.amount, 10 ) || 0;
						if ( offLabel && data.label ) { offLabel.textContent = data.label; }
						sayPromo( data.message, true );
						paintTotal();
						return;
					}

					discount = 0;
					paintTotal();

					// A reply we cannot read is not a refusal. Only a reason
					// from the server is allowed to say no.
					if ( data && typeof data === 'object' && data.message ) {
						sayPromo( data.message, false );
					} else {
						sayPromo( held, true );
					}
				} )
				.catch( function () {
					// Nothing answered at all. The code still travels with the
					// booking and is priced on the server, so nothing is lost.
					sayPromo( held, true );
				} );
		}
		if ( ! quantity || ! guests ) {
			return;
		}

		var max = parseInt( guests.getAttribute( 'data-max' ), 10 ) || 1;
		var blocks = Array.prototype.slice.call( guests.querySelectorAll( '.bfnet-guest' ) );

		function currentQuantity() {
			var n = parseInt( quantity.value, 10 );
			if ( isNaN( n ) || n < 1 ) {
				n = 1;
			}
			return Math.min( n, max );
		}

		function priceFor( block ) {
			var control = block.querySelector( '[data-bfnet-price]' );
			if ( ! control ) {
				return 0;
			}
			if ( control.tagName === 'SELECT' ) {
				var option = control.options[ control.selectedIndex ];
				return option ? parseInt( option.getAttribute( 'data-price' ), 10 ) || 0 : 0;
			}
			return parseInt( control.getAttribute( 'data-price' ), 10 ) || 0;
		}

		function sync() {
			var n = currentQuantity();
			quantity.value = n;

			blocks.forEach( function ( block, index ) {
				var visible = index < n;
				block.hidden = ! visible;

				block.querySelectorAll( 'input, select' ).forEach( function ( field ) {
					field.disabled = ! visible;
				} );
			} );

			if ( totalValue ) {
				var total = 0;
				for ( var i = 0; i < n; i++ ) {
					total += priceFor( blocks[ i ] );
				}
				subtotal = total;
				paintTotal();
			}

			form.querySelectorAll( '[data-bfnet-step]' ).forEach( function ( button ) {
				var step = parseInt( button.getAttribute( 'data-bfnet-step' ), 10 );
				button.disabled = ( step < 0 && n <= 1 ) || ( step > 0 && n >= max );
			} );
		}

		var applyBtn = form.querySelector( '[data-bfnet-code-apply]' );
		if ( applyBtn ) {
			applyBtn.addEventListener( 'click', function ( e ) {
				// The button is a real submit. Only take it over if we can
				// genuinely do better; otherwise let the server answer, which
				// always works.
				if ( ! window.fetch || ! window.FormData ) {
					return;
				}
				e.preventDefault();
				checkPromo();
			} );
		}
		if ( promoField ) {
			promoField.addEventListener( 'keydown', function ( e ) {
				if ( e.key === 'Enter' ) { e.preventDefault(); checkPromo(); }
			} );
			promoField.addEventListener( 'input', function () {
				promoField.value = promoField.value.toUpperCase();
				if ( discount ) { discount = 0; sayPromo( '', false ); paintTotal(); }
			} );
		}

		form.querySelectorAll( '[data-bfnet-step]' ).forEach( function ( button ) {
			button.addEventListener( 'click', function () {
				quantity.value = currentQuantity() + parseInt( button.getAttribute( 'data-bfnet-step' ), 10 );
				sync();
			} );
		} );

		quantity.addEventListener( 'input', sync );
		quantity.addEventListener( 'change', sync );
		guests.addEventListener( 'change', sync );

		// Mirror the buyer's name into the first guest row until it is edited by hand.
		form.querySelectorAll( '[data-bfnet-mirror]' ).forEach( function ( target ) {
			var source = document.getElementById( target.getAttribute( 'data-bfnet-mirror' ) );
			if ( ! source ) {
				return;
			}
			var touched = false;
			target.addEventListener( 'input', function () {
				touched = true;
			} );
			source.addEventListener( 'input', function () {
				if ( ! touched ) {
					target.value = source.value;
				}
			} );
		} );

		// Guard against double submission.
		form.addEventListener( 'submit', function () {
			var submit = form.querySelector( '.bfnet-submit' );
			if ( submit ) {
				window.setTimeout( function () {
					submit.disabled = true;
				}, 0 );
			}
		} );

		sync();
	}

	/**
	 * Countdown and viewer-local time.
	 *
	 * Both are driven by an absolute UTC timestamp printed by PHP, so someone opening
	 * the page from one country sees the same remaining time as someone in another, and the
	 * event hour is shown in the venue's clock plus, separately, in their own.
	 */
	function initClocks( root ) {
		var box = root.querySelector( '[data-bfnet-countdown]' );

		if ( box ) {
			var target = parseInt( box.getAttribute( 'data-bfnet-countdown' ), 10 ) * 1000;
			var value = box.querySelector( '.bfnet-countdown__value' );

			var tick = function () {
				var left = target - Date.now();

				if ( left <= 0 ) {
					box.hidden = true;
					return;
				}

				var s = Math.floor( left / 1000 );
				var d = Math.floor( s / 86400 );
				var h = Math.floor( ( s % 86400 ) / 3600 );
				var m = Math.floor( ( s % 3600 ) / 60 );
				var sec = s % 60;

				value.textContent = d > 0
					? d + 'd ' + h + 'h ' + m + 'm'
					: ( h > 0 ? h + 'h ' + m + 'm ' + sec + 's' : m + 'm ' + sec + 's' );
			};

			tick();
			window.setInterval( tick, 1000 );
		}

		var local = root.querySelector( '[data-bfnet-localtime]' );
		if ( local ) {
			var ts = parseInt( local.getAttribute( 'data-bfnet-localtime' ), 10 ) * 1000;
			if ( ts ) {
				try {
					var zone = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
					var text = new Date( ts ).toLocaleString( undefined, {
						weekday: 'short', day: 'numeric', month: 'short',
						hour: '2-digit', minute: '2-digit'
					} );
					local.textContent = 'That is ' + text + ( zone ? ' in your time zone (' + zone + ')' : ' your time' );
					local.hidden = false;
				} catch ( e ) {
					/* Older browser: the venue time alone is still correct. */
				}
			}
		}
	}

	/**
	 * Flag an under-age date of birth before the form is submitted.
	 * The server checks this again — this is only to save the visitor a round trip.
	 */
	function initAgeCheck( root ) {
		var fields = root.querySelectorAll( '[data-bfnet-dob]' );
		if ( ! fields.length ) {
			return;
		}

		fields.forEach( function ( field ) {
			field.addEventListener( 'change', function () {
				var max = field.getAttribute( 'max' );
				if ( ! max || ! field.value ) {
					field.setCustomValidity( '' );
					return;
				}
				field.setCustomValidity( field.value > max ? 'Under the minimum age for this event.' : '' );
			} );
		} );
	}

	function boot() {
		document.querySelectorAll( '.bfnet' ).forEach( function ( root ) {
			initForm( root );
			initClocks( root );
			initAgeCheck( root );
		} );
	}

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