import type { Ref } from 'vue'
import { computed, isRef, shallowReactive } from 'vue'

const htmlReplacers = {
	'': /<[^>]*>?/g,
	' ': /&nbsp;/g,
	'"': /&quot;/g,
	'\'': /&apos;/g,
	'&': /&amp;/g,
	'<': /&lt;/g,
	'>': /&gt;/g,
} as const

function clearHtml(value?: string): string {
	if (!value) { return '' }
	return Object.entries(htmlReplacers).reduce(
		(text, [replacement, regex]) => text.replace(regex, replacement),
		value
	)
}

const normalizeSearchChars = /[\p{N}\p{L}\s]/gu
function normalizeText(text: string): string {
	return text.match(normalizeSearchChars)?.join('').toLowerCase() ?? ''
}

function getValueByPath(obj: any, path: string): any {
	let result = obj
	for (const key of path.split('.')) {
		if (result === null || result === undefined || typeof result !== 'object') { return undefined }
		result = result[key]
	}
	return result
}

function collectAllLeafPaths(obj: any, prefix = ''): string[] {
	if (!obj || typeof obj !== 'object') { return [] }
	return Object.keys(obj).flatMap((key) => {
		const value = obj[key]
		const path = prefix ? `${prefix}.${key}` : key
		if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
			return collectAllLeafPaths(value, path)
		}
		return [path]
	})
}

function toSearchableString(value: unknown): string {
	if (value === null || value === undefined) { return '' }
	if (typeof value === 'string') { return clearHtml(value) }
	if (typeof value === 'number' || typeof value === 'boolean') { return String(value) }
	if (value instanceof Date) { return value.toISOString() }
	return ''
}

interface FilterRefOptions<Item extends Record<string, any>> {
	filter?: Ref<(item: Item) => boolean> | ((item: Item) => boolean)
	/** Dot-path keys to index (e.g. 'user.address.city'). Defaults to all leaf fields. */
	keys?: string[]
	minChars?: number
}

export function filterRef<T extends Record<string, any>[]>(
	initialValue: T = [] as unknown as T,
	options: FilterRefOptions<T[number]> = {}
) {
	type Item = T[number]
	const { minChars = 1 } = options
	const state = shallowReactive({ _list: initialValue as Item[], searchTerm: '' })

	let searchIndex: string[] = []

	function buildIndexEntry(item: Item, seen = new WeakSet()): string {
		if (seen.has(item as object)) { return '' }
		seen.add(item as object)

		const paths = options.keys ?? collectAllLeafPaths(item)
		const parts: string[] = []
		for (const path of paths) {
			const value = getValueByPath(item, path)
			if (Array.isArray(value)) {
				for (const el of value) {
					if (el && typeof el === 'object' && !(el instanceof Date)) {
						parts.push(buildIndexEntry(el as Item, seen))
					} else {
						const s = toSearchableString(el)
						if (s) { parts.push(normalizeText(s)) }
					}
				}
			} else {
				const s = toSearchableString(value)
				if (s) { parts.push(normalizeText(s)) }
			}
		}
		return parts.join(' ')
	}

	function buildIndex(list: Item[]) {
		searchIndex = list.map(item => buildIndexEntry(item))
	}

	buildIndex(initialValue as Item[])

	const filtered = computed(() => {
		let result = state._list

		if (state.searchTerm.length >= minChars) {
			const terms = normalizeText(state.searchTerm).split(/\s+/).filter(t => t.length > 0)
			if (terms.length > 0) {
				result = result.filter((_, i) => {
					const entry = searchIndex[i] ?? ''
					return terms.every(term => entry.includes(term))
				})
			}
		}

		if (options.filter) {
			const fn = isRef(options.filter) ? options.filter.value : options.filter
			result = result.filter(fn)
		}

		return result
	})

	return {
		get searchTerm() { return state.searchTerm },
		set searchTerm(v: string) { state.searchTerm = v },
		get value() { return filtered.value },
		set value(v: Item[]) { state._list = v; buildIndex(v) },
		get length() { return filtered.value.length },
		[Symbol.iterator](): IterableIterator<Item> { return filtered.value[Symbol.iterator]() },
	}
}
