<template>
	<NewModal ref="modal" :on-show="onShow" :on-hide="onHide" width="544px">
		<template v-if="project" #title>
			<Avatar :src="project.icon_url" :alt="project.title" class="icon" size="32px" />
			<div class="truncate text-lg font-extrabold text-contrast">
				{{ formatMessage(messages.downloadTitle, { title: project.title }) }}
			</div>
		</template>
		<template #default>
			<div v-if="project" class="mx-auto flex w-full flex-col gap-4">
				<InstallWithModrinthApp :project="project" />
				<DownloadProject
					:project="project"
					:versions="versions"
					:download-reason="downloadReason"
					:initial-game-version="initialGameVersion"
					:initial-platform="initialPlatform"
					:reset-key="downloadProjectResetKey"
					@select-game-version="selectGameVersion"
					@select-platform="selectPlatform"
					@update:selection="projectDownloadSelection = $event"
					@download="onDownload"
				/>
				<div class="flex flex-col gap-4">
					<DownloadDependencies
						:project="project"
						:selected-version="selectedVersion"
						:current-game-version="currentGameVersion"
						:current-platform="currentPlatform"
						:download-reason="downloadReason"
						@download="onDownload"
					/>
					<div v-if="additionalFiles.length > 0" class="flex flex-col gap-2">
						<h3 class="m-0 text-sm font-bold text-contrast">
							{{ formatMessage(messages.additionalFilesTitle) }}
						</h3>
						<div class="flex flex-col gap-2">
							<a
								v-for="file in additionalFiles"
								:key="file.hashes?.sha1 ?? file.filename"
								:href="getDownloadUrl(file.url)"
								:download="file.filename"
								class="grid min-h-9 grid-cols-[minmax(0,1fr)_min-content] items-center gap-2 rounded-xl bg-button-bg px-3 py-2 text-primary no-underline"
								@click="onDownload"
							>
								<span class="flex min-w-0 items-center gap-2">
									<FileIcon aria-hidden="true" class="size-5 flex-shrink-0 text-secondary" />
									<span class="min-w-0 truncate font-semibold text-contrast">
										{{ file.filename }}
									</span>
									<span
										class="rounded-full bg-button-bgSelected px-2 py-0.5 text-xs text-secondary"
									>
										{{ fileTypeLabel(file.file_type) }}
									</span>
								</span>
								<DownloadIcon aria-hidden="true" class="size-5 text-secondary" />
							</a>
						</div>
					</div>
				</div>
				<ServersPromo
					v-if="flags.showProjectPageDownloadModalServersPromo"
					:link="`/hosting#plan`"
					@close="
						() => {
							flags.showProjectPageDownloadModalServersPromo = false
							saveFeatureFlags()
						}
					"
				/>
			</div>
		</template>
	</NewModal>
</template>

<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { DownloadIcon, FileIcon } from '@modrinth/assets'
import {
	Avatar,
	type CdnDownloadReason,
	defineMessages,
	injectModrinthClient,
	NewModal,
	ServersPromo,
	useDebugLogger,
	useVIntl,
} from '@modrinth/ui'
import type { DisplayProjectType } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import { computed, nextTick, ref, watch } from 'vue'

import { navigateTo } from '#app'
import { saveFeatureFlags } from '~/composables/featureFlags.ts'
import { STALE_TIME, STALE_TIME_LONG } from '~/composables/queries/project'

import DownloadDependencies from './DownloadDependencies.vue'
import DownloadProject from './DownloadProject.vue'
import InstallWithModrinthApp from './InstallWithModrinthApp.vue'

type DownloadModalProject = Omit<Labrinth.Projects.v2.Project, 'project_type'> & {
	project_type: DisplayProjectType
	actualProjectType: Labrinth.Projects.v2.ProjectType
}

type ProjectDownloadSelection = {
	currentGameVersion: string | null
	currentPlatform: string | null
	selectedVersion: Labrinth.Versions.v3.Version | null
	selectedPrimaryFile: Labrinth.Versions.v3.VersionFile | null
}

type NewModalRef = {
	show: (event?: MouseEvent) => void
	hide: () => void
}

const props = withDefaults(
	defineProps<{
		projectId?: string
		downloadReason?: CdnDownloadReason
		useRouteHash?: boolean
	}>(),
	{
		downloadReason: 'standalone',
		useRouteHash: true,
	},
)

const emit = defineEmits<{
	download: []
}>()

const route = useRoute()
const flags = useFeatureFlags()
const tags = useGeneratedState()
const client = injectModrinthClient()
const { createProjectDownloadUrl } = useCdnDownloadContext()
const { formatMessage } = useVIntl()
const debug = useDebugLogger('DownloadModal')

const modal = ref<NewModalRef | null>(null)
const modalOpen = ref(false)
const showProjectId = ref<string | null>(null)
const downloadProjectResetKey = ref(0)
const projectDownloadSelection = ref<ProjectDownloadSelection>(getDefaultProjectDownloadSelection())

const routeProjectId = computed(() => showProjectId.value ?? props.projectId ?? null)

const {
	data: projectRaw,
	error: projectV2Error,
	refetch: refetchProject,
} = useQuery({
	queryKey: computed(() => ['project', 'v2', routeProjectId.value]),
	queryFn: () => client.labrinth.projects_v2.get(routeProjectId.value!),
	enabled: computed(() => !!routeProjectId.value),
	staleTime: STALE_TIME,
})

const resolvedProjectId = computed(() => projectRaw.value?.id)

const project = computed<DownloadModalProject | null>(() => {
	if (!projectRaw.value) return null

	return {
		...projectRaw.value,
		actualProjectType: projectRaw.value.project_type,
		project_type: getProjectTypeForUrl(projectRaw.value.project_type, projectRaw.value.loaders),
	}
})

const versionsEnabled = ref(false)
const {
	data: versionsV3,
	error: _versionsV3Error,
	isFetching: versionsV3Loading,
} = useQuery({
	queryKey: computed(() => ['project', resolvedProjectId.value, 'versions', 'v3']),
	queryFn: () =>
		client.labrinth.versions_v3.getProjectVersions(resolvedProjectId.value!, {
			include_changelog: false,
			apiVersion: 3,
		}),
	staleTime: STALE_TIME_LONG,
	enabled: computed(() => !!resolvedProjectId.value && versionsEnabled.value),
})

const versions = computed<Labrinth.Versions.v3.Version[]>(() => {
	const isModpack =
		project.value?.actualProjectType === 'modpack' || project.value?.project_type === 'modpack'

	return (versionsV3.value ?? []).map((version) => {
		const files = Array.isArray(version.files) ? version.files : []
		const gameVersions = Array.isArray(version.game_versions) ? version.game_versions : []
		const loaders = Array.isArray(version.loaders) ? version.loaders : []
		const mrpackLoaders = Array.isArray(version.mrpack_loaders) ? version.mrpack_loaders : []

		return {
			...version,
			files,
			game_versions: gameVersions,
			loaders: isModpack && mrpackLoaders.length ? mrpackLoaders : loaders,
		}
	})
})

const initialGameVersion = computed(() => {
	const version = route.query.version
	if (typeof version !== 'string' || !project.value?.game_versions.includes(version)) return null
	return version
})
const initialPlatform = computed(() => {
	const loader = route.query.loader
	if (typeof loader !== 'string' || !project.value?.loaders.includes(loader)) return null
	return loader
})

const currentGameVersion = computed(() => projectDownloadSelection.value.currentGameVersion)
const currentPlatform = computed(() => projectDownloadSelection.value.currentPlatform)
const selectedVersion = computed(() => projectDownloadSelection.value.selectedVersion)
const selectedPrimaryFile = computed(() => projectDownloadSelection.value.selectedPrimaryFile)

const additionalFiles = computed(() => {
	if (!selectedVersion.value || !selectedPrimaryFile.value) return []
	return selectedVersion.value.files.filter((file) => file !== selectedPrimaryFile.value)
})

watch(projectV2Error, (error) => {
	if (error) {
		debug('project query failed', error)
	}
})

const messages = defineMessages({
	downloadTitle: {
		id: 'project.download.title',
		defaultMessage: 'Download {title}',
	},
	additionalFilesTitle: {
		id: 'project.download.additional-files-title',
		defaultMessage: 'Additional files',
	},
})

const fileTypeLabels: Partial<Record<Labrinth.Versions.v3.FileType, string>> = {
	'required-resource-pack': 'Resourcepack',
	'optional-resource-pack': 'Resourcepack',
	unknown: 'File',
}

function fileTypeLabel(type?: Labrinth.Versions.v3.FileType) {
	return fileTypeLabels[type ?? 'unknown'] || 'File'
}

function getProjectTypeForUrl(
	type: Labrinth.Projects.v2.ProjectType,
	loaders: string[],
): DisplayProjectType {
	if (type !== 'mod') return type as DisplayProjectType

	const isMod = loaders.some((loader) => tags.value.loaderData.modLoaders.includes(loader))
	const isPlugin = loaders.some((loader) => tags.value.loaderData.allPluginLoaders.includes(loader))
	const isDataPack = loaders.some((loader) =>
		tags.value.loaderData.dataPackLoaders.includes(loader),
	)

	if (isDataPack) return 'datapack'
	if (isPlugin) return 'plugin'
	if (isMod) return 'mod'

	return 'mod'
}

function getDownloadUrl(url: string) {
	return createProjectDownloadUrl(url, {
		reason: props.downloadReason,
		gameVersion: currentGameVersion.value ?? undefined,
		loader: currentPlatform.value ?? undefined,
	})
}

function updateDownloadQuery({
	gameVersion,
	platform,
}: {
	gameVersion: string | null
	platform: string | null
}) {
	navigateTo(
		{
			query: {
				...route.query,
				...(gameVersion && {
					version: gameVersion,
				}),
				...(platform && {
					loader: platform,
				}),
			},
			hash: route.hash,
		},
		{ replace: true },
	)
}

function selectGameVersion(gameVersion: string) {
	updateDownloadQuery({
		gameVersion,
		platform: currentPlatform.value,
	})
}

function selectPlatform(platform: string) {
	updateDownloadQuery({
		gameVersion: currentGameVersion.value,
		platform,
	})
}

function onShow() {
	modalOpen.value = true
	debug('on-show fired')
	versionsEnabled.value = true
	if (props.useRouteHash && !showProjectId.value) {
		navigateTo({ query: route.query, hash: '#download' }, { replace: true })
	}
}

function onHide() {
	const hadShowProjectId = !!showProjectId.value
	modalOpen.value = false
	showProjectId.value = null
	if (props.useRouteHash && !hadShowProjectId) {
		navigateTo({ query: route.query, hash: '' }, { replace: true })
	}
}

async function show(event?: MouseEvent, projectId?: string): Promise<void> {
	if (!modal.value || modalOpen.value) return
	showProjectId.value = projectId ?? null
	await nextTick()
	if (!(await loadProjectForModal(!!projectId))) return
	modalOpen.value = true
	modal.value.show(event)
}

async function hide() {
	if (!modal.value || !modalOpen.value) return
	modal.value?.hide()
	await nextTick()
	downloadProjectResetKey.value += 1
}

function onDownload() {
	emit('download')
}

function getDefaultProjectDownloadSelection(): ProjectDownloadSelection {
	return {
		currentGameVersion: null,
		currentPlatform: null,
		selectedVersion: null,
		selectedPrimaryFile: null,
	}
}

async function loadProjectForModal(forceRefetch: boolean) {
	if (!routeProjectId.value) return false
	if (!forceRefetch && projectRaw.value) return true

	const { data } = await refetchProject()
	return !!data
}

function openFromHash() {
	if (
		!props.useRouteHash ||
		!modal.value ||
		modalOpen.value ||
		showProjectId.value ||
		route.hash !== '#download'
	) {
		return
	}

	debug('hash #download watch fired, opening modal')
	show()
}

if (
	props.useRouteHash &&
	(route.hash === '#download' ||
		route.query.version !== undefined ||
		route.query.loader !== undefined)
) {
	debug('eager loadVersions from setup', {
		hash: route.hash,
		version: route.query.version,
		loader: route.query.loader,
		loading: versionsV3Loading.value,
	})
	versionsEnabled.value = true
}

watch(modal, openFromHash)
watch(() => route.hash, openFromHash)
watch(routeProjectId, () => {
	projectDownloadSelection.value = getDefaultProjectDownloadSelection()
	downloadProjectResetKey.value += 1
})

defineExpose({ show, hide })
</script>
