/**
 * Custom Address Provider Client Implementation
 */
const addressProvider = {

	id: 'posten-bring-checkout-address-provider',

	canSearch: function ( country ) {
		const supportedCountries = [ 'NO' ]
		return supportedCountries.includes( country )
	},

	/**
	 * Search for address suggestions
	 *
	 * @param {string} query - The search query entered by the user
	 * @param {string} country - The selected country code
	 * @param {string} type - Address type ('billing' or 'shipping')
	 * @return {Promise<Array>} Array of suggestion objects
	 */
	search: async function ( query, country, type ) {
		return await fetchSuggestionsDebounced(query, country)
	},

	/**
	 * Get full address details for a selected suggestion
	 *
	 * @param {string} addressId - The ID of the selected suggestion
	 * @return {Promise<Object>} Address details object
	 */
	select: async function ( addressId ) {

		return fetch(PostenBringCheckoutAddressProvider.apiUrl + `address-details?addressId=${addressId}`, {
			method: 'GET',
			headers: {
				'X-WP-Nonce': PostenBringCheckoutAddressProvider.nonce,
				'Accept': 'application/json'
			}
		}).then(async response => {
			if (!response.ok) {
				return null
			}
			const address = await response.json()

			if (!address) {
				return null
			}

			return {
				address_1: address.address,
				city: address.city,
				state: '',
				postcode: address.postalCode,
				country: address.countryCode,
			}
		}).catch(error => {
			console.error(error)
			return null
		})
	},
}

const fetchSuggestions = async (query, country, { signal }) => {
	return await fetch(PostenBringCheckoutAddressProvider.apiUrl + `address-search?query=${encodeURIComponent(query)}&countryCode=${encodeURIComponent(country)}`, {
		method: 'GET',
		headers: {
			'X-WP-Nonce': PostenBringCheckoutAddressProvider.nonce,
			'Accept': 'application/json'
		},
		signal: signal
	}).then(async response => {
		if (!response.ok) {
			return []
		}
		const address = await response.json()

		const highlightMatches = buildMatchHighLighter(query)
		return address.suggestions.map(it => {
			const label = it.address ? `${it.address}, ${it.postalCode} ${it.city}` : `${it.postalCode} ${it.city}`
			return {
				id: it.id,
				label: label,
				matchedSubstrings: highlightMatches(label)
			}
		})
	})
}

const debouncedFetch = (func, delay = 180) => {
	let timeout = null
	let pending = null
	let abortController = null
	let latestRunId = 0

	return function (...args) {
		if (timeout) {
			clearTimeout(timeout)
		}

		if (pending) {
			pending.resolve(undefined)
			pending = null
		}

		if (abortController) {
			abortController.abort()
			abortController = null
		}

		return new Promise((resolve, reject) => {
			pending = { resolve, reject }
			const runId = ++latestRunId
			timeout = setTimeout(() => {
				timeout = null
				abortController = new AbortController()
				Promise.resolve(func.apply(this, [...args, { signal: abortController.signal }]))
					.then(val => {
						if (runId !== latestRunId) return
						if (pending) {
							pending.resolve(val)
							pending = null
						}
					})
					.catch(err => {
						if (runId !== latestRunId) return
						if (pending) {
							pending.reject(err)
							pending = null
						}
					})
					.finally(() => {
						if (runId === latestRunId) {
							abortController = null
						}
					})
			}, delay)
		})
	}
}

const fetchSuggestionsDebounced = debouncedFetch(fetchSuggestions)

const replaceRegexSymbols = s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")

const stripDiacritics = (s) => s.normalize("NFD").replace(/\p{M}+/gu, "")

const normalizeAndPreserveOriginalIndexOfEachCharacter = (originalAddressLabel) => {
	let normalizedAddressLabel = ""
	const indexMapFromOriginalString = []
	for (let i = 0; i < originalAddressLabel.length; ) {
		const unicodeValue = originalAddressLabel.codePointAt(i)
		const characterAsString = String.fromCodePoint(unicodeValue)
		const normalizedCharacter = stripDiacritics(characterAsString)
		normalizedAddressLabel += normalizedCharacter
		for (let k = 0; k < normalizedCharacter.length; k++) indexMapFromOriginalString.push(i)
		i += characterAsString.length
	}
	return { normalizedAddressLabel, indexMapFromOriginalString }
};

const buildMatchHighLighter = (query) => {
	const tokens = [...new Set(
		query.trim().split(/\s+/).filter(Boolean)
	)].map(stripDiacritics)

	if (tokens.length === 0) {
		return () => []
	}

	tokens.sort((a, b) => b.length - a.length)

	const wordsToMatch = tokens.map(replaceRegexSymbols).join("|")
	const extractIndicesRegex = new RegExp(wordsToMatch, "ugid")

	return function calculateMatchingRanges(addressLabel) {
		const { normalizedAddressLabel, indexMapFromOriginalString } = normalizeAndPreserveOriginalIndexOfEachCharacter(addressLabel)
		const matches = []

		for (const match of normalizedAddressLabel.matchAll(extractIndicesRegex)) {
			const [startIndex, endIndex] = match.indices[0]
			if (startIndex === endIndex) {
				continue
			}

			const start = indexMapFromOriginalString[startIndex]
			const end = indexMapFromOriginalString[endIndex - 1] + 1
			matches.push({ offset: start, length: end - start })
		}

		return mergeRanges(matches)
	};
}

const mergeRanges = (ranges) => {
	ranges.sort((a, b) => a.offset - b.offset)
	const mergedRanges = [ranges[0]]

	for (let i = 1; i < ranges.length; i++) {
		const current = mergedRanges[mergedRanges.length - 1]
		const next = ranges[i]
		const currentEnd = current.offset + current.length
		const nextEnd = next.offset + next.length

		if (next.offset <= currentEnd) {
			current.length = Math.max(currentEnd, nextEnd) - current.offset
		} else {
			mergedRanges.push({ offset: next.offset, length: next.length })
		}
	}

	return mergedRanges
}

if (
	window.wc &&
	window.wc.addressAutocomplete &&
	window.wc.addressAutocomplete.registerAddressAutocompleteProvider
) {
	window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
		addressProvider
	);
}
