/* global document */

/**
 * Row renderer for the conditional tags builder.
 *
 * @package EDD\Pro\Emails\ConditionalTags
 */

import { initChosen } from 'utils/chosen.js';
import { initDatePicker } from 'admin/components/date-picker/index.js';

let rowControlId = 0;

/**
 * Build a unique DOM id for row controls.
 *
 * @param {string} prefix Input prefix.
 * @return {string}
 */
function getControlId( prefix ) {
	rowControlId += 1;

	return `edd-ct-${ prefix }-${ rowControlId }`;
}

/**
 * Escape a string for safe use inside HTML markup.
 *
 * @param {string} str Raw string.
 * @return {string}
 */
function esc( str ) {
	const el = document.createElement( 'span' );
	el.textContent = str;

	return el.innerHTML;
}

/**
 * Build an <option> tag string.
 *
 * @param {string}  value    Option value.
 * @param {string}  label    Display text.
 * @param {boolean} selected Whether the option is selected.
 * @return {string}
 */
function optionHtml( value, label, selected ) {
	return `<option value="${ esc( value ) }"${ selected ? ' selected' : '' }>${ esc( label ) }</option>`;
}

/**
 * Apply boolean-condition visibility to operator and value groups.
 *
 * When the selected condition has no operators (boolean mode), the operator
 * and value groups are hidden but still occupy layout space for alignment.
 *
 * @param {HTMLElement} rowEl          The row element.
 * @param {boolean}     isBooleanMode  Whether the condition is boolean.
 * @return {void}
 */
function applyBooleanVisibility( rowEl, isBooleanMode ) {
	const operatorGroup = rowEl.querySelector( '.edd-ct__operator' );
	const valueGroup    = rowEl.querySelector( '.edd-ct__value' );
	if ( operatorGroup ) {
		operatorGroup.style.visibility = isBooleanMode ? 'hidden' : '';
	}
	if ( valueGroup ) {
		valueGroup.style.visibility = isBooleanMode ? 'hidden' : '';
	}
}

/**
 * Merge stored AJAX-searched options into the base option list.
 *
 * When a searchable multi-select is re-rendered, options that were
 * added via AJAX search are not in the base list from PHP. This
 * merges them so previously selected values survive re-renders.
 *
 * @param {Array} baseOptions   Options from the condition's inputType.
 * @param {Array} storedOptions Options captured from user selections.
 * @return {Array} Merged options with no duplicates.
 */
function mergeSelectOptions( baseOptions, storedOptions ) {
	if ( ! storedOptions?.length ) {
		return baseOptions;
	}

	const knownValues = new Set( baseOptions.map( ( o ) => o.value ) );
	const extras = storedOptions.filter( ( so ) => ! knownValues.has( so.value ) );

	return [ ...baseOptions, ...extras ];
}

/**
 * Collect selected values from a multi-select as state update data.
 *
 * @param {HTMLSelectElement} selectEl A <select multiple> element.
 * @return {Object} State update with `value` (comma-separated) and `selectedOptions`.
 */
function collectMultiSelectState( selectEl ) {
	const selected = [];
	for ( const opt of selectEl.selectedOptions ) {
		if ( opt.value ) {
			selected.push( { value: opt.value, label: opt.textContent } );
		}
	}

	return {
		value: selected.map( ( s ) => s.value ).join( ',' ),
		selectedOptions: selected,
	};
}

/**
 * Resolve the display label for an operator symbol.
 *
 * Prefers a condition-specific label when available, then falls back
 * to the global i18n operator map, then the raw symbol.
 *
 * @param {string} op        Operator symbol (e.g. '=', '!=').
 * @param {Object} condition Condition metadata (may have operatorLabels).
 * @param {Object} i18n      Localized strings with global operators map.
 * @return {string} Human-readable label.
 */
function getOperatorLabel( op, condition, i18n ) {
	return condition?.operatorLabels?.[ op ] || i18n.operators?.[ op ] || op;
}

/**
 * Build the HTML for a select input, supporting search, multiple, and
 * re-hydration of previously selected options from state.
 *
 * @param {Object} inputType    Input type descriptor from condition metadata.
 * @param {string} valId        DOM id for the select.
 * @param {Object} i18n         Localized strings.
 * @param {string} value        Current comma-separated or single value.
 * @param {Array}  storedOptions Previously selected options from row state.
 * @return {string} HTML string for the select element.
 */
function buildSelectInput( inputType, valId, i18n, value, storedOptions ) {
	const isMultiple = !! inputType.multiple;
	const attrs = [];

	if ( inputType.search || inputType.chosen ) {
		attrs.push( `class="edd-form-group__input edd-select-chosen"` );
		if ( inputType.search ) {
			attrs.push(
				`data-search-type="${ esc( inputType.search ) }"`,
				`data-placeholder="${ esc( i18n.searchPlaceholder || 'Type to search...' ) }"`
			);
			if ( 'download_category' === inputType.search ) {
				attrs.push( 'data-search-value-type="id"' );
			}
		} else {
			attrs.push( `data-placeholder="${ esc( i18n.chosenPlaceholder || 'Choose one or more...' ) }"` );
		}
	} else {
		attrs.push( `class="edd-form-group__input"` );
	}

	attrs.push( `id="${ valId }"` );
	if ( isMultiple ) {
		attrs.push( 'multiple' );
	}

	const selectedValues = isMultiple
		? String( value ?? '' ).split( ',' ).map( ( v ) => v.trim() ).filter( Boolean )
		: [];

	const allOptions = isMultiple
		? mergeSelectOptions( inputType.options || [], storedOptions )
		: ( inputType.options || [] );

	let optionsHtml = '';
	if ( inputType.search && ! isMultiple ) {
		optionsHtml += `<option value="">${ esc( i18n.searchPlaceholder || 'Type to search...' ) }</option>`;
	}
	optionsHtml += allOptions.map( ( opt ) => {
		const isSelected = isMultiple
			? selectedValues.includes( opt.value )
			: opt.value === value;
		return optionHtml( opt.value, opt.label, isSelected );
	} ).join( '' );

	return `<select ${ attrs.join( ' ' ) }>${ optionsHtml }</select>`;
}

/**
 * Build a currency amount input using EDD's AmountType wrapper structure.
 *
 * Reuses the same HTML/CSS pattern as EDD\HTML\AmountType so admin styles
 * (edd-amount-type-wrapper, edd-input__symbol) apply. Extendible for other
 * currency-type conditions (e.g. recurring amount).
 *
 * @param {string} valId   DOM id for the input.
 * @param {Object} i18n    Localized strings (currencySymbol, currencyPosition, currencyPlaceholder).
 * @param {string} value   Current input value.
 * @return {string} HTML string for the wrapped currency input.
 */
function buildCurrencyInput( valId, i18n, value ) {
	// Use raw symbol so HTML entities from the server (e.g. &#36; for $) render correctly.
	const symbol = i18n.currencySymbol || '$';
	const position = i18n.currencyPosition || 'before';
	const placeholder = esc( i18n.currencyPlaceholder || '0.00' );
	const inputHtml = `<input class="edd-form-group__input" type="number" step="any" id="${ valId }" placeholder="${ placeholder }" value="${ esc( value ?? '' ) }" />`;
	const symbolClass = position === 'before' ? 'edd-input__symbol--prefix' : 'edd-input__symbol--suffix';
	const symbolHtml = `<span class="edd-input__symbol ${ symbolClass }">${ symbol }</span>`;

	if ( position === 'before' ) {
		return `<span class="edd-amount-type-wrapper">${ symbolHtml }${ inputHtml }</span>`;
	}
	return `<span class="edd-amount-type-wrapper">${ inputHtml }${ symbolHtml }</span>`;
}

/**
 * Build the value input HTML for a condition row.
 *
 * @param {Object} inputType    Input type descriptor from condition metadata.
 * @param {string} valId        DOM id for the input.
 * @param {Object} i18n         Localized strings.
 * @param {string} value        Current value.
 * @param {Array}  storedOptions Previously selected options (multi-select only).
 * @return {string} HTML string for the value input.
 */
function buildValueInput( inputType, valId, i18n, value, storedOptions ) {
	const type = inputType?.type || 'text';
	const placeholder = esc( i18n.valueLabel || 'Value' );

	switch ( type ) {
		case 'select':
			return buildSelectInput( inputType, valId, i18n, value, storedOptions );
		case 'currency':
			return buildCurrencyInput( valId, i18n, value );
		case 'date':
			return `<input class="edd-form-group__input regular-text edd-ct__date-input edd_datepicker" type="text" id="${ valId }" placeholder="yyyy-mm-dd" maxlength="10" value="${ esc( value ?? '' ) }" />`;
		case 'number':
			return `<input class="edd-form-group__input regular-text" type="number" step="any" id="${ valId }" placeholder="${ placeholder }" value="${ esc( value ?? '' ) }" />`;
		case 'hidden':
			return '';
		default:
			return `<input class="edd-form-group__input regular-text" type="text" id="${ valId }" placeholder="${ placeholder }" value="${ esc( value ?? '' ) }" />`;
	}
}

/**
 * Initialize Tom Select on a searchable select inside a row.
 *
 * @param {HTMLElement} rowEl The row element.
 * @return {void}
 */
function initChosenInRow( rowEl ) {
	const chosenEl = rowEl.querySelector( '.edd-select-chosen' );
	if ( ! chosenEl ) {
		return;
	}

	initChosen( chosenEl );
}

/**
 * Initialize jQuery UI datepicker on a date input inside a row.
 *
 * Uses the shared date-picker component with a fixed yyyy-mm-dd format
 * because the conditional tag backend validation requires this exact format.
 *
 * @param {HTMLElement} rowEl    The row element.
 * @param {Function}    onChange Callback receiving the new value object.
 * @return {void}
 */
function initDatePickerInRow( rowEl, onChange ) {
	const dateInput = rowEl.querySelector( '.edd-ct__date-input' );
	if ( ! dateInput ) {
		return;
	}

	initDatePicker( dateInput, {
		dateFormat: 'yy-mm-dd',
		onSelect: function ( dateText ) {
			onChange( { value: dateText } );
		},
	} );
}

/**
 * Bind a change or input event on the value element inside a row.
 *
 * @param {HTMLElement} rowEl    The row element.
 * @param {Function}    onChange Callback receiving the new value object.
 * @return {void}
 */
function bindValueEvent( rowEl, onChange ) {
	const valueEl = rowEl.querySelector( '.edd-ct__value select, .edd-ct__value input' );
	if ( ! valueEl ) {
		return;
	}

	const eventType = valueEl.tagName === 'SELECT' ? 'change' : 'input';
	valueEl.addEventListener( eventType, ( e ) => {
		if ( valueEl.multiple ) {
			onChange( collectMultiSelectState( valueEl ) );
		} else {
			onChange( { value: e.target.value } );
		}
	} );
}

/**
 * Update the operator options for a single row element.
 *
 * Called when the condition select changes to repopulate the operator
 * dropdown without a full re-render.
 *
 * @param {HTMLElement} rowEl        The row element to update.
 * @param {Object[]}    conditions   All available conditions.
 * @param {string}      conditionKey The newly selected condition key.
 * @param {Object}      i18n         Localized strings.
 * @return {void}
 */
export function updateOperatorOptions( rowEl, conditions, conditionKey, i18n ) {
	const select = rowEl.querySelector( '.edd-ct__operator select' );
	if ( ! select ) {
		return;
	}

	const condition = conditions.find( ( c ) => c.key === conditionKey );
	const operators = condition?.operators || [];
	const defaultOperator = operators[0] || '';

	select.innerHTML = operators.map( ( op ) =>
		optionHtml( op, getOperatorLabel( op, condition, i18n ), op === defaultOperator )
	).join( '' );
	select.value = defaultOperator;

	const hideFields = '' === conditionKey || ( operators.length === 0 && conditionKey !== '' );
	applyBooleanVisibility( rowEl, hideFields );
}

/**
 * Replace the value input for a single row element in-place.
 *
 * Called alongside updateOperatorOptions when the condition select
 * changes, to swap the value input to the correct type without
 * a full re-render.
 *
 * @param {HTMLElement} rowEl        The row element to update.
 * @param {Object[]}    conditions   All available conditions.
 * @param {string}      conditionKey The newly selected condition key.
 * @param {Object}      i18n         Localized strings.
 * @param {Function}    onChange     Callback for value changes.
 * @return {void}
 */
export function updateValueInput( rowEl, conditions, conditionKey, i18n, onChange ) {
	const valueGroup = rowEl.querySelector( '.edd-ct__value' );
	if ( ! valueGroup ) {
		return;
	}

	const condition = conditions.find( ( c ) => c.key === conditionKey );
	const inputType = condition?.inputType;
	const valId     = getControlId( 'value' );

	valueGroup.innerHTML =
		`<label class="edd-form-group__label screen-reader-text" for="${ valId }">${ esc( i18n.valueLabel || 'Value' ) }</label>` +
		buildValueInput( inputType, valId, i18n, '' );

	bindValueEvent( rowEl, onChange );
	initChosenInRow( rowEl );
	initDatePickerInRow( rowEl, onChange );

	const operators   = condition?.operators || [];
	const isBoolOrHid = ( operators.length === 0 && conditionKey !== '' ) || inputType?.type === 'hidden';
	valueGroup.style.visibility = isBoolOrHid ? 'hidden' : '';
}

/**
 * Creates a condition row element.
 *
 * @param {Object}   args Row rendering args.
 * @param {number}   args.index Row index.
 * @param {Object}   args.row Row state.
 * @param {Object[]} args.conditions List of available conditions.
 * @param {Object}   args.i18n Localized strings.
 * @param {boolean}  args.canRemove Whether remove is allowed.
 * @param {Function} args.onConjunctionChange Callback for conjunction changes.
 * @param {Function} args.onChange Callback for row changes.
 * @param {Function} args.onRemove Callback for remove action.
 * @return {HTMLElement}
 */
export function createConditionRow( { index, row, conditions, i18n, canRemove, onConjunctionChange, onChange, onRemove } ) {
	const conjId = getControlId( 'conjunction' );
	const condId = getControlId( 'condition' );
	const opId   = getControlId( 'operator' );
	const valId  = getControlId( 'value' );

	const selectedCondition  = conditions.find( ( c ) => c.key === row.conditionKey );
	const supportedOperators = selectedCondition?.operators || [];
	const defaultOperator    = row.operator || supportedOperators[0] || '';

	const conjunctionOptions = [ 'AND', 'OR' ].map( ( value ) =>
		optionHtml( value, i18n[ value.toLowerCase() ] || value, ( row.conjunction || 'AND' ) === value )
	).join( '' );

	const conditionOptions = conditions.map( ( c ) =>
		optionHtml( c.key, c.label, c.key === row.conditionKey )
	).join( '' );

	const operatorOptions = supportedOperators.map( ( op ) =>
		optionHtml( op, getOperatorLabel( op, selectedCondition, i18n ), op === defaultOperator )
	).join( '' );

	const rowEl = document.createElement( 'div' );
	rowEl.className = 'edd-form-row';

	rowEl.innerHTML = `
		<div class="edd-form-group edd-form-row__column edd-ct__conjunction">
			${ 0 === index
				? `<span class="edd-ct__if-label">${ esc( i18n.ifLabel || 'If' ) }</span>`
				: `<label class="edd-form-group__label screen-reader-text" for="${ conjId }">${ esc( i18n.conjunctionLabel || 'Logical operator' ) }</label>
				   <select class="edd-form-group__input small-text" id="${ conjId }">${ conjunctionOptions }</select>`
			}
		</div>
		<div class="edd-form-group edd-form-row__column edd-ct__condition">
			<label class="edd-form-group__label screen-reader-text" for="${ condId }">${ esc( i18n.selectCondition || 'Select condition' ) }</label>
			<select class="edd-form-group__input" id="${ condId }">
				<option value="">${ esc( i18n.selectCondition || 'Select condition' ) }</option>
				${ conditionOptions }
			</select>
		</div>
		<div class="edd-form-group edd-form-row__column edd-ct__operator">
			<label class="edd-form-group__label screen-reader-text" for="${ opId }">${ esc( i18n.operatorLabel || 'Operator' ) }</label>
			<select class="edd-form-group__input" id="${ opId }">
				${ operatorOptions }
			</select>
		</div>
		<div class="edd-form-group edd-form-row__column edd-ct__value">
			<label class="edd-form-group__label screen-reader-text" for="${ valId }">${ esc( i18n.valueLabel || 'Value' ) }</label>
			${ buildValueInput( selectedCondition?.inputType, valId, i18n, row.value, row.selectedOptions ) }
		</div>
		<div class="edd-form-group edd-form-row__column edd-ct__remove">
			<button type="button" class="button button-link-delete" aria-label="${ esc( i18n.removeCondition || 'Remove condition' ) }"${ canRemove ? '' : ' disabled' }>
				<span class="dashicons dashicons-no-alt" aria-hidden="true"></span>
				<span class="screen-reader-text">${ esc( i18n.removeCondition || 'Remove condition' ) }</span>
			</button>
		</div>
	`;

	const shouldHideFields = '' === row.conditionKey || ( supportedOperators.length === 0 && row.conditionKey !== '' );
	applyBooleanVisibility( rowEl, shouldHideFields );

	if ( selectedCondition?.inputType?.type === 'hidden' ) {
		const valueGroup = rowEl.querySelector( '.edd-ct__value' );
		if ( valueGroup ) {
			valueGroup.style.visibility = 'hidden';
		}
	}

	if ( index > 0 ) {
		rowEl.querySelector( '.edd-ct__conjunction select' ).addEventListener( 'change', ( e ) => {
			onConjunctionChange( e.target.value );
		} );
	}

	rowEl.querySelector( '.edd-ct__condition select' ).addEventListener( 'change', ( e ) => {
		const nextCondition = conditions.find( ( c ) => c.key === e.target.value );
		const nextOperator  = nextCondition?.operators?.[0] || '';
		onChange( { conditionKey: e.target.value, operator: nextOperator } );
	} );

	rowEl.querySelector( '.edd-ct__operator select' ).addEventListener( 'change', ( e ) => {
		onChange( { operator: e.target.value } );
	} );

	bindValueEvent( rowEl, onChange );
	initChosenInRow( rowEl );
	initDatePickerInRow( rowEl, onChange );

	rowEl.querySelector( '.edd-ct__remove button' ).addEventListener( 'click', () => {
		onRemove();
	} );

	return rowEl;
}
