/**
 * KooKoa Folders — pure utility functions (framework-free, side-effect-free).
 *
 * These helpers carry the folder-ordering and id-parsing logic that the rest
 * of the admin UI depends on. They are deliberately kept free of jQuery, the
 * DOM and any plugin state so they can be unit-tested in isolation (Vitest)
 * and reused across modules.
 *
 * UMD wrapper: in the browser the module attaches itself to
 * window.KookoaWPFUtils; under Node/CommonJS (the test runner) it exports the
 * same object. No build step is required for either consumer.
 *
 * @package KookoaWPF
 */
( function ( root, factory ) {
	'use strict';
	if ( typeof module === 'object' && module.exports ) {
		module.exports = factory();
	} else {
		root.KookoaWPFUtils = factory();
	}
}( typeof self !== 'undefined' ? self : this, function () {
	'use strict';

	/**
	 * Strip HTML tags from a string and trim surrounding whitespace.
	 *
	 * Folder node text can carry badge markup (pin/lock icons); the raw name
	 * is what natural-sort compares. Mirrors the inline cleanup the tree used
	 * before this logic was centralised.
	 *
	 * @param {*} text Arbitrary value; coerced to string.
	 * @return {string} Tag-free, trimmed text ('' for null/undefined).
	 */
	function stripTagsTrim( text ) {
		if ( text === null || typeof text === 'undefined' ) {
			return '';
		}
		return String( text ).replace( /<[^>]*>/g, '' ).trim();
	}

	/**
	 * Resolve a jsTree folder node id to its numeric WordPress term id.
	 *
	 * Folder nodes look like "folder_42". The pseudo-nodes "all" and
	 * "uncategorized" are NOT folders and must resolve to null — never to NaN.
	 * (parseInt('uncategorized', 10) is NaN, and NaN !== NaN, which historically
	 * leaked the Uncategorized view into folder-only code paths.)
	 *
	 * @param {string|number} id Node id ('folder_42', 42, 'uncategorized', …).
	 * @return {number|null} Positive term id, or null when the id is not a folder.
	 */
	function parseFolderId( id ) {
		if ( typeof id === 'number' ) {
			return Number.isInteger( id ) && id > 0 ? id : null;
		}
		if ( typeof id !== 'string' || id === '' ) {
			return null;
		}
		const raw = id.indexOf( 'folder_' ) === 0 ? id.slice( 7 ) : id;
		if ( ! /^\d+$/.test( raw ) ) {
			return null;
		}
		const num = parseInt( raw, 10 );
		return num > 0 ? num : null;
	}

	/**
	 * Comparator for two folder tree nodes, mirroring the server-side order.
	 *
	 * Ordering rules, in priority order:
	 *   1. Pinned folders always sort before non-pinned siblings.
	 *   2. In any auto sort (not 'custom'), smart/dynamic folders sink last.
	 *   3. Then by the chosen field:
	 *        - 'name'  : case-insensitive natural sort (strnatcasecmp parity)
	 *        - 'date'  : numeric term id (proxy for creation order)
	 *        - 'count' : item count
	 *        - 'custom': stored manual order (folder_order), direction-agnostic
	 *   The direction multiplier applies to the field comparison only; the pin
	 *   and dynamic-sink rules are always absolute.
	 *
	 * @param {Object} a       First node ({ id, text, data:{ pinned, is_dynamic, original_text, count, order } }).
	 * @param {Object} b       Second node, same shape.
	 * @param {string} sortBy  'name' | 'date' | 'count' | 'custom'.
	 * @param {string} sortDir 'ASC' | 'DESC'.
	 * @return {number} Negative if a precedes b, positive if a follows b, 0 if equal.
	 */
	function compareFolders( a, b, sortBy, sortDir ) {
		const dirMult = sortDir === 'DESC' ? -1 : 1;

		// 1. Pinned first (absolute, ignores direction).
		const pa = ( a && a.data && a.data.pinned ) ? 1 : 0;
		const pb = ( b && b.data && b.data.pinned ) ? 1 : 0;
		if ( pa !== pb ) {
			return pb - pa;
		}

		// 2. In auto sort modes, dynamic folders sink to the bottom.
		if ( sortBy !== 'custom' ) {
			const da = ( a && a.data && a.data.is_dynamic ) ? 1 : 0;
			const db = ( b && b.data && b.data.is_dynamic ) ? 1 : 0;
			if ( da !== db ) {
				return da - db;
			}
		}

		// 3. Sort by the chosen field.
		switch ( sortBy ) {
			case 'name': {
				const ta = ( a && a.data && a.data.original_text )
					? a.data.original_text
					: ( a ? stripTagsTrim( a.text ) : '' );
				const tb = ( b && b.data && b.data.original_text )
					? b.data.original_text
					: ( b ? stripTagsTrim( b.text ) : '' );
				return dirMult * ta.localeCompare( tb, undefined, { numeric: true, sensitivity: 'base' } );
			}
			case 'date': {
				const ia = ( a ? parseFolderId( a.id ) : null ) || 0;
				const ib = ( b ? parseFolderId( b.id ) : null ) || 0;
				return dirMult * ( ia - ib );
			}
			case 'count': {
				const ca = ( a && a.data && typeof a.data.count !== 'undefined' ) ? parseInt( a.data.count, 10 ) : 0;
				const cb = ( b && b.data && typeof b.data.count !== 'undefined' ) ? parseInt( b.data.count, 10 ) : 0;
				return dirMult * ( ca - cb );
			}
			default: { // 'custom'
				const oa = ( a && a.data && typeof a.data.order !== 'undefined' ) ? parseInt( a.data.order, 10 ) : 0;
				const ob = ( b && b.data && typeof b.data.order !== 'undefined' ) ? parseInt( b.data.order, 10 ) : 0;
				return oa - ob;
			}
		}
	}

	/**
	 * Validate a CSS hex colour string (#RGB or #RRGGBB).
	 *
	 * Defence-in-depth before a colour value reaches an inline style or a
	 * data-attribute in the media-modal dialogs: PHP already sanitises folder
	 * colours with sanitize_hex_color(), so this is belt-and-braces against a
	 * corrupted option or a future caller passing an unvalidated value. Returns
	 * the original string when valid (so callers can use it inline) or null when
	 * it is not a 3- or 6-digit hex colour.
	 *
	 * @param {*} value Candidate colour; anything non-string returns null.
	 * @return {string|null} The validated colour, or null when invalid.
	 */
	function isValidHexColor( value ) {
		if ( typeof value !== 'string' ) {
			return null;
		}
		return /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/.test( value ) ? value : null;
	}

	return {
		stripTagsTrim: stripTagsTrim,
		parseFolderId: parseFolderId,
		compareFolders: compareFolders,
		isValidHexColor: isValidHexColor,
	};
} ) );
