<!--
 - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
 - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
	<NcModal id="unified-search"
		ref="unifiedSearchModal"
		:show.sync="internalIsVisible"
		:clear-view-delay="0"
		@close="closeModal">
		<CustomDateRangeModal :is-open="showDateRangeModal"
			class="unified-search__date-range"
			@set:custom-date-range="setCustomDateRange"
			@update:is-open="showDateRangeModal = $event" />
		<!-- Unified search form -->
		<div ref="unifiedSearch" class="unified-search-modal">
			<div class="unified-search-modal__header">
				<h2>{{ t('core', 'Unified search') }}</h2>
				<NcInputField ref="searchInput"
					data-cy-unified-search-input
					:value.sync="searchQuery"
					type="text"
					:label="t('core', 'Search apps, files, tags, messages') + '...'"
					@update:value="debouncedFind" />
				<div class="unified-search-modal__filters" data-cy-unified-search-filters>
					<NcActions :menu-name="t('core', 'Places')" :open.sync="providerActionMenuIsOpen" data-cy-unified-search-filter="places">
						<template #icon>
							<ListBox :size="20" />
						</template>
						<!-- Provider id's may be duplicated since, plugin filters could depend on a provider that is already in the defaults.
						provider.id concatenated to provider.name is used to create the item id, if same then, there should be an issue. -->
						<NcActionButton v-for="provider in providers"
							:key="`${provider.id}-${provider.name.replace(/\s/g, '')}`"
							@click="addProviderFilter(provider)">
							<template #icon>
								<img :src="provider.icon" class="filter-button__icon" alt="">
							</template>
							{{ provider.name }}
						</NcActionButton>
					</NcActions>
					<NcActions :menu-name="t('core', 'Date')" :open.sync="dateActionMenuIsOpen" data-cy-unified-search-filter="date">
						<template #icon>
							<CalendarRangeIcon :size="20" />
						</template>
						<NcActionButton :close-after-click="true" @click="applyQuickDateRange('today')">
							{{ t('core', 'Today') }}
						</NcActionButton>
						<NcActionButton :close-after-click="true" @click="applyQuickDateRange('7days')">
							{{ t('core', 'Last 7 days') }}
						</NcActionButton>
						<NcActionButton :close-after-click="true" @click="applyQuickDateRange('30days')">
							{{ t('core', 'Last 30 days') }}
						</NcActionButton>
						<NcActionButton :close-after-click="true" @click="applyQuickDateRange('thisyear')">
							{{ t('core', 'This year') }}
						</NcActionButton>
						<NcActionButton :close-after-click="true" @click="applyQuickDateRange('lastyear')">
							{{ t('core', 'Last year') }}
						</NcActionButton>
						<NcActionButton :close-after-click="true" @click="applyQuickDateRange('custom')">
							{{ t('core', 'Custom date range') }}
						</NcActionButton>
					</NcActions>
					<SearchableList :label-text="t('core', 'Search people')"
						:search-list="userContacts"
						:empty-content-text="t('core', 'Not found')"
						data-cy-unified-search-filter="people"
						@search-term-change="debouncedFilterContacts"
						@item-selected="applyPersonFilter">
						<template #trigger>
							<NcButton>
								<template #icon>
									<AccountGroup :size="20" />
								</template>
								{{ t('core', 'People') }}
							</NcButton>
						</template>
					</SearchableList>
					<NcButton v-if="supportFiltering" data-cy-unified-search-filter="current-view" @click="closeModal">
						{{ t('core', 'Filter in current view') }}
						<template #icon>
							<FilterIcon :size="20" />
						</template>
					</NcButton>
				</div>
				<div class="unified-search-modal__filters-applied">
					<FilterChip v-for="filter in filters"
						:key="filter.id"
						:text="filter.name ?? filter.text"
						:pretext="''"
						@delete="removeFilter(filter)">
						<template #icon>
							<NcAvatar v-if="filter.type === 'person'"
								:user="filter.user"
								:size="24"
								:disable-menu="true"
								:show-user-status="false"
								:hide-favorite="false" />
							<CalendarRangeIcon v-else-if="filter.type === 'date'" />
							<img v-else :src="filter.icon" alt="">
						</template>
					</FilterChip>
				</div>
			</div>
			<div v-if="noContentInfo.show" class="unified-search-modal__no-content">
				<NcEmptyContent :name="noContentInfo.text">
					<template #icon>
						<component :is="noContentInfo.icon" />
					</template>
				</NcEmptyContent>
			</div>
			<div v-else class="unified-search-modal__results">
				<div v-for="providerResult in results" :key="providerResult.id" class="result">
					<div class="result-title">
						<span>{{ providerResult.provider }}</span>
					</div>
					<ul class="result-items">
						<SearchResult v-for="(result, index) in providerResult.results" :key="index" v-bind="result" />
					</ul>
					<div class="result-footer">
						<NcButton type="tertiary-no-background" @click="loadMoreResultsForProvider(providerResult.id)">
							{{ t('core', 'Load more results') }}
							<template #icon>
								<DotsHorizontalIcon :size="20" />
							</template>
						</NcButton>
						<NcButton v-if="providerResult.inAppSearch" alignment="end-reverse" type="tertiary-no-background">
							{{ t('core', 'Search in') }} {{ providerResult.provider }}
							<template #icon>
								<ArrowRight :size="20" />
							</template>
						</NcButton>
					</div>
				</div>
			</div>
		</div>
	</NcModal>
</template>

<script>
import ArrowRight from 'vue-material-design-icons/ArrowRight.vue'
import AccountGroup from 'vue-material-design-icons/AccountGroup.vue'
import CalendarRangeIcon from 'vue-material-design-icons/CalendarRange.vue'
import CustomDateRangeModal from '../components/UnifiedSearch/CustomDateRangeModal.vue'
import DotsHorizontalIcon from 'vue-material-design-icons/DotsHorizontal.vue'
import FilterIcon from 'vue-material-design-icons/Filter.vue'
import FilterChip from '../components/UnifiedSearch/SearchFilterChip.vue'
import ListBox from 'vue-material-design-icons/ListBox.vue'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js'
import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
import MagnifyIcon from 'vue-material-design-icons/Magnify.vue'
import SearchableList from '../components/UnifiedSearch/SearchableList.vue'
import SearchResult from '../components/UnifiedSearch/SearchResult.vue'

import debounce from 'debounce'
import { emit, subscribe } from '@nextcloud/event-bus'
import { useBrowserLocation } from '@vueuse/core'
import { getProviders, search as unifiedSearch, getContacts } from '../services/UnifiedSearchService.js'
import { useSearchStore } from '../store/unified-search-external-filters.js'

export default {
	name: 'UnifiedSearchModal',
	components: {
		ArrowRight,
		AccountGroup,
		CalendarRangeIcon,
		CustomDateRangeModal,
		DotsHorizontalIcon,
		FilterIcon,
		FilterChip,
		ListBox,
		NcActions,
		NcActionButton,
		NcAvatar,
		NcButton,
		NcEmptyContent,
		NcModal,
		NcInputField,
		MagnifyIcon,
		SearchableList,
		SearchResult,
	},
	props: {
		isVisible: {
			type: Boolean,
			required: true,
		},
	},
	setup() {
		/**
		 * Reactive version of window.location
		 */
		const currentLocation = useBrowserLocation()
		const searchStore = useSearchStore()
		return {
			currentLocation,
			externalFilters: searchStore.externalFilters,
		}
	},
	data() {
		return {
			providers: [],
			providerActionMenuIsOpen: false,
			dateActionMenuIsOpen: false,
			providerResultLimit: 5,
			dateFilter: { id: 'date', type: 'date', text: '', startFrom: null, endAt: null },
			personFilter: { id: 'person', type: 'person', name: '' },
			dateFilterIsApplied: false,
			personFilterIsApplied: false,
			filteredProviders: [],
			searching: false,
			searchQuery: '',
			placessearchTerm: '',
			dateTimeFilter: null,
			filters: [],
			results: [],
			contacts: [],
			debouncedFind: debounce(this.find, 300),
			debouncedFilterContacts: debounce(this.filterContacts, 300),
			showDateRangeModal: false,
			internalIsVisible: false,
		}
	},

	computed: {
		userContacts() {
			return this.contacts
		},
		noContentInfo() {
			const isEmptySearch = this.searchQuery.length === 0
			const hasNoResults = this.searchQuery.length > 0 && this.results.length === 0
			return {
				show: isEmptySearch || hasNoResults,
				text: this.searching && hasNoResults ? t('core', 'Searching …') : (isEmptySearch ? t('core', 'Start typing to search') : t('core', 'No matching results')),
				icon: MagnifyIcon,
			}
		},
		supportFiltering() {
			/* Hard coded apps for the moment this would be improved in coming updates. */
			const providerPaths = ['/settings/users', '/apps/files', '/apps/deck']
			return providerPaths.some((path) => this.currentLocation.pathname?.includes?.(path))
		},
	},
	watch: {
		isVisible(value) {
			if (value) {
				/*
				 * Before setting the search UI to visible, reset previous search event emissions.
				 * This allows apps to restore defaults after "Filter in current view" if the user opens the search interface once more.
				 * Additionally, it's a new search, so it's better to reset all previous events emitted.
				 */
				emit('nextcloud:unified-search.reset', { query: '' })
			}
			this.internalIsVisible = value
		},
		internalIsVisible(value) {
			this.$emit('update:isVisible', value)
			this.$nextTick(() => {
				if (value) {
					this.focusInput()
				}
			})
		},

	},
	mounted() {
		subscribe('nextcloud:unified-search:add-filter', this.handlePluginFilter)
		getProviders().then((providers) => {
			this.providers = providers
			this.externalFilters.forEach(filter => {
				this.providers.push(filter)
			})
			this.providers = this.groupProvidersByApp(this.providers)
			console.debug('Search providers', this.providers)
		})
		getContacts({ searchTerm: '' }).then((contacts) => {
			this.contacts = this.mapContacts(contacts)
			console.debug('Contacts', this.contacts)
		})
	},
	methods: {
		find(query) {
			this.searching = true
			if (query.length === 0) {
				this.results = []
				this.searching = false
				emit('nextcloud:unified-search.reset', { query })
				return
			}
			emit('nextcloud:unified-search.search', { query })
			const newResults = []
			const providersToSearch = this.filteredProviders.length > 0 ? this.filteredProviders : this.providers
			const searchProvider = (provider, filters) => {
				const params = {
					type: provider.id,
					query,
					cursor: null,
					extraQueries: provider.extraParams,
				}

				if (filters.dateFilterIsApplied) {
					if (provider.filters.since && provider.filters.until) {
						params.since = this.dateFilter.startFrom
						params.until = this.dateFilter.endAt
					} else {
						// Date filter is applied but provider does not support it, no need to search provider
						return
					}
				}

				if (filters.personFilterIsApplied) {
					if (provider.filters.person) {
						params.person = this.personFilter.user
					} else {
						// Person filter is applied but provider does not support it, no need to search provider
						return
					}
				}

				if (this.providerResultLimit > 5) {
					params.limit = this.providerResultLimit
				}

				const request = unifiedSearch(params).request

				request().then((response) => {
					newResults.push({
						id: provider.id,
						provider: provider.name,
						inAppSearch: provider.inAppSearch,
						results: response.data.ocs.data.entries,
					})

					console.debug('New results', newResults)
					console.debug('Unified search results:', this.results)

					this.updateResults(newResults)
					this.searching = false
				})
			}
			providersToSearch.forEach(provider => {
				const dateFilterIsApplied = this.dateFilterIsApplied
				const personFilterIsApplied = this.personFilterIsApplied
				searchProvider(provider, { dateFilterIsApplied, personFilterIsApplied })
			})

		},
		updateResults(newResults) {
			let updatedResults = [...this.results]
			// If filters are applied, remove any previous results for providers that are not in current filters
			if (this.filters.length > 0) {
				updatedResults = updatedResults.filter(result => {
					return this.filters.some(filter => filter.id === result.id)
				})
			}
			// Process the new results
			newResults.forEach(newResult => {
				const existingResultIndex = updatedResults.findIndex(result => result.id === newResult.id)
				if (existingResultIndex !== -1) {
					if (newResult.results.length === 0) {
						// If the new results data has no matches for and existing result, remove the existing result
						updatedResults.splice(existingResultIndex, 1)
					} else {
						// If input triggered a change in existing results, update existing result
						updatedResults.splice(existingResultIndex, 1, newResult)
					}
				} else if (newResult.results.length > 0) {
					// Push the new result to the array only if its results array is not empty
					updatedResults.push(newResult)
				}
			})
			const sortedResults = updatedResults.slice(0)
			// Order results according to provider preference
			sortedResults.sort((a, b) => {
				const aProvider = this.providers.find(provider => provider.id === a.id)
				const bProvider = this.providers.find(provider => provider.id === b.id)
				const aOrder = aProvider ? aProvider.order : 0
				const bOrder = bProvider ? bProvider.order : 0
				return aOrder - bOrder
			})
			this.results = sortedResults
		},
		mapContacts(contacts) {
			return contacts.map(contact => {
				return {
					// id: contact.id,
					// name: '',
					displayName: contact.fullName,
					isNoUser: false,
					subname: contact.emailAddresses[0] ? contact.emailAddresses[0] : '',
					icon: '',
					user: contact.id,
					isUser: contact.isUser,
				}
			})
		},
		filterContacts(query) {
			getContacts({ searchTerm: query }).then((contacts) => {
				this.contacts = this.mapContacts(contacts)
				console.debug(`Contacts filtered by ${query}`, this.contacts)
			})
		},
		applyPersonFilter(person) {
			this.personFilterIsApplied = true
			const existingPersonFilter = this.filters.findIndex(filter => filter.id === person.id)
			if (existingPersonFilter === -1) {
				this.personFilter.id = person.id
				this.personFilter.user = person.user
				this.personFilter.name = person.displayName
				this.filters.push(this.personFilter)
			} else {
				this.filters[existingPersonFilter].id = person.id
				this.filters[existingPersonFilter].user = person.user
				this.filters[existingPersonFilter].name = person.displayName
			}

			this.debouncedFind(this.searchQuery)
			console.debug('Person filter applied', person)
		},
		loadMoreResultsForProvider(providerId) {
			this.providerResultLimit += 5
			this.filters = this.filters.filter(filter => filter.type !== 'provider')
			const provider = this.providers.find(provider => provider.id === providerId)
			this.addProviderFilter(provider, true)
		},
		addProviderFilter(providerFilter, loadMoreResultsForProvider = false) {
			if (!providerFilter.id) return
			if (providerFilter.isPluginFilter) {
				providerFilter.callback()
			}
			this.providerResultLimit = loadMoreResultsForProvider ? this.providerResultLimit : 5
			this.providerActionMenuIsOpen = false
			// With the possibility for other apps to add new filters
			// Resulting in a possible id/provider collision
			// If a user tries to apply a filter that seems to already exist, we remove the current one and add the new one.
			const existingFilterIndex = this.filteredProviders.findIndex(existing => existing.id === providerFilter.id)
			if (existingFilterIndex > -1) {
				this.filteredProviders.splice(existingFilterIndex, 1)
				this.filters = this.syncProviderFilters(this.filters, this.filteredProviders)
			}
			this.filteredProviders.push({
				id: providerFilter.id,
				name: providerFilter.name,
				icon: providerFilter.icon,
				type: providerFilter.type || 'provider',
				filters: providerFilter.filters,
				isPluginFilter: providerFilter.isPluginFilter || false,
			})
			this.filters = this.syncProviderFilters(this.filters, this.filteredProviders)
			console.debug('Search filters (newly added)', this.filters)
			this.debouncedFind(this.searchQuery)
		},
		removeFilter(filter) {
			if (filter.type === 'provider') {
				for (let i = 0; i < this.filteredProviders.length; i++) {
					if (this.filteredProviders[i].id === filter.id) {
						this.filteredProviders.splice(i, 1)
						break
					}
				}
				this.filters = this.syncProviderFilters(this.filters, this.filteredProviders)
				console.debug('Search filters (recently removed)', this.filters)

			} else {
				for (let i = 0; i < this.filters.length; i++) {
					// Remove date and person filter
					if (this.filters[i].id === 'date' || this.filters[i].id === filter.id) {
						this.dateFilterIsApplied = false
						this.filters.splice(i, 1)
						if (filter.type === 'person') {
							this.personFilterIsApplied = false
						}
						break
					}
				}
			}
			this.debouncedFind(this.searchQuery)
		},
		syncProviderFilters(firstArray, secondArray) {
			// Create a copy of the first array to avoid modifying it directly.
			const synchronizedArray = firstArray.slice()
			// Remove items from the synchronizedArray that are not in the secondArray.
			synchronizedArray.forEach((item, index) => {
				const itemId = item.id
				if (item.type === 'provider') {
					if (!secondArray.some(secondItem => secondItem.id === itemId)) {
						synchronizedArray.splice(index, 1)
					}
				}
			})
			// Add items to the synchronizedArray that are in the secondArray but not in the firstArray.
			secondArray.forEach(secondItem => {
				const itemId = secondItem.id
				if (secondItem.type === 'provider') {
					if (!synchronizedArray.some(item => item.id === itemId)) {
						synchronizedArray.push(secondItem)
					}
				}
			})

			return synchronizedArray
		},
		updateDateFilter() {
			const currFilterIndex = this.filters.findIndex(filter => filter.id === 'date')
			if (currFilterIndex !== -1) {
				this.filters[currFilterIndex] = this.dateFilter
			} else {
				this.filters.push(this.dateFilter)
			}
			this.dateFilterIsApplied = true
			this.debouncedFind(this.searchQuery)
		},
		applyQuickDateRange(range) {
			this.dateActionMenuIsOpen = false
			const today = new Date()
			let startDate
			let endDate

			switch (range) {
			case 'today':
				// For 'Today', both start and end are set to today
				startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0, 0)
				endDate = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59, 999)
				this.dateFilter.text = t('core', 'Today')
				break
			case '7days':
				// For 'Last 7 days', start date is 7 days ago, end is today
				startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 6, 0, 0, 0, 0)
				this.dateFilter.text = t('core', 'Last 7 days')
				break
			case '30days':
				// For 'Last 30 days', start date is 30 days ago, end is today
				startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29, 0, 0, 0, 0)
				this.dateFilter.text = t('core', 'Last 30 days')
				break
			case 'thisyear':
				// For 'This year', start date is the first day of the year, end is the last day of the year
				startDate = new Date(today.getFullYear(), 0, 1, 0, 0, 0, 0)
				endDate = new Date(today.getFullYear(), 11, 31, 23, 59, 59, 999)
				this.dateFilter.text = t('core', 'This year')
				break
			case 'lastyear':
				// For 'Last year', start date is the first day of the previous year, end is the last day of the previous year
				startDate = new Date(today.getFullYear() - 1, 0, 1, 0, 0, 0, 0)
				endDate = new Date(today.getFullYear() - 1, 11, 31, 23, 59, 59, 999)
				this.dateFilter.text = t('core', 'Last year')
				break
			case 'custom':
				this.showDateRangeModal = true
				return
			default:
				return
			}
			this.dateFilter.startFrom = startDate
			this.dateFilter.endAt = endDate
			this.updateDateFilter()

		},
		setCustomDateRange(event) {
			console.debug('Custom date range', event)
			this.dateFilter.startFrom = event.startFrom
			this.dateFilter.endAt = event.endAt
			this.dateFilter.text = t('core', `Between ${this.dateFilter.startFrom.toLocaleDateString()} and ${this.dateFilter.endAt.toLocaleDateString()}`)
			this.updateDateFilter()
		},
		handlePluginFilter(addFilterEvent) {
			for (let i = 0; i < this.filteredProviders.length; i++) {
				const provider = this.filteredProviders[i]
				if (provider.id === addFilterEvent.id) {
					provider.name = addFilterEvent.filterUpdateText
					// Filters attached may only make sense with certain providers,
					// So, find the provider attached, add apply the extra parameters to those providers only
					const compatibleProviderIndex = this.providers.findIndex(provider => provider.id === addFilterEvent.id)
					if (compatibleProviderIndex > -1) {
						provider.extraParams = addFilterEvent.filterParams
						this.filteredProviders[i] = provider
					}
					break
				}
			}
			this.debouncedFind(this.searchQuery)
		},
		groupProvidersByApp(filters) {
			const groupedByProviderApp = {}

			filters.forEach(filter => {
				const provider = filter.appId ? filter.appId : 'general'
				if (!groupedByProviderApp[provider]) {
					groupedByProviderApp[provider] = []
				}
				groupedByProviderApp[provider].push(filter)
			})

			const flattenedArray = []
			Object.values(groupedByProviderApp).forEach(group => {
				flattenedArray.push(...group)
			})

			return flattenedArray
		},
		focusInput() {
			this.$refs.searchInput.$el.children[0].children[0].focus()
		},
		closeModal() {
			this.internalIsVisible = false
			this.searchQuery = ''
		},
	},
}
</script>

<style lang="scss" scoped>
.unified-search-modal {
	box-sizing: border-box;
	height: 100%;
	min-height: 80vh;

	display: flex;
	flex-direction: column;
	padding-block: 10px 0;

	// inline padding on direct children to make sure the scrollbar is on the modal container
	>* {
		padding-inline: 20px;
	}

	&__header {
		padding-block-end: 8px;
	}

	&__heading {
		font-size: 16px;
		font-weight: bolder;
		line-height: 2em;
		margin-bottom: 0;
	}

	&__filters {
		display: flex;
		flex-wrap: wrap;
		gap: 4px;
		justify-content: start;
		padding-top: 4px;
	}

	&__filters-applied {
		padding-top: 4px;
		display: flex;
		flex-wrap: wrap;
	}

	&__no-content {
		display: flex;
		align-items: center;
		height: 100%;
	}

	&__results {
		overflow: hidden scroll;
		padding-block: 0 10px;

		.result {
			&-title {
				span {
					color: var(--color-primary-element);
					font-weight: bolder;
					font-size: 16px;
				}
			}

			&-footer {
				justify-content: space-between;
				align-items: center;
				display: flex;
			}
		}

	}
}

.filter-button__icon {
	height: 20px;
	width: 20px;
	object-fit: contain;
	filter: var(--background-invert-if-bright);
	padding: 11px; // align with text to fit at least 44px
}

// Ensure modal is accessible on small devices
@media only screen and (max-height: 400px) {
	.unified-search-modal__results {
		overflow: unset;
	}
}
</style>