/**
 * @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 splitLongname = require( '../../src/helpers/split-longname' );

const LINK_REGEXP = /\\?{(?:@linkapi|@link|@linksdk|@linkexample)\s+[^{]+?({[^}]+})?[^{}]*\\?}/g;

const findTargetDoclet = require( '../../src/api-builder/utils/findtargetdoclet' );
const { hasDedicatedApiPages, LONG_NAME_LABEL_REGEXP } = require( '../../src/api-builder/utils/utils' );
const logCrossProjectReference = require( './logcrossprojectreference' );

/**
 * Converts @link or @linkapi tags into working links to guides or API docs.
 *
 * @param {Object} page
 * @param {Object} hexo
 * @return {Boolean}
 */
function parseLinks( page, hexo ) {
	const isSilentError = hexo.projectGlobals.common._disabledFilters.has( parseLinks.name );
	const relativeUrlHelper = hexo.extend.helper.store.relative_url;

	page.content = page.content.replace( LINK_REGEXP, match => {
		if ( match.startsWith( '\\{' ) ) {
			return match.replace( /^\\{/, '{' ).replace( /\\}$/, '}' );
		}
		if ( match.includes( 'module:' ) || match.includes( '@linkapi' ) ) {
			return linkToApi( match, page, hexo, { relativeUrlHelper, isSilentError } );
		} else if ( match.includes( '@linksdk' ) || match.includes( '@linkexample' ) ) {
			return linkToSdk( match, page, hexo, { relativeUrlHelper, isSilentError } );
		} else {
			return linkToGuide( match, page, hexo, { relativeUrlHelper, isSilentError } );
		}
	} );

	return page;
};

/**
 * @param {String} str
 * @param {Object} data
 * @param {Object} hexo
 * @param {Object} options
 * @param {Function} options.relativeUrlHelper
 * @param {Boolean} options.isSilentError
 */
function linkToApi( str, data, hexo, { relativeUrlHelper, isSilentError }, onlyHref = false ) {
	const linkMatchResult = str.match( /^{((?:@linkapi|@link)[\s\S]+)}$/ );

	if ( !linkMatchResult ) {
		return onError( {
			value: str,
			source: data.source,
			isSilent: isSilentError
		} );
	}

	const linkContentMatchResult = linkMatchResult[ 1 ].match( /^(?:@linkapi|@link)\s*(?:@(\w+))*\s*(\S+)\s*([\s\S]*)$/ );

	if ( !linkContentMatchResult ) {
		return onError( {
			value: str,
			source: data.source,
			isSilent: isSilentError
		} );
	}

	const projectName = linkContentMatchResult[ 1 ];

	// Cross project references are not supported.
	if ( projectName ) {
		return logCrossProjectReference( { expression: str, source: data.source } );
	}

	const { name, label } = splitLongname( linkContentMatchResult[ 2 ] );
	const itemName = linkContentMatchResult[ 2 ].replace( LONG_NAME_LABEL_REGEXP, '' );
	let linkText = linkContentMatchResult[ 3 ] || name;

	// Support link texts with backticks, wrapping them in <code>.
	// TODO - a naive implementation.
	if ( linkText.startsWith( '`' ) && linkText.endsWith( '`' ) ) {
		linkText = linkText.replace( '`', '<code>' ).replace( '`', '</code>' );
	}

	if ( !itemName ) {
		return onError( {
			value: str,
			source: data.source,
			isSilent: isSilentError
		} );
	}

	const baseItemName = getBaseItemName( itemName );
	let hashWithoutPrefix = itemName.replace( baseItemName, '' ).slice( 1 );
	const hash = hashWithoutPrefix ? `#${ hashWithoutPrefix }` : '';
	const project = hexo.projectGlobals[ data.projectName ];

	if ( hashWithoutPrefix.includes( '-' ) ) {
		let [ type, description ] = hashWithoutPrefix.split( '-' );

		// JSDuck replaces `$` with `s-`.
		if ( type === 's' ) {
			description = `s-${ description }`;
		}

		if ( type === 'event' ) {
			hashWithoutPrefix = type;
		} else {
			hashWithoutPrefix = description;
		}
	}

	let apiSlug;

	if ( project && project.config ) {
		apiSlug = getSlug( project.config, 'api-reference' ) || 'api-reference';
	} else if ( isIgnoredProject( data.projectName, hexo ) || hexo.projectGlobals.common.isSingleProject ) {
		return str;
	} else {
		return onError( {
			value: str,
			source: data.source,
			isSilent: isSilentError,
			description: `Project ${ styleText( 'redBright', data.projectName ) } is not defined.`
		} );
	}

	const basePath = data.BASE_PATH;
	const fileName = getFileName( baseItemName );

	let href = relativeUrlHelper( data.path, upath.join( basePath, apiSlug, fileName + hash ) );

	if ( !project || !project.doclets ) {
		return getOutputLink( href, linkText, !!linkContentMatchResult[ 3 ] );
	}

	const targetDoclet = findTargetDoclet( project.doclets, {
		module: itemName,
		structure: baseItemName,
		member: hashWithoutPrefix,
		label
	} );

	if ( !targetDoclet ) {
		return onError( {
			value: str, source: data.source, isSilent: isSilentError, description: 'No doclet found. Link remain not transformed.'
		} );
	}

	if ( hasDedicatedApiPages( targetDoclet ) ) {
		let propertyName = itemName;

		// Properties of `type` do not have own doclets. Instead, they are specified as `properties` in the `type`
		// (parent) doclet. Hence, we must link to it manually. See: #1182.
		if ( targetDoclet.kind === 'typedef' ) {
			propertyName = propertyName.replace( '#', '#member-' );
		}

		href = relativeUrlHelper( data.path, upath.join( basePath, apiSlug, getFileName( propertyName ) ) );
	} else {
		href = [
			relativeUrlHelper( data.path, upath.join( basePath, apiSlug, fileName ) ),
			encodeURI( targetDoclet.extraId )
		].join( '#' );
	}

	if ( !linkContentMatchResult[ 3 ] && targetDoclet.kind === 'event' ) {
		linkText = `event-${ targetDoclet.name }`;
	}

	if ( onlyHref ) {
		return href;
	}

	return getOutputLink( href, linkText, !!linkContentMatchResult[ 3 ] );
}

/**
 * Returns parent doclet for the given item.
 * See #500.
 *
 * @param {String} itemName
 */
function getBaseItemName( itemName ) {
	const lastHashIndex = itemName.lastIndexOf( '#' );
	const lastDotIndex = itemName.lastIndexOf( '.' );
	const lastTildeIndex = itemName.lastIndexOf( '~' );

	if ( lastHashIndex !== -1 ) {
		return itemName.substring( 0, lastHashIndex );
	}

	if ( lastDotIndex !== -1 ) {
		return itemName.substring( 0, lastDotIndex );
	}

	if ( lastTildeIndex !== -1 ) {
		return itemName.substring( 0, lastTildeIndex );
	}

	return itemName;
}

function getFileName( itemName ) {
	const output = itemName.replace( /[/:.]/g, '_' ).replace( '~', '-' );
	const [ fileNameWithHash, extension = 'html' ] = output.split( '.' );
	const [ fileName, hash ] = fileNameWithHash.split( '#' );

	if ( hash ) {
		return `${ fileName }.${ extension }#${ hash }`;
	}

	return `${ fileName }.${ extension }`;
}

function getOutputLink( href, description, isCustomDescription ) {
	if ( isCustomDescription ) {
		return `<a href=${ href }>${ description }</a>`;
	}

	return `<a href=${ href }><code>${ description }</code></a>`;
}

/**
 * @param {String} str
 * @param {Object} data
 * @param {Object} hexo
 * @param {Object} options
 * @param {Function} options.relativeUrlHelper
 * @param {Boolean} options.isSilentError
 */
function linkToSdk( str, data, hexo, { relativeUrlHelper, isSilentError }, onlyHref = false ) {
	const regExp = /(?:@linksdk|@linkexample) (?:@(\w+) )?(\w+)(?:\.html)?(#\S+)? ([^}]+)/;
	const match = regExp.exec( str );

	if ( !match ) {
		return onError( {
			value: str,
			source: data.source,
			isSilent: isSilentError
		} );
	}

	const projectName = match[ 1 ];

	// Cross project references are not supported.
	if ( projectName ) {
		return logCrossProjectReference( { expression: str, source: data.source } );
	}

	const sdkSlug = getSlug( hexo.projectGlobals[ data.projectName ].config, 'sdk' );
	const basePath = data.BASE_PATH;
	const sdkFileName = match[ 2 ];
	const sdkFileHash = match[ 3 ] || '';
	const sdkLinkName = match[ 4 ];
	const link = relativeUrlHelper( data.path, upath.join( basePath, sdkSlug, sdkFileName ) );
	const finalHref = `${ link }.html${ sdkFileHash }`;

	if ( onlyHref ) {
		return finalHref;
	}

	return `<a href="${ finalHref }">${ sdkLinkName }</a>`;
}

/**
 * @param {String} str
 * @param {Object} data
 * @param {Object} hexo
 * @param {Object} options
 * @param {Function} options.relativeUrlHelper
 * @param {Boolean} options.isSilentError
 */
function linkToGuide( str, data, hexo, { relativeUrlHelper, isSilentError }, onlyHref = false ) {
	const rgxp = /@link(?:\s+@(\w+))?\s+([^}\s]+)\s*([^}]*)}/;
	const match = rgxp.exec( str );

	if ( !match ) {
		return onError( {
			value: str,
			source: data.source,
			isSilent: isSilentError
		} );
	}

	const pathToGuideSplit = match[ 2 ].split( '#' );
	const pathToGuide = pathToGuideSplit[ 0 ];
	const hash = pathToGuideSplit.length > 1 ? `#${ pathToGuideSplit[ 1 ] }` : '';
	const linkText = match[ 3 ] ? match[ 3 ] : pathToGuide;
	const projectName = match[ 1 ];

	// Cross project references are not supported.
	if ( projectName ) {
		return logCrossProjectReference( { expression: str, source: data.source } );
	}

	const pagePaths = hexo.projectGlobals[ data.projectName ] ? hexo.projectGlobals[ data.projectName ].pagePaths : null;
	const projectBasePath = data.BASE_PATH;

	let targetPath;

	if ( pathToGuide === 'index' ) {
		targetPath = upath.join( projectBasePath, 'index.html' );
	} else {
		if ( !pagePaths ) {
			return onError( {
				value: str,
				source: data.source,
				isSilent: isSilentError
			} );
		}

		// pathToGuide is guide's physical path in project directory before being processed by hexo.
		// That's why it's found by oldDataPath.
		const pagePathData = pagePaths.find( p => {
			const minusBasePath = p.oldDataPath.replace( projectBasePath + '/', '' );
			const minusExtension = minusBasePath.substring( 0, minusBasePath.lastIndexOf( '.' ) );

			return minusExtension === pathToGuide;
		} );

		if ( !pagePathData && pathToGuide !== 'index' ) {
			return onError( {
				value: str,
				source: data.source,
				isSilent: isSilentError
			} );
		}

		targetPath = pagePathData.newDataPath;
	}

	const href = relativeUrlHelper( data.path, targetPath );
	const finalHref = `${ href }${ hash }`;

	if ( onlyHref ) {
		return finalHref;
	}

	return `<a href=${ finalHref }>${ linkText }</a>`;
}

/**
 * @param {String} value An expression that could not be transformed into a link.
 * @param {String} source A path to the file where the link was found.
 * @param {Boolean} [isSilent] Whether to report an error when conversion into the link failed.
 * @param {String} [description] Optional message describing the error.
 * @return {String}
 */
function onError( { value, source, isSilent, description } ) {
	if ( !isSilent ) {
		process.exitCode = 1;

		const message = [
			styleText( 'red', 'Error:' ),
			`Failed to convert ${ styleText( 'cyanBright', value ) } tag`,
			source && `in ${ styleText( 'magentaBright', source ) }`,
			description && `because ${ styleText( 'italic', description ) }`
		]
			.filter( Boolean )
			.join( ' ' );

		console.log( message );
	}

	return value;
}

function isIgnoredProject( projectName, hexo ) {
	return hexo.projectGlobals.common.ignoredProjects.includes( projectName );
}

function getSlug( config, name ) {
	if ( config && Array.isArray( config.groups ) ) {
		const group = config.groups.find( g => g.id === name );

		return group.slug;
	}

	return null;
}

module.exports = {
	parseLinks,
	linkToApi,
	linkToSdk,
	linkToGuide
};
