/* eslint-disable jsdoc/no-undefined-types */
const $ = window.jQuery;

interface GPIFieldData {
	isExhausted: boolean;
}

interface GPIChoiceBasedFieldData extends GPIFieldData {
	choices: {
		label: string;
		value: string;
		available: number;
	}[];
	isExhausted: boolean;
}

interface GPINonChoiceFieldData extends GPIFieldData {
	available: number;
}

interface GPIConditionalLogicData {
	[fieldId: number | string]: GPIChoiceBasedFieldData | GPINonChoiceFieldData;
}

export default class GPIConditionalLogic {
	public formId: number;

	public data: GPIConditionalLogicData;

	private choiceId: string;

	constructor({
		formId,
		data,
	}: Pick<GPIConditionalLogic, 'formId' | 'data'>) {
		this.formId = formId;
		this.data = data;
		this.choiceId = '';

		// Fetch dynamic choices if not already set
		this.syncDynamicChoices();

		this.addHooks();
	}

	addHooks = () => {
		window.gform.addFilter('gform_is_value_match', this.isValueMatch);
		window.gform.addAction('gpi_field_refreshed', this.onFieldRefresh);
	};

	isValueMatch = (
		isMatch: boolean,
		formId: GFFormID,
		rule: GravityFormsConditionalLogicRule
	) => {
		if (rule.value === '__return_true') {
			return true;
		} else if (rule.value === '__return_false') {
			return false;
		}

		if (!isNaN(parseInt(rule.fieldId))) {
			return isMatch;
		}

		const regex = /(gpi_available)_([0-9]+)(?:_([0-9]+))?/;
		const match = regex.exec(rule.fieldId);

		if (!match) {
			return isMatch;
		}

		const fieldId = match[2];
		// store the 'choiceId' for checkbox fields
		this.choiceId = match[3]
			? match[2] + '_' + (parseInt(match[3]) + 1)
			: '';

		let limit = this.getFieldAvailability(formId, fieldId);

		if (limit === undefined && this.isFieldExhausted(formId, fieldId)) {
			limit = 0;
		}

		return window.gf_matches_operation(
			limit + '',
			rule.value,
			rule.operator
		);
	};

	isFieldExhausted = (formId: GFFormID, fieldId: GFFieldID) => {
		return this.data[fieldId]?.isExhausted;
	};

	getFieldAvailability = (formId: GFFormID, fieldId: GFFieldID) => {
		const data = this.data[fieldId];

		if ((data as GPIChoiceBasedFieldData)?.choices) {
			return this.getChoiceAvailability(formId, fieldId);
		}

		return (data as GPINonChoiceFieldData)?.available;
	};

	getChoiceAvailability = (
		formId: GFFormID,
		fieldId: GFFieldID,
		choiceValue?: any
	) => {
		const data = this.data[fieldId] as GPIChoiceBasedFieldData;
		const choices = data.choices;

		if (typeof choiceValue === 'undefined') {
			choiceValue = this.getChoiceValue(formId, fieldId);
		}

		for (const choice of choices) {
			// eslint-disable-next-line eqeqeq
			if (choice.value == choiceValue) {
				return Math.max(choice.available, 0);
			}
		}

		return undefined;
	};

	syncDynamicChoices = () => {
		// If the choices are not set, check if there is dynamic populate data (via GPPA) to fetch.
		$(document).on(
			'gppa_updated_batch_fields',
			(event, updatedFormId, updatedFieldIds) => {
				if (+updatedFormId !== +this.formId) {
					return;
				}

				for (const fieldId of updatedFieldIds) {
					const $input = $('#input_' + this.formId + '_' + fieldId);
					const updatedChoices = $input
						.find('.gchoice')
						.map(function () {
							const value =
								$(this).find('input[type="radio"]').val() || '';
							const text = $(this).find('label').text().trim();
							const [, label = text, available = 0] =
								text.match(
									/^(.*?)\s*\((\d+)\s+items remaining\)$/i
								) || [];

							return {
								label: label.trim(),
								value,
								available: parseInt(String(available), 10) || 0,
							};
						})
						.get();

					// Update the data object with the new choices
					if ('choices' in this.data[fieldId]) {
						(
							this.data[fieldId] as GPIChoiceBasedFieldData
						).choices = updatedChoices.map((choice) => ({
							...choice,
							value: String(choice.value),
						}));
					}
				}
			}
		);
	};

	getChoiceValue = (formId: GFFormID, fieldId: GFFieldID) => {
		const $input = $('#input_' + formId + '_' + fieldId);
		let choiceValue: any;

		// radio
		if ($input.is('.gfield_radio')) {
			choiceValue = $input.find('input:checked').val();
		}
		// checkbox
		else if ($input.is('.gfield_checkbox')) {
			const choice = '#choice_' + formId + '_' + this.choiceId;
			$input.find('input:checked').each(function () {
				if ($(this).val() === $(choice).val()) {
					choiceValue = $(this).val();
				}
			});
		}
		// select
		else {
			choiceValue = $input.val();
		}

		// split up product-based values (i.e. "value|price") to get just the "value"
		if (typeof choiceValue === 'string' && choiceValue) {
			choiceValue = choiceValue.split('|')[0];
		}

		return choiceValue;
	};

	/**
	 * Update conditional logic data for field when it gets refreshed and re-trigger conditional logic accordingly.
	 *
	 * @param {JQuery}  $targetField  Field with inventory that was refreshed.
	 * @param {JQuery}  $triggerField Property field that caused the field with inventory to be refreshed.
	 * @param {boolean} initialLoad   Whether the field was refreshed on the initial load of the form.
	 * @param {Object}  requestData   Payload used to initiate the `gpi_refresh_field` request.
	 * @param {Object}  response      AJAX response from `gpi_refresh_field` admin-ajax.php action.
	 */
	onFieldRefresh = (
		$targetField: JQuery,
		$triggerField: JQuery,
		initialLoad: boolean,
		requestData: any,
		response: any
	) => {
		// eslint-disable-next-line eqeqeq
		if (requestData.form_id != this.formId) {
			return;
		}

		if (!response.data.conditional_logic_data) {
			return;
		}

		this.data[requestData.target_field_id] =
			response.data.conditional_logic_data;

		// Re-trigger change now that the data is updated.
		window.gform.doAction(
			'gform_input_change',
			$targetField,
			requestData.form_id,
			requestData.target_field_id
		);
	};
}
