/**
 * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

import type { Node, Parent } from 'unist'
import type { Router } from 'vue-router'

import { getBaseUrl, getRootUrl } from '@nextcloud/router'
import { u } from 'unist-builder'
import { SKIP, visitParents } from 'unist-util-visit-parents'
import NcRichTextExternalLink from './NcRichTextExternalLink.vue'
import { logger } from '../../utils/logger.ts'
import { URL_PATTERN_AUTOLINK } from './helpers.js'

/**
 * Remark plugin to autolink URLs in text nodes.
 *
 * @param options - Options for the plugin
 * @param options.autolink - Whether to enable autolinking of URLs
 * @param options.useMarkdown - Whether to use markdown parsing
 * @param options.useExtendedMarkdown - Whether to use extended markdown parsing
 */
export function remarkAutolink({ autolink, useMarkdown, useExtendedMarkdown }) {
	return function(tree) {
		// remark-gfm has its own autolink parser which can not be disabled
		// and thus a local one is not needed
		if (useExtendedMarkdown || !useMarkdown || !autolink) {
			return
		}

		visitParents(tree, (node) => node.type === 'text', (node, ancestors: Parent[]) => {
			// Do not autolink text already inside a link node
			if (ancestors.some((ancestor) => ancestor.type === 'link' || ancestor.type === 'linkReference')) {
				return
			}

			const parent = ancestors.at(-1)
			const index = parent!.children.indexOf(node) ?? 0

			const parsed = parseUrl(node.value)
			const parsedNodes: Node[] = (typeof parsed === 'string')
				? [u('text', parsed)]
				: parsed.map((n) => {
						if (typeof n === 'string') {
							return u('text', n)
						}

						return u('link', {
							url: n.props.href,
						}, [u('text', n.props.href)])
					})
						.filter((x) => x)
						.flat()

			parent!.children.splice(index, 1, ...parsedNodes)
			return [SKIP, index + parsedNodes.length]
		})
	}
}

/**
 * Parses a text and returns either an array of text nodes and link nodes or the original text if no links were found.
 *
 * @param text - The text to parse
 */
export function parseUrl(text: string) {
	let match = URL_PATTERN_AUTOLINK.exec(text)
	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	const list: (string | Record<string, any>)[] = []
	let start = 0
	while (match !== null) {
		let href = match[2]!
		let textAfter
		let textBefore = text.substring(start, match.index + match[1]!.length)
		if (href[0] === ' ') {
			textBefore += href[0]
			href = href.substring(1).trim()
		}
		const lastChar = href[(href.length - 1)]
		if (lastChar === '.' || lastChar === ',' || lastChar === ';' || (match[0][0] === '(' && lastChar === ')')) {
			href = href.substring(0, href.length - 1)
			textAfter = lastChar
		}
		list.push(textBefore)
		list.push({ component: NcRichTextExternalLink, props: { href: href.trim(), decorateExternal: true } })
		if (textAfter) {
			list.push(textAfter)
		}
		start = match.index + match[0].length
		match = URL_PATTERN_AUTOLINK.exec(text)
	}
	list.push(text.substring(start))
	const joinedText = list.map((item) => typeof item === 'string' ? item : item.props.href).join('')
	if (text === joinedText) {
		return list
	}
	logger.error('[NcRichText] Failed to reassemble the chunked text: ' + text)
	return text
}

/**
 * Try to get path for router link from an absolute or relative URL.
 *
 * @param router - VueRouter instance of the router link
 * @param url - absolute URL to parse
 * @return a path that can be used in the router link or null if this URL doesn't match this router config
 * @example http://cloud.ltd/nextcloud/index.php/apps/files/favorites?fileid=2#fragment => /files/favorites?fileid=2#fragment
 */
export function getRoute(router: Router | null, url: string): string | null {
	/**
	 * http://cloud.ltd /nextcloud /index.php /apps/files /favorites?fileid=2#fragment
	 * |_____origin____|___________router-base____________|                           |
	 * |__________base____________|          |______________relative-url______________|
	 * |               |___root___|_optional_|__app-base__|_________router-path_______|
	 */

	const removePrefix = (str, prefix) => str.startsWith(prefix) ? str.slice(prefix.length) : str
	const removePrefixes = (str, ...prefixes) => prefixes.reduce((acc, prefix) => removePrefix(acc, prefix), str)

	// Router is not defined in the app => not an app route
	if (!router) {
		return null
	}

	const isAbsoluteURL = /^https?:\/\//.test(url)

	// URL is a "mailto:", "tel:" or any custom app protocol link
	const isNonHttpLink = /^[a-z][a-z0-9+.-]*:.+/.test(url)
	if (!isAbsoluteURL && isNonHttpLink) {
		return null
	}

	// URL is not a link to this Nextcloud server instance => not an app route
	if (isAbsoluteURL && !url.startsWith(getBaseUrl())) {
		return null
	}

	if (!isAbsoluteURL && !url.startsWith('/')) {
		// Relative URL without a leading slash are not allowed
		// VueRouter handles such urls according to the URL RFC,
		// for example, being on /call/abc page, link "def" is resolved as "/call/def" relative to "/call/".
		// We don't want to support such links,
		// so relative URL MUST start with a slash.
		return null
	}

	// URL relative to the instance root
	const relativeUrl = isAbsoluteURL ? removePrefixes(url, getBaseUrl(), '/index.php') : url

	// Router base relative to the instance root (app-base above)
	const relativeRouterBase = removePrefixes(router.options.history.base, getRootUrl(), '/index.php')

	// Root route may have an empty '' path, fallback to '/'
	const potentialRouterPath = removePrefixes(relativeUrl, relativeRouterBase) || '/'

	// Check if there is actually matching route in the router for this path
	const route = router.resolve(potentialRouterPath)

	if (!route.matched.length) {
		return null
	}

	return route.fullPath
}
