<template>
	<div class="md:pl-1">
		<div class="flex flex-wrap items-center gap-2 md:hidden">
			<MultiSelect
				v-if="showProjectRow"
				v-model="draftSelectedProjectIds"
				class="min-w-0 max-w-full"
				:options="projectSelectOptions"
				:disabled="!hasProjectOptions"
				:max-height="QUERY_BUILDER_DROPDOWN_MAX_HEIGHT"
				dropdown-min-width="360px"
				:placeholder="formatMessage(analyticsMessages.selectProjects)"
				:no-options-message="noProjectsMessage"
				:searchable="projectOptions.length > 6"
				:max-tag-rows="1"
				:trigger-class="analyticsQueryChipTriggerClass"
				fit-content
				checkbox-position="right"
				show-selection-actions
				@open="handleProjectSelectOpen"
				@close="handleProjectSelectClose"
			>
				<template #input-content="{ isOpen, openDirection }">
					<div class="flex min-h-7 min-w-0 max-w-full flex-1 items-center gap-1.5 pr-1">
						<img
							v-if="selectedProjectIconUrl"
							:src="selectedProjectIconUrl"
							:alt="formatMessage(analyticsMessages.projectIconAlt, { name: selectedProjectLabel })"
							class="size-5 shrink-0 rounded object-cover"
							loading="lazy"
							decoding="async"
						/>
						<LayersIcon v-else class="size-5 shrink-0 text-primary" />
						<span class="min-w-0 flex-1 truncate px-0.5 font-semibold text-primary">
							{{ selectedProjectLabel }}
						</span>
						<ChevronLeftIcon
							class="size-5 shrink-0 text-primary transition-transform duration-150"
							:class="
								isOpen ? (openDirection === 'down' ? 'rotate-90' : '-rotate-90') : '-rotate-90'
							"
						/>
					</div>
				</template>
				<template #option="{ item, selected }">
					<div class="flex min-w-0 flex-1 items-center gap-2">
						<img
							v-if="getProjectIconUrl(item.value)"
							:src="getProjectIconUrl(item.value)"
							:alt="formatMessage(analyticsMessages.projectIconAlt, { name: item.label })"
							class="h-5 w-5 shrink-0 rounded object-cover"
							loading="lazy"
							decoding="async"
						/>
						<BoxIcon
							v-else
							class="h-5 w-5 shrink-0 text-primary"
							:class="selected ? 'text-contrast' : 'text-primary'"
						/>
						<span
							v-tooltip="item.label"
							class="min-w-0 truncate font-semibold leading-tight"
							:class="selected ? 'text-contrast' : 'text-primary'"
						>
							{{ item.label }}
						</span>
					</div>
				</template>
				<template v-if="hasProjectOptions" #top>
					<div>
						<button
							type="button"
							class="flex w-full cursor-pointer items-center gap-1.5 border-0 bg-surface-4 px-4 py-3 text-left shadow-none transition-all duration-150 hover:brightness-[115%] focus:brightness-[115%]"
							:aria-selected="isAllProjectsOptionSelected"
							:class="isAllProjectsOptionSelected ? 'text-contrast' : 'text-primary'"
							role="option"
							@click="selectAllProjectsMode"
							@keydown.enter.stop
							@keydown.space.stop
						>
							<LayersIcon
								class="h-5 w-5 shrink-0 text-primary"
								:class="isAllProjectsOptionSelected ? 'text-contrast' : 'text-primary'"
							/>
							<span class="min-w-0 flex-1 font-semibold leading-tight">
								{{ formatMessage(analyticsMessages.allProjects) }}
							</span>
							<span class="flex shrink-0 items-center justify-center text-brand">
								<CheckIcon v-if="isAllProjectsOptionSelected" aria-hidden="true" class="size-5" />
							</span>
						</button>
					</div>
				</template>
				<template v-if="hasProjectOptions" #bottom>
					<DownloadsThresholdInput
						class="border-0 border-t border-solid border-surface-5 px-6 py-2.5"
						:label="formatMessage(analyticsMessages.projectsAbove)"
						:input-aria-label="formatMessage(analyticsMessages.projectDownloadsThresholdAria)"
						:threshold="projectDownloadsThreshold"
						input-width-class="w-20"
						@update:threshold="setProjectDownloadsThreshold"
						@submit="runProjectDownloadsThresholdQuery"
					/>
				</template>
			</MultiSelect>

			<TimeFramePicker
				class="!w-auto min-w-0 max-w-full"
				:trigger-class="analyticsQueryChipTriggerClass"
			>
				<template #prefix>
					<CalendarIcon class="size-5 shrink-0 text-primary" />
				</template>
			</TimeFramePicker>

			<Combobox
				v-model="selectedGroupBy"
				class="!w-auto min-w-0 max-w-full"
				:options="groupByOptions"
				:max-height="QUERY_BUILDER_DROPDOWN_MAX_HEIGHT"
				:dropdown-min-width="QUERY_BUILDER_DROPDOWN_MIN_WIDTH"
				:display-value="selectedGroupByLabel"
				:trigger-class="analyticsQueryChipTriggerClass"
			>
				<template #prefix>
					<ClockIcon class="size-5 shrink-0 text-primary" />
				</template>
			</Combobox>

			<MultiSelect
				v-model="selectedBreakdownValue"
				class="min-w-0 max-w-full"
				:options="breakdownOptions"
				:max-height="QUERY_BUILDER_DROPDOWN_MAX_HEIGHT"
				:dropdown-width="QUERY_BUILDER_DROPDOWN_MIN_WIDTH"
				:dropdown-min-width="QUERY_BUILDER_DROPDOWN_MIN_WIDTH"
				:trigger-class="analyticsQueryChipTriggerClass"
				fit-content
				checkbox-position="right"
				:placeholder="formatMessage(analyticsMessages.none)"
				show-selection-actions
				@open="handleBreakdownSelectOpen"
				@close="handleBreakdownSelectClose"
			>
				<template #input-content="{ isOpen, openDirection }">
					<div class="flex min-h-7 min-w-0 max-w-full flex-1 items-center gap-1.5 pr-1">
						<BlocksIcon class="size-5 shrink-0 text-primary" />
						<span
							class="min-w-0 flex-1 truncate px-0.5 font-semibold text-primary"
							:title="mobileSelectedBreakdownLabel"
						>
							{{ mobileSelectedBreakdownLabel }}
						</span>
						<ChevronLeftIcon
							class="size-5 shrink-0 text-primary transition-transform duration-150"
							:class="
								isOpen ? (openDirection === 'down' ? 'rotate-90' : '-rotate-90') : '-rotate-90'
							"
						/>
					</div>
				</template>
			</MultiSelect>

			<QueryBuilderFilter
				:add-label="formatMessage(analyticsMessages.addFilterButton)"
				:show-label="false"
				show-preview-filter-icon
				:show-clear-action="false"
				:add-button-class="analyticsQueryAddFilterButtonClass"
			/>
		</div>

		<div class="hidden flex-col gap-3 md:flex">
			<div v-if="showProjectRow" class="flex items-start gap-2">
				<div class="my-1.5 flex w-32 items-center gap-2 text-primary">
					<FolderOpenIcon class="size-5" />
					<span class="text-base font-medium">{{
						formatMessage(analyticsMessages.projectLabel)
					}}</span>
				</div>
				<div class="w-fit">
					<MultiSelect
						v-model="draftSelectedProjectIds"
						:options="projectSelectOptions"
						:disabled="!hasProjectOptions"
						:max-height="QUERY_BUILDER_DROPDOWN_MAX_HEIGHT"
						dropdown-min-width="360px"
						:placeholder="formatMessage(analyticsMessages.selectProjects)"
						:no-options-message="noProjectsMessage"
						:searchable="projectOptions.length > 6"
						:max-tag-rows="1"
						checkbox-position="right"
						show-selection-actions
						@open="handleProjectSelectOpen"
						@close="handleProjectSelectClose"
					>
						<template #input-content="{ isOpen, openDirection }">
							<div class="flex min-h-7 min-w-0 flex-1 items-center gap-1.5 pr-1">
								<div class="flex items-center gap-0.5">
									<img
										v-if="selectedProjectIconUrl"
										:src="selectedProjectIconUrl"
										:alt="
											formatMessage(analyticsMessages.projectIconAlt, {
												name: selectedProjectLabel,
											})
										"
										class="size-6 shrink-0 rounded object-cover"
										loading="lazy"
										decoding="async"
									/>
									<LayersIcon
										v-else-if="isAllProjectsOptionSelected || areAllProjectsSelected"
										class="size-5 shrink-0 text-primary"
									/>
									<BoxIcon v-else class="size-5 shrink-0 text-primary" />
									<span class="min-w-0 flex-1 truncate px-1.5 font-semibold text-primary">
										{{ selectedProjectLabel }}
									</span>
								</div>
								<ChevronLeftIcon
									class="size-5 shrink-0 text-primary transition-transform duration-150"
									:class="
										isOpen ? (openDirection === 'down' ? 'rotate-90' : '-rotate-90') : '-rotate-90'
									"
								/>
							</div>
						</template>
						<template #option="{ item, selected }">
							<div class="flex min-w-0 flex-1 items-center gap-2">
								<img
									v-if="getProjectIconUrl(item.value)"
									:src="getProjectIconUrl(item.value)"
									:alt="formatMessage(analyticsMessages.projectIconAlt, { name: item.label })"
									class="h-5 w-5 shrink-0 rounded object-cover"
									loading="lazy"
									decoding="async"
								/>
								<BoxIcon
									v-else
									class="h-5 w-5 shrink-0 text-primary"
									:class="selected ? 'text-contrast' : 'text-primary'"
								/>
								<span
									v-tooltip="item.label"
									class="min-w-0 truncate font-semibold leading-tight"
									:class="selected ? 'text-contrast' : 'text-primary'"
								>
									{{ item.label }}
								</span>
							</div>
						</template>
						<template v-if="hasProjectOptions" #top>
							<div>
								<button
									type="button"
									class="flex w-full cursor-pointer items-center gap-2 border-0 bg-surface-4 px-4 py-3 text-left shadow-none transition-all duration-150 hover:brightness-[115%] focus:brightness-[115%]"
									:aria-selected="isAllProjectsOptionSelected"
									:class="isAllProjectsOptionSelected ? 'text-contrast' : 'text-primary'"
									role="option"
									@click="selectAllProjectsMode"
									@keydown.enter.stop
									@keydown.space.stop
								>
									<LayersIcon
										class="h-5 w-5 shrink-0 text-primary"
										:class="isAllProjectsOptionSelected ? 'text-contrast' : 'text-primary'"
									/>
									<span class="min-w-0 flex-1 font-semibold leading-tight">
										{{ formatMessage(analyticsMessages.allProjects) }}
									</span>
									<span class="flex shrink-0 items-center justify-center text-brand">
										<CheckIcon
											v-if="isAllProjectsOptionSelected"
											aria-hidden="true"
											class="size-5"
										/>
									</span>
								</button>
							</div>
						</template>
						<template v-if="hasProjectOptions" #bottom>
							<DownloadsThresholdInput
								class="border-0 border-t border-solid border-surface-5 px-6 py-2.5"
								:label="formatMessage(analyticsMessages.projectsAbove)"
								:input-aria-label="formatMessage(analyticsMessages.projectDownloadsThresholdAria)"
								:threshold="projectDownloadsThreshold"
								input-width-class="w-20"
								@update:threshold="setProjectDownloadsThreshold"
								@submit="runProjectDownloadsThresholdQuery"
							/>
						</template>
					</MultiSelect>
				</div>
			</div>

			<div class="flex flex-wrap items-center gap-2">
				<div class="flex items-center gap-2">
					<div class="flex w-32 items-center gap-2 text-primary">
						<CalendarIcon class="size-5" />
						<span class="text-base font-medium">
							{{ formatMessage(analyticsMessages.timeframeLabel) }}
						</span>
					</div>
					<div>
						<TimeFramePicker />
					</div>
				</div>
				<div class="flex items-center gap-2">
					<span class="text-base font-medium text-primary">
						{{ formatMessage(analyticsMessages.groupedByLabel) }}
					</span>
					<div>
						<Combobox
							v-model="selectedGroupBy"
							:options="groupByOptions"
							:max-height="QUERY_BUILDER_DROPDOWN_MAX_HEIGHT"
							:dropdown-min-width="QUERY_BUILDER_DROPDOWN_MIN_WIDTH"
						/>
					</div>
				</div>
				<ButtonStyled v-if="!isTimeframeAndGroupByDefault" type="transparent">
					<button
						type="button"
						:disabled="isTimeframeAndGroupByDefault"
						@click="resetTimeframeAndGroupBy"
					>
						{{ formatMessage(analyticsMessages.resetButton) }}
					</button>
				</ButtonStyled>
			</div>

			<div class="flex flex-wrap items-start gap-2">
				<div class="flex items-center gap-2">
					<div class="flex w-32 items-center gap-2 text-primary">
						<BlocksIcon class="size-5" />
						<span class="text-base font-medium">
							{{ formatMessage(analyticsMessages.breakdownLabel) }}
						</span>
					</div>
					<div class="flex flex-col gap-2">
						<div class="flex flex-wrap items-center gap-2">
							<div>
								<MultiSelect
									v-model="selectedBreakdownValue"
									:options="breakdownOptions"
									:max-height="QUERY_BUILDER_DROPDOWN_MAX_HEIGHT"
									:dropdown-width="QUERY_BUILDER_DROPDOWN_MIN_WIDTH"
									:dropdown-min-width="QUERY_BUILDER_DROPDOWN_MIN_WIDTH"
									checkbox-position="right"
									:placeholder="formatMessage(analyticsMessages.none)"
									show-selection-actions
									@open="handleBreakdownSelectOpen"
									@close="handleBreakdownSelectClose"
								>
									<template #input-content="{ isOpen, openDirection }">
										<div class="flex min-h-7 min-w-0 flex-1 items-center gap-1.5 pr-1">
											<span
												class="min-w-0 flex-1 truncate font-semibold text-primary"
												:title="selectedBreakdownLabel"
											>
												{{ selectedBreakdownLabel }}
											</span>
											<ChevronLeftIcon
												class="size-5 shrink-0 text-primary transition-transform duration-150"
												:class="
													isOpen
														? openDirection === 'down'
															? 'rotate-90'
															: '-rotate-90'
														: '-rotate-90'
												"
											/>
										</div>
									</template>
								</MultiSelect>
							</div>
						</div>
					</div>
				</div>
				<QueryBuilderFilter />
			</div>
		</div>
	</div>
</template>

<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import {
	BlocksIcon,
	BoxIcon,
	CalendarIcon,
	CheckIcon,
	ChevronLeftIcon,
	ClockIcon,
	FolderOpenIcon,
	LayersIcon,
} from '@modrinth/assets'
import {
	ButtonStyled,
	Combobox,
	type ComboboxOption,
	MultiSelect,
	type MultiSelectItem,
	type MultiSelectOption,
	useVIntl,
} from '@modrinth/ui'

import {
	buildDefaultAnalyticsQueryBuilderState,
	getAnalyticsBreakdownPresetsForProjectSelection,
	MAX_ANALYTICS_BREAKDOWN_PRESETS,
} from '~/components/analytics-dashboard/analytics-route-query'
import {
	type AnalyticsBreakdownPreset,
	type AnalyticsDashboardProject,
	type AnalyticsDashboardStat,
	type AnalyticsGroupByPreset,
	type AnalyticsQueryFilterCategory,
	type AnalyticsSelectedBreakdowns,
	type AnalyticsSelectedFilters,
	getProjectIdsMatchingStatusFilter,
	injectAnalyticsDashboardContext,
} from '~/providers/analytics/analytics'

import {
	analyticsMessages,
	formatAnalyticsBreakdownLabel,
	formatAnalyticsGroupByLabel,
	formatAnalyticsGroupBySelectedLabel,
} from '../analytics-messages.ts'
import DownloadsThresholdInput from './DownloadsThresholdInput.vue'
import {
	getAnalyticsStatsForBreakdown,
	getAnalyticsStatsForFilterCategory,
	getEnabledAnalyticsStatsForState,
} from './query-filter-utils.ts'
import QueryBuilderFilter from './QueryFilter.vue'
import {
	ensureMinimumTimeRange,
	getAnalyticsGroupByPresetMinutes,
	MAX_ANALYTICS_TIME_SLICES,
	useSelectedAnalyticsTimeRange,
} from './timeframe.ts'
import TimeFramePicker from './TimeframePicker.vue'

const QUERY_BUILDER_DROPDOWN_MAX_HEIGHT = 500
const QUERY_BUILDER_DROPDOWN_MIN_WIDTH = '12rem'
const analyticsQueryChipTriggerClass = 'h-10 '
const analyticsQueryAddFilterButtonClass = '!h-10 max-w-full !w-max !px-3.5 flex !gap-2'
const projectOptionCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })

const {
	hasProjectContext,
	projectGroups,
	projects,
	selectedProjectIds,
	selectedTimeframeMode,
	selectedTimeframe,
	selectedLastTimeframeAmount,
	selectedLastTimeframeUnit,
	selectedCustomTimeframeStartDate,
	selectedCustomTimeframeEndDate,
	selectedGroupBy,
	selectedBreakdowns,
	selectedFilters,
	activeStat,
	showPreviousPeriod,
	projectStatusById,
	projectDownloadsById,
	queryResetToken,
	refreshAnalyticsQuery,
	setFetchRequest,
} = injectAnalyticsDashboardContext()
const route = useRoute()
const { formatMessage } = useVIntl()
const { selectedTimeRange, selectedTimeframeDurationMinutes } = useSelectedAnalyticsTimeRange()
const defaultQueryState = buildDefaultAnalyticsQueryBuilderState([])

function getProjectOption(
	project: AnalyticsDashboardProject,
	groupTitle?: string,
): MultiSelectOption<string> {
	return {
		value: project.id,
		label: project.name,
		searchTerms: groupTitle ? [groupTitle] : undefined,
	}
}

function compareProjectOptions(
	left: MultiSelectOption<string>,
	right: MultiSelectOption<string>,
): number {
	return projectOptionCollator.compare(left.label, right.label)
}

const projectOptions = computed<MultiSelectOption<string>[]>(() =>
	projects.value.map((project) => getProjectOption(project)),
)
const projectIconUrlsById = computed(
	() =>
		new Map(
			projects.value
				.filter((project) => project.iconUrl)
				.map((project) => [project.id, project.iconUrl as string]),
		),
)

const projectSelectOptions = computed<MultiSelectItem<string>[]>(() => {
	const options: MultiSelectItem<string>[] = []

	for (const group of projectGroups.value) {
		if (group.projects.length === 0) {
			continue
		}

		if (group.title) {
			options.push({
				type: 'section-header',
				label: group.title,
				key: group.key ?? `organization-${group.title}`,
			})
		}

		options.push(
			...group.projects
				.map((project) => getProjectOption(project, group.title))
				.sort(compareProjectOptions),
		)
	}

	return options
})

const allProjectIds = computed(() => projectOptions.value.map((project) => project.value))
const hasProjectOptions = computed(() => projectOptions.value.length > 0)
const noProjectsMessage = computed(() =>
	hasProjectContext.value
		? formatMessage(analyticsMessages.noDataAvailableForAnalytics)
		: formatMessage(analyticsMessages.noProjectsAvailable),
)
const isProjectSelectOpen = ref(false)
const draftSelectedProjectIds = ref<string[]>([...selectedProjectIds.value])
const projectDownloadsThreshold = ref<number | null>(null)
const projectDownloadsThresholdProjectIds = ref<string[] | null>(null)

function isSameProjectSelection(left: string[], right: string[]) {
	if (left.length !== right.length) {
		return false
	}

	const rightProjectIds = new Set(right)
	return left.every((projectId) => rightProjectIds.has(projectId))
}

function normalizeProjectSelection(projectIds: string[]) {
	return projectIds.length > 0 ? [...projectIds] : [...allProjectIds.value]
}

watch(selectedProjectIds, (nextSelectedProjectIds) => {
	if (isProjectSelectOpen.value) {
		return
	}

	draftSelectedProjectIds.value = [...nextSelectedProjectIds]
})

watch(draftSelectedProjectIds, (nextSelectedProjectIds) => {
	if (projectDownloadsThreshold.value === null) {
		return
	}

	const normalizedProjectIds = normalizeProjectSelection(nextSelectedProjectIds)
	if (
		projectDownloadsThresholdProjectIds.value &&
		isSameProjectSelection(normalizedProjectIds, projectDownloadsThresholdProjectIds.value)
	) {
		return
	}

	clearProjectDownloadsThreshold()
})

watch(queryResetToken, () => {
	isProjectSelectOpen.value = false
	isBreakdownSelectOpen.value = false
	draftSelectedBreakdowns.value = [...selectedBreakdowns.value]
	clearProjectDownloadsThreshold()
	draftSelectedProjectIds.value = isSameProjectSelection(
		selectedProjectIds.value,
		allProjectIds.value,
	)
		? []
		: [...selectedProjectIds.value]
})

const areAllProjectsSelected = computed(() => {
	return isSameProjectSelection(draftSelectedProjectIds.value, allProjectIds.value)
})
const isAllProjectsOptionSelected = computed(() => draftSelectedProjectIds.value.length === 0)

const selectedProjectLabel = computed(() => {
	if (!hasProjectOptions.value) {
		return noProjectsMessage.value
	}

	if (isAllProjectsOptionSelected.value || areAllProjectsSelected.value) {
		return formatMessage(analyticsMessages.allProjects)
	}

	if (draftSelectedProjectIds.value.length === 1) {
		const selectedProject = projectOptions.value.find(
			(project) => project.value === draftSelectedProjectIds.value[0],
		)
		return selectedProject?.label ?? formatMessage(analyticsMessages.projectCount, { count: 1 })
	}

	return formatMessage(analyticsMessages.projectCount, {
		count: draftSelectedProjectIds.value.length,
	})
})

const selectedProjectIconUrl = computed(() => {
	if (
		isAllProjectsOptionSelected.value ||
		areAllProjectsSelected.value ||
		draftSelectedProjectIds.value.length !== 1
	) {
		return undefined
	}

	return getProjectIconUrl(draftSelectedProjectIds.value[0])
})

function getProjectIconUrl(projectId: string): string | undefined {
	return projectIconUrlsById.value.get(projectId)
}

function handleProjectSelectOpen() {
	isProjectSelectOpen.value = true
	draftSelectedProjectIds.value = isSameProjectSelection(
		selectedProjectIds.value,
		allProjectIds.value,
	)
		? []
		: [...selectedProjectIds.value]
}

function handleProjectSelectClose(
	nextSelectedProjectIds: string[] = draftSelectedProjectIds.value,
) {
	isProjectSelectOpen.value = false
	commitDraftSelectedProjects(nextSelectedProjectIds)
}

function commitDraftSelectedProjects(
	nextSelectedProjectIds: string[] = draftSelectedProjectIds.value,
) {
	const nextProjectIds = normalizeProjectSelection(nextSelectedProjectIds)

	draftSelectedProjectIds.value = [...nextProjectIds]
	if (!isSameProjectSelection(selectedProjectIds.value, nextProjectIds)) {
		if (isSameProjectSelection(nextProjectIds, allProjectIds.value)) {
			showPreviousPeriod.value = false
		}
		selectedProjectIds.value = nextProjectIds
	}
}

function selectAllProjectsMode() {
	clearProjectDownloadsThreshold()
	draftSelectedProjectIds.value = []
}

const draftSelectedBreakdowns = ref<AnalyticsSelectedBreakdowns>([...selectedBreakdowns.value])
const isBreakdownSelectOpen = ref(false)

const selectedBreakdownValue = computed<AnalyticsSelectedBreakdowns>({
	get: () => draftSelectedBreakdowns.value,
	set: (nextBreakdowns) => {
		draftSelectedBreakdowns.value = getAnalyticsBreakdownPresetsForProjectSelection(
			nextBreakdowns.slice(0, MAX_ANALYTICS_BREAKDOWN_PRESETS),
			selectedProjectIds.value,
		)
	},
})

watch(selectedBreakdowns, (nextBreakdowns) => {
	if (isBreakdownSelectOpen.value) {
		return
	}
	draftSelectedBreakdowns.value = [...nextBreakdowns]
})

function handleBreakdownSelectOpen() {
	isBreakdownSelectOpen.value = true
	draftSelectedBreakdowns.value = [...selectedBreakdowns.value]
}

function handleBreakdownSelectClose() {
	isBreakdownSelectOpen.value = false
	commitDraftSelectedBreakdowns()
}

function commitDraftSelectedBreakdowns() {
	const nextBreakdowns = [...draftSelectedBreakdowns.value]
	if (!areSelectedBreakdownsEqual(selectedBreakdowns.value, nextBreakdowns)) {
		showPreviousPeriod.value = false
		selectedBreakdowns.value = nextBreakdowns
	}
}

function areSelectedBreakdownsEqual(
	left: readonly AnalyticsBreakdownPreset[],
	right: readonly AnalyticsBreakdownPreset[],
) {
	if (left.length !== right.length) return false
	for (let index = 0; index < left.length; index += 1) {
		if (left[index] !== right[index]) return false
	}
	return true
}

const isDashboardAnalyticsRoute = computed(
	() => route.path.replace(/\/$/, '') === '/dashboard/analytics',
)
const showProjectRow = computed(() => isDashboardAnalyticsRoute.value || projects.value.length > 1)
const isTimeframeAndGroupByDefault = computed(
	() =>
		selectedTimeframeMode.value === defaultQueryState.selectedTimeframeMode &&
		selectedTimeframe.value === defaultQueryState.selectedTimeframe &&
		selectedGroupBy.value === defaultQueryState.selectedGroupBy,
)

function resetTimeframeAndGroupBy() {
	if (isTimeframeAndGroupByDefault.value) {
		return
	}

	selectedTimeframeMode.value = defaultQueryState.selectedTimeframeMode
	selectedTimeframe.value = defaultQueryState.selectedTimeframe
	selectedLastTimeframeAmount.value = defaultQueryState.selectedLastTimeframeAmount
	selectedLastTimeframeUnit.value = defaultQueryState.selectedLastTimeframeUnit
	selectedCustomTimeframeStartDate.value = defaultQueryState.selectedCustomTimeframeStartDate
	selectedCustomTimeframeEndDate.value = defaultQueryState.selectedCustomTimeframeEndDate
	selectedGroupBy.value = defaultQueryState.selectedGroupBy
}

function applyProjectDownloadsThreshold(threshold: number | null) {
	if (threshold === null) {
		return
	}

	const projectIds = projects.value
		.filter((project) => (projectDownloadsById.value.get(project.id) ?? 0) > threshold)
		.map((project) => project.id)

	projectDownloadsThresholdProjectIds.value = projectIds
	draftSelectedProjectIds.value = projectIds
}

function setProjectDownloadsThreshold(threshold: number | null) {
	projectDownloadsThreshold.value = threshold
	if (threshold === null) {
		projectDownloadsThresholdProjectIds.value = null
		draftSelectedProjectIds.value = []
		return
	}

	applyProjectDownloadsThreshold(threshold)
}

function clearProjectDownloadsThreshold() {
	projectDownloadsThreshold.value = null
	projectDownloadsThresholdProjectIds.value = null
}

function closeProjectSelectDropdown(event: KeyboardEvent) {
	const eventTarget = event.target
	if (!(eventTarget instanceof HTMLElement)) {
		isProjectSelectOpen.value = false
		return
	}

	const dropdown = eventTarget.closest('[role="listbox"][aria-multiselectable="true"]')
	if (!dropdown) {
		isProjectSelectOpen.value = false
		return
	}

	dropdown.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
}

async function runProjectDownloadsThresholdQuery(event: KeyboardEvent) {
	commitDraftSelectedProjects()
	closeProjectSelectDropdown(event)
	await nextTick()
	await refreshAnalyticsQuery()
}

const groupByPresetOptions: Array<{
	value: AnalyticsGroupByPreset
}> = [
	{ value: '1h' },
	{ value: '6h' },
	{ value: 'day' },
	{ value: 'week' },
	{ value: 'month' },
	{ value: 'year' },
]

const selectedBreakdownLabel = computed(() => {
	if (selectedBreakdownValue.value.length === 0) {
		return formatMessage(analyticsMessages.none)
	}

	return selectedBreakdownValue.value
		.map((breakdown) => getBreakdownOptionLabel(breakdown))
		.join(' + ')
})
const selectedGroupByLabel = computed(() => {
	return formatAnalyticsGroupBySelectedLabel(selectedGroupBy.value, formatMessage)
})
const mobileSelectedBreakdownLabel = computed(() => {
	if (selectedBreakdownValue.value.length === 0) {
		return formatMessage(analyticsMessages.noBreakdown)
	}

	return formatMessage(analyticsMessages.breakdownBy, {
		breakdown: selectedBreakdownLabel.value,
	})
})
const breakdownOptions = computed<MultiSelectOption<Exclude<AnalyticsBreakdownPreset, 'none'>>[]>(
	() => {
		const selectedBreakdownSet = new Set(selectedBreakdownValue.value)
		const hasReachedBreakdownLimit =
			selectedBreakdownValue.value.length >= MAX_ANALYTICS_BREAKDOWN_PRESETS
		const options: MultiSelectOption<Exclude<AnalyticsBreakdownPreset, 'none'>>[] = []

		if (selectedProjectIds.value.length > 1) {
			options.push({
				value: 'project',
				label: formatAnalyticsBreakdownLabel('project', formatMessage),
			})
		}

		options.push(
			{
				value: 'country',
				label: formatAnalyticsBreakdownLabel('country', formatMessage),
			},
			{
				value: 'monetization',
				label: formatAnalyticsBreakdownLabel('monetization', formatMessage),
			},
			{
				value: 'user_agent',
				label: formatAnalyticsBreakdownLabel('user_agent', formatMessage),
			},
			{
				value: 'download_reason',
				label: formatAnalyticsBreakdownLabel('download_reason', formatMessage),
			},
			{
				value: 'version_id',
				label: formatAnalyticsBreakdownLabel('version_id', formatMessage),
			},
			{
				value: 'loader',
				label: formatAnalyticsBreakdownLabel('loader', formatMessage),
			},
			{
				value: 'game_version',
				label: formatAnalyticsBreakdownLabel('game_version', formatMessage),
			},
		)

		return options.map((option) => ({
			...option,
			disabled: hasReachedBreakdownLimit && !selectedBreakdownSet.has(option.value),
		}))
	},
)

function getBreakdownOptionLabel(breakdown: Exclude<AnalyticsBreakdownPreset, 'none'>): string {
	return (
		breakdownOptions.value.find((option) => option.value === breakdown)?.label ??
		formatAnalyticsBreakdownLabel(breakdown, formatMessage)
	)
}

function isRevenueHourlyGroupBy(groupBy: AnalyticsGroupByPreset): boolean {
	return groupBy === '1h' || groupBy === '6h'
}

const groupByOptions = computed<ComboboxOption<AnalyticsGroupByPreset>[]>(() => {
	const timeframeMinutes = selectedTimeframeDurationMinutes.value
	const options = groupByPresetOptions.map((option) => {
		const groupByMinutes = getAnalyticsGroupByPresetMinutes(option.value)
		const isTooCoarse = groupByMinutes >= timeframeMinutes
		const isTooFine = timeframeMinutes / groupByMinutes > MAX_ANALYTICS_TIME_SLICES
		const isRevenueHourlyGroupByOption =
			activeStat.value === 'revenue' && isRevenueHourlyGroupBy(option.value)
		const isRevenueDailyFallback = activeStat.value === 'revenue' && option.value === 'day'
		return {
			value: option.value,
			label: formatAnalyticsGroupByLabel(option.value, formatMessage),
			disabled:
				isRevenueHourlyGroupByOption || (!isRevenueDailyFallback && (isTooCoarse || isTooFine)),
		}
	})

	if (options.every((option) => option.disabled)) {
		const fallbackOption =
			options.find((option) => activeStat.value === 'revenue' && option.value === 'day') ??
			options[0]
		if (fallbackOption) {
			fallbackOption.disabled = false
		}
	}

	return options
})

watch(
	groupByOptions,
	(nextOptions) => {
		const selectedOption = nextOptions.find((option) => option.value === selectedGroupBy.value)
		if (selectedOption && !selectedOption.disabled) {
			return
		}

		const fallbackOption = nextOptions.find((option) => !option.disabled) ?? nextOptions[0]
		if (fallbackOption && selectedGroupBy.value !== fallbackOption.value) {
			selectedGroupBy.value = fallbackOption.value
		}
	},
	{ immediate: true },
)

function unique<T>(values: T[]): T[] {
	return Array.from(new Set(values))
}

function sortStrings<T extends string>(values: T[]): T[] {
	return [...values].sort((left, right) => left.localeCompare(right))
}

function includesStat(stats: readonly string[], stat: string): boolean {
	return stats.includes(stat)
}

function withBreakdownFields(
	breakdowns: readonly AnalyticsBreakdownPreset[],
	filters: AnalyticsSelectedFilters,
): {
	views: Labrinth.Analytics.v3.ProjectViewsField[]
	downloads: Labrinth.Analytics.v3.ProjectDownloadsField[]
	playtime: Labrinth.Analytics.v3.ProjectPlaytimeField[]
	revenue: Labrinth.Analytics.v3.ProjectRevenueField[]
} {
	const views: Labrinth.Analytics.v3.ProjectViewsField[] = ['project_id']
	const downloads: Labrinth.Analytics.v3.ProjectDownloadsField[] = ['project_id']
	const playtime: Labrinth.Analytics.v3.ProjectPlaytimeField[] = ['project_id']
	const revenue: Labrinth.Analytics.v3.ProjectRevenueField[] = ['project_id']
	const enabledStats = getEnabledAnalyticsStatsForState(breakdowns, filters)

	for (const breakdown of breakdowns) {
		const breakdownStats = getAnalyticsStatsForBreakdown(breakdown)

		switch (breakdown) {
			case 'project':
			case 'none':
				break
			case 'country':
				if (includesStat(breakdownStats, 'views') && includesStat(enabledStats, 'views')) {
					views.push('country')
				}
				if (includesStat(breakdownStats, 'downloads') && includesStat(enabledStats, 'downloads')) {
					downloads.push('country')
				}
				if (includesStat(breakdownStats, 'playtime') && includesStat(enabledStats, 'playtime')) {
					playtime.push('country')
				}
				break
			case 'monetization':
				if (includesStat(breakdownStats, 'views') && includesStat(enabledStats, 'views')) {
					views.push('monetized')
				}
				if (includesStat(breakdownStats, 'downloads') && includesStat(enabledStats, 'downloads')) {
					downloads.push('monetized')
				}
				break
			case 'user_agent':
				if (includesStat(breakdownStats, 'downloads') && includesStat(enabledStats, 'downloads')) {
					downloads.push('user_agent')
				}
				break
			case 'download_reason':
				if (includesStat(breakdownStats, 'downloads') && includesStat(enabledStats, 'downloads')) {
					downloads.push('reason')
				}
				break
			case 'version_id':
				if (includesStat(breakdownStats, 'downloads') && includesStat(enabledStats, 'downloads')) {
					downloads.push('version_id')
				}
				if (includesStat(breakdownStats, 'playtime') && includesStat(enabledStats, 'playtime')) {
					playtime.push('version_id')
				}
				break
			case 'loader':
				if (includesStat(breakdownStats, 'downloads') && includesStat(enabledStats, 'downloads')) {
					downloads.push('loader')
				}
				if (includesStat(breakdownStats, 'playtime') && includesStat(enabledStats, 'playtime')) {
					playtime.push('loader')
				}
				break
			case 'game_version':
				if (includesStat(breakdownStats, 'downloads') && includesStat(enabledStats, 'downloads')) {
					downloads.push('game_version')
				}
				if (includesStat(breakdownStats, 'playtime') && includesStat(enabledStats, 'playtime')) {
					playtime.push('game_version')
				}
				break
			default:
				break
		}
	}

	return {
		views: unique(views),
		downloads: unique(downloads),
		playtime: unique(playtime),
		revenue: unique(revenue),
	}
}

function getFilterValuesForStat(
	category: AnalyticsQueryFilterCategory,
	stat: AnalyticsDashboardStat,
	enabledStats: readonly AnalyticsDashboardStat[],
	values: string[],
): string[] {
	if (values.length === 0) {
		return []
	}

	const filterStats = getAnalyticsStatsForFilterCategory(category)
	return includesStat(filterStats, stat) && includesStat(enabledStats, stat)
		? sortStrings(values)
		: []
}

function getMonetizationFilterValues(
	stat: AnalyticsDashboardStat,
	enabledStats: readonly AnalyticsDashboardStat[],
	filters: AnalyticsSelectedFilters,
): boolean[] {
	const values = getFilterValuesForStat('monetization', stat, enabledStats, filters.monetization)
	const monetizedValues: boolean[] = []
	if (values.includes('monetized')) {
		monetizedValues.push(true)
	}
	if (values.includes('unmonetized')) {
		monetizedValues.push(false)
	}
	return monetizedValues
}

function getDownloadReasonFilterValues(
	enabledStats: readonly AnalyticsDashboardStat[],
	filters: AnalyticsSelectedFilters,
): Labrinth.Analytics.v3.DownloadReason[] {
	const validReasons = new Set<Labrinth.Analytics.v3.DownloadReason>([
		'standalone',
		'dependency',
		'modpack',
		'update',
	])
	return getFilterValuesForStat(
		'download_reason',
		'downloads',
		enabledStats,
		filters.download_reason,
	).filter((reason): reason is Labrinth.Analytics.v3.DownloadReason =>
		validReasons.has(reason as Labrinth.Analytics.v3.DownloadReason),
	)
}

function buildMetricFilters(
	breakdowns: readonly AnalyticsBreakdownPreset[],
	filters: AnalyticsSelectedFilters,
): {
	views: Labrinth.Analytics.v3.ProjectViewsFilters
	downloads: Labrinth.Analytics.v3.ProjectDownloadsFilters
	playtime: Labrinth.Analytics.v3.ProjectPlaytimeFilters
	revenue: Labrinth.Analytics.v3.ProjectRevenueFilters
} {
	const enabledStats = getEnabledAnalyticsStatsForState(breakdowns, filters)

	return {
		views: {
			country: getFilterValuesForStat('country', 'views', enabledStats, filters.country),
			monetized: getMonetizationFilterValues('views', enabledStats, filters),
		},
		downloads: {
			country: getFilterValuesForStat('country', 'downloads', enabledStats, filters.country),
			monetized: getMonetizationFilterValues('downloads', enabledStats, filters),
			user_agent: getFilterValuesForStat(
				'user_agent',
				'downloads',
				enabledStats,
				filters.user_agent,
			),
			reason: getDownloadReasonFilterValues(enabledStats, filters),
			version_id: getFilterValuesForStat(
				'version_id',
				'downloads',
				enabledStats,
				filters.version_id,
			),
			game_version: getFilterValuesForStat(
				'game_version',
				'downloads',
				enabledStats,
				filters.game_version,
			),
			loader: getFilterValuesForStat('loader_type', 'downloads', enabledStats, filters.loader_type),
		},
		playtime: {
			country: getFilterValuesForStat('country', 'playtime', enabledStats, filters.country),
			version_id: getFilterValuesForStat(
				'version_id',
				'playtime',
				enabledStats,
				filters.version_id,
			),
			game_version: getFilterValuesForStat(
				'game_version',
				'playtime',
				enabledStats,
				filters.game_version,
			),
			loader: getFilterValuesForStat('loader_type', 'playtime', enabledStats, filters.loader_type),
		},
		revenue: {},
	}
}

const fetchRequest = computed<Labrinth.Analytics.v3.FetchRequest>(() => {
	const rawRange = selectedTimeRange.value
	const { start, end } = ensureMinimumTimeRange(rawRange.start, rawRange.end)

	const groupByMs = getAnalyticsGroupByPresetMinutes(selectedGroupBy.value) * 60 * 1000
	const desiredSlices = Math.max(1, Math.floor((end.getTime() - start.getTime()) / groupByMs))
	const resolutionSlices = Math.min(MAX_ANALYTICS_TIME_SLICES, desiredSlices)

	const bucketBy = withBreakdownFields(selectedBreakdowns.value, selectedFilters.value)
	const filterBy = buildMetricFilters(selectedBreakdowns.value, selectedFilters.value)
	const filteredProjectIds = getProjectIdsMatchingStatusFilter(
		selectedProjectIds.value,
		projectStatusById.value,
		selectedFilters.value,
	)

	return {
		time_range: {
			start: start.toISOString(),
			end: end.toISOString(),
			resolution: {
				slices: resolutionSlices,
			},
		},
		project_ids: sortStrings(filteredProjectIds),
		return_metrics: {
			project_views: {
				bucket_by: sortStrings(bucketBy.views),
				filter_by: filterBy.views,
			},
			project_downloads: {
				bucket_by: sortStrings(bucketBy.downloads),
				filter_by: filterBy.downloads,
			},
			project_playtime: {
				bucket_by: sortStrings(bucketBy.playtime),
				filter_by: filterBy.playtime,
			},
			project_revenue: {
				bucket_by: sortStrings(bucketBy.revenue),
				filter_by: filterBy.revenue,
			},
		},
	}
})

watch(
	fetchRequest,
	(nextFetchRequest) => {
		setFetchRequest(nextFetchRequest)
	},
	{ deep: true, immediate: true },
)

defineExpose({
	fetchRequest,
})
</script>

<style lang="scss" scoped>
.checkbox-shadow {
	box-shadow: 1px 1px 2px 0 rgba(0, 0, 0, 0.08);
}
</style>
