/**
 * Settings screen behaviour.
 *
 * Talks to admin-ajax with the nonce localized by Admin\Menu. No credential ever
 * reaches this file — the server reads stored secrets itself and returns only
 * display strings.
 */
( function () {
	'use strict';

	var config = window.textTangoAdmin || {};
	var strings = config.i18n || {};

	/**
	 * POST to admin-ajax and resolve with the parsed payload.
	 *
	 * @param {string} action Ajax action name.
	 * @param {Object} data   Extra fields to send.
	 * @return {Promise<Object>} Resolves on success, rejects with a message.
	 */
	function request( action, data ) {
		var body = new window.FormData();

		body.append( 'action', action );
		body.append( 'nonce', config.nonce || '' );

		Object.keys( data || {} ).forEach( function ( key ) {
			body.append( key, data[ key ] );
		} );

		return window
			.fetch( config.ajaxUrl, {
				method: 'POST',
				credentials: 'same-origin',
				body: body,
			} )
			.then( function ( response ) {
				return response.json().catch( function () {
					throw new Error( strings.genericError );
				} );
			} )
			.then( function ( payload ) {
				if ( ! payload || ! payload.success ) {
					var message =
						( payload && payload.data && payload.data.message ) ||
						strings.genericError;
					throw new Error( message );
				}

				return payload.data || {};
			} );
	}

	/**
	 * Write a status message into an element, colouring it by outcome.
	 *
	 * Only the colour classes are toggled — the element keeps whatever layout
	 * classes the template gave it, so restyling the markup does not require
	 * touching this file.
	 *
	 * @param {string}  selector Element to write into.
	 * @param {string}  message  Text to show.
	 * @param {boolean} isError  Whether to style it as a failure.
	 */
	function setMessage( selector, message, isError ) {
		var target = document.querySelector( selector );

		if ( ! target ) {
			return;
		}

		target.textContent = message;
		target.classList.remove( 'tt-text-red-700', 'tt-text-emerald-700' );
		target.classList.add(
			'tt-font-medium',
			isError ? 'tt-text-red-700' : 'tt-text-emerald-700'
		);
	}

	/**
	 * Write a status message next to the connection buttons.
	 *
	 * @param {string}  message Text to show.
	 * @param {boolean} isError Whether to style it as a failure.
	 */
	function setResult( message, isError ) {
		setMessage( '[data-texttango-result]', message, isError );
	}

	/**
	 * Run an action with the button disabled and a busy label.
	 *
	 * @param {HTMLElement} button    Button that was clicked.
	 * @param {string}      busyLabel Label while the request is in flight.
	 * @param {Function}    run       Returns a promise.
	 */
	function withBusyButton( button, busyLabel, run ) {
		var original = button.textContent;

		button.disabled = true;
		button.textContent = busyLabel;

		run().finally( function () {
			button.disabled = false;
			button.textContent = original;
		} );
	}

	/**
	 * Test the stored credentials against GET /v2/wallet.
	 *
	 * @param {HTMLElement} button Button that was clicked.
	 */
	function testConnection( button ) {
		withBusyButton( button, strings.testing, function () {
			return request( 'texttango_test_connection', {} )
				.then( function ( data ) {
					setResult( data.message, false );

					var balance = document.querySelector(
						'[data-texttango-balance]'
					);

					if ( balance && data.balance ) {
						balance.textContent = data.balance;
					}
				} )
				.catch( function ( error ) {
					setResult( error.message, true );
				} );
		} );
	}

	/**
	 * Move to the next wizard step, if that is where we are.
	 *
	 * The setup page derives each stage's state by asking the site what is done,
	 * so an action that completes a stage has to reload for its row to tick.
	 * Everywhere else these same buttons just report and stay put, which is why
	 * this is gated on the page rather than on the action.
	 *
	 * @param {number} delay Milliseconds to let the result message be read.
	 */
	function advanceWizard( delay ) {
		var wizard = document.querySelector( '[data-texttango-wizard]' );

		if ( ! wizard ) {
			return;
		}

		window.setTimeout( function () {
			window.location.reload();
		}, delay || 1200 );
	}

	/**
	 * Write a status message next to the webhook buttons.
	 *
	 * @param {string}  message Text to show.
	 * @param {boolean} isError Whether to style it as a failure.
	 */
	function setWebhookResult( message, isError ) {
		setMessage( '[data-texttango-webhook-result]', message, isError );
	}

	/**
	 * Point the token's SMS callback at this site.
	 *
	 * @param {HTMLElement} button Button that was clicked.
	 */
	function registerWebhook( button ) {
		withBusyButton( button, strings.working, function () {
			return request( 'texttango_webhook_register', {} )
				.then( function ( data ) {
					setWebhookResult( data.message, false );

					if ( data.test_goes_elsewhere ) {
						setWebhookResult( strings.webhookElsewhere, true );

						return;
					}

					advanceWizard();
				} )
				.catch( function ( error ) {
					setWebhookResult( error.message, true );
				} );
		} );
	}

	/**
	 * Ask TextTango to send a test event, then wait for it to land.
	 *
	 * TextTango dispatches the event from a queued job, so the reply to the
	 * request that asked for it only means the request was accepted. Arrival is
	 * something only this site can observe, so it is polled for here rather than
	 * assumed from a successful dispatch.
	 *
	 * @param {HTMLElement} button Button that was clicked.
	 */
	function testWebhook( button ) {
		withBusyButton( button, strings.waiting, function () {
			return request( 'texttango_webhook_test', {} )
				.then( function ( data ) {
					setWebhookResult( strings.webhookWaiting, false );

					return pollForWebhook(
						data.since || 0,
						( data.timeout || 20 ) * 1000
					);
				} )
				.then( function ( arrived ) {
					if ( arrived ) {
						setWebhookResult( strings.webhookArrived, false );
						advanceWizard();
						return;
					}

					setWebhookResult( strings.webhookNever, true );
				} )
				.catch( function ( error ) {
					setWebhookResult( error.message, true );
				} );
		} );
	}

	/**
	 * Poll until the test event lands or the deadline passes.
	 *
	 * @param {number} since  Arrival stamp recorded before dispatch.
	 * @param {number} budget How long to keep asking, in milliseconds.
	 * @return {Promise<boolean>} Whether anything arrived.
	 */
	function pollForWebhook( since, budget ) {
		var deadline = Date.now() + budget;

		function attempt() {
			return request( 'texttango_webhook_test_result', {
				since: since,
			} ).then( function ( data ) {
				if ( data.arrived ) {
					return true;
				}

				if ( Date.now() >= deadline ) {
					return false;
				}

				return new Promise( function ( resolve ) {
					window.setTimeout( resolve, 1500 );
				} ).then( attempt );
			} );
		}

		return attempt();
	}

	/**
	 * Replace the webhook signing secret.
	 *
	 * @param {HTMLElement} button Button that was clicked.
	 */
	function rotateWebhookSecret( button ) {
		var confirmation = button.getAttribute( 'data-texttango-confirm' );

		if ( confirmation && ! window.confirm( confirmation ) ) {
			return;
		}

		withBusyButton( button, strings.working, function () {
			return request( 'texttango_webhook_rotate', {} )
				.then( function ( data ) {
					setWebhookResult( data.message, false );
				} )
				.catch( function ( error ) {
					setWebhookResult( error.message, true );
				} );
		} );
	}

	/**
	 * Write a status message under the test-SMS field.
	 *
	 * @param {string}  message Text to show.
	 * @param {boolean} isError Whether to style it as a failure.
	 */
	function setTestResult( message, isError ) {
		setMessage( '[data-texttango-test-result]', message, isError );
	}

	/**
	 * Send one real SMS to the admin's own number.
	 *
	 * Always confirms first: this bypasses sandbox mode by design, so it spends
	 * real credits even on a staging site.
	 *
	 * @param {HTMLElement} button Button that was clicked.
	 */
	function sendTestSms( button ) {
		var field = document.querySelector( '[data-texttango-test-phone]' );
		var phone = field ? field.value.trim() : '';

		if ( ! phone ) {
			setTestResult( strings.missingTestPhone, true );

			if ( field ) {
				field.focus();
			}

			return;
		}

		var inSandbox = '1' === button.getAttribute( 'data-sandbox' );
		var question = inSandbox
			? strings.confirmRealSend
			: strings.confirmTestSend;

		if ( ! window.confirm( question ) ) {
			return;
		}

		withBusyButton( button, strings.sending, function () {
			return request( 'texttango_send_test_sms', { phone: phone } )
				.then( function ( data ) {
					setTestResult( data.message, false );

					// Longer than elsewhere: this one is worth reading before the
					// page moves, since it names the number the SMS went to.
					advanceWizard( 2500 );
				} )
				.catch( function ( error ) {
					setTestResult( error.message, true );
				} );
		} );
	}

	/**
	 * Populate the sender ID datalist from the account.
	 *
	 * @param {HTMLElement} button Button that was clicked.
	 */
	function loadSenderIds( button ) {
		withBusyButton( button, strings.testing, function () {
			return request( 'texttango_sender_ids', {} )
				.then( function ( data ) {
					var list = document.getElementById(
						'texttango-sender-ids'
					);

					if ( list ) {
						list.innerHTML = '';

						( data.senderIds || [] ).forEach( function ( name ) {
							var option = document.createElement( 'option' );
							option.value = name;
							list.appendChild( option );
						} );
					}

					setResult( data.message, false );
				} )
				.catch( function ( error ) {
					setResult( error.message, true );
				} );
		} );
	}

	/**
	 * Count the characters a message costs, and how many SMS that is.
	 *
	 * Mirrors WooCommerce\Templates::estimate(). The alphabet and segment sizes
	 * come from the server rather than being restated here, so the two cannot
	 * disagree about which characters are cheap.
	 *
	 * @param {string} body Message text.
	 * @return {{encoding: string, length: number, segments: number}} Estimate.
	 */
	function estimate( body ) {
		var charset = config.charset || {};
		var basic = Array.from( charset.basic || '' );
		var extended = Array.from( charset.extended || '' );
		var characters = Array.from( body );
		var isGsm7 = characters.every( function ( character ) {
			return (
				-1 !== basic.indexOf( character ) ||
				-1 !== extended.indexOf( character )
			);
		} );

		var encoding = isGsm7 ? 'GSM-7' : 'UCS-2';
		var limits = ( charset.limits || {} )[ encoding ] || {
			single: 160,
			multi: 153,
		};
		var length = 0;

		characters.forEach( function ( character ) {
			if ( isGsm7 ) {
				length += -1 !== extended.indexOf( character ) ? 2 : 1;
				return;
			}

			// Anything outside the Basic Multilingual Plane, emoji included,
			// takes two UCS-2 units.
			length += character.codePointAt( 0 ) > 0xffff ? 2 : 1;
		} );

		var segments = 0;

		if ( length > 0 ) {
			segments =
				length <= limits.single
					? 1
					: Math.ceil( length / limits.multi );
		}

		return { encoding: encoding, length: length, segments: segments };
	}

	/**
	 * Fill merge tags with the sample values the server supplied.
	 *
	 * @param {string} template Raw template.
	 * @return {string} Rendered preview.
	 */
	function renderPreview( template ) {
		var samples = config.sampleValues || {};

		return template
			.replace( /\{[a-z_]+\}/g, function ( tag ) {
				return Object.prototype.hasOwnProperty.call( samples, tag )
					? samples[ tag ]
					: '';
			} )
			.replace( /[ \t]{2,}/g, ' ' )
			.trim();
	}

	/**
	 * Refresh one event row's preview, counts, and Default/Customised badge.
	 *
	 * @param {HTMLElement} field The template textarea.
	 */
	function refreshEventRow( field ) {
		var row = field.closest( '[data-texttango-event]' );

		if ( ! row ) {
			return;
		}

		var preview = renderPreview( field.value );
		var counts = estimate( preview );
		var isCustom = field.value.trim() !== ( field.dataset.default || '' ).trim();

		var previewEl = row.querySelector( '[data-texttango-preview]' );
		var estimateEl = row.querySelector( '[data-texttango-estimate]' );
		var segmentsEl = row.querySelector( '[data-texttango-segments]' );
		var stateEl = row.querySelector( '[data-texttango-state]' );
		var resetEl = row.querySelector(
			'[data-texttango-action="reset-template"]'
		);

		if ( previewEl ) {
			previewEl.textContent = preview;
		}

		if ( estimateEl ) {
			estimateEl.textContent = ( strings.estimate || '' )
				.replace( '%1$d', String( counts.length ) )
				.replace( '%2$s', counts.encoding )
				.replace( '%3$d', String( counts.segments ) );
		}

		if ( segmentsEl ) {
			segmentsEl.textContent = String( counts.segments ) + ' SMS';
		}

		if ( stateEl ) {
			stateEl.textContent = isCustom
				? strings.customised
				: strings.usingDefault;
			stateEl.classList.toggle( 'tt-badge-ok', isCustom );
			stateEl.classList.toggle( 'tt-badge-off', ! isCustom );
		}

		if ( resetEl ) {
			resetEl.disabled = ! isCustom;
		}
	}

	// Ticking an event's checkbox must not also open or close its row.
	document.addEventListener( 'click', function ( event ) {
		if ( event.target.closest( '[data-texttango-toggle]' ) ) {
			event.stopPropagation();
		}
	} );

	/**
	 * Show a message under the phone-attach widget.
	 *
	 * @param {string}  text    Message to show.
	 * @param {boolean} isError Whether it reports a failure.
	 */
	function setAttachResult( text, isError ) {
		setMessage( '[data-texttango-attach-result]', text, isError );
	}

	/**
	 * Switch the attach widget between its two steps.
	 *
	 * @param {string} step Either "phone" or "code".
	 */
	function showAttachStep( step ) {
		[ 'phone', 'code' ].forEach( function ( name ) {
			var panel = document.querySelector(
				'[data-texttango-attach-step="' + name + '"]'
			);

			if ( panel ) {
				panel.hidden = name !== step;
			}
		} );

		if ( 'code' === step ) {
			var codeField = document.querySelector(
				'[data-texttango-attach-code]'
			);

			if ( codeField ) {
				codeField.focus();
			}
		}
	}

	/**
	 * Ask for a code to prove the number belongs to this user.
	 *
	 * @param {HTMLElement} button Button that was clicked.
	 */
	function requestAttachCode( button ) {
		var field = document.querySelector( '[data-texttango-attach-phone]' );
		var phone = field ? field.value.trim() : '';

		if ( ! phone ) {
			setAttachResult( strings.needPhone, true );
			return;
		}

		// Always a real send, even in sandbox, so confirm the cost first.
		if ( ! window.confirm( strings.confirmAttach ) ) {
			return;
		}

		withBusyButton( button, strings.sending, function () {
			return request( 'texttango_attach_phone_request', { phone: phone } )
				.then( function ( data ) {
					setAttachResult( data.message, false );
					showAttachStep( 'code' );
				} )
				.catch( function ( error ) {
					setAttachResult( error.message, true );
				} );
		} );
	}

	/**
	 * Submit the code and attach the number.
	 *
	 * @param {HTMLElement} button Button that was clicked.
	 */
	function verifyAttachCode( button ) {
		var phoneField = document.querySelector(
			'[data-texttango-attach-phone]'
		);
		var codeField = document.querySelector( '[data-texttango-attach-code]' );
		var code = codeField ? codeField.value.trim() : '';

		if ( ! code ) {
			setAttachResult( strings.needCode, true );
			return;
		}

		withBusyButton( button, strings.verifying, function () {
			return request( 'texttango_attach_phone_verify', {
				phone: phoneField ? phoneField.value.trim() : '',
				code: code,
			} )
				.then( function ( data ) {
					setAttachResult( data.message, false );

					// Reload so the number and the two-factor checkbox reflect it.
					window.location.reload();
				} )
				.catch( function ( error ) {
					setAttachResult( error.message, true );

					if ( codeField ) {
						codeField.value = '';
						codeField.focus();
					}
				} );
		} );
	}

	// The message box a merge tag should be inserted into.
	var activeTemplateField = null;

	document.addEventListener( 'focusin', function ( event ) {
		if ( event.target.matches( '[data-texttango-template]' ) ) {
			activeTemplateField = event.target;
		}
	} );

	/*
	 * Keep the caret where it is. Without this the button takes focus on
	 * mousedown, the textarea's selection collapses, and the tag lands at the
	 * start of the message instead of where the cursor was.
	 */
	document.addEventListener( 'mousedown', function ( event ) {
		if ( event.target.closest( '[data-texttango-insert-tag]' ) ) {
			event.preventDefault();
		}
	} );

	document.addEventListener( 'click', function ( event ) {
		var tagButton = event.target.closest( '[data-texttango-insert-tag]' );

		if ( ! tagButton ) {
			return;
		}

		event.preventDefault();

		// Nothing to insert into: no row has been opened and focused yet.
		if ( ! activeTemplateField || ! activeTemplateField.isConnected ) {
			return;
		}

		var tag = tagButton.getAttribute( 'data-texttango-insert-tag' ) || '';
		var field = activeTemplateField;
		var start = field.selectionStart;
		var end = field.selectionEnd;

		field.value =
			field.value.slice( 0, start ) + tag + field.value.slice( end );

		var caret = start + tag.length;
		field.setSelectionRange( caret, caret );
		field.focus();

		refreshEventRow( field );
	} );

	/**
	 * Count a campaign body as it is typed.
	 *
	 * Deliberately not refreshEventRow(): that one substitutes merge tags before
	 * counting, and a campaign has no order behind it. Running it here would
	 * silently delete anything in braces from the count — and then from nothing
	 * else, so the figure shown and the message sent would disagree.
	 *
	 * @param {HTMLElement} field The campaign textarea.
	 */
	function refreshCampaignEstimate( field ) {
		var target = document.querySelector(
			field.dataset.texttangoEstimate || ''
		);

		if ( ! target ) {
			return;
		}

		var counts = estimate( field.value );

		target.textContent = ( strings.estimate || '' )
			.replace( '%1$d', String( counts.length ) )
			.replace( '%2$s', counts.encoding )
			.replace( '%3$d', String( counts.segments ) );
	}

	/**
	 * Show the supplied-numbers block only for the audiences that use it.
	 *
	 * Presentation only. The server decides what each audience means and ignores
	 * anything in this block when the campaign is going to customers, so a
	 * browser with no JavaScript sees the box always and is still safe.
	 */
	document.addEventListener( 'change', function ( event ) {
		if ( event.target.matches( '[data-texttango-segment-kind]' ) ) {
			var buyers = document.querySelector( '[data-texttango-buyers]' );

			if ( buyers ) {
				buyers.hidden = 'buyers' !== event.target.value;
			}
		}

		if ( event.target.matches( '[data-texttango-timing]' ) ) {
			var when = document.querySelector( '[data-texttango-when]' );

			if ( when ) {
				when.hidden = 'later' !== event.target.value;
			}
		}

		if ( ! event.target.matches( '[data-texttango-audience]' ) ) {
			return;
		}

		var block = document.querySelector( '[data-texttango-supplied]' );

		if ( block ) {
			block.hidden = 'customers' === event.target.value;
		}

		// A segment narrows the store's own customers, so it has no meaning when
		// the campaign is going only to a pasted list.
		var segment = document.querySelector( '[data-texttango-segment]' );

		if ( segment ) {
			segment.hidden = 'supplied' === event.target.value;
		}
	} );

	document.addEventListener( 'input', function ( event ) {
		if ( event.target.matches( '[data-texttango-template]' ) ) {
			refreshEventRow( event.target );
		}

		if ( event.target.matches( '[data-texttango-campaign-body]' ) ) {
			refreshCampaignEstimate( event.target );
		}
	} );

	document.addEventListener( 'click', function ( event ) {
		var button = event.target.closest( '[data-texttango-action]' );

		if ( ! button ) {
			return;
		}

		var action = button.getAttribute( 'data-texttango-action' );

		if ( 'reset-template' === action ) {
			event.preventDefault();

			var row = button.closest( '[data-texttango-event]' );
			var field = row
				? row.querySelector( '[data-texttango-template]' )
				: null;

			if ( field ) {
				field.value = field.dataset.default || '';
				refreshEventRow( field );
				field.focus();
			}

			return;
		}

		if ( 'test-connection' === action ) {
			event.preventDefault();
			testConnection( button );
		}

		if ( 'load-senders' === action ) {
			event.preventDefault();
			loadSenderIds( button );
		}

		if ( 'webhook-register' === action ) {
			event.preventDefault();
			registerWebhook( button );
		}

		if ( 'webhook-test' === action ) {
			event.preventDefault();
			testWebhook( button );
		}

		if ( 'webhook-rotate' === action ) {
			event.preventDefault();
			rotateWebhookSecret( button );
		}

		if ( 'load-groups' === action ) {
			event.preventDefault();

			withBusyButton( button, strings.testing, function () {
				return request( 'texttango_contact_groups', {} )
					.then( function ( data ) {
						var list = document.getElementById(
							'texttango-contact-groups'
						);

						if ( list ) {
							list.innerHTML = '';

							( data.groups || [] ).forEach( function ( group ) {
								var option = document.createElement( 'option' );
								option.value = group.id;
								option.label = group.name;
								option.textContent = group.name;
								list.appendChild( option );
							} );
						}

						setResult( data.message, false );
					} )
					.catch( function ( error ) {
						setResult( error.message, true );
					} );
			} );
		}

		if ( 'send-test-sms' === action ) {
			event.preventDefault();
			sendTestSms( button );
		}

		if ( 'attach-request' === action ) {
			event.preventDefault();
			requestAttachCode( button );
			return;
		}

		if ( 'attach-verify' === action ) {
			event.preventDefault();
			verifyAttachCode( button );
			return;
		}

		if ( 'attach-back' === action ) {
			event.preventDefault();

			var codeField = document.querySelector(
				'[data-texttango-attach-code]'
			);

			if ( codeField ) {
				codeField.value = '';
			}

			setAttachResult( '', false );
			showAttachStep( 'phone' );
			return;
		}

		if ( 'resync-pending' === action ) {
			event.preventDefault();

			withBusyButton( button, strings.checking, function () {
				return request( 'texttango_resync_pending', {} )
					.then( function ( data ) {
						setMessage(
							'[data-texttango-resync-result]',
							data.message,
							false
						);

						// Statuses may have moved, so show the new ones.
						if ( data.updated > 0 ) {
							window.location.reload();
						}
					} )
					.catch( function ( error ) {
						setMessage(
							'[data-texttango-resync-result]',
							error.message,
							true
						);
					} );
			} );
		}

		if ( 'import-consent' === action ) {
			event.preventDefault();

			var source = button.getAttribute( 'data-texttango-source' ) || '';

			withBusyButton( button, strings.working, function () {
				return request( 'texttango_import_consent', { source: source } )
					.then( function ( data ) {
						setMessage(
							'[data-texttango-import-result]',
							data.message,
							false
						);
					} )
					.catch( function ( error ) {
						setMessage(
							'[data-texttango-import-result]',
							error.message,
							true
						);
					} );
			} );
		}

		if ( 'resend-message' === action ) {
			event.preventDefault();

			if ( ! window.confirm( strings.confirmResend ) ) {
				return;
			}

			var id = button.getAttribute( 'data-texttango-id' ) || '';

			withBusyButton( button, strings.sending, function () {
				return request( 'texttango_resend_message', { id: id } )
					.then( function ( data ) {
						setMessage(
							'[data-texttango-resync-result]',
							data.message,
							false
						);

						// The resend is its own row, so the table is out of date the
						// moment it is queued.
						window.location.reload();
					} )
					.catch( function ( error ) {
						setMessage(
							'[data-texttango-resync-result]',
							error.message,
							true
						);
					} );
			} );
		}
	} );
} )();
