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

'use strict';

const upath = require( 'upath' );
const decodeHtmlEntities = require( '../../../utils/decode-html-entities' );
const dropUrlProtocol = require( '../../../utils/drop-url-protocol' );
const concatUrlParts = require( '../../../utils/concat-url-parts' );
const relative_url = require( 'hexo/dist/plugins/helper/relative_url' ); // eslint-disable-line camelcase
const dropInitSlash = require( '../../../utils/drop-init-slash' );

/**
 * Adds breadcrumbs data to the page by finding links to parent categories.
 *
 * Each category typically has an index.md file which contains an "Overview" page.
 * These overview pages are treated as index pages for their respective categories.
 *
 * The filter traverses up the category hierarchy and builds breadcrumbs from:
 * - Parent category overview pages (using the "Overview" pages as targets)
 * - Group index pages for top-level categories
 * - The current page being rendered
 */
hexo.extend.filter.register( 'before_post_render', page => {
	if ( page.projectTheme !== 'gloria' ) {
		return;
	}

	const [ projectName ] = page.path.split( '/' );
	const projectGlobals = hexo.projectGlobals?.[ projectName ];

	if ( !projectGlobals || !page.category ) {
		page.breadcrumbs = {
			items: [],
			jsonLd: null
		};

		return page;
	}

	const { categoryIds, config } = projectGlobals;

	const basePath = hexo.projectGlobals[ page.projectName ].BASE_PATH;
	const pages = hexo.model( 'Page' ).toArray();

	// Get all categories and groups from the project globals.
	const categoriesBreadcrumbs = getAllParentCategories( categoryIds, page.category )
		.map( ( categoryOrGroup, index, array ) => {
			const { category, groupId } = categoryOrGroup;

			if ( category === null ) {
				const extendedGroup = projectGlobals.groups.find( g => g.id === groupId );
				const path = (
					extendedGroup?._url ?
						upath.join( '/', basePath, extendedGroup.slug, 'index.html' ) :
						null
				);

				return {
					path,
					name: extendedGroup?.name,
					active: false
				};
			}

			// It turns out that `page.path` is not always the correct one.
			// Umberto might generate incorrect page path when the page file
			// is placed within directory of another category but is mapped
			// to the different category in `umberto.json`.
			const path = ( () => {
				const overviewPage = pages.find(
					nestedPage =>
						nestedPage.category === category.id &&
						isOverviewPage( nestedPage )
				);

				if ( !overviewPage?.path ) {
					return null;
				}

				const filename = upath.basename( overviewPage.path );
				const slugs = array
					.slice( 0, index + 1 )
					.map( item => item.category?.slug ?? item.groupSlug )
					.filter( Boolean );

				return upath.join( '/', basePath, ...slugs, filename );
			} )();

			return {
				path,
				name: category.name,
				active: false
			};
		} )
		.filter( page => page?.name?.length > 0 );

	// Prepend home and current page. Make sure that we have absolute URLs.
	const breadcrumbs = [
		{
			path: upath.join( '/', basePath, 'index.html' ),
			name: 'Home',
			active: false
		},
		...categoriesBreadcrumbs,
		{
			path: upath.join( '/', page.path ),
			name: getPageTitle( page ),
			active: true
		}
	]
		.map( item => {
			const newLatestPath = item.path?.replace( config.version, 'latest' );

			if ( !newLatestPath || !page.canonicalUrlBeginning ) {
				return item;
			}

			const url = concatUrlParts( `https://${ dropUrlProtocol( page.canonicalUrlBeginning ) }`, newLatestPath );
			const relativePath = relative_url( page.path, dropInitSlash( item.path ) );

			return {
				...item,
				path: relativePath,
				url
			};
		} )
		.reduce( ( result, currentItem, index, array ) => {
			if ( index &&
					array[ index - 1 ].url === currentItem.url &&
					( isOverviewPage( currentItem.name ) || isIndexPage( currentItem ) ) ) {
				array[ index - 1 ].active = array[ index ].active;
				return result;
			}

			result.push( currentItem );
			return result;
		}, [] );

	page.breadcrumbs = {
		items: breadcrumbs,
		jsonLd: generateBreadcrumbsJsonLd( breadcrumbs )
	};

	return page;
}, 10 );

/**
 * Generates schema.org JSON-LD structured data for breadcrumbs.
 *
 * @param breadcrumbs Array of breadcrumb items
 * @returns JSON-LD object for breadcrumbs
 */
function generateBreadcrumbsJsonLd( breadcrumbs ) {
	const validItems = breadcrumbs.filter( item => item.url );

	if ( !validItems.length ) {
		return null;
	}

	return {
		'@context': 'https://schema.org',
		'@type': 'BreadcrumbList',
		'itemListElement': validItems.map( ( item, index ) => ( {
			'@type': 'ListItem',
			'position': index + 1,
			'name': item.name,
			'item': item.url
		} ) )
	};
}

/**
 * Check if the page is an index page.
 * An index page is typically the overview page for a category.
 */
function isIndexPage( page ) {
	return page.path?.endsWith( 'index.html' );
}

/**
 * Check if the page is an overview page.
 */
function isOverviewPage( pageOrTitle ) {
	if ( typeof pageOrTitle !== 'string' ) {
		pageOrTitle = getPageTitle( pageOrTitle );
	}

	return pageOrTitle.trim().toLowerCase().includes( 'overview' );
}

/**
 * Get the page title.
 */
function getPageTitle( page ) {
	return page[ 'menu-title' ] || tryExtractTitleFromMarkdown( page.content ) || page.title;
}

/**
 * Try to extract the title from the markdown content.
 * Searches for the first level 1 header (# Title) anywhere in the content.
 */
function tryExtractTitleFromMarkdown( content ) {
	if ( !content ) {
		return null;
	}

	// Look for the first # header anywhere in the content
	const lines = content.split( '\n' );

	for ( const line of lines ) {
		const headerMatch = line.match( /^\s*#\s+(.+)$/ );

		if ( headerMatch ) {
			const rawTitle = headerMatch[ 1 ].trim();

			// Decode HTML entities in the title
			return decodeHtmlEntities( rawTitle );
		}
	}

	return null;
}

/**
 * Get all parent categories of a given category (including the category itself).
 * Max 20 iterations to avoid infinite loops due to clunky data.
 *
 * @returns Array of category objects sorted from the deepest (child) to the top-level (parent).
 *
 * @example
 * ```js
 * [
 *	// ...
 *	{
 *		categoryId: 'framework-deep-dive',
 *		parentCategoryId: 'framework',
 *		groupId: 'framework',
 *		groupSlug: 'framework',
 *		path: 'ckeditor5/45.0.0/framework/deep-dive'
 *	},
 *	{
 *		categoryId: null,
 *		parentCategoryId: null,
 *		groupId: 'framework',
 *		groupSlug: 'framework',
 *		path: 'ckeditor5/45.0.0/framework'
 *	}
 * ]
 * ```
 */
function getAllParentCategories( allCategoriesMap, category ) {
	const buildPath = ( categoryId, path = [] ) => {
		if ( !categoryId || path.length >= 20 ) {
			return path;
		}

		const category = allCategoriesMap.get( categoryId );
		if ( !category ) {
			return path;
		}

		return buildPath( category.parentCategoryId, [ category, ...path ] );
	};

	return buildPath( category );
}
