/**
 * @license Copyright (c) 2017-2025, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md.
 */

import { BaseComponent } from '../components/base-component';
import { identity } from '../helpers/identity';
import { injectScript } from '../helpers/inject-script';
import { pipe } from '../helpers/pipe';

const DOCSEARCH_JS_URL = 'https://cdn.jsdelivr.net/npm/@docsearch/js@3.9.0/dist/umd/index.min.js';

/**
 * Component responsible for binding the algolia search component to the header.
 */
export class AlgoliaSearch extends BaseComponent {
	/**
	 * Creates a algolia search component.
	 *
	 * @param element - The algolia search DOM element
	 */
	constructor( element ) {
		super();

		this.config = JSON.parse( element.dataset.algoliaSearchConfig );
		this.filters = JSON.parse( element.dataset.algoliaSearchFilters || '{}' );
		this.boost = JSON.parse( element.dataset.algoliaSearchBoost || '{}' );

		this.elements = {
			wrapper: element
		};
	}

	/**
	 * Creates algolia search component.
	 */
	static async attach() {
		const headerElements = [ ...document.querySelectorAll( '.js-algolia-search' ) ];

		await Promise.all(
			headerElements.map( headerElement => new AlgoliaSearch( headerElement ).init() )
		);
	}

	/**
	 * Initializes the algolia search component.
	 */
	async init() {
		await injectScript( DOCSEARCH_JS_URL, {
			attributes: {
				crossorigin: 'anonymous'
			}
		} );

		const { boost, filters } = this;
		const { slug, tags } = filters;

		window.docsearch( {
			container: this.elements.wrapper,
			...this.config,

			// `transformItems` do not support sorting whole set of results at once.
			// It's called individually for each result, or the result group.
			// It's problematic when we want to reorder whole groups of results, e.g. move API below Features.
			transformSearchClient: client => {
				const originalSearch = client.search.bind( client );

				client.search = async ( options, ...rest ) => {
					const { results: [ firstResult ] } = await originalSearch( options, ...rest );
					const mappedResults = pipe(
						filterByTags( { slug, tags } ),
						boostByTags( boost.tags )
					)( firstResult.hits );

					return {
						results: [
							{
								...firstResult,
								hits: mappedResults,
								hitsPerPage: mappedResults.length,
								nbHits: mappedResults.length
							}
						]
					};
				};

				return client;
			}
		} );
	}
}

/**
 * Crates filters for Algolia that allow to filter out results from different projects.
 * Algolia shares the same index for all projects, so we need to filter results
 */
function filterByTags( { slug, tags } ) {
	return hits => hits.filter(
		hit =>
			( !slug || hit.tags.includes( slug ) ) &&
			( !tags || hit.tags.some( tag => tags.includes( tag ) ) )
	);
}

/**
 * Boosts Algolia search results by tags. The `other` will be placed at the end of the results.
 */
function boostByTags( tags ) {
	if ( !tags?.length ) {
		return identity;
	}

	return pipe(
		groupByTag,
		sortObjectKeysByIndices( tags ),
		Object.values,
		array => array.flat()
	);
}

/**
 * Groups Algolia search results by tags.
 */
function groupByTag( hits ) {
	return hits.reduce( ( acc, hit ) => {
		if ( hit.tags.length === 0 ) {
			( acc.other ??= [] ).push( hit );
		} else {
			hit.tags.forEach( tag => {
				( acc[ tag ] ??= [] ).push( hit );
			} );
		}

		return acc;
	}, Object.create( {} ) );
}

/**
 * Sorts object keys by the order defined in the `keysOrder` array.
 * If a key is not present in `keysOrder`, it will be placed at the end.
 */
function sortObjectKeysByIndices( keysOrder ) {
	return obj => {
		const sortedKeys = Object.keys( obj ).sort( ( a, b ) => {
			const indexA = keysOrder.indexOf( a );
			const indexB = keysOrder.indexOf( b );

			if ( indexA === -1 ) {
				return 1;
			}

			if ( indexB === -1 ) {
				return -1;
			}

			if ( indexA === -1 && indexB === -1 ) {
				return -2;
			}

			return indexA - indexB;
		} );

		return sortedKeys.reduce( ( acc, key ) => {
			acc[ key ] = obj[ key ];
			return acc;
		}, Object.create( {} ) );
	};
}
