PasswordImportExportService.kt

package de.pflugradts.passbird.application.exchange
import de.pflugradts.kotlinextensions.Option
import de.pflugradts.kotlinextensions.TryResult
import de.pflugradts.passbird.application.PasswordInfo
import de.pflugradts.passbird.application.PasswordInfoMap
import de.pflugradts.passbird.application.failure.ImportFailure
import de.pflugradts.passbird.application.failure.reportFailure
import de.pflugradts.passbird.domain.model.event.EggsExported
import de.pflugradts.passbird.domain.model.event.EggsImported
import de.pflugradts.passbird.domain.model.nest.Nest
import de.pflugradts.passbird.domain.model.shell.Shell
import de.pflugradts.passbird.domain.model.shell.Shell.Companion.emptyShell
import de.pflugradts.passbird.domain.model.shell.ShellPair
import de.pflugradts.passbird.domain.model.slot.Slot
import de.pflugradts.passbird.domain.model.slot.Slot.Companion.slotAt
import de.pflugradts.passbird.domain.service.eventhandling.EventRegistry
import de.pflugradts.passbird.domain.service.nest.NestService
import de.pflugradts.passbird.domain.service.password.PasswordService
class PasswordImportExportService constructor(
    private val exchangeFactory: ExchangeFactory,
    private val passwordService: PasswordService,
    private val nestService: NestService,
    private val eventRegistry: EventRegistry,
) : ImportExportService {
    override fun peekImportEggIdShells(): TryResult<ShellMap> = receiveImportData().map { eggsByNest ->
        try {
            toShellMap(eggsByNest)
        } finally {
            eggsByNest.scrambleShells()
        }
    }
    override fun peekImportNests(): TryResult<List<ImportNestPreview>> = receiveImportData().map { eggsByNest ->
        try {
            eggsByNest.entries.map { (nest, passwordInfos) ->
                ImportNestPreview(
                    nestId = nest.viewNestId(),
                    slot = nest.slot,
                    eggIds = passwordInfos.map { passwordInfo -> passwordInfo.first.first.copy() },
                )
            }
        } finally {
            eggsByNest.scrambleShells()
        }
    }
    override fun importEggs() {
        receiveImportData().onSuccess { eggsByNest ->
            try {
                importEggs(eggsByNest)
            } finally {
                eggsByNest.scrambleShells()
            }
        }
    }
    override fun importEggs(sourceSlot: Slot, targetSlot: Slot) {
        receiveImportData().onSuccess { eggsByNest ->
            try {
                eggsByNest.entries.firstOrNull { (nest, _) -> nest.slot == sourceSlot }?.let { (nest, passwordInfos) ->
                    importEggs(listOf(Triple(nest, targetSlot, passwordInfos)))
                }
            } finally {
                eggsByNest.scrambleShells()
            }
        }
    }
    override fun exportEggs() = exportEggs(allNestSlots())
    override fun exportEggs(slots: Set<Slot>) {
        if (slots.isEmpty()) {
            return
        }
        val currentNest = nestService.currentNest()
        val eggsByNest = mutableMapOf<Nest, List<PasswordInfo>>()
        try {
            try {
                nestService.all(includeDefault = true)
                    .filter { it.isPresent }
                    .map { it.get() }
                    .filter { it.slot in slots }
                    .forEach { nest ->
                        nestService.moveToNestAt(nest.slot)
                        eggsByNest[nest] = passwordService.findAllEggIds()
                            .map { eggId ->
                                PasswordInfo(
                                    first = ShellPair(eggId, passwordService.viewPassword(eggId).get()),
                                    second = passwordService.viewProteinTypes(eggId).toShellList()
                                        .zip(passwordService.viewProteinStructures(eggId).toShellList()),
                                )
                            }.toList()
                    }
            } finally {
                nestService.moveToNestAt(currentNest.slot)
            }
            exchangeFactory.createPasswordExchange().send(eggsByNest).onSuccess {
                eventRegistry.register(EggsExported(eggsByNest.values.sumOf { it.size }))
                eventRegistry.processEvents()
            }
        } finally {
            eggsByNest.scrambleShells()
        }
    }
    private fun PasswordInfoMap.scrambleShells() {
        values.flatten().forEach {
            it.first.first.scramble()
            it.first.second.scramble()
            it.second.forEach { protein ->
                protein.first.scramble()
                protein.second.scramble()
            }
        }
    }
    private fun receiveImportData() = exchangeFactory.createPasswordExchange().receive()
    private fun toShellMap(eggsByNest: Map<Nest, List<PasswordInfo>>) = eggsByNest.entries.associate { (nest, passwordInfos) ->
        nest.slot to passwordInfos.map { passwordInfo -> passwordInfo.first.first.copy() }
    }
    private fun importEggs(eggsByNest: Map<Nest, List<PasswordInfo>>) {
        importEggs(eggsByNest.entries.map { (nest, passwordInfos) -> Triple(nest, nest.slot, passwordInfos) })
    }
    private fun importEggs(imports: List<Triple<Nest, Slot, List<PasswordInfo>>>) {
        if (imports.isEmpty()) {
            return
        }
        if (imports.any { (nest, targetSlot, _) -> nestIdentityConflicts(nest, targetSlot) }) {
            reportFailure(ImportFailure(IllegalStateException("Imported NestId does not match occupied target slot")))
            return
        }
        val currentNest = nestService.currentNest()
        var importedEggCount = 0
        try {
            imports.forEach { (nest, targetSlot, passwordInfos) ->
                val deployedNest = nestService.atNestSlot(targetSlot)
                if (deployedNest.isEmpty) {
                    if (nestService.place(nest.viewNestId(), targetSlot).failure) {
                        eventRegistry.clearEvents()
                        return
                    }
                }
                nestService.moveToNestAt(targetSlot)
                passwordInfos.forEach { passwordInfo ->
                    if (putEgg(passwordInfo.first.first, passwordInfo.first.second).failure) {
                        eventRegistry.clearEvents()
                        return
                    }
                    if (!importProteins(passwordInfo)) {
                        return
                    }
                    importedEggCount++
                }
            }
        } finally {
            nestService.moveToNestAt(currentNest.slot)
        }
        eventRegistry.register(EggsImported(importedEggCount))
        eventRegistry.processEvents()
    }
    private fun nestIdentityConflicts(nest: Nest, targetSlot: Slot) = nestService.atNestSlot(targetSlot)
        .map { deployedNest -> deployedNest.viewNestId() != nest.viewNestId() }
        .orElse(false)
    private fun importProteins(passwordInfo: PasswordInfo): Boolean {
        passwordInfo.second.forEachIndexed { index, shellPair ->
            if (shellPair.first.isNotEmpty && shellPair.second.isNotEmpty) {
                if (putProtein(passwordInfo.first.first, slotAt(index), shellPair.first, shellPair.second).failure) {
                    eventRegistry.clearEvents()
                    return false
                }
            }
        }
        return true
    }
    private fun putEgg(eggIdShell: Shell, passwordShell: Shell): TryResult<Unit> {
        val eggIdShellCopy = eggIdShell.copy()
        val passwordShellCopy = passwordShell.copy()
        return try {
            passwordService.putEgg(eggIdShellCopy, passwordShellCopy)
        } finally {
            eggIdShellCopy.scramble()
            passwordShellCopy.scramble()
        }
    }
    private fun putProtein(eggIdShell: Shell, slot: Slot, typeShell: Shell, structureShell: Shell): TryResult<Unit> {
        val eggIdShellCopy = eggIdShell.copy()
        val typeShellCopy = typeShell.copy()
        val structureShellCopy = structureShell.copy()
        return try {
            passwordService.putProtein(
                eggIdShell = eggIdShellCopy,
                slot = slot,
                typeShell = typeShellCopy,
                structureShell = structureShellCopy,
            )
        } finally {
            eggIdShellCopy.scramble()
            typeShellCopy.scramble()
            structureShellCopy.scramble()
        }
    }
    private fun allNestSlots() = nestService.all(includeDefault = true)
        .filter { it.isPresent }
        .map { it.get().slot }
        .toList()
        .toSet()
}
fun Option<List<Option<Shell>>>.toShellList() = map { list -> list.map { it.orElse(emptyShell()) } }.orElse(List(10) { emptyShell() })