import type { ComputedRef, MaybeRefOrGetter } from 'vue'
import { computed, ref, watch, toValue } from 'vue'
/**
 * Clears HTML tags from a string
 * @param html - HTML string to clean
 * @returns Plain text without HTML tags
 */

export function clearHtml(value?: string) {
	if (!value) return ''
	return value
		.replace(/<[^>]*>?/g, '')
		.replace(/&nbsp;/g, ' ')
		.replace(/&quot;/g, '"')
		.replace(/&apos;/g, `'`)
		.replace(/&amp;/g, '&')
		.replace(/&lt;/g, '<')
		.replace(/&gt;/g, '>')
}

/**
 * Normalizes text by removing special characters and converting to lowercase
 * @param text - Text to normalize
 * @returns Normalized text
 */
export function normalizeText(text: string): string {
	const searchChars = /[\p{N}\p{L}\s]/gu
	return text.match(searchChars)?.join('').toLowerCase() ?? ''
}

/**
 * Gets a value from an object by path
 * Supports nested paths like "user.address.street"
 * @param obj - The object to get the value from
 * @param path - The path to the value (e.g., "user.address.street")
 * @returns The value at the path or undefined if not found
 */
function getValueByPath(obj: any, path: string): any {
	const keys = path.split('.')
	let result = obj

	for (const key of keys) {
		if (result === null || result === undefined || typeof result !== 'object') {
			return undefined
		}
		result = result[key]
	}

	return result
}

/**
 * Calculate relevance score based on search terms matching
 * Higher score = more relevant
 * @param stringValue - Text to check
 * @param searchTerms - Array of search terms
 * @returns Relevance score
 */
function calculateRelevance(stringValue: string, searchTerms: string[]): number {
	let score = 0

	if (stringValue.length === 0) return 0

	// Track matched character count for density calculation
	let totalMatchedChars = 0

	// Base score is the number of terms that match
	for (const term of searchTerms) {
		if (stringValue.includes(term)) {
			// Count occurrences for total matched text calculation
			const regex = new RegExp(term, 'g')
			const matches = stringValue.match(regex)
			if (matches) {
				totalMatchedChars += matches.length * term.length
			}

			score += 1

			// Give additional weight to exact matches (not just contains)
			// Example: "jordan" is more relevant for "jordan" than "jordanian history"
			const words = stringValue.split(/\s+/)
			if (words.includes(term)) {
				score += 0.5
			}

			// Give more weight to matches at the beginning of the text
			if (stringValue.startsWith(term)) {
				score += 0.5
			}
		}
	}

	// Calculate the percentage of text that was matched (0 to 1)
	// Cap at 1.0 for cases where the same text is matched multiple times
	const matchDensity = Math.min(1.0, totalMatchedChars / stringValue.length)

	// Weight by match density (multiply by a factor to make it significant)
	// This gives higher relevance to fields where the match covers more of the content
	score *= (1.0 + matchDensity * 2)

	return score
}

export interface SearchItemParams<T> {
	searchTerm?: MaybeRefOrGetter<string>
	items?: MaybeRefOrGetter<T[]>
	keysToSearch?: (keyof T extends string ? keyof T : string)[] // Allow string paths and handle primitive types
	fieldWeights?: Record<string, number> // Use simple string keys for weights
	minChars?: number // Minimum characters required to trigger search
	serverSearch?: (query: string) => Promise<T[]> // Function to perform server-side search
	debounceMs?: number // Debounce time for server requests in milliseconds
}

export interface SearchResult<T> {
	results: ComputedRef<T[]>
	resultCount: ComputedRef<number>
	hasResults: ComputedRef<boolean>
	isSearching: ComputedRef<boolean>
	isLoading: ComputedRef<boolean> // New property to track server-side loading state
}

/**
 * Check if a value is a primitive type (string, number, boolean)
 * @param value - The value to check
 * @returns True if the value is a primitive
 */
function isPrimitive(value: any): boolean {
	return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
}

/**
 * Safely convert any value to a searchable string
 * @param value - Value to convert
 * @returns String representation or empty string if value can't be converted
 */
function toSearchableString(value: any): 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 (typeof value === 'object' && value instanceof Date) return value.toISOString()
	return ''
}

/**
 * Generic search function that searches for a term within specified object properties
 * If keysToSearch is not provided, searches all keys (including nested ones)
 * @param params - Search parameters including searchTerm, items, keys and weights
 * @returns Filtered and sorted array of items that match the search terms
 */
export function searchItems<T>(params: SearchItemParams<T>): T[] {
	const { searchTerm, items = [], keysToSearch, fieldWeights = {}, minChars = 2 } = params

	// Handle both string and ref values
	const searchValue = toValue(searchTerm)
	const itemsArray = toValue(items) || []

	// Return all items if search is empty or doesn't meet minimum character threshold
	if (!searchValue || searchValue.length < minChars) return itemsArray

	// Split search term into individual words for better matching
	const searchTerms = normalizeText(searchValue)
		.split(/\s+/)
		.filter(term => term.length > 1)

	if (searchTerms.length === 0) return itemsArray

	// ---- Field weight configuration ----
	// Default field weights for common fields
	const defaultWeights: Record<string, number> = {
		// Give higher weight to name/title fields
		name: 2,
		first_name: 2,
		last_name: 2,
		title: 2,
		headline: 2,
		// Lower weight for longer text fields
		description: 0.7,
		subtitle: 0.7,
		bio: 0.7,
		content: 0.7,
		body: 0.7,
		email: 0.7,
		phone: 0.7,
		// Even lower for metadata
		id: 0.3,
		created_at: 0.3,
		updated_at: 0.3
	}

	/**
	 * Get the weight for a specific field path
	 * @param path - Path to the field
	 * @returns Appropriate weight for the field
	 */
	function getFieldWeight(path: string): number {
		// First check custom weights
		if (path in fieldWeights) {
			const customWeight = fieldWeights[path]
			if (typeof customWeight === 'number') {
				return customWeight
			}
		}

		// Try with just the leaf key (last part of the path)
		const leafKey = path.split('.').pop() || ''
		if (leafKey in defaultWeights) {
			return defaultWeights[leafKey]
		}

		// Default weight
		return 1
	}

	/**
	 * Recursively collect all keys from an object, including nested ones
	 * @param obj - Object to collect keys from
	 * @param prefix - Prefix for nested keys
	 * @returns Array of flattened key paths
	 */
	function collectAllKeys(obj: any, prefix = ''): string[] {
		if (!obj || typeof obj !== 'object') return []

		return Object.keys(obj).flatMap((key) => {
			const value = obj[key]
			const newPrefix = prefix ? `${prefix}.${key}` : key

			if (value && typeof value === 'object' && !Array.isArray(value)) {
				// Recurse for nested objects
				return [newPrefix, ...collectAllKeys(value, newPrefix)]
			}

			return [newPrefix]
		})
	}

	/**
	 * Calculate relevance score for a single item against search terms
	 * @param item - Item to score
	 * @returns Relevance score
	 */
	function calculateItemRelevance(item: any): number {
		let totalRelevance = 0

		// CASE 1: Handle primitive types directly
		if (isPrimitive(item)) {
			const normalizedValue = normalizeText(String(item))
			return calculateRelevance(normalizedValue, searchTerms)
		}

		// CASE 2: Handle arrays of primitives
		if (Array.isArray(item)) {
			for (const element of item) {
				if (isPrimitive(element)) {
					const normalizedValue = normalizeText(String(element))
					totalRelevance += calculateRelevance(normalizedValue, searchTerms)
				}
			}

			return totalRelevance
		}

		// CASE 3: Handle regular objects
		// Determine which keys to search in
		const keysToProcess = keysToSearch || collectAllKeys(item)

		// Process each field
		for (const keyPath of keysToProcess) {
			const value = getValueByPath(item, String(keyPath))

			// Skip null/undefined values
			if (value === null || value === undefined) continue

			// CASE 3.1: Handle array field values
			if (Array.isArray(value)) {
				for (const element of value) {
					if (isPrimitive(element)) {
						const cleanValue = clearHtml(String(element))
						const normalizedValue = normalizeText(cleanValue)
						const relevance = calculateRelevance(normalizedValue, searchTerms)
						const fieldWeight = getFieldWeight(String(keyPath))

						totalRelevance += relevance * fieldWeight
					}
				}
				continue
			}

			// CASE 3.2: Handle object fields (skip and process their properties separately)
			if (typeof value === 'object' && !isPrimitive(value)) {
				continue
			}

			// CASE 3.3: Handle primitive field values
			const stringValue = toSearchableString(value)
			const normalizedValue = normalizeText(stringValue)
			const baseRelevance = calculateRelevance(normalizedValue, searchTerms)
			const fieldWeight = getFieldWeight(String(keyPath))

			totalRelevance += baseRelevance * fieldWeight
		}

		return totalRelevance
	}

	// Score each item and create [item, score] pairs
	const scoredItems = itemsArray.map((item) => {
		const relevance = calculateItemRelevance(item)
		return [item, relevance] as [T, number]
	})

	// Filter items with non-zero relevance and sort by relevance (descending)
	return scoredItems
		.filter(([, score]) => score > 0)
		.sort(([, scoreA], [, scoreB]) => scoreB - scoreA)
		.map(([item]) => item)
}

/**
 * Vue composable for searching items with reactive results
 * Supports both client-side filtering and server-side search
 * @param params Search parameters
 * @returns Reactive search results and metadata
 */
export function useSearch<T>(
	params: SearchItemParams<T>
): SearchResult<T> {
	const { searchTerm, minChars = 2, serverSearch, debounceMs = 300 } = params

	// For tracking server-side loading state
	const isLoading = ref(false)
	const serverResults = ref<T[]>([])
	let debounceTimeout: number | null = null

	// Watch for changes in the search term to trigger server-side search
	if (serverSearch) {
		watch(
			() => toValue(searchTerm),
			async (newTerm) => {
				// Clear previous timeout if it exists
				if (debounceTimeout !== null) {
					clearTimeout(debounceTimeout)
				}

				// Skip search if term is too short
				if (!newTerm || typeof newTerm !== 'string' || newTerm.length < minChars) {
					serverResults.value = []
					return
				}

				// Set up debounce
				debounceTimeout = window.setTimeout(async () => {
					try {
						isLoading.value = true
						serverResults.value = await serverSearch(newTerm)
					} catch (error) {
						console.error('Server search error:', error)
						serverResults.value = []
					} finally {
						isLoading.value = false
					}
				}, debounceMs)
			}
		)
	}

	// Create a reactive function to get current search term value
	const getSearchTermValue = () => toValue(searchTerm)

	// Function to get filtered results
	const getFilteredResults = (): T[] => {
		const term = getSearchTermValue()

		// If using server-side search and has search term, return server results
		if (serverSearch && term && typeof term === 'string' && term.length >= minChars) {
			return serverResults.value as T[]
		}

		// Otherwise use client-side filtering
		return searchItems(params)
	}
	// Create computed references
	const results = computed(() => getFilteredResults())
	const resultCount = computed(() => results.value.length)
	const hasResults = computed(() => resultCount.value > 0)
	const isSearching = computed(() => {
		const term = getSearchTermValue()
		return !!term && typeof term === 'string' && term.length >= minChars
	})

	return {
		results,
		resultCount,
		hasResults,
		isSearching,
		isLoading: computed(() => isLoading.value),
	}
}
