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

'use strict';

const { styleText } = require( 'util' );
const upath = require( 'upath' );
const fs = require( 'fs-extra' );
const { globSync } = require( 'glob' );
const { parseDocument } = require( 'htmlparser2' );

module.exports = ( buildPath, options = {} ) => {
	const pattern = upath.join( buildPath, '**', '*' );
	let pathsToFiles = globSync( pattern ).map( path => upath.normalize( path ) );

	const links = new Set();

	pathsToFiles = pathsToFiles.filter( p => {
		if ( p.includes( '/vendors/' ) ) {
			return false;
		}

		if ( options.skipApi && ( p.includes( '/api/' ) ) ) {
			return false;
		}

		if ( !p.endsWith( '.html' ) ) {
			// Add assets like .png, .xlsx, .docx as valid links.
			const resolvePath = upath.resolve( p );

			if ( fs.statSync( resolvePath ).isFile() ) {
				links.add( upath.resolve( resolvePath ) );
			}

			return false;
		}

		return true;
	} );

	// Obtain every possible hash
	for ( const filePath of pathsToFiles ) {
		links.add( upath.resolve( filePath ) );
		links.add( upath.resolve( filePath + '#' ) );

		const content = fs.readFileSync( filePath, 'utf-8' );
		const ids = ( content.match( /id="[^"]+"/g ) || [] ).map( el => el.replace( /^id="/, '' ).replace( /"$/, '' ) );

		for ( const id of ids ) {
			links.add( upath.resolve( `${ filePath }#${ id }` ) );
		}
	}

	// Check if links are in set of possible hashes.
	for ( const filePath of pathsToFiles ) {
		const invalidHrefs = new Set();
		const content = fs.readFileSync( filePath, 'utf-8' );
		const linkElements = [];

		processNode( parseDocument( content ), linkElements );

		for ( let { href, text, skipValidation } of linkElements ) {
			if (
				href.match( /[a-z:]*\/\// ) ||
				href.match( /mailto:/ ) ||
				skipValidation
			) {
				continue;
			}

			if ( href.endsWith( '/' ) && !href.includes( '#' ) ) {
				href += 'index.html';
			}

			let resolvedPath = getResolvedPath( href, filePath, { publicDir: options.publicDir } );

			if ( options.skipApi && resolvedPath.includes( '/api/' ) ) {
				// Sometimes api url are obtained after resolving entire URL.

				continue;
			}

			if ( resolvedPath.includes( 'latest' ) ) {
				const projectInfo = options.projectsInfo ?
					options.projectsInfo.find( i => resolvedPath.includes( `/${ i.slug }/` ) ) :
					null;
				const projectVersion = projectInfo ? projectInfo.version : 'latest';

				resolvedPath = resolvedPath.replace( 'latest', projectVersion );
			}

			if ( options.projectsInfo ) {
				const isCorrectLocalPath = options.projectsInfo.some( info => {
					return resolvedPath.includes( info.basePath );
				} );

				if ( !isCorrectLocalPath ) {
					continue;
				}
			}

			if ( !links.has( resolvedPath ) ) {
				invalidHrefs.add( { href, text } );
			}
		}

		if ( invalidHrefs.size ) {
			process.exitCode = 1;
			console.log(
				styleText( 'red', 'Error: ' ) +
				`Invalid internal links in ${ styleText( 'magenta', filePath.replace( buildPath, '' ) ) }:`
			);

			for ( const url of invalidHrefs ) {
				console.log( styleText( 'gray', `  * '${ url.href }' - ${ url.text.replace( /\s+/g, ' ' ) }` ) );
			}
		}
	}
};

/**
 * @param {String} href A relative or absolute URL to the page.
 * @param {String} filePath A relative path (to the options.publicDir) to the checked file.
 * @param {Object} options
 * @param {String} options.publicDir An absolute path to the directory where all files are located.
 * @return {string}
 */
function getResolvedPath( href, filePath, options ) {
	if ( href.startsWith( '#' ) ) {
		return upath.resolve( filePath + href );
	}

	if ( href.startsWith( '/' ) ) {
		const commonPoint = href.match( /^\/[^/]+/ )[ 0 ];

		// The `commonPoint` should be removed from the end of the `options.publicDir` string.
		// It may happen that `commonPoint` is present at the middle of that path and removing it produces invalid results.
		// See: #909.
		const regExpCommonPoint = new RegExp( commonPoint + '$' );

		return upath.resolve( options.publicDir.replace( regExpCommonPoint, '' ) + href );
	}

	return upath.resolve( filePath, '..', href );
}

function processNode( node, linkElements ) {
	if ( node.type === 'tag' && node.name === 'a' ) {
		const text = node.children.find( child => child.type === 'text' )?.data;
		const href = node.attribs?.href;
		const skipValidation = node.attribs?.[ 'data-skip-validation' ] !== undefined;

		if ( text && href ) {
			linkElements.push( { href, text, skipValidation } );
		}
	}

	if ( !node.childNodes ) {
		return;
	}

	for ( const childNode of node.childNodes ) {
		processNode( childNode, linkElements );
	}
}
