/**
 * VMPlayer Admin JavaScript
 *
 * Drives the settings page: tab switching, color picker init,
 * station card rendering, mount editor, drag-to-reorder,
 * media library for logo upload, test stream button, and
 * all AJAX save/delete/reorder calls.
 *
 * Uses vanilla JS for everything except wp-color-picker and
 * jQuery UI sortable, which are jQuery dependencies.
 *
 * @package VMPlayer
 */

( function ( $ ) {
	'use strict';

	var config = window.vmplayerAdminConfig || {};

	// Monotonic counter for unsaved draft cards. Ensures each new
	// station gets a unique DOM identifier that will not collide
	// with server-assigned integer indexes.
	var draftCounter = 0;

	// BroadcastChannel used to notify any open frontend players when
	// stations change so they refresh their internal list without a
	// page reload. Same-origin browser tabs receive the message and
	// update their station counter, previous/next enabled state, and
	// station data in place. Older browsers without BroadcastChannel
	// just silently skip this and the frontend picks up the change on
	// its next natural reload.
	var syncChannel = null;
	function getSyncChannel() {
		if ( null !== syncChannel ) {
			return syncChannel;
		}
		if ( 'undefined' === typeof window.BroadcastChannel ) {
			return null;
		}
		try {
			syncChannel = new window.BroadcastChannel( 'vmplayer_stations' );
		} catch ( e ) {
			syncChannel = null;
		}
		return syncChannel;
	}
	function broadcastStationsChanged( stations ) {
		var channel = getSyncChannel();
		if ( ! channel || ! Array.isArray( stations ) ) {
			return;
		}
		try {
			channel.postMessage( {
				type: 'vmplayer:stations_updated',
				stations: stations,
				at: Date.now()
			} );
		} catch ( e ) {
			// Postmessage can throw on data-clone edge cases. Silent
			// failure is fine because the frontend will just reload
			// naturally when the visitor navigates.
		}
	}

	/* ==================================================================
	 * Module: helpers
	 * =============================================================== */

	function el( tag, attrs, children ) {
		var node = document.createElement( tag );
		if ( attrs ) {
			Object.keys( attrs ).forEach( function ( key ) {
				if ( 'className' === key ) {
					node.className = attrs[ key ];
				} else if ( 'dataset' === key ) {
					Object.keys( attrs.dataset ).forEach( function ( dkey ) {
						node.dataset[ dkey ] = attrs.dataset[ dkey ];
					} );
				} else if ( 'textContent' === key ) {
					node.textContent = attrs[ key ];
				} else if ( 'checked' === key || 'disabled' === key || 'selected' === key ) {
					if ( attrs[ key ] ) {
						node[ key ] = true;
					}
				} else {
					node.setAttribute( key, attrs[ key ] );
				}
			} );
		}
		if ( children ) {
			children.forEach( function ( child ) {
				if ( null === child || undefined === child ) {
					return;
				}
				if ( 'string' === typeof child ) {
					node.appendChild( document.createTextNode( child ) );
				} else {
					node.appendChild( child );
				}
			} );
		}
		return node;
	}

	function showNotice( message, type ) {
		var notice = document.getElementById( 'vmplayer-notice' );
		if ( ! notice ) {
			return;
		}
		notice.textContent = message;
		notice.className = 'vmplayer-notice is-visible' + ( type ? ' is-' + type : '' );
		clearTimeout( showNotice.timer );
		showNotice.timer = setTimeout( function () {
			notice.className = 'vmplayer-notice';
			notice.textContent = '';
		}, 4000 );
	}

	function ajax( action, data ) {
		var body = new FormData();
		body.append( 'action', action );
		body.append( 'nonce', config.nonce );
		Object.keys( data || {} ).forEach( function ( key ) {
			var value = data[ key ];
			if ( null === value || undefined === value ) {
				return;
			}
			if ( 'object' === typeof value ) {
				appendObject( body, key, value );
			} else {
				body.append( key, value );
			}
		} );

		return fetch( config.ajaxUrl, {
			method: 'POST',
			credentials: 'same-origin',
			body: body
		} ).then( function ( res ) {
			return res.json();
		} );
	}

	function appendObject( formData, prefix, obj ) {
		if ( Array.isArray( obj ) ) {
			obj.forEach( function ( item, i ) {
				var key = prefix + '[' + i + ']';
				if ( null !== item && 'object' === typeof item ) {
					appendObject( formData, key, item );
				} else {
					formData.append( key, item );
				}
			} );
			return;
		}
		Object.keys( obj ).forEach( function ( k ) {
			var key = prefix + '[' + k + ']';
			var v = obj[ k ];
			if ( null !== v && 'object' === typeof v ) {
				appendObject( formData, key, v );
			} else {
				formData.append( key, v );
			}
		} );
	}

	/* ==================================================================
	 * Module: General tab
	 * =============================================================== */

	function initGeneralForm() {
		var form = document.getElementById( 'vmplayer-general-form' );
		if ( ! form ) {
			return;
		}

		// Init color pickers via jQuery (wp-color-picker is a jQuery plugin).
		$( '.vmplayer-color-picker', form ).wpColorPicker();

		form.addEventListener( 'submit', function ( e ) {
			e.preventDefault();
			saveGeneralSettings( form );
		} );
	}

	function saveGeneralSettings( form ) {
		var btn = form.querySelector( '#vmplayer-save-general' );
		var prev = btn.textContent;
		btn.disabled = true;
		btn.textContent = config.strings.saving;

		var data = {
			theme_color_primary: form.elements.theme_color_primary.value,
			theme_color_secondary: form.elements.theme_color_secondary.value,
			enable_visualizer: form.elements.enable_visualizer.checked ? 1 : 0,
			enable_popout: form.elements.enable_popout.checked ? 1 : 0,
			auto_resume: form.elements.auto_resume.checked ? 1 : 0,
			itunes_enabled: form.elements.itunes_enabled.checked ? 1 : 0,
			itunes_country: form.elements.itunes_country ? form.elements.itunes_country.value : 'us',
			deezer_enabled: form.elements.deezer_enabled.checked ? 1 : 0,
			musicbrainz_enabled: form.elements.musicbrainz_enabled.checked ? 1 : 0,
			metadata_refresh_interval: form.elements.metadata_refresh_interval.value
		};
		if ( form.elements.default_station ) {
			data.default_station = form.elements.default_station.value;
		}

		ajax( 'vmplayer_save_settings', { settings: data } )
			.then( function ( response ) {
				btn.disabled = false;
				btn.textContent = prev;
				if ( response && response.success ) {
					config.settings = response.data.settings;
					showNotice( response.data.message || config.strings.saved, 'success' );
				} else {
					showNotice( ( response && response.data && response.data.message ) || config.strings.saveFailed, 'error' );
				}
			} )
			.catch( function () {
				btn.disabled = false;
				btn.textContent = prev;
				showNotice( config.strings.saveFailed, 'error' );
			} );
	}

	/* ==================================================================
	 * Module: Stations tab
	 * =============================================================== */

	function initStationsTab() {
		var list = document.getElementById( 'vmplayer-stations-list' );
		if ( ! list ) {
			return;
		}

		renderAllStations();
		bindAddStation();
		bindSortable();
	}

	function renderAllStations() {
		var list = document.getElementById( 'vmplayer-stations-list' );
		list.innerHTML = '';

		var stations = config.stations || [];
		if ( 0 === stations.length ) {
			renderEmptyState( list );
			return;
		}

		stations.forEach( function ( station, index ) {
			list.appendChild( buildStationCard( station, index ) );
		} );
	}

	function renderEmptyState( list ) {
		var empty = el( 'div', { className: 'vmplayer-empty-state' }, [
			el( 'p', { textContent: 'No stations yet. Click Add Station to set up your first preset.' } )
		] );
		list.appendChild( empty );
	}

	function buildStationCard( station, index ) {
		var card = el( 'div', {
			className: 'vmplayer-station-card is-collapsed',
			dataset: { index: index }
		} );

		card.appendChild( buildStationHeader( station, index ) );
		card.appendChild( buildStationBody( station, index ) );
		card.appendChild( buildStationActions( station, index ) );

		return card;
	}

	function buildStationHeader( station, index ) {
		var header = el( 'div', { className: 'vmplayer-station-header' } );

		var handle = el( 'span', {
			className: 'vmplayer-drag-handle',
			textContent: '\u2630',
			title: 'Drag to reorder'
		} );

		var title = el( 'h3', { className: 'vmplayer-station-title' }, [
			document.createTextNode( station.name || config.strings.untitledStation )
		] );

		if ( station.stream_type ) {
			title.appendChild( el( 'span', {
				className: 'vmplayer-station-meta',
				textContent: '(' + station.stream_type + ')'
			} ) );
		}

		var toggle = el( 'button', {
			type: 'button',
			className: 'vmplayer-station-toggle',
			textContent: 'Expand'
		} );

		toggle.addEventListener( 'click', function ( e ) {
			e.stopPropagation();
			var card = e.target.closest( '.vmplayer-station-card' );
			card.classList.toggle( 'is-collapsed' );
			toggle.textContent = card.classList.contains( 'is-collapsed' ) ? 'Expand' : 'Collapse';
		} );

		header.appendChild( handle );
		header.appendChild( title );
		header.appendChild( toggle );

		return header;
	}

	function buildStationBody( station, index ) {
		var body = el( 'div', { className: 'vmplayer-station-body' } );
		var table = el( 'table', { className: 'form-table', role: 'presentation' } );
		var tbody = el( 'tbody' );

		tbody.appendChild( fieldRow( 'Name', textInput( 'name', station.name, index, true ) ) );
		tbody.appendChild( fieldRow( 'Tagline', textInput( 'tagline', station.tagline, index ) ) );
		tbody.appendChild( fieldRow( 'Description', textareaInput( 'description', station.description, index ) ) );
		tbody.appendChild( fieldRow( 'Station Logo', logoField( station, index ) ) );
		tbody.appendChild( fieldRow( 'Stream URL', textInput( 'stream_url', station.stream_url, index, true, 'url' ) ) );
		tbody.appendChild( fieldRow( 'Stream Type', streamTypeField( station, index ) ) );
		tbody.appendChild( fieldRow( 'Genre Override', textInput( 'genre', station.genre, index ) ) );
		tbody.appendChild( fieldRow( 'Mount Points', mountsField( station, index ) ) );
		tbody.appendChild( fieldRow( 'Social URLs', socialField( station, index ) ) );
		tbody.appendChild( fieldRow( 'Detect Live Broadcasts', liveDetectionField( station, index ) ) );
		tbody.appendChild( fieldRow( 'Show Listener Count', listenerCountField( station, index ) ) );
		tbody.appendChild( fieldRow( 'Show Clock', clockField( station, index ) ) );

		table.appendChild( tbody );
		body.appendChild( table );

		return body;
	}

	function fieldRow( label, field ) {
		var labelText = label;
		var labelId = 'vmplayer-field-' + label.toLowerCase().replace( /\s+/g, '-' ) + '-' + Math.random().toString( 36 ).substr( 2, 5 );

		var th = el( 'th', { scope: 'row' }, [
			el( 'label', { textContent: labelText } )
		] );
		var td = el( 'td' );
		td.appendChild( field );

		return el( 'tr', {}, [ th, td ] );
	}

	function textInput( name, value, index, required, type ) {
		return el( 'input', {
			type: type || 'text',
			className: 'regular-text vmplayer-field',
			dataset: { field: name, index: index },
			value: value || '',
			required: required ? 'required' : null
		} );
	}

	function textareaInput( name, value, index ) {
		var ta = el( 'textarea', {
			className: 'large-text vmplayer-field',
			rows: '3',
			dataset: { field: name, index: index }
		} );
		ta.value = value || '';
		return ta;
	}

	/**
	 * Live detection field. Renders a checkbox toggle and a keyword
	 * text input in a single control cell. When the checkbox is off,
	 * the keyword input is disabled so it does not look editable.
	 * The keyword field's initial value comes from the station config;
	 * blank means the sanitizer will restore the default list on save.
	 */
	function liveDetectionField( station, index ) {
		var wrap = el( 'div', { className: 'vmplayer-live-detection' } );

		var toggleWrap = el( 'label', { className: 'vmplayer-checkbox-label' } );
		var toggle = el( 'input', {
			type: 'checkbox',
			className: 'vmplayer-field vmplayer-live-detect-toggle',
			dataset: { field: 'detect_live', index: index }
		} );
		if ( station.detect_live ) {
			toggle.checked = true;
		}
		toggleWrap.appendChild( toggle );
		toggleWrap.appendChild( document.createTextNode( ' Show LIVE indicator when a live source is on air' ) );
		wrap.appendChild( toggleWrap );

		var hint = el( 'p', {
			className: 'description vmplayer-field-hint',
			textContent: 'Useful when a DJ goes on air through a console or an encoder like BUTT that does not send song metadata to the stream server. Detection fires when the stream title is empty, matches the station name, or contains one of the keywords below.'
		} );
		wrap.appendChild( hint );

		var keywordLabel = el( 'label', {
			className: 'vmplayer-live-keywords-label',
			textContent: 'Live indicators (comma-separated):'
		} );
		wrap.appendChild( keywordLabel );

		var keywordInput = el( 'input', {
			type: 'text',
			className: 'regular-text vmplayer-field vmplayer-live-keywords-input',
			dataset: { field: 'live_keywords', index: index },
			value: station.live_keywords || '',
			placeholder: 'live, on air, live show, live mix, live dj, live stream'
		} );
		if ( ! station.detect_live ) {
			keywordInput.disabled = true;
		}
		wrap.appendChild( keywordInput );

		// Enable/disable the keyword field as the toggle changes so
		// the UI reflects that the keywords do nothing when detection
		// is off.
		toggle.addEventListener( 'change', function () {
			keywordInput.disabled = ! toggle.checked;
		} );

		return wrap;
	}

	/**
	 * Listener count field. Single checkbox that controls whether the
	 * player displays the current listener count when the stream
	 * server reports one. Server-side default is off because not
	 * every operator wants that number visible.
	 */
	function listenerCountField( station, index ) {
		var wrap = el( 'div', { className: 'vmplayer-listener-count-field' } );

		var toggleWrap = el( 'label', { className: 'vmplayer-checkbox-label' } );
		var toggle = el( 'input', {
			type: 'checkbox',
			className: 'vmplayer-field',
			dataset: { field: 'show_listener_count', index: index }
		} );
		if ( station.show_listener_count ) {
			toggle.checked = true;
		}
		toggleWrap.appendChild( toggle );
		toggleWrap.appendChild( document.createTextNode( ' Show current listener count on the player' ) );
		wrap.appendChild( toggleWrap );

		var hint = el( 'p', {
			className: 'description vmplayer-field-hint',
			textContent: 'Requires that the stream server exposes listener counts. Shoutcast v1, Shoutcast v2, and Icecast are supported. The display hides automatically when the server does not report a count.'
		} );
		wrap.appendChild( hint );

		return wrap;
	}

	/**
	 * Clock display field. Single checkbox that controls whether the
	 * clock and date appear in the header of the main player and
	 * popout. Defaults to on because existing stations upgrading from
	 * 1.1.3 should keep the header they already had; admins who want
	 * a cleaner look can flip it off per station. Uses the same
	 * strict-boolean initial-state pattern as the other toggles so a
	 * stale string value from a legacy save cannot render as checked
	 * when it should be unchecked.
	 */
	function clockField( station, index ) {
		var wrap = el( 'div', { className: 'vmplayer-clock-field' } );

		var toggleWrap = el( 'label', { className: 'vmplayer-checkbox-label' } );
		var toggle = el( 'input', {
			type: 'checkbox',
			className: 'vmplayer-field',
			dataset: { field: 'show_clock', index: index }
		} );
		// Default true when the station has no explicit value so
		// existing stations render the checkbox as on after upgrade.
		var initial = ( 'undefined' === typeof station.show_clock )
			? true
			: !! station.show_clock;
		if ( initial ) {
			toggle.checked = true;
		}
		toggleWrap.appendChild( toggle );
		toggleWrap.appendChild( document.createTextNode( ' Show the clock and date in the player header' ) );
		wrap.appendChild( toggleWrap );

		var hint = el( 'p', {
			className: 'description vmplayer-field-hint',
			textContent: 'Displays the current time and date in the top row of the main player and pop-out. Turn off if you want a cleaner header or if your placement is too narrow for the clock to fit gracefully. The compact variant never shows the clock regardless of this setting.'
		} );
		wrap.appendChild( hint );

		return wrap;
	}

	function streamTypeField( station, index ) {
		var wrap = el( 'fieldset', { className: 'vmplayer-stream-types' } );
		var types = config.streamTypes || [ 'shoutcast', 'icecast', 'hls', 'direct' ];

		types.forEach( function ( type ) {
			var label = el( 'label', {} );
			var radio = el( 'input', {
				type: 'radio',
				name: 'stream_type_' + index,
				value: type,
				className: 'vmplayer-field',
				dataset: { field: 'stream_type', index: index },
				checked: station.stream_type === type
			} );
			label.appendChild( radio );
			label.appendChild( document.createTextNode( ' ' + type ) );
			wrap.appendChild( label );
		} );

		var testWrap = el( 'div', { className: 'vmplayer-test-wrap' } );
		var testBtn = el( 'button', {
			type: 'button',
			className: 'button vmplayer-test-stream',
			textContent: 'Test Connection'
		} );
		var resultSpan = el( 'span', { className: 'vmplayer-test-result' } );

		testBtn.addEventListener( 'click', function () {
			testStream( index, resultSpan, testBtn );
		} );

		testWrap.appendChild( testBtn );
		testWrap.appendChild( resultSpan );
		wrap.appendChild( testWrap );

		return wrap;
	}

	function logoField( station, index ) {
		var wrap = el( 'div', { className: 'vmplayer-logo-field' } );

		var preview = el( 'div', { className: 'vmplayer-logo-preview' + ( station.logo_url ? '' : ' is-empty' ) } );
		if ( station.logo_url ) {
			preview.style.backgroundImage = 'url(' + JSON.stringify( station.logo_url ) + ')';
		} else {
			preview.textContent = 'No logo';
		}

		var hiddenInput = el( 'input', {
			type: 'hidden',
			className: 'vmplayer-field',
			dataset: { field: 'logo_url', index: index },
			value: station.logo_url || ''
		} );

		var btnWrap = el( 'div', { className: 'vmplayer-logo-buttons' } );
		var chooseBtn = el( 'button', {
			type: 'button',
			className: 'button',
			textContent: station.logo_url ? 'Change Logo' : 'Select Logo'
		} );
		var removeBtn = el( 'button', {
			type: 'button',
			className: 'button-link-delete vmplayer-logo-remove' + ( station.logo_url ? '' : ' is-hidden' ),
			textContent: 'Remove'
		} );

		chooseBtn.addEventListener( 'click', function () {
			openMediaFrame( function ( url ) {
				hiddenInput.value = url;
				preview.style.backgroundImage = 'url(' + JSON.stringify( url ) + ')';
				preview.classList.remove( 'is-empty' );
				preview.textContent = '';
				chooseBtn.textContent = 'Change Logo';
				removeBtn.classList.remove( 'is-hidden' );
				markCardDirty( index );
			} );
		} );

		removeBtn.addEventListener( 'click', function () {
			hiddenInput.value = '';
			preview.style.backgroundImage = '';
			preview.classList.add( 'is-empty' );
			preview.textContent = 'No logo';
			chooseBtn.textContent = 'Select Logo';
			removeBtn.classList.add( 'is-hidden' );
			markCardDirty( index );
		} );

		btnWrap.appendChild( chooseBtn );
		btnWrap.appendChild( removeBtn );

		wrap.appendChild( preview );
		wrap.appendChild( hiddenInput );
		wrap.appendChild( btnWrap );

		return wrap;
	}

	function openMediaFrame( onSelect ) {
		var frame = wp.media( {
			title: config.strings.selectLogo,
			button: { text: config.strings.useThisImage },
			library: { type: 'image' },
			multiple: false
		} );
		frame.on( 'select', function () {
			var attachment = frame.state().get( 'selection' ).first().toJSON();
			onSelect( attachment.url );
		} );
		frame.open();
	}

	function mountsField( station, index ) {
		var wrap = el( 'div', { className: 'vmplayer-mounts-wrap' } );
		var list = el( 'div', { className: 'vmplayer-mounts', dataset: { index: index } } );

		( station.mounts || [] ).forEach( function ( mount, mi ) {
			list.appendChild( buildMountRow( mount, index, mi ) );
		} );

		var addBtn = el( 'button', {
			type: 'button',
			className: 'button vmplayer-add-mount',
			textContent: 'Add Mount'
		} );

		addBtn.addEventListener( 'click', function () {
			var nextIndex = list.children.length;
			list.appendChild( buildMountRow( { label: '', url: '', bitrate: 128, format: 'mp3' }, index, nextIndex ) );
			markCardDirty( index );
		} );

		wrap.appendChild( list );
		wrap.appendChild( addBtn );

		return wrap;
	}

	function buildMountRow( mount, stationIndex, mountIndex ) {
		var row = el( 'div', { className: 'vmplayer-mount-row' } );

		var label = el( 'input', {
			type: 'text',
			placeholder: 'Label',
			className: 'vmplayer-mount-field',
			dataset: { field: 'label', station: stationIndex, mount: mountIndex },
			value: mount.label || ''
		} );

		var url = el( 'input', {
			type: 'url',
			placeholder: 'https://stream.example.com/mount',
			className: 'vmplayer-mount-field',
			dataset: { field: 'url', station: stationIndex, mount: mountIndex },
			value: mount.url || ''
		} );

		var bitrate = el( 'input', {
			type: 'number',
			placeholder: 'kbps',
			min: '0',
			className: 'vmplayer-mount-field',
			dataset: { field: 'bitrate', station: stationIndex, mount: mountIndex },
			value: mount.bitrate || 128
		} );

		var format = el( 'select', {
			className: 'vmplayer-mount-field',
			dataset: { field: 'format', station: stationIndex, mount: mountIndex }
		} );
		( config.audioFormats || [ 'mp3', 'aac', 'ogg', 'opus' ] ).forEach( function ( fmt ) {
			var opt = el( 'option', { value: fmt, textContent: fmt.toUpperCase() } );
			if ( mount.format === fmt ) {
				opt.selected = true;
			}
			format.appendChild( opt );
		} );

		var remove = el( 'button', {
			type: 'button',
			className: 'vmplayer-mount-remove',
			textContent: 'Remove',
			title: 'Remove this mount'
		} );
		remove.addEventListener( 'click', function () {
			row.remove();
			markCardDirty( stationIndex );
		} );

		row.appendChild( label );
		row.appendChild( url );
		row.appendChild( bitrate );
		row.appendChild( format );
		row.appendChild( remove );

		return row;
	}

	function socialField( station, index ) {
		var grid = el( 'div', { className: 'vmplayer-social-grid' } );
		var platforms = config.socialPlatforms || [ 'facebook', 'bsky', 'instagram', 'youtube', 'whatsapp', 'x' ];
		var platformConfig = config.socialConfig || {};
		var current = station.social || {};

		platforms.forEach( function ( platform ) {
			var info = platformConfig[ platform ] || {};
			var field = el( 'div', { className: 'vmplayer-social-field' } );

			// Label carries the display name for the platform.
			field.appendChild( el( 'label', {
				textContent: info.label || platform,
				className: 'vmplayer-social-label'
			} ) );

			// Row that shows the fixed URL prefix beside the editable handle.
			var row = el( 'div', { className: 'vmplayer-social-row' } );
			if ( info.base_url ) {
				row.appendChild( el( 'span', {
					textContent: info.base_url,
					className: 'vmplayer-social-prefix'
				} ) );
			}
			row.appendChild( el( 'input', {
				type: 'text',
				placeholder: info.placeholder || '',
				className: 'vmplayer-social-input',
				dataset: { platform: platform, index: index },
				value: current[ platform ] || ''
			} ) );
			field.appendChild( row );

			// Helper text underneath explaining what to enter.
			if ( info.hint ) {
				field.appendChild( el( 'p', {
					textContent: info.hint,
					className: 'vmplayer-social-hint'
				} ) );
			}

			grid.appendChild( field );
		} );

		return grid;
	}

	function buildStationActions( station, index ) {
		var wrap = el( 'div', { className: 'vmplayer-station-actions' } );
		var left = el( 'div', { className: 'vmplayer-station-actions-left' } );
		var right = el( 'div', { className: 'vmplayer-station-actions-right' } );

		var deleteBtn = el( 'button', {
			type: 'button',
			className: 'button button-link-delete',
			textContent: 'Delete Station'
		} );
		deleteBtn.addEventListener( 'click', function () {
			if ( confirm( config.strings.deleteConfirm ) ) {
				deleteStation( index );
			}
		} );

		var saveBtn = el( 'button', {
			type: 'button',
			className: 'button button-primary',
			textContent: 'Save Station'
		} );
		saveBtn.addEventListener( 'click', function () {
			saveStation( index );
		} );

		left.appendChild( deleteBtn );
		right.appendChild( saveBtn );
		wrap.appendChild( left );
		wrap.appendChild( right );

		// Mark card dirty on any field change.
		setTimeout( function () {
			var card = document.querySelector( '.vmplayer-station-card[data-index="' + index + '"]' );
			if ( card ) {
				card.addEventListener( 'input', function () { markCardDirty( index ); } );
				card.addEventListener( 'change', function () { markCardDirty( index ); } );
			}
		}, 0 );

		return wrap;
	}

	function markCardDirty( index ) {
		var card = document.querySelector( '.vmplayer-station-card[data-index="' + index + '"]' );
		if ( card ) {
			card.classList.add( 'is-dirty' );
		}
	}

	function collectStationData( index ) {
		var card = document.querySelector( '.vmplayer-station-card[data-index="' + index + '"]' );
		if ( ! card ) {
			return null;
		}

		var data = {
			name: '',
			tagline: '',
			description: '',
			logo_url: '',
			stream_url: '',
			stream_type: 'shoutcast',
			genre: '',
			mounts: [],
			social: {},
			detect_live: false,
			live_keywords: '',
			show_listener_count: false,
			// Default true so a station saved before the admin ever
			// touched the checkbox lands on the sanitizer's
			// template-default branch and keeps the clock visible.
			show_clock: true
		};

		// Plain fields. Checkboxes and radio buttons need their own
		// serialization path since input.value on a checkbox always
		// returns "on" whether checked or not.
		card.querySelectorAll( '.vmplayer-field' ).forEach( function ( input ) {
			var field = input.dataset.field;
			if ( 'stream_type' === field ) {
				if ( input.checked ) {
					data.stream_type = input.value;
				}
				return;
			}
			if ( 'checkbox' === input.type ) {
				data[ field ] = input.checked;
				return;
			}
			data[ field ] = input.value;
		} );

		// Mounts.
		card.querySelectorAll( '.vmplayer-mount-row' ).forEach( function ( row ) {
			var mount = { label: '', url: '', bitrate: 0, format: 'mp3' };
			row.querySelectorAll( '.vmplayer-mount-field' ).forEach( function ( input ) {
				mount[ input.dataset.field ] = input.value;
			} );
			data.mounts.push( mount );
		} );

		// Social.
		card.querySelectorAll( '.vmplayer-social-input' ).forEach( function ( input ) {
			if ( input.value.trim() ) {
				data.social[ input.dataset.platform ] = input.value.trim();
			}
		} );

		return data;
	}

	function saveStation( index ) {
		var data = collectStationData( index );
		if ( ! data ) {
			return;
		}

		var card = document.querySelector( '.vmplayer-station-card[data-index="' + index + '"]' );
		var saveBtn = card.querySelector( '.vmplayer-station-actions-right .button-primary' );
		var prev = saveBtn.textContent;
		saveBtn.disabled = true;
		saveBtn.textContent = config.strings.saving;

		// The is-new dataset flag is the source of truth for whether
		// this card represents an unsaved draft. An unsaved draft
		// sends no index so the server treats it as an add.
		var isNew = card.dataset.isNew === 'true';
		var payload = { station: data };
		if ( ! isNew ) {
			payload.index = index;
		}

		ajax( 'vmplayer_save_station', payload )
			.then( function ( response ) {
				saveBtn.disabled = false;
				saveBtn.textContent = prev;
				if ( response && response.success ) {
					config.stations = response.data.stations;
					broadcastStationsChanged( config.stations );
					showNotice( response.data.message || config.strings.saved, 'success' );

					// New station: full re-render so DOM indexes match the
					// server. Existing station: just update the title in place
					// to preserve scroll position and expanded state.
					if ( isNew ) {
						renderAllStations();
						updateStationCounter();
					} else {
						card.classList.remove( 'is-dirty' );
						var title = card.querySelector( '.vmplayer-station-title' );
						title.firstChild.textContent = data.name || config.strings.untitledStation;
					}
				} else {
					showNotice( ( response && response.data && response.data.message ) || config.strings.saveFailed, 'error' );
				}
			} )
			.catch( function () {
				saveBtn.disabled = false;
				saveBtn.textContent = prev;
				showNotice( config.strings.saveFailed, 'error' );
			} );
	}

	function deleteStation( index ) {
		// If this is an unsaved draft, just remove the card. No AJAX needed
		// because the server never knew about it in the first place.
		var card = document.querySelector( '.vmplayer-station-card[data-index="' + index + '"]' );
		if ( card && card.dataset.isNew === 'true' ) {
			card.remove();
			if ( 0 === config.stations.length ) {
				renderAllStations();
			}
			updateStationCounter();
			return;
		}

		ajax( 'vmplayer_delete_station', { index: index } )
			.then( function ( response ) {
				if ( response && response.success ) {
					config.stations = response.data.stations;
					config.settings = response.data.settings;
					broadcastStationsChanged( config.stations );
					renderAllStations();
					updateStationCounter();
					showNotice( response.data.message || config.strings.deleted, 'success' );
				} else {
					showNotice( ( response && response.data && response.data.message ) || config.strings.deleteFailed, 'error' );
				}
			} )
			.catch( function () {
				showNotice( config.strings.deleteFailed, 'error' );
			} );
	}

	function bindAddStation() {
		var btn = document.getElementById( 'vmplayer-add-station' );
		if ( ! btn ) {
			return;
		}
		btn.addEventListener( 'click', function () {
			if ( ! canAddStation() ) {
				showNotice( config.strings.stationLimitReached, 'error' );
				return;
			}

			var blankStation = {
				name: '',
				tagline: '',
				description: '',
				logo_url: '',
				stream_url: '',
				stream_type: 'shoutcast',
				genre: '',
				mounts: [],
				social: {}
			};

			var list = document.getElementById( 'vmplayer-stations-list' );
			var emptyState = list.querySelector( '.vmplayer-empty-state' );
			if ( emptyState ) {
				emptyState.remove();
			}

			// Draft cards use a synthetic string index so they cannot
			// collide with any real integer index from the server.
			// The is-new dataset flag drives all downstream behavior:
			// saveStation treats it as an add, deleteStation skips AJAX,
			// bindSortable ignores it in the reorder payload.
			draftCounter++;
			var draftIndex = 'new-' + draftCounter;

			var card = buildStationCard( blankStation, draftIndex );
			card.dataset.isNew = 'true';
			card.classList.remove( 'is-collapsed' );
			card.classList.add( 'is-dirty' );
			list.appendChild( card );

			card.scrollIntoView( { behavior: 'smooth', block: 'center' } );
			updateStationCounter();
		} );
	}

	/**
	 * Whether the user can still add another station preset.
	 * Factors in both saved stations and any open drafts.
	 */
	function canAddStation() {
		var drafts = document.querySelectorAll( '.vmplayer-station-card[data-is-new="true"]' ).length;
		return ( config.stations.length + drafts ) < config.stationLimit;
	}

	function updateStationCounter() {
		var counter = document.querySelector( '.vmplayer-stations-count' );
		if ( counter ) {
			counter.textContent = config.stations.length + ' of ' + config.stationLimit + ' station presets configured.';
		}
		var addBtn = document.getElementById( 'vmplayer-add-station' );
		if ( addBtn ) {
			// Disable the button once saved stations plus open drafts
			// reach the cap, so the user cannot start a draft they
			// would not be allowed to save.
			addBtn.disabled = ! canAddStation();
		}
	}

	function bindSortable() {
		$( '#vmplayer-stations-list' ).sortable( {
			handle: '.vmplayer-drag-handle',
			items: '.vmplayer-station-card',
			placeholder: 'vmplayer-station-card ui-sortable-placeholder',
			forcePlaceholderSize: true,
			update: function () {
				// Draft cards (unsaved) carry a synthetic string index and
				// are not tracked on the server, so they must be excluded
				// from the reorder payload. Saved cards keep their relative
				// order regardless of where any drafts sit in the DOM.
				var order = [];
				$( '#vmplayer-stations-list .vmplayer-station-card' ).each( function () {
					if ( 'true' === this.dataset.isNew ) {
						return;
					}
					var idx = parseInt( this.dataset.index, 10 );
					if ( ! isNaN( idx ) ) {
						order.push( idx );
					}
				} );

				// Nothing to reorder if payload is empty or the length
				// does not match what the server has.
				if ( 0 === order.length || order.length !== config.stations.length ) {
					return;
				}

				ajax( 'vmplayer_reorder_stations', { order: order } )
					.then( function ( response ) {
						if ( response && response.success ) {
							config.stations = response.data.stations;
							broadcastStationsChanged( config.stations );
							renderAllStations();
							showNotice( response.data.message, 'success' );
						}
					} );
			}
		} );
	}

	/* ==================================================================
	 * Module: Test stream button
	 * =============================================================== */

	function testStream( index, resultSpan, btn ) {
		var card = document.querySelector( '.vmplayer-station-card[data-index="' + index + '"]' );
		var urlInput = card.querySelector( '[data-field="stream_url"]' );
		var typeInput = card.querySelector( '[data-field="stream_type"]:checked' );

		var url = urlInput ? urlInput.value.trim() : '';
		var type = typeInput ? typeInput.value : '';

		if ( ! url ) {
			resultSpan.className = 'vmplayer-test-result is-error';
			resultSpan.textContent = 'Enter a stream URL first.';
			return;
		}

		var prev = btn.textContent;
		btn.disabled = true;
		btn.textContent = config.strings.testing;
		resultSpan.className = 'vmplayer-test-result is-testing';
		resultSpan.textContent = config.strings.testing;

		ajax( 'vmplayer_test_stream', { url: url, stream_type: type } )
			.then( function ( response ) {
				btn.disabled = false;
				btn.textContent = prev;
				if ( response && response.success && response.data.reachable ) {
					resultSpan.className = 'vmplayer-test-result is-success';
					resultSpan.textContent = response.data.message + ' (' + response.data.elapsed_ms + 'ms)';
				} else {
					resultSpan.className = 'vmplayer-test-result is-error';
					resultSpan.textContent = ( response && response.data && response.data.message ) || config.strings.testFailure;
				}
			} )
			.catch( function () {
				btn.disabled = false;
				btn.textContent = prev;
				resultSpan.className = 'vmplayer-test-result is-error';
				resultSpan.textContent = config.strings.testFailure;
			} );
	}

	/* ==================================================================
	 * Init on DOM ready
	 * =============================================================== */

	document.addEventListener( 'DOMContentLoaded', function () {
		initGeneralForm();
		initStationsTab();
	} );

} )( jQuery );
