<script lang="ts" setup>
import type { IconType, Option } from '@bagelink/vue'
import type { AlignedPlacement } from '../../Dropdown.vue'
import { Btn, Card, Skeleton, Dropdown, Icon, TextInput, useSearch, useI18n, resolveI18n } from '@bagelink/vue'
import { computed, onMounted, ref, watch } from 'vue'
import { getOptionIcon, getOptionLabel, getOptionValue } from '../../../utils/options'
import 'floating-vue/style.css'

const props = withDefaults(defineProps<PropTypes>(), {
	placement: 'bottom-start',
})

const emit = defineEmits(['update:modelValue'])

const { $t } = useI18n()

type OptionsSource = Option[] | ((query: string) => Promise<Option[]>)
const isAsyncSource = (src: OptionsSource): src is (q: string) => Promise<Option[]> => typeof src === 'function'

type Primitive = string | number | boolean

interface PropTypes {
	options: OptionsSource
	placeholder?: string
	disabled?: boolean
	modelValue?: Option
	searchable?: boolean
	required?: boolean
	label?: string
	fullWidth?: boolean
	multiselect?: boolean
	icon?: IconType
	hideLabel?: boolean
	defaultValue?: Option
	helpText?: string
	clearable?: boolean
	placement?: AlignedPlacement
	searchPlaceholder?: string
	error?: string
	underlined?: boolean
}

const searchInput = ref<HTMLElement | undefined>()
const triggerBtn = ref<HTMLButtonElement | undefined>()

const selectedItems = ref<Option[]>([])

const getLabel = (option: Option) => getOptionLabel(option)
const getValue = (option?: Option): Primitive | undefined => getOptionValue(option) as Primitive | undefined

const selectedItemCount = computed(() => selectedItems.value.length ?? 0)
const searchTerm = ref<string>('')

const dropdown = ref<InstanceType<typeof Dropdown> | undefined>()
const selected = ref(false)
const open = ref(false)
const selectPlaceholder = computed(() => resolveI18n(props.placeholder) ?? $t('select.placeholder'))

const selectedLabel = computed((): string => {
	if (selectedItemCount.value === 0) { return selectPlaceholder.value }
	if (selectedItemCount.value > 4) {
		const str = selectedItems.value
			.slice(0, 4)
			.map(item => getLabel(item))
			.join(', ')
		return `${str}... +${selectedItemCount.value - 4}`
	}
	return selectedItems.value.map(item => getLabel(item)).join(', ')
})

const searchPlaceholder = computed(() => props.searchPlaceholder ?? selectedLabel.value ?? $t('select.search'))
const { results, isLoading } = useSearch<Option>({
	searchTerm: () => searchTerm.value,
	serverSearch: isAsyncSource(props.options) ? props.options : undefined,
	items: () => (Array.isArray(props.options) ? props.options : []),
	minChars: isAsyncSource(props.options) ? 0 : 2,
})

const highlightedIndex = ref(-1)

const selectOptions = ref<HTMLElement | undefined>()

function navigate(direction: 'up' | 'down') {
	if (!open.value) {
		open.value = true
		setTimeout(() => { navigate(direction) }, 210)
		return
	}
	if (direction === 'up') {
		highlightedIndex.value = highlightedIndex.value > 0 ? highlightedIndex.value - 1 : results.value.length - 1
	} else if (direction === 'down') {
		highlightedIndex.value = highlightedIndex.value < results.value.length - 1 ? highlightedIndex.value + 1 : 0
	}
	setTimeout(() => {
		const el = selectOptions.value?.children[highlightedIndex.value] as HTMLElement
		if (el) { el.focus() }
		else { highlightedIndex.value = -1 }
	}, 10)
}

const isSelected = (option: Option) => selectedItems.value.find(item => getValue(option) === getValue(item)) !== undefined

function scrollToSelectedItem() {
	if (!selectOptions.value || selectedItemCount.value === 0) { return }

	// Find the first selected item in the results
	const selectedIndex = results.value.findIndex(option => isSelected(option))
	if (selectedIndex === -1) { return }

	// Get the selected option element
	const selectedElement = selectOptions.value.children[selectedIndex] as HTMLElement
	if (!selectedElement) { return }

	// Scroll the selected item into view
	selectedElement.scrollIntoView({
		block: 'center',
		behavior: 'instant'
	})
}

function focusInput() {
	open.value = true
}

function select(option: Option) {
	selected.value = true
	const existingIndex = selectedItems.value.findIndex(
		item => getValue(item) === getValue(option),
	)
	if (existingIndex > -1) {
		selectedItems.value.splice(existingIndex, 1)
	}
	else if (props.multiselect) {
		const current = [...selectedItems.value]
		current.push(option)

		selectedItems.value = current
	} else {
		selectedItems.value.splice(0, selectedItemCount.value, option)
	}

	// Move focus away from popper content before it gets aria-hidden
	const active = document.activeElement as HTMLElement | null
	if (active && selectOptions.value?.contains(active)) { active.blur() }

	if (!props.multiselect) {
		open.value = false
		// After closing, return focus to the trigger for accessibility
		setTimeout(() => { triggerBtn.value?.focus() }, 0)
	}
	emitUpdate()
}

function emitUpdate() {
	if (props.multiselect) {
		emit('update:modelValue', selectedItems.value.map(getValue).filter(v => v != null))
	} else {
		// selectedItems.value.splice(1, selectedItemCount.value - 1);
		const [item] = selectedItems.value
		emit('update:modelValue', getValue(item))
	}
}

function compareArrays(arr1: Option[], arr2: Option[]) {
	if (arr1.length !== arr2.length) { return false }
	const s1 = arr1.map(getValue).filter(v => v != null).map(String).sort()
	const s2 = arr2.map(getValue).filter(v => v != null).map(String).sort()
	return s1.every((v, i) => v === s2[i])
}

watch(
	() => props.modelValue as Option | Option[] | undefined,
	(newVal) => {
		if (!props.multiselect) {
			if (newVal == null) {
				// Clear selection when value is undefined/null
				if (selectedItems.value.length > 0) { selectedItems.value = [] }
			} else {
				const singleVal = Array.isArray(newVal) ? newVal[0] : newVal
				const newOption = Array.isArray(props.options)
					? (props.options.find(o => getValue(o) === getValue(singleVal)) ?? singleVal)
					: singleVal
				if (newOption != null && !isSelected(newOption)) { selectedItems.value = [newOption] }
			}
		} else {
			const newData = (Array.isArray(newVal) ? newVal : newVal != null ? [newVal] : [])
			const isSame = compareArrays(newData, selectedItems.value)
			if (!isSame) {
				selectedItems.value.splice(0, selectedItemCount.value, ...newData)
			}
		}
	},
	{ immediate: true },
)

watch(() => open.value, (value) => {
	if (value) {
		highlightedIndex.value = -1
		// Scroll to selected item when dropdown opens
		setTimeout(() => { scrollToSelectedItem() }, 10)
	}
})

watch(
	() => props.options,
	() => {
		const opts = props.options
		if (!Array.isArray(opts)) { return }
		selectedItems.value.forEach((option, i) => {
			const exists = opts.find(
				(o: Option) => getValue(o) === getValue(option),
			)
			if (exists === undefined) { selectedItems.value.splice(i, 1) }
			else { selectedItems.value.splice(i, 1, exists) }
		})
		// const original = JSON.stringify(props.options.map(getValue));
		// const newSelection = JSON.stringify(selectedItems.value.map(getValue));
		// if (original !== newSelection) emitUpdate();
	},
	{ deep: true, immediate: true },
)

// Watch for async results to update selected items with full option objects
watch(
	() => results.value,
	(newResults) => {
		if (isAsyncSource(props.options) && newResults.length > 0) {
			selectedItems.value.forEach((option, i) => {
				const optionValue = getValue(option)
				// If the selected item is just a primitive value (no label), find the full option
				if (typeof option !== 'object' || !option.label) {
					const fullOption = newResults.find(
						(o: Option) => getValue(o) === optionValue,
					)
					if (fullOption !== undefined) {
						selectedItems.value.splice(i, 1, fullOption)
					}
				}
			})
		}
	},
	{ deep: true },
)

onMounted(() => {
	const opts = props.options
	// Only apply defaultValue if modelValue is not set
	if (props.modelValue == null && props.defaultValue !== undefined && Array.isArray(opts)) {
		const defaultOption = opts.find(
			(o: Option) => getValue(o) === getValue(props.defaultValue),
		)

		if (defaultOption === undefined) { return }

		selectedItems.value = [defaultOption]
	}
})
</script>

<template>
	<Dropdown
		ref="dropdown" v-model:shown="open" :placement="placement" class="bagel-input selectinput"
		:class="{ 'has-error': !!error, underlined, open, 'has-value': selectedItemCount > 0 }" no-auto-focus
		@click.stop
	>
		<template #trigger>
			<label>
				<span v-if="label || (underlined && placeholder)" class="label-text">{{ resolveI18n(label)
					|| resolveI18n(placeholder) }}<span v-if="required"> *</span></span>
				<div class="flex gap-05">
					<TextInput
						v-if="searchable && open" ref="searchInput" v-model="searchTerm" class="mb-0"
						:placeholder="searchPlaceholder" icon="search" @input="selected = false" @click="focusInput"
						@keydown.down.prevent="navigate('down')" @keydown.up.prevent="navigate('up')"
					/>
					<button
						v-else ref="triggerBtn" :disabled="disabled" type="button" class="selectinput-btn"
						:class="{ isEmpty: selectedItemCount === 0 }" @click="open = !open"
						@keydown.down.prevent="navigate('down')" @keydown.up.prevent="navigate('up')"
					>
						<Icon v-if="icon" :icon="icon" />
						<p v-if="!hideLabel">
							{{ selectedItemCount > 0 ? selectedLabel : (underlined ? '' : (resolveI18n(placeholder)
								|| 'Select...')) }}
						</p>

						<div v-if="clearable && selectedItemCount > 0" class="ms-auto ps-05 me-05">
							<Btn flat thin icon="clear" class="color-gray" @click="selectedItems = []; emitUpdate()" />
						</div>
						<Transition v-if="!disabled" name="icon-swap" mode="out-in">
							<Icon v-if="open" key="open" thin :icon="underlined ? 'expand_less' : 'unfold_less'" />
							<Icon v-else key="closed" thin :icon="underlined ? 'expand_more' : 'unfold_more'" />
						</Transition>
					</button>
					<input
						v-if="required && !underlined" tabindex="-1"
						style="width: 0; height: 0; position: absolute; opacity: 0; z-index: -1" :value="selectedItems"
						required
					>
				</div>
				<div v-if="error" class="error-message">
					{{ error }}
				</div>
			</label>
			<p v-if="helpText" class="opacity-7 txt-small light">
				{{ helpText }}
			</p>
		</template>
		<Skeleton v-if="isLoading" :count="6" width="200px" height="25px" class="mx-1 my-1" />
		<Card class="p-05" frame :style="{ width: fullWidth ? '100%' : 'auto' }" @click.stop>
			<div ref="selectOptions" class="selectinput-options" :class="{ multiselect }">
				<div
					v-for="(option, i) in results" :key="`${String(getValue(option))}-${i}`"
					class="selectinput-option hover gap-1 align-items-center" :class="{ selected: isSelected(option) }"
					role="option" tabindex="0" :aria-selected="isSelected(option)" @click="select(option)"
					@keydown.enter="select(option)" @keydown.down.prevent="navigate('down')"
					@keydown.up.prevent="navigate('up')"
				>
					<template v-if="multiselect">
						<Icon v-if="isSelected(option)" :size="1.1" icon="select_check_box" />
						<Icon v-if="!isSelected(option)" class="opacity-3" icon="check_box_outline_blank" :size="1.1" />
					</template>
					<Icon v-if="getOptionIcon(option)" :icon="getOptionIcon(option)!" :size="1" />
					<span class="block">
						{{ getLabel(option) }}
					</span>
				</div>
				<p v-if="results.length === 0" class="selectinput-option opacity-3 pointer-events-none">
					No options found
				</p>
			</div>
			<slot name="last" />
		</Card>
	</Dropdown>
</template>

<style scoped>
.selectinput {
	width: 100%;
}

.selectinput-option {
	padding: 6px 12px;
	cursor: pointer;
	border-radius: 5px;
	transition: all 0.2s;
	display: grid;
	grid-template-columns: 1fr;
	justify-content: space-between;
	width: 100%;
	font-size: var(--input-font-size);
	margin-block: 0.15rem;
}

.selectinput-option .bgl_icon-font {
	line-height: normal;
}

.selectinput-options.multiselect .selectinput-option {
	grid-template-columns: 10px 1fr;

}

.selectinput-options {
	max-height: 300px;
	overflow-y: auto;
}

.selectinput-option:hover,
.highlight {
	background: var(--bgl-gray-tint);
}

.isEmpty p {
	opacity: 0.3;
}

.icon-swap-enter-active,
.icon-swap-leave-active {
	transition: opacity 0.12s ease, transform 0.12s ease;
}

.icon-swap-enter-from {
	opacity: 0;
	transform: translateY(-4px);
}

.icon-swap-leave-to {
	opacity: 0;
	transform: translateY(4px);
}

.selected {
	color: var(--bgl-primary);
	background: var(--bgl-selected);
}
</style>

<style>
.bagel-input label {
	font-size: var(--label-font-size);
}

.selectinput-btn {
	display: flex;
	justify-content: space-between;
	align-items: center;
	height: var(--input-height);
	border-radius: var(--input-border-radius);
	border: none;
	background: var(--input-bg);
	padding: 0.7rem;
	color: var(--input-color);
	width: 100%;
	font-family: inherit;
	font-size: var(--input-font-size);
}

.selectinput-btn p {
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

.selectinput .bagel-input.mb-0 input {
	/* background: transparent !important; */
}

.selectinput-btn:disabled {
	color: var(--input-disabled-color);
	background-color: transparent;
}

.selectinput-btn:focus {
	outline: none;
	box-shadow: inset 0 0 10px #00000012;
}

.selectinput.has-error .selectinput-btn {
	border-color: var(--bgl-red, #dc3545) !important;
		outline: 1px solid var(--bgl-red, #dc3545) !important;
	}

	.selectinput.underlined.has-error .selectinput-btn {
		border-color: var(--bgl-red, #dc3545) !important;
}

/* Underlined mode styling */
.selectinput.underlined {
	padding-top: 1.1rem;
}

.selectinput.underlined .selectinput-btn {
	outline: none !important;
	box-shadow: none !important;
	background: transparent !important;
	border-radius: 0 !important;
	border: none !important;
	border-bottom: 1.5px solid var(--border-color) !important;
	padding-inline: 0.25rem !important;
}

.selectinput.underlined.open .selectinput-btn,
.selectinput.underlined .selectinput-btn:focus {
	outline: none !important;
	box-shadow: none !important;
}
</style>
