FilePasswordExchange.kt

package de.pflugradts.passbird.adapter.exchange
import com.fasterxml.jackson.databind.json.JsonMapper
import de.pflugradts.kotlinextensions.tryCatching
import de.pflugradts.passbird.application.ExchangeAdapterPort
import de.pflugradts.passbird.application.PasswordInfo
import de.pflugradts.passbird.application.PasswordInfoMap
import de.pflugradts.passbird.application.RunContext
import de.pflugradts.passbird.application.configuration.ReadableConfiguration.Companion.EXCHANGE_FILENAME
import de.pflugradts.passbird.application.failure.ExportFailure
import de.pflugradts.passbird.application.failure.ImportFailure
import de.pflugradts.passbird.application.failure.reportFailure
import de.pflugradts.passbird.application.toFileName
import de.pflugradts.passbird.application.util.SystemOperation
import de.pflugradts.passbird.domain.model.egg.requireValidEggId
import de.pflugradts.passbird.domain.model.nest.Nest
import de.pflugradts.passbird.domain.model.nest.Nest.Companion.createNest
import de.pflugradts.passbird.domain.model.shell.Shell.Companion.emptyShell
import de.pflugradts.passbird.domain.model.shell.Shell.Companion.shellOf
import de.pflugradts.passbird.domain.model.shell.ShellPair
import de.pflugradts.passbird.domain.model.slot.Slot
import java.nio.file.Files
class FilePasswordExchange constructor(
    private val systemOperation: SystemOperation,
    private val runContext: RunContext,
) : ExchangeAdapterPort {
    private val mapper = JsonMapper()
    override fun send(data: PasswordInfoMap) = tryCatching {
        systemOperation.writeToSensitiveFile(
            systemOperation.resolvePath(runContext.homeDirectory, EXCHANGE_FILENAME.toFileName()),
        ) { outputStream ->
            mapper.writerWithDefaultPrettyPrinter().writeValue(outputStream, ExchangeWrapper(data.toSerializable()))
        }
        Unit
    }.onFailure { reportFailure(ExportFailure(it)) }
    override fun receive() = tryCatching {
        mapper.readValue(
            Files.readString(systemOperation.resolvePath(runContext.homeDirectory, EXCHANGE_FILENAME.toFileName())),
            ExchangeWrapper::class.java,
        ).exportedContent.toPasswordInfoMap()
    }.onFailure { reportFailure(ImportFailure(it)) }
    private fun PasswordInfoMap.toSerializable() = entries.map { nest ->
        EggsPerNest(
            exportedNest = ExportedNest(nest.key.viewNestId().asString(), nest.key.slot.index()),
            exportedEggs = nest.value.map {
                ExportedEgg(
                    eggId = it.first.first.asString(),
                    password = it.first.second.asString(),
                    proteins = it.second.mapIndexed { index, protein ->
                        ExportedProtein(
                            proteinType = protein.first.asString(),
                            proteinStructure = protein.second.asString(),
                            slot = index,
                        )
                    },
                )
            },
        )
    }
    private fun List<EggsPerNest>.toPasswordInfoMap() = associate { entry ->
        entry.exportedNest.toValidatedNest() to
            entry.exportedEggs.toValidatedPasswordInfos()
    }.also { passwordInfoMap ->
        require(passwordInfoMap.size == size) { "Duplicate nest slot in import file" }
    }
    private fun List<ExportedEgg>.toValidatedPasswordInfos(): List<PasswordInfo> {
        val eggIds = mutableSetOf<String>()
        forEach {
            it.validateEggId(eggIds)
        }
        return map {
            PasswordInfo(
                first = ShellPair(shellOf(it.eggId), shellOf(it.password)),
                second = it.proteins.toShellPairsBySlot(),
            )
        }
    }
    private fun ExportedEgg.validateEggId(eggIds: MutableSet<String>) {
        val eggIdShell = shellOf(eggId)
        requireValidEggId(eggIdShell)
        eggIdShell.scramble()
        require(eggIds.add(eggId)) { "Duplicate eggId in import file" }
    }
    private fun ExportedNest.toValidatedSlot(): Slot {
        val slot = requireNotNull(slot) { "Missing nest slot in import file" }
        require(slot in Slot.entries.indices) { "Invalid nest slot $slot" }
        return Slot.entries[slot]
    }
    private fun ExportedNest.toValidatedNest(): Nest {
        require(nestId.isNotBlank()) { "Missing nestId in import file" }
        return createNest(shellOf(nestId), toValidatedSlot())
    }
    private fun List<ExportedProtein>.toShellPairsBySlot(): List<ShellPair> {
        val proteinsBySlot = mutableMapOf<Int, ShellPair>()
        forEach { protein ->
            val slot = requireNotNull(protein.slot) { "Missing protein slot in import file" }
            require(slot in Slot.entries.indices) { "Invalid protein slot $slot" }
            require(protein.proteinType.isEmpty() == protein.proteinStructure.isEmpty()) {
                "Partial protein record in import file"
            }
            require(
                proteinsBySlot.putIfAbsent(
                    slot,
                    ShellPair(shellOf(protein.proteinType), shellOf(protein.proteinStructure)),
                ) == null,
            ) { "Duplicate protein slot $slot" }
        }
        return Slot.entries.indices.map { slot ->
            proteinsBySlot[slot] ?: ShellPair(emptyShell(), emptyShell())
        }
    }
}
private class ExportedProtein(var proteinType: String = "", var proteinStructure: String = "", var slot: Int? = null)
private class ExportedEgg(var eggId: String = "", var password: String = "", var proteins: List<ExportedProtein> = emptyList())
private class ExportedNest(var nestId: String = "", var slot: Int? = null)
private class EggsPerNest(var exportedNest: ExportedNest = ExportedNest(), var exportedEggs: List<ExportedEgg> = emptyList())
private class ExchangeWrapper(val exportedContent: List<EggsPerNest> = emptyList())