package de.arxsilex.maptools.converter.desktop

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Modifier
import de.arxsilex.maptools.converter.core.ContentSelection
import de.arxsilex.maptools.converter.core.ExportSelection
import de.arxsilex.maptools.converter.core.GeoBounds
import de.arxsilex.maptools.converter.core.InputFileInfo
import de.arxsilex.maptools.converter.core.InputFileInspector
import de.arxsilex.maptools.converter.core.InputFormat
import de.arxsilex.maptools.converter.core.MapFormatVersion
import de.arxsilex.maptools.converter.core.MapSelectionShape
import de.arxsilex.maptools.converter.core.NormalizedMapArea
import de.arxsilex.maptools.converter.core.RectangleSelection
import de.arxsilex.maptools.converter.core.PoiSchemaVersion
import de.arxsilex.maptools.converter.core.toGeoRegions
import de.arxsilex.maptools.converter.core.unionOrNull
import de.arxsilex.maptools.converter.core.elevation.ElevationPreviewBuilder
import de.arxsilex.maptools.converter.core.elevation.ElevationPreviewRaster
import de.arxsilex.maptools.converter.core.elevation.ElevationMetadataReader
import de.arxsilex.maptools.converter.core.pbf.PbfHeaderReader
import de.arxsilex.maptools.converter.core.mapsforge.MapsforgeMapSource
import de.arxsilex.maptools.converter.core.preview.PreviewBuildProgress
import de.arxsilex.maptools.converter.core.preview.PreviewDataset
import de.arxsilex.maptools.converter.core.preview.PreviewDatasetCombiner
import de.arxsilex.maptools.converter.core.preview.PreviewLoadResult
import de.arxsilex.maptools.converter.core.preview.PreviewRepository
import java.io.File
import java.nio.file.Path
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext

internal enum class WorkflowStep(val textKey: String) {
    SOURCE("step.source"),
    AREA("step.area"),
    CONTENT("step.content"),
    EXPORT("step.export"),
}

internal sealed interface PreviewUiState {
    data object Unavailable : PreviewUiState
    data class Loading(
        val progress: PreviewBuildProgress?,
        val sourceIndex: Int = 1,
        val sourceCount: Int = 1,
        val elevation: Boolean = false,
    ) : PreviewUiState
    data class Ready(
        val dataset: PreviewDataset,
        val loadedFromCache: Boolean,
        val renderIndex: PreviewRenderIndex,
        val elevationPreview: ElevationPreviewRaster? = null,
        val mapSourceCount: Int = 0,
        val warnings: List<String> = emptyList(),
    ) : PreviewUiState
    data class Failed(val message: String) : PreviewUiState
}

private data class PreviewSourceFailure(
    val source: InputFileInfo,
    val exception: Exception,
)

@Composable
internal fun MapToolsConverterApp(
    initialInputs: List<File> = emptyList(),
    onChooseInputs: () -> List<File>,
    onChooseArchive: () -> List<File>,
    onChooseOutputBase: (suggestedName: String) -> File?,
    onOpenContainingDirectory: (Path) -> Unit,
    onExportCompleted: (List<Path>) -> Unit,
    onOpenExternalLink: (String) -> Unit,
    onConfirmOverwrite: (List<Path>) -> Boolean,
    onPreferencesChanged: (AppPreferences) -> Unit = {},
) {
    var preferences by remember { mutableStateOf(AppPreferencesStore.load()) }
    val strings = remember(preferences.language) { LocalizationCatalog.strings(preferences.language) }
    val measurements = remember(preferences) {
        MeasurementFormatter(preferences.measurementSystem, preferences.language)
    }
    fun updatePreferences(updated: AppPreferences) {
        preferences = updated
        runCatching { AppPreferencesStore.save(updated) }
    }
    LaunchedEffect(preferences) { onPreferencesChanged(preferences) }
    val initialInspections = remember(initialInputs.map(File::getAbsolutePath), strings.language) {
        initialInputs.map { inspectInputFile(it, strings) }
    }
    val inspectedInitialInputs = initialInspections.mapNotNull(Result<InputFileInfo>::getOrNull)
    val rejectedInitialInputs = initialInputs.zip(initialInspections).mapNotNull { (file, inspection) ->
        inspection.exceptionOrNull()?.let { inputFileRejection(file, it, strings) }
    }
    val initialSources = inspectedInitialInputs.toInputSourceSet()
    val initialPoiTarget = when (initialSources.poiSources.firstOrNull()?.mapsforgePoiMetadata?.schemaVersion) {
        PoiSchemaVersion.V4.metadataVersion -> PoiSchemaVersion.V3
        else -> PoiSchemaVersion.V4
    }
    val initialStep = when {
        inspectedInitialInputs.isNotEmpty() -> WorkflowStep.AREA
        else -> WorkflowStep.SOURCE
    }
    var currentStep by remember { mutableStateOf(initialStep) }
    var furthestVisitedStep by remember { mutableStateOf(initialStep) }
    var inputFiles by remember { mutableStateOf(inspectedInitialInputs) }
    var inputErrors by remember { mutableStateOf(rejectedInitialInputs) }
    val fullSelection: List<MapSelectionShape> = listOf(RectangleSelection(NormalizedMapArea.Full))
    var selectedAreas by remember { mutableStateOf<List<MapSelectionShape>>(emptyList()) }
    var contentSelection by remember { mutableStateOf(ContentSelection()) }
    var exportSelection by remember {
        mutableStateOf(
            ExportSelection(
                mapsforgeMap = initialSources.hasMapData,
                mapsforgePoi = initialSources.hasPoiData,
                elevationData = initialSources.hasElevationData,
                poiSchemaVersion = initialPoiTarget,
            ),
        )
    }
    var previewState by remember { mutableStateOf<PreviewUiState>(PreviewUiState.Unavailable) }
    var previewSourceKey by remember { mutableStateOf<String?>(null) }
    var overviewPreview by remember { mutableStateOf<PreviewUiState.Ready?>(null) }
    var detailRequested by remember { mutableStateOf(false) }
    var detailSources by remember { mutableStateOf<List<PreviewDetailSource>>(emptyList()) }
    var requestedDetailBounds by remember { mutableStateOf<GeoBounds?>(null) }
    var detailStatus by remember { mutableStateOf<String?>(null) }
    var exportState by remember { mutableStateOf<ExportUiState>(ExportUiState.Idle) }
    var exportJob by remember { mutableStateOf<Job?>(null) }
    val coroutineScope = rememberCoroutineScope()

    val inputSourceSet = inputFiles.toInputSourceSet()
    val combinedBounds = inputSourceSet.selectionSources.mapNotNull(InputFileInfo::bounds).unionOrNull()
    val directPoiDetailSources = inputSourceSet.poiSources.map { poi ->
        PreviewDetailSource(poi, mapPath = null, poiPath = Path.of(poi.path))
    }
    val searchableDetailSources = (detailSources + directPoiDetailSources)
        .distinctBy { it.poiPath?.toAbsolutePath()?.normalize() }
    val sourceKey = inputFiles.joinToString("|") { it.path }
    val portableStorageStatus = remember(sourceKey) {
        PlatformDirectories.conversionStorageStatus(inputFiles.sumOf(InputFileInfo::sizeBytes))
    }

    LaunchedEffect(sourceKey, currentStep) {
        if (previewSourceKey != sourceKey) {
            previewSourceKey = sourceKey
            overviewPreview = null
            detailRequested = false
            detailSources = emptyList()
            requestedDetailBounds = null
            detailStatus = null
            previewState = PreviewUiState.Unavailable
        }
        if (currentStep != WorkflowStep.AREA) return@LaunchedEffect
        overviewPreview?.let { ready ->
            previewState = ready
            return@LaunchedEffect
        }
        val currentInputs = inputFiles
        val currentSources = currentInputs.toInputSourceSet()
        val bounds = currentSources.selectionSources.mapNotNull(InputFileInfo::bounds).unionOrNull()
        if (currentInputs.isEmpty() || bounds == null) {
            previewState = PreviewUiState.Unavailable
        } else {
            if (currentSources.previewSources.isEmpty() && currentSources.elevationSources.isEmpty()) {
                previewState = PreviewUiState.Unavailable
                return@LaunchedEffect
            }
            val previewSourceCount = currentSources.previewSources.size + currentSources.elevationSources.size
            previewState = PreviewUiState.Loading(
                progress = null,
                sourceCount = previewSourceCount,
                elevation = currentSources.previewSources.isEmpty(),
            )
            val parentJob = currentCoroutineContext()[Job]
            try {
                val loadedPreviewState = withContext(Dispatchers.IO) {
                    val cacheDirectory = PlatformDirectories.previewCacheDirectory()
                    val results = mutableListOf<PreviewLoadResult>()
                    val failures = mutableListOf<PreviewSourceFailure>()
                    currentSources.previewSources.forEachIndexed { index, currentInput ->
                        val progressListener: (PreviewBuildProgress) -> Unit = { progress ->
                            Snapshot.withMutableSnapshot {
                                previewState = PreviewUiState.Loading(
                                    progress,
                                    sourceIndex = index + 1,
                                    sourceCount = previewSourceCount,
                                )
                            }
                        }
                        try {
                            results += when (currentInput.format) {
                                InputFormat.OSM_PBF -> PreviewRepository.loadOrBuild(
                                    source = Path.of(currentInput.path),
                                    bounds = requireNotNull(currentInput.bounds),
                                    cacheDirectory = cacheDirectory,
                                    onProgress = progressListener,
                                    isCancelled = { parentJob?.isActive == false },
                                )
                                InputFormat.MAPSFORGE_MAP -> MapsforgeMapSource.loadOrBuildPreview(
                                    source = Path.of(currentInput.path),
                                    bounds = requireNotNull(currentInput.bounds),
                                    cacheDirectory = cacheDirectory,
                                    onProgress = progressListener,
                                    isCancelled = { parentJob?.isActive == false },
                                )
                                InputFormat.MAPSFORGE_POI -> PreviewLoadResult(
                                    dataset = MapsforgePoiSource.loadPreview(
                                        Path.of(currentInput.path),
                                        requireNotNull(currentInput.bounds),
                                    ),
                                    loadedFromCache = false,
                                )
                                else -> error(strings("error.format_unsupported", currentInput.format.displayName))
                            }
                        } catch (exception: CancellationException) {
                            throw exception
                        } catch (exception: Exception) {
                            failures += PreviewSourceFailure(currentInput, exception)
                            PreviewProblemLog.append(currentInput, exception)
                        }
                    }
                    val elevationPreview = currentSources.elevationSources.takeIf { it.isNotEmpty() }?.let { sources ->
                        val sourceIndex = currentSources.previewSources.size + 1
                        Snapshot.withMutableSnapshot {
                            previewState = PreviewUiState.Loading(
                                progress = null,
                                sourceIndex = sourceIndex,
                                sourceCount = previewSourceCount,
                                elevation = true,
                            )
                        }
                        try {
                            ElevationPreviewBuilder.build(
                                inputs = sources.map { Path.of(it.path) },
                                requestedBounds = bounds,
                                isCancelled = { parentJob?.isActive == false },
                            )
                        } catch (exception: CancellationException) {
                            throw exception
                        } catch (exception: Exception) {
                            sources.forEach { source -> failures += PreviewSourceFailure(source, exception) }
                            sources.forEach { source -> PreviewProblemLog.append(source, exception) }
                            null
                        }
                    }
                    if (results.isEmpty() && elevationPreview == null) {
                        PreviewUiState.Failed(previewFailureSummary(failures, strings))
                    } else {
                        val combined = if (results.isEmpty()) {
                            PreviewDataset(
                                bounds = bounds,
                                points = emptyList(),
                                polylines = emptyList(),
                                sourceNodeCount = 0,
                                sourceWayCount = 0,
                            )
                        } else {
                            PreviewDatasetCombiner.combine(bounds, results.map { it.dataset })
                        }
                        val warnings = buildList {
                            addAll(results.mapNotNull { it.cacheWarning })
                            addAll(failures.map { failure -> previewFailureSummary(listOf(failure), strings) })
                        }
                        PreviewUiState.Ready(
                            dataset = combined,
                            loadedFromCache = results.isNotEmpty() && results.all { it.loadedFromCache },
                            renderIndex = PreviewRenderIndex.build(combined),
                            elevationPreview = elevationPreview,
                            mapSourceCount = currentSources.previewSources.size,
                            warnings = warnings,
                        )
                    }
                }
                previewState = loadedPreviewState
                overviewPreview = loadedPreviewState as? PreviewUiState.Ready
            } catch (exception: CancellationException) {
                throw exception
            } catch (exception: Exception) {
                PreviewProblemLog.append(null, exception)
                previewState = PreviewUiState.Failed(
                    exception.message ?: strings["error.preview_unknown"],
                )
            }
        }
    }

    LaunchedEffect(sourceKey, detailRequested) {
        val currentInputs = inputSourceSet.previewSources
        if (!detailRequested || currentInputs.isEmpty()) return@LaunchedEffect
        val parentJob = currentCoroutineContext()[Job]
        detailStatus = strings["detail.preparing"]
        try {
            detailSources = withContext(Dispatchers.IO) {
                PreviewDetailCache.prepare(
                    currentInputs,
                    strings = strings,
                    onStatus = { message -> Snapshot.withMutableSnapshot { detailStatus = message } },
                    isCancelled = { parentJob?.isActive == false },
                )
            }
            detailStatus = strings["detail.ready"]
        } catch (exception: CancellationException) {
            throw exception
        } catch (exception: Exception) {
            detailStatus = strings("detail.unavailable", exception.message ?: exception.javaClass.simpleName)
            PreviewProblemLog.append(null, exception)
        }
    }

    LaunchedEffect(sourceKey, detailSources, requestedDetailBounds) {
        val visibleBounds = requestedDetailBounds ?: return@LaunchedEffect
        val base = overviewPreview ?: return@LaunchedEffect
        if (detailSources.size != inputSourceSet.previewSources.size) return@LaunchedEffect
        delay(220)
        detailStatus = strings["detail.loading"]
        try {
            val details = withContext(Dispatchers.IO) {
                detailSources.filter { source -> source.input.bounds?.intersects(visibleBounds) != false }.flatMap { source ->
                    buildList {
                        source.mapPath?.let { mapPath ->
                            add(MapsforgeMapSource.loadDetailedPreview(mapPath, visibleBounds, requireNotNull(combinedBounds)))
                        }
                        if (source.input.format == InputFormat.MAPSFORGE_POI) {
                            source.poiPath?.let { poiPath ->
                                add(MapsforgePoiSource.loadPreview(poiPath, visibleBounds, limit = 120_000))
                            }
                        }
                    }
                }
            }
            val combined = PreviewDatasetCombiner.combine(
                requireNotNull(combinedBounds),
                listOf(base.dataset) + details,
            )
            previewState = base.copy(dataset = combined, renderIndex = PreviewRenderIndex.build(combined))
            detailStatus = strings["detail.loaded"]
        } catch (exception: CancellationException) {
            throw exception
        } catch (exception: Exception) {
            detailStatus = strings("detail.failed", exception.message ?: exception.javaClass.simpleName)
            PreviewProblemLog.append(null, exception)
        }
    }

    fun navigateTo(step: WorkflowStep) {
        currentStep = step
        if (step.ordinal > furthestVisitedStep.ordinal) {
            furthestVisitedStep = step
        }
    }

    fun addChosenInputs(chosen: List<File>) {
        if (chosen.isEmpty()) return
        val inspections = chosen.map { inspectInputFile(it, strings) }
        val knownPaths = inputFiles.mapTo(HashSet()) { Path.of(it.path).toAbsolutePath().normalize() }
        val additions = mutableListOf<InputFileInfo>()
        val rejections = mutableListOf<InputFileRejection>()
        chosen.zip(inspections).forEach { (file, inspection) ->
            val inspected = inspection.getOrNull()
            if (inspected == null) {
                rejections += inputFileRejection(file, requireNotNull(inspection.exceptionOrNull()), strings)
            } else if (!knownPaths.add(Path.of(inspected.path).toAbsolutePath().normalize())) {
                rejections += InputFileRejection(file.name, file.absolutePath, strings["error.source_duplicate"])
            } else {
                additions += inspected
            }
        }
        val acceptedAdditions = additions
        val previousSources = inputFiles.toInputSourceSet()
        inputFiles = inputFiles + acceptedAdditions
        val resultingSources = inputFiles.toInputSourceSet()
        if (acceptedAdditions.any { it.format == InputFormat.MAPSFORGE_MAP }) {
            exportSelection = exportSelection.copy(filteredPbf = false)
        }
        exportSelection = exportSelection.copy(
            mapsforgeMap = resultingSources.hasMapData && (exportSelection.mapsforgeMap || !previousSources.hasMapData),
            mapsforgePoi = resultingSources.hasPoiData && (exportSelection.mapsforgePoi || !previousSources.hasPoiData),
            filteredPbf = exportSelection.filteredPbf && resultingSources.mapSources.isNotEmpty() &&
                resultingSources.mapSources.all { it.format == InputFormat.OSM_PBF },
            elevationData = resultingSources.hasElevationData &&
                (exportSelection.elevationData || !previousSources.hasElevationData),
        )
        resultingSources.poiSources.firstOrNull()?.let { poi ->
            val sourceSchema = requireNotNull(poi.mapsforgePoiMetadata).schemaVersion
            exportSelection = exportSelection.copy(
                mapsforgePoi = true,
                filteredPbf = false,
                poiSchemaVersion = if (sourceSchema == PoiSchemaVersion.V4.metadataVersion) {
                    PoiSchemaVersion.V3
                } else {
                    PoiSchemaVersion.V4
                },
            )
        }
        selectedAreas = emptyList()
        inputErrors = rejections
        exportState = ExportUiState.Idle
        furthestVisitedStep = WorkflowStep.SOURCE
    }

    CompositionLocalProvider(
        LocalAppStrings provides strings,
        LocalMeasurements provides measurements,
    ) {
    MapToolsConverterTheme {
        Surface(modifier = Modifier.fillMaxSize()) {
            Column(modifier = Modifier.fillMaxSize()) {
                WorkflowHeader(
                    currentStep = currentStep,
                    furthestVisitedStep = furthestVisitedStep,
                    onStepSelected = ::navigateTo,
                    onOpenExternalLink = onOpenExternalLink,
                    language = preferences.language,
                    measurementSystem = preferences.measurementSystem,
                    onLanguageChange = { updatePreferences(preferences.copy(language = it)) },
                    onMeasurementSystemChange = {
                        updatePreferences(preferences.copy(measurementSystem = it))
                    },
                )
                HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
                when (currentStep) {
                        WorkflowStep.SOURCE -> SourceStep(
                            inputFiles = inputFiles,
                            inputErrors = inputErrors,
                            portableStorageStatus = portableStorageStatus,
                            onChooseInputs = { addChosenInputs(onChooseInputs()) },
                            onChooseArchive = { addChosenInputs(onChooseArchive()) },
                            onRemoveInput = { removed ->
                                inputFiles = inputFiles.filterNot { it.path == removed.path }
                                val remainingSources = inputFiles.toInputSourceSet()
                                exportSelection = exportSelection.copy(
                                    mapsforgeMap = exportSelection.mapsforgeMap && remainingSources.hasMapData,
                                    mapsforgePoi = exportSelection.mapsforgePoi && remainingSources.hasPoiData,
                                    filteredPbf = exportSelection.filteredPbf &&
                                        remainingSources.mapSources.isNotEmpty() &&
                                        remainingSources.mapSources.all { it.format == InputFormat.OSM_PBF },
                                    elevationData = exportSelection.elevationData && remainingSources.hasElevationData,
                                )
                                exportState = ExportUiState.Idle
                                selectedAreas = emptyList()
                                furthestVisitedStep = WorkflowStep.SOURCE
                            },
                            onNext = {
                                navigateTo(WorkflowStep.AREA)
                            },
                            onConvertWholeFile = {
                                selectedAreas = fullSelection
                                if (!inputSourceSet.hasMapData) {
                                    exportSelection = exportSelection.copy(
                                        mapsforgeMap = false,
                                        mapsforgePoi = inputSourceSet.hasPoiData,
                                        filteredPbf = false,
                                        elevationData = inputSourceSet.hasElevationData,
                                    )
                                } else {
                                    inputFiles.singleOrNull()?.mapsforgeMetadata?.fileVersion?.let { sourceVersion ->
                                        exportSelection = exportSelection.copy(
                                            mapFormatVersion = if (sourceVersion == MapFormatVersion.V5.metadataVersion) {
                                                MapFormatVersion.V4
                                            } else {
                                                MapFormatVersion.V5
                                            },
                                        )
                                    }
                                }
                                navigateTo(WorkflowStep.CONTENT)
                            },
                        )

                        WorkflowStep.AREA -> AreaSelectionStep(
                            inputFiles = inputFiles,
                            sourceBounds = requireNotNull(combinedBounds),
                            previewState = previewState,
                            selectedAreas = selectedAreas,
                            onAreasChange = { selectedAreas = it },
                            detailStatus = detailStatus,
                            detailSources = searchableDetailSources,
                            onDetailRequest = { bounds, _ ->
                                requestedDetailBounds = bounds
                                detailRequested = true
                            },
                            onBack = { navigateTo(WorkflowStep.SOURCE) },
                            onNext = { if (selectedAreas.isNotEmpty()) navigateTo(WorkflowStep.CONTENT) },
                        )

                        WorkflowStep.CONTENT -> ContentStep(
                            contentSelection = contentSelection,
                            exportSelection = exportSelection,
                            rawPbfOutputAvailable = inputSourceSet.mapSources.isNotEmpty() &&
                                inputSourceSet.mapSources.all { it.format == InputFormat.OSM_PBF },
                            hasMapSource = inputSourceSet.hasMapData,
                            hasPoiSource = inputSourceSet.poiSources.isNotEmpty(),
                            hasElevationSource = inputSourceSet.hasElevationData,
                            hasPoiCompanion = inputSourceSet.hasPoiCompanion,
                            sourceMapVersion = inputSourceSet.mapSources.singleOrNull()?.mapsforgeMetadata?.fileVersion,
                            onContentSelectionChange = { contentSelection = it },
                            onExportSelectionChange = { exportSelection = it },
                            onBack = {
                                navigateTo(WorkflowStep.AREA)
                            },
                            onNext = { navigateTo(WorkflowStep.EXPORT) },
                        )

                        WorkflowStep.EXPORT -> ExportStep(
                            inputFiles = inputFiles,
                            sourceBounds = requireNotNull(combinedBounds),
                            selectedAreas = selectedAreas,
                            contentSelection = contentSelection,
                            exportSelection = exportSelection,
                            exportState = exportState,
                            onBack = { navigateTo(WorkflowStep.CONTENT) },
                            onOpenContainingDirectory = onOpenContainingDirectory,
                            onStartExport = export@{
                                val sources = inputFiles
                                val sourceSet = sources.toInputSourceSet()
                                val mapSources = sourceSet.mapSources
                                val companionPoiSources = sourceSet.poiSources
                                val elevationSources = sourceSet.elevationSources
                                if (selectedAreas.isEmpty()) return@export
                                val fullFileSelection = selectedAreas == fullSelection
                                val sourceBounds = sourceSet.selectionSources.mapNotNull(InputFileInfo::bounds).unionOrNull()
                                    ?: return@export
                                val needsConversionCache = sources.size > 1 || !fullFileSelection ||
                                    sources.any {
                                        it.format == InputFormat.MAPSFORGE_MAP || it.format == InputFormat.MAPSFORGE_POI
                                    }
                                if (needsConversionCache) {
                                    val storage = PlatformDirectories.conversionStorageStatus(
                                        sources.sumOf(InputFileInfo::sizeBytes),
                                    )
                                    if (!storage.hasEnoughSpace) {
                                        exportState = ExportUiState.Failed(storage.insufficientSpaceMessage(strings))
                                        return@export
                                    }
                                }
                                val geographicAreas = selectedAreas.toGeoRegions(sourceBounds)
                                val geographicBounds = requireNotNull(geographicAreas.map { it.bounds }.unionOrNull())
                                val namingSource = mapSources.singleOrNull() ?: companionPoiSources.firstOrNull()
                                    ?: elevationSources.singleOrNull()
                                val baseName = if (namingSource != null) {
                                    sourceNameWithoutKnownExtension(namingSource.name)
                                } else {
                                    "kombinierte-karte"
                                }
                                val selectedBase = onChooseOutputBase(
                                    ExportTargets.suggestedBaseName(
                                        sourceBaseName = baseName,
                                        fullFile = fullFileSelection,
                                        selection = exportSelection,
                                    ),
                                )
                                    ?: return@export
                                val targets = ExportTargets.fromSelectedBase(
                                    selectedBase.toPath(),
                                    exportSelection,
                                )
                                val sourceConflicts = targets.sourceConflicts(sources.map { Path.of(it.path) })
                                if (sourceConflicts.isNotEmpty()) {
                                    exportState = ExportUiState.Failed(
                                        strings(
                                            "error.output_equals_source",
                                            sourceConflicts.joinToString { it.fileName.toString() },
                                        ),
                                    )
                                    return@export
                                }
                                val conflicts = targets.all.filter { java.nio.file.Files.exists(it) }
                                if (conflicts.isNotEmpty()) {
                                    if (!onConfirmOverwrite(conflicts)) return@export
                                    runCatching { conflicts.forEach(::deleteExportTarget) }
                                        .onFailure { exception ->
                                            exportState = ExportUiState.Failed(
                                                strings("error.replace_target", exception.message ?: strings["dialog.unknown_error"]),
                                            )
                                        }
                                        .getOrElse { return@export }
                                }
                                exportState = ExportUiState.Running(strings["progress.worker_starting"])
                                exportJob = coroutineScope.launch {
                                    val currentJob = currentCoroutineContext()[Job]
                                    var temporaryCropDirectory: Path? = null
                                    var temporaryMapsforgeInput: Path? = null
                                    val temporarySourceInputs = mutableListOf<Path>()
                                    try {
                                        val completedOutputs = linkedMapOf<Path, Long>()
                                        val logs = mutableListOf<Path>()
                                        val needsMapData = targets.filteredPbf != null || targets.mapsforgeMap != null ||
                                            (targets.mapsforgePoi != null && companionPoiSources.isEmpty())
                                        val relevantSources = if (needsMapData) {
                                            mapSources.filter { source ->
                                                source.bounds?.let { bounds ->
                                                    geographicAreas.any { bounds.intersects(it.bounds) }
                                                } != false
                                            }
                                        } else {
                                            emptyList()
                                        }
                                        val mapsforgeSourceCount = relevantSources.count {
                                            it.format == InputFormat.MAPSFORGE_MAP
                                        }
                                        val preparesMapData = needsMapData && (
                                            targets.filteredPbf != null || relevantSources.size != 1 ||
                                                relevantSources.singleOrNull()?.format != InputFormat.OSM_PBF ||
                                                !fullFileSelection
                                            )
                                        val progressTracker = ExportProgressTracker(
                                            plan = ExportProgressPlan.create(
                                                mapsforgeSourceCount = mapsforgeSourceCount,
                                                preparesMapData = preparesMapData,
                                                writesMap = targets.mapsforgeMap != null,
                                                writesPoi = targets.mapsforgePoi != null,
                                                writesElevation = targets.elevationData != null,
                                            ),
                                            strings = strings,
                                            onUpdate = { progress ->
                                                Snapshot.withMutableSnapshot {
                                                    exportState = ExportUiState.Running(
                                                        progress.message,
                                                        progress.progress,
                                                        progress.phase,
                                                    )
                                                }
                                            },
                                        )
                                        val prepareProgress: (String) -> Unit = if (preparesMapData) {
                                            progressTracker.listener("prepare")
                                        } else {
                                            { _ -> }
                                        }
                                        val effectiveSourcePaths = if (!needsMapData) {
                                            emptyList()
                                        } else if (relevantSources.any {
                                                it.format == InputFormat.MAPSFORGE_MAP
                                            }
                                        ) {
                                            val temporaryDirectory = withContext(Dispatchers.IO) {
                                                java.nio.file.Files.createTempDirectory(
                                                    PlatformDirectories.sessionTemporaryDirectory(),
                                                    "export-",
                                                )
                                            }
                                            temporaryCropDirectory = temporaryDirectory
                                            var mapsforgeSourceOrdinal = 0
                                            relevantSources.mapIndexed { index, source ->
                                                if (source.format == InputFormat.OSM_PBF) {
                                                    Path.of(source.path)
                                                } else {
                                                    val sourceProgress = progressTracker.listener(
                                                        "source-${mapsforgeSourceOrdinal++}",
                                                    )
                                                    val materialized = temporaryDirectory.resolve(
                                                        "mapsforge-${index.toString().padStart(3, '0')}.osm.pbf",
                                                    )
                                                    val result = withContext(Dispatchers.IO) {
                                                        WorkerLauncher.materializeMapsforgeSource(
                                                            input = Path.of(source.path),
                                                            output = materialized,
                                                            bounds = geographicBounds,
                                                            namespace = index,
                                                            areas = geographicAreas,
                                                            onLogLine = sourceProgress,
                                                            isCancelled = { currentJob?.isActive == false },
                                                        )
                                                    }
                                                    logs.add(result.logPath)
                                                    temporarySourceInputs.add(materialized)
                                                    materialized
                                                }
                                            }
                                        } else {
                                            relevantSources.map { Path.of(it.path) }
                                        }
                                        require(!needsMapData || effectiveSourcePaths.isNotEmpty()) {
                                            strings["error.no_source_overlap"]
                                        }
                                        targets.filteredPbf?.let { pbfOutput ->
                                            val result = withContext(Dispatchers.IO) {
                                                if (effectiveSourcePaths.size == 1) {
                                                    WorkerLauncher.cropPbf(
                                                        input = effectiveSourcePaths.single(),
                                                        output = pbfOutput,
                                                        bounds = geographicBounds,
                                                        areas = geographicAreas,
                                                        onLogLine = prepareProgress,
                                                        isCancelled = { currentJob?.isActive == false },
                                                    )
                                                } else {
                                                    WorkerLauncher.combinePbf(
                                                        inputs = effectiveSourcePaths,
                                                        output = pbfOutput,
                                                        bounds = geographicBounds,
                                                        areas = geographicAreas,
                                                        preCroppedInputs = temporarySourceInputs.toSet(),
                                                        onLogLine = prepareProgress,
                                                        isCancelled = { currentJob?.isActive == false },
                                                    )
                                                }
                                            }
                                            completedOutputs += result.outputs
                                            logs.add(result.logPath)
                                            withContext(Dispatchers.IO) { PbfHeaderReader.read(pbfOutput) }
                                            currentCoroutineContext().ensureActive()
                                        }
                                        if (needsMapData && (targets.mapsforgeMap != null || targets.mapsforgePoi != null)) {
                                            val mapsforgeInput = targets.filteredPbf ?: if (
                                                effectiveSourcePaths.size == 1 &&
                                                relevantSources.singleOrNull()?.format == InputFormat.OSM_PBF &&
                                                fullFileSelection
                                            ) {
                                                effectiveSourcePaths.single()
                                            } else if (
                                                effectiveSourcePaths.size == 1 &&
                                                relevantSources.singleOrNull()?.format == InputFormat.MAPSFORGE_MAP
                                            ) {
                                                effectiveSourcePaths.single()
                                            } else {
                                                val temporaryDirectory = temporaryCropDirectory ?: withContext(Dispatchers.IO) {
                                                    java.nio.file.Files.createTempDirectory(
                                                        PlatformDirectories.sessionTemporaryDirectory(),
                                                        "export-",
                                                    ).also { temporaryCropDirectory = it }
                                                }
                                                temporaryCropDirectory = temporaryDirectory
                                                val temporaryInput = temporaryDirectory.resolve("ausschnitt.osm.pbf")
                                                temporaryMapsforgeInput = temporaryInput
                                                val cropResult = withContext(Dispatchers.IO) {
                                                    if (effectiveSourcePaths.size == 1) {
                                                        WorkerLauncher.cropPbf(
                                                            input = effectiveSourcePaths.single(),
                                                            output = temporaryInput,
                                                            bounds = geographicBounds,
                                                            areas = geographicAreas,
                                                            onLogLine = prepareProgress,
                                                            isCancelled = { currentJob?.isActive == false },
                                                        )
                                                    } else {
                                                        WorkerLauncher.combinePbf(
                                                            inputs = effectiveSourcePaths,
                                                            output = temporaryInput,
                                                            bounds = geographicBounds,
                                                            areas = geographicAreas,
                                                            preCroppedInputs = temporarySourceInputs.toSet(),
                                                            onLogLine = prepareProgress,
                                                            isCancelled = { currentJob?.isActive == false },
                                                        )
                                                    }
                                                }
                                                logs.add(cropResult.logPath)
                                                withContext(Dispatchers.IO) { PbfHeaderReader.read(temporaryInput) }
                                                currentCoroutineContext().ensureActive()
                                                temporaryInput
                                            }
                                            val generatedPoiOutput = targets.mapsforgePoi.takeIf {
                                                companionPoiSources.isEmpty()
                                            }
                                            val mapProgress = targets.mapsforgeMap?.let {
                                                progressTracker.listener("map")
                                            }
                                            val poiProgress = generatedPoiOutput?.let {
                                                progressTracker.listener("poi")
                                            }
                                            val preferDiskProcessor = relevantSources.any {
                                                it.format == InputFormat.MAPSFORGE_MAP
                                            }
                                            val resourceProfile = ExportResourcePolicy.detect()
                                            val runOutputsInParallel = targets.mapsforgeMap != null &&
                                                generatedPoiOutput != null &&
                                                resourceProfile.parallelMapsforgeOutputs
                                            val results = if (runOutputsInParallel) {
                                                coroutineScope {
                                                    val mapResult = async(Dispatchers.IO) {
                                                        WorkerLauncher.mapsforge(
                                                            input = mapsforgeInput,
                                                            mapOutput = targets.mapsforgeMap,
                                                            poiOutput = null,
                                                            bounds = geographicBounds,
                                                            content = contentSelection,
                                                            mapFormatVersion = exportSelection.mapFormatVersion,
                                                            poiSchemaVersion = exportSelection.poiSchemaVersion,
                                                            preferDiskProcessor = preferDiskProcessor,
                                                            onLogLine = requireNotNull(mapProgress),
                                                            isCancelled = { currentJob?.isActive == false },
                                                        )
                                                    }
                                                    val poiResult = async(Dispatchers.IO) {
                                                        WorkerLauncher.mapsforge(
                                                            input = mapsforgeInput,
                                                            mapOutput = null,
                                                            poiOutput = generatedPoiOutput,
                                                            bounds = geographicBounds,
                                                            content = contentSelection,
                                                            mapFormatVersion = exportSelection.mapFormatVersion,
                                                            poiSchemaVersion = exportSelection.poiSchemaVersion,
                                                            preferDiskProcessor = false,
                                                            onLogLine = requireNotNull(poiProgress),
                                                            isCancelled = { currentJob?.isActive == false },
                                                        )
                                                    }
                                                    listOf(mapResult.await(), poiResult.await())
                                                }
                                            } else {
                                                val defaultProgress = mapProgress ?: requireNotNull(poiProgress)
                                                listOf(
                                                    withContext(Dispatchers.IO) {
                                                        WorkerLauncher.mapsforge(
                                                            input = mapsforgeInput,
                                                            mapOutput = targets.mapsforgeMap,
                                                            poiOutput = generatedPoiOutput,
                                                            bounds = geographicBounds,
                                                            content = contentSelection,
                                                            mapFormatVersion = exportSelection.mapFormatVersion,
                                                            poiSchemaVersion = exportSelection.poiSchemaVersion,
                                                            preferDiskProcessor = preferDiskProcessor,
                                                            onLogLine = defaultProgress,
                                                            isCancelled = { currentJob?.isActive == false },
                                                        )
                                                    },
                                                )
                                            }
                                            results.forEach { result ->
                                                completedOutputs += result.outputs
                                                logs.add(result.logPath)
                                            }
                                            currentCoroutineContext().ensureActive()
                                        }
                                        if (targets.mapsforgePoi != null && companionPoiSources.isNotEmpty()) {
                                            val poiInputs = companionPoiSources.map { poiSource ->
                                                val sourceSchema = PoiSchemaVersion.entries.single {
                                                    it.metadataVersion == requireNotNull(
                                                        poiSource.mapsforgePoiMetadata,
                                                    ).schemaVersion
                                                }
                                                Path.of(poiSource.path) to sourceSchema
                                            }
                                            val poiResult = withContext(Dispatchers.IO) {
                                                WorkerLauncher.mergePoi(
                                                    sources = poiInputs,
                                                    output = targets.mapsforgePoi,
                                                    targetSchema = exportSelection.poiSchemaVersion,
                                                    areas = geographicAreas,
                                                    onLogLine = progressTracker.listener("poi"),
                                                    isCancelled = { currentJob?.isActive == false },
                                                )
                                            }
                                            completedOutputs += poiResult.outputs
                                            logs.add(poiResult.logPath)
                                            currentCoroutineContext().ensureActive()
                                        }
                                        targets.elevationData?.let { elevationTarget ->
                                            require(elevationSources.isNotEmpty()) {
                                                "Für die gewählte Höhendatenausgabe fehlt eine Höhendatenquelle."
                                            }
                                            val elevationResult = withContext(Dispatchers.IO) {
                                                WorkerLauncher.elevation(
                                                    inputs = elevationSources.map { Path.of(it.path) },
                                                    output = elevationTarget,
                                                    outputFormat = exportSelection.elevationOutputFormat,
                                                    areas = geographicAreas,
                                                    onLogLine = progressTracker.listener("elevation"),
                                                    isCancelled = { currentJob?.isActive == false },
                                                )
                                            }
                                            completedOutputs += elevationResult.outputs
                                            logs.add(elevationResult.logPath)
                                            currentCoroutineContext().ensureActive()
                                        }
                                        progressTracker.start("validate", strings["phase.validation"])
                                        withContext(Dispatchers.IO) {
                                            targets.mapsforgeMap?.let {
                                                ExportOutputValidator.validateMapsforgeMap(
                                                    it,
                                                    geographicBounds,
                                                    exportSelection.mapFormatVersion,
                                                )
                                            }
                                            targets.mapsforgePoi?.let {
                                                ExportOutputValidator.validateMapsforgePoi(
                                                    it,
                                                    exportSelection.poiSchemaVersion,
                                                )
                                            }
                                            targets.elevationData?.let {
                                                ExportOutputValidator.validateElevation(
                                                    it,
                                                    exportSelection.elevationOutputFormat,
                                                )
                                            }
                                        }
                                        progressTracker.complete("validate")
                                        val exportedOutputs =
                                            completedOutputs.map { (path, size) -> ExportedOutput(path, size) }
                                        exportState = ExportUiState.Completed(
                                            exportedOutputs,
                                            logs,
                                        )
                                        onExportCompleted(exportedOutputs.map(ExportedOutput::path))
                                    } catch (exception: CancellationException) {
                                        exportState = ExportUiState.Idle
                                        throw exception
                                    } catch (exception: WorkerExecutionException) {
                                        exportState = ExportUiState.Failed(
                                            exception.message ?: strings["error.worker_unknown"],
                                            listOf(exception.logPath),
                                        )
                                    } catch (exception: Exception) {
                                        exportState = ExportUiState.Failed(
                                            exception.message ?: strings["error.worker_unknown"],
                                        )
                                    } finally {
                                        withContext(NonCancellable + Dispatchers.IO) {
                                            runCatching {
                                                temporaryMapsforgeInput?.let { temporaryInput ->
                                                    java.nio.file.Files.deleteIfExists(
                                                        temporaryInput.resolveSibling("${temporaryInput.fileName}.partial"),
                                                    )
                                                    java.nio.file.Files.deleteIfExists(temporaryInput)
                                                }
                                                temporarySourceInputs.forEach { temporaryInput ->
                                                    java.nio.file.Files.deleteIfExists(
                                                        temporaryInput.resolveSibling("${temporaryInput.fileName}.partial"),
                                                    )
                                                    java.nio.file.Files.deleteIfExists(temporaryInput)
                                                }
                                                temporaryCropDirectory?.let { directory ->
                                                    java.nio.file.Files.deleteIfExists(directory)
                                                }
                                            }
                                        }
                                        exportJob = null
                                    }
                                }
                            },
                            onCancelExport = { exportJob?.cancel() },
                        )
                }
            }
        }
    }
    }
}

private fun previewFailureSummary(failures: List<PreviewSourceFailure>, strings: AppStrings): String {
    if (failures.isEmpty()) return strings["error.preview_unknown"]
    return failures.joinToString(separator = " · ") { failure ->
        val cause = generateSequence<Throwable>(failure.exception) { it.cause }.last()
        "${failure.source.name}: ${cause.message ?: cause.javaClass.simpleName}"
    }
}

private fun sourceNameWithoutKnownExtension(name: String): String {
    val suffix = listOf(
        ".osm.pbf", ".geotiff", ".tiff", ".pbf", ".map", ".poi", ".tif", ".asc", ".hgt", ".dt0", ".dt1", ".dt2",
    ).firstOrNull { name.endsWith(it, ignoreCase = true) }
    return suffix?.let { name.dropLast(it.length) }?.takeIf(String::isNotBlank) ?: name
}

private fun deleteExportTarget(path: Path) {
    if (!java.nio.file.Files.exists(path)) return
    if (java.nio.file.Files.isDirectory(path)) {
        java.nio.file.Files.walk(path).use { paths ->
            paths.sorted(java.util.Comparator.reverseOrder()).forEach(java.nio.file.Files::deleteIfExists)
        }
    } else {
        java.nio.file.Files.delete(path)
    }
}

private fun PortableStorageStatus.insufficientSpaceMessage(strings: AppStrings): String = strings(
    "error.storage_insufficient",
    formatFileSize(requiredBytes),
    formatFileSize(usableBytes),
    directory,
)

internal fun inspectInputFile(
    file: File,
    strings: AppStrings = LocalizationCatalog.strings(AppLanguage.GERMAN),
): Result<InputFileInfo> = runCatching {
    val inspected = InputFileInspector.inspect(
        path = file.absolutePath,
        name = file.name,
        sizeBytes = file.length(),
    ) ?: run {
        error(strings["error.unknown_format"])
    }
    when (inspected.format) {
        InputFormat.OSM_PBF -> inspected.copy(pbfMetadata = PbfHeaderReader.read(file.toPath()))
        InputFormat.MAPSFORGE_MAP -> inspected.copy(mapsforgeMetadata = MapsforgeMapSource.readMetadata(file.toPath()))
        InputFormat.MAPSFORGE_POI -> inspected.copy(mapsforgePoiMetadata = MapsforgePoiSource.readMetadata(file.toPath()))
        InputFormat.ELEVATION_HGT,
        InputFormat.ELEVATION_GEOTIFF,
        InputFormat.ELEVATION_ASC,
        InputFormat.ELEVATION_DTED,
        -> inspected.copy(
            elevationMetadata = ElevationMetadataReader.read(file.toPath(), inspected.format),
        )
        else -> error(
            strings("error.format_unsupported", inspected.format.displayName),
        )
    }
}

internal fun inputFileRejection(
    file: File,
    exception: Throwable,
    strings: AppStrings = LocalizationCatalog.strings(AppLanguage.GERMAN),
): InputFileRejection = InputFileRejection(
    fileName = file.name,
    path = file.absolutePath,
    reason = exception.message?.takeIf(String::isNotBlank) ?: strings["error.file_unreadable"],
)
