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

import { throttle } from '../helpers/throttle';

/**
 * Maximum number of preloaded elements to keep in the DOM
 */
const MAX_PRELOADED_ELEMENTS = 20;

/**
 * Link prefetching singleton class.
 * Preloads links when users hover over them for better navigation experience.
 */
export class LinksPrefetch {
	/**
	 * Gets the singleton instance of LinkPrefetch
	 */
	static get the() {
		this.instance = this.instance || new LinksPrefetch();

		return this.instance;
	}

	constructor() {
		if ( this.instance ) {
			return this.instance;
		}

		this.preloadedMap = new Map();
		this.handleMouseover = null;
	}

	/**
	 * Initialize link prefetching on the document
	 * @returns The current instance for chaining
	 */
	init() {
		this.handleMouseover = throttle( event => {
			const link = event.target.closest( 'a' );

			if ( link ) {
				this.preload( link.href );
			}
		}, 50 );

		document.addEventListener( 'mouseover', this.handleMouseover );
		this._preloadMarkedLinks();

		return this;
	}

	/**
	 * Preload a URL if it is valid and not already preloaded
	 *
	 * @param url - The URL to preload
	 */
	preload( url ) {
		if ( !isPreloadable( url ) || this.preloadedMap.has( url ) ) {
			return;
		}

		const element = preloadWithSpeculationRules( url ) || preloadWithLinkPrefetch( url );

		if ( element ) {
			this.preloadedMap.set( url, element );
			this._removeOldestPreloadedElement();
		}
	}

	/**
	 * Cleanup and remove event listeners
	 */
	destroy() {
		document.removeEventListener( 'mouseover', this.handleMouseover );

		this.preloadedMap.forEach( element => {
			if ( element && element.parentNode ) {
				element.parentNode.removeChild( element );
			}
		} );

		this.preloadedMap.clear();
	}

	/**
	 * Preload all links with js-link-preload class
	 */
	_preloadMarkedLinks() {
		const preloadLinks = document.querySelectorAll( 'a.js-link-preload' );

		preloadLinks.forEach( link => {
			if ( link.href ) {
				this.preload( link.href );
			}
		} );
	}

	/**
	 * Removes the oldest preloaded element when we exceed the maximum limit
	 */
	_removeOldestPreloadedElement() {
		if ( this.preloadedMap.size <= MAX_PRELOADED_ELEMENTS ) {
			return;
		}

		const oldestUrl = this.preloadedMap.keys().next().value;
		const element = this.preloadedMap.get( oldestUrl );

		if ( element && element.parentNode ) {
			element.parentNode.removeChild( element );
		}

		this.preloadedMap.delete( oldestUrl );
	}
}

/**
 * Checks if a URL should be preloaded.
 *
 * @param url - The URL to check
 * @returns Whether the URL should be preloaded
 */
function isPreloadable( url ) {
	try {
		const urlObj = new URL( url );
		const currentDomain = window.location.hostname;

		// Must be from the same domain
		if ( urlObj.hostname !== currentDomain ) {
			return false;
		}

		// Must not have UTM parameters
		if ( urlObj.searchParams.toString().toLowerCase().includes( 'utm_' ) ) {
			return false;
		}

		return true;
	} catch ( error ) {
		console.error( 'Invalid URL for preloading:', url, error );
		return false;
	}
}

/**
 * Preload using modern speculationrules API.
 *
 * @param url - The URL to preload
 * @returns The created script element if successful, null otherwise
 */
function preloadWithSpeculationRules( url ) {
	if ( !( 'supports' in document.createElement( 'script' ).style ) ||
			!CSS.supports( 'speculationrules', '*' ) ) {
		return null;
	}

	try {
		const specScript = document.createElement( 'script' );
		specScript.type = 'speculationrules';
		specScript.text = JSON.stringify( {
			prerender: [ {
				source: 'list',
				urls: [ url ]
			} ]
		} );

		document.head.appendChild( specScript );
		return specScript;
	} catch ( error ) {
		console.error( 'Failed to preload with speculationrules:', error );
		return null;
	}
}

/**
 * Fallback preload using link prefetch.
 *
 * @param url - The URL to preload
 * @returns The created link element if successful, null otherwise
 */
function preloadWithLinkPrefetch( url ) {
	try {
		const linkElement = document.createElement( 'link' );
		linkElement.rel = 'prefetch';
		linkElement.href = url;

		document.head.appendChild( linkElement );
		return linkElement;
	} catch ( error ) {
		console.error( 'Failed to preload with link prefetch:', error );
		return null;
	}
}
