/**
 * @file assets/js/adminjs/mbs-admin-utils.js
 * Shared admin utilities — HTML escaping, reading-order sortable factory,
 * blob-download helpers, and file-upload AJAX wrapper. Exposed as window.MBS_AdminUtils.
 * Load this script before any admin child module that calls MBS_AdminUtils methods.
 * @package MyBookShowroom
 * @since   1.1.0
 */

( function () {
	'use strict';

	/**
	 * Escape a value for safe insertion as HTML text content.
	 *
	 * Converts the five HTML-sensitive characters to their named entities.
	 * Equivalent to PHP esc_html() for inline string building in JS.
	 *
	 * @param {*} val  Value to escape (coerced to string).
	 * @return {string}
	 */
	function escHtml( val ) {
		return String( val )
			.replace( /&/g,  '&amp;'  )
			.replace( /</g,  '&lt;'   )
			.replace( />/g,  '&gt;'   )
			.replace( /"/g,  '&quot;' )
			.replace( /'/g,  '&#039;' );
	}

	/**
	 * Escape a value for safe insertion into an HTML attribute.
	 *
	 * For attribute contexts the same five-character entity set is sufficient.
	 * Equivalent to PHP esc_attr() for inline string building in JS.
	 *
	 * @param {*} val  Value to escape (coerced to string).
	 * @return {string}
	 */
	function escAttr( val ) {
		return escHtml( val );
	}

	/**
	 * Trigger a browser file download from an in-memory Blob.
	 *
	 * Creates a temporary <a> element, simulates a click, then cleans up.
	 * Centralises the blob-anchor pattern used by all three export handlers
	 * in mbs-admin-csv-export.js (D-007).
	 *
	 * @param {Blob}   blob      Data to download.
	 * @param {string} filename  Suggested filename for the download.
	 */
	function triggerBlobDownload( blob, filename ) {
		var url  = URL.createObjectURL( blob );
		var link = document.createElement( 'a' );
		link.href     = url;
		link.download = filename;
		document.body.appendChild( link );
		link.click();
		document.body.removeChild( link );
		URL.revokeObjectURL( url );
	}

	/**
	 * Toggle an export button between its idle and busy states.
	 *
	 * @param {jQuery}  $btn          The button element.
	 * @param {string}  originalText  The label to restore when not busy.
	 * @param {boolean} busy          true = disable + show "Exporting…"; false = restore.
	 */
	function setExportButtonState( $btn, originalText, busy ) {
		if ( busy ) {
			$btn.prop( 'disabled', true ).text( 'Exporting\u2026' );
		} else {
			$btn.prop( 'disabled', false ).text( originalText );
		}
	}

	/**
	 * Initialize jQuery UI Sortable (or \u25b2/\u25bc fallback) for a reading-order
	 * card list on any admin entity tab.
	 *
	 * All DOM selector fragments are derived from config.entity, which must match
	 * the CSS class/ID prefix used in the entity tab template, e.g.
	 * "series"     => .mbs-series-card, #mbs-series-reading-order-list-{id}, ...
	 * "collection" => .mbs-collection-card, #mbs-collection-reading-order-list-{id}, ...
	 * "universe"   => .mbs-universe-card, #mbs-universe-reading-order-list-{id}, ...
	 *
	 * Replaces the formerly-duplicated sortable machinery in admin-series.js,
	 * admin-collections.js, and admin-universes.js (D-006).
	 *
	 * @param {Object} config
	 * @param {string} config.entity  Entity prefix string.
	 */
	function initReadingOrderSortable( config ) {
		var entity      = config.entity;
		var $           = window.jQuery;
		var initialized = {};

		/**
		 * Attach MBS.SortableList to an entity card's book list.
		 * Delegates drag-and-drop (PRO) and up/down fallback (all tiers) to the
		 * shared factory. No-op if already initialized or list not in DOM.
		 *
		 * @param {number} termId
		 */
		function initSortable( termId ) {
			if ( initialized[ termId ] ) {
				return;
			}
			var listId = '#mbs-' + entity + '-reading-order-list-' + termId;
			if ( ! $( listId ).length ) {
				return;
			}

			MBS.SortableList.create( {
				container:        listId,
				itemSelector:     '> li',
				handleSelector:   '.mbs-drag-handle',
				controlsSelector: '.mbs-reorder-controls',
				upSelector:       '.mbs-reorder-move-up',
				downSelector:     '.mbs-reorder-move-down',
				placeholderClass: 'mbs-book-order-placeholder',
				onReorder: function() { syncOrderInput( termId ); }
			} );

			initialized[ termId ] = true;
		}

		/**
		 * Walk the current DOM order of an entity card's book list and
		 * serialize the IDs as JSON into the hidden input that gets POSTed.
		 *
		 * @param {number} termId
		 */
		function syncOrderInput( termId ) {
			var $list  = $( '#mbs-' + entity + '-reading-order-list-' + termId );
			var $input = $( '#mbs-' + entity + '-reading-order-input-' + termId );
			if ( ! $list.length || ! $input.length ) {
				return;
			}
			var ids = [];
			$list.find( '> li[data-book-id]' ).each( function () {
				var id = parseInt( $( this ).attr( 'data-book-id' ), 10 );
				if ( ! isNaN( id ) && id > 0 ) {
					ids.push( id );
				}
			} );
			$input.val( JSON.stringify( ids ) );
		}

		$( function () {
			$( '.mbs-' + entity + '-card' ).each( function () {
				var el     = this;
				var $el    = $( el );
				var termId = parseInt( $el.attr( 'data-term-id' ), 10 );
				if ( ! termId ) {
					return;
				}

				// Lazy-init when the admin first opens the card. The 'toggle'
				// event on <details> is a native DOM event and does not bubble
				// -- bind directly on the element, not via delegation.
				el.addEventListener( 'toggle', function () {
					if ( el.open ) {
						initSortable( termId );
					}
				} );

				// If this card auto-expanded (e.g. after a PRG redirect),
				// initialize Sortable immediately on DOM ready.
				if ( el.open ) {
					initSortable( termId );
				}

				// Sync the order input on form submit even when the admin never
				// dragged. This locks in the displayed default order on first save.
				$el.closest( 'form' ).on( 'submit', function () {
					syncOrderInput( termId );
				} );
			} );
		} );
	}


	/**
	 * Send a file to a WordPress admin-ajax endpoint via FormData + jQuery AJAX.
	 *
	 * Centralises the repeated FormData-build + $.ajax pattern used by the
	 * file-upload import handlers (DUP-011, Session 137).
	 *
	 * @param {object}   opts
	 * @param {string}   opts.action    WordPress AJAX action string (e.g. 'mbs_import_mybooktable').
	 * @param {string}   opts.nonce     Security nonce value from data-nonce attribute.
	 * @param {File}     opts.file      The File object to upload.
	 * @param {string}   [opts.url]     AJAX endpoint URL. Defaults to the global ajaxurl.
	 * @param {number}   [opts.timeout] Request timeout in ms. Defaults to 120 000.
	 * @param {object}   [opts.extra]   Additional key/value pairs appended to FormData.
	 * @param {Function} [opts.success] Called with (response) on a successful HTTP response.
	 * @param {Function} [opts.error]   Called with (jqXHR, textStatus) on network/HTTP failure.
	 * @param {Function} [opts.complete] Called with (jqXHR, textStatus) on completion (always).
	 * @return {jqXHR}  The jQuery XHR object returned by $.ajax().
	 */
	function fileUploadAjax( opts ) {
		var ajaxUrl = opts.url || ( typeof ajaxurl !== 'undefined' ? ajaxurl : '' );
		var timeout = opts.timeout || 120000;

		var fd = new FormData();
		fd.append( 'action', opts.action );
		fd.append( 'nonce',  opts.nonce  );
		fd.append( 'file',   opts.file   );

		if ( opts.extra ) {
			Object.keys( opts.extra ).forEach( function ( key ) {
				fd.append( key, opts.extra[ key ] );
			} );
		}

		return jQuery.ajax( {
			url:         ajaxUrl,
			method:      'POST',
			data:        fd,
			processData: false,
			contentType: false,
			timeout:     timeout,
			success:     opts.success  || function () {},
			error:       opts.error    || function () {},
			complete:    opts.complete || function () {},
		} );
	}


	/**
	 * Create and open a wp.media modal frame, applying the PLG-052 local-frame
	 * pattern to prevent the close-before-select TypeError.
	 *
	 * WordPress fires 'close' before 'select' when the user clicks the "Use this"
	 * button. Binding handlers to a local 'frame' variable (not the outer reference)
	 * keeps the frame reachable inside onSelect even after onClose nulls the outer
	 * reference. PLG-052 / BNR-004.
	 *
	 * @param {Object}   config
	 * @param {string}   config.title         Modal title.
	 * @param {string}   config.buttonText    "Use this" button label.
	 * @param {string}   [config.libraryType] 'image' (default) or 'video'.
	 * @param {Function} config.onSelect      Called with the attachment toJSON() object.
	 * @param {Function} [config.onClose]     Called when the frame closes (optional).
	 * @param {Object}   [config.outerRef]    Object with a .frame property to null on
	 *                                        select/close (optional — for single-frame
	 *                                        re-open guards).
	 * @return {wp.media.view.MediaFrame|null} The created (and opened) frame, or null
	 *                                         if wp.media is unavailable.
	 */
	function createMediaFrame( config ) {
		if ( typeof wp === 'undefined' || typeof wp.media !== 'function' ) {
				return null;
		}

		var frame = wp.media( {
			title:    config.title,
			button:   { text: config.buttonText },
			multiple: false,
			library:  { type: config.libraryType || 'image' },
		} );

		frame.on( 'select', function () {
			var attachment = frame.state().get( 'selection' ).first().toJSON();
			if ( config.outerRef ) {
				config.outerRef.frame = null;
			}
			if ( typeof config.onSelect === 'function' ) {
				config.onSelect( attachment );
			}
		} );

		frame.on( 'close', function () {
			if ( config.outerRef ) {
				config.outerRef.frame = null;
			}
			if ( typeof config.onClose === 'function' ) {
				config.onClose();
			}
		} );

		frame.open();
		return frame;
	}

	window.MBS_AdminUtils = {
		escHtml                 : escHtml,
		escAttr                 : escAttr,
		triggerBlobDownload     : triggerBlobDownload,
		setExportButtonState    : setExportButtonState,
		initReadingOrderSortable: initReadingOrderSortable,
		fileUploadAjax          : fileUploadAjax,
		createMediaFrame        : createMediaFrame,
	};

}() );
