PassbirdLauncher.kt

package de.pflugradts.passbird.application.boot.launcher

import de.pflugradts.passbird.application.RunContext
import de.pflugradts.passbird.application.UserInterfaceAdapterPort
import de.pflugradts.passbird.application.boot.Bootable
import de.pflugradts.passbird.application.boot.main.ApplicationGraph
import de.pflugradts.passbird.application.boot.migration.MigrationGraph
import de.pflugradts.passbird.application.boot.setup.SetupGraph
import de.pflugradts.passbird.application.configuration.ReadableConfiguration
import de.pflugradts.passbird.application.process.migration.MigrationRequest
import de.pflugradts.passbird.application.process.migration.PreLaunchMigrationLocator
import de.pflugradts.passbird.application.toDirectory
import de.pflugradts.passbird.application.toFileName
import de.pflugradts.passbird.application.util.SystemOperation
import de.pflugradts.passbird.domain.model.shell.Shell.Companion.shellOf
import de.pflugradts.passbird.domain.model.transfer.Output
import de.pflugradts.passbird.domain.model.transfer.Output.Companion.emptyOutput
import de.pflugradts.passbird.domain.model.transfer.Output.Companion.outputOf
import de.pflugradts.passbird.domain.model.transfer.OutputFormatting.DEFAULT
import de.pflugradts.passbird.domain.model.transfer.OutputFormatting.ERROR_MESSAGE
import de.pflugradts.passbird.domain.model.transfer.OutputFormatting.NEST
import de.pflugradts.passbird.domain.model.transfer.OutputFormatting.OPERATION_ABORTED
import de.pflugradts.passbird.domain.model.transfer.OutputFormatting.SPECIAL
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.util.jar.JarInputStream

private const val COPYRIGHT = "\tCopyright 2020 - 2025 Christian Pflugradt"
private const val LICENSE = """${'\t'}This software is licensed under the Apache License, Version 2.0 (APLv2)
${'\t'}You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
"""
private const val SLOGAN = "\tguarding your digital nest with secure feathers"

class PassbirdLauncher(
    private val configuration: ReadableConfiguration,
    private val preLaunchMigrationLocator: PreLaunchMigrationLocator,
    private val runContext: RunContext,
    private val userInterfaceAdapterPort: UserInterfaceAdapterPort,
    private val systemOperation: SystemOperation,
    private val setupBoot: (RunContext) -> Unit = { SetupGraph(it).bootable.boot() },
    private val migrationBoot: (RunContext, MigrationRequest) -> Unit = { context, request ->
        MigrationGraph(context, request).bootable.boot()
    },
    private val applicationBoot: (RunContext) -> Unit = { ApplicationGraph(it).bootable.boot() },
) : Bootable {

    private val keyStoreLocation get() = configuration.adapter.keyStore.location
    private val ansiEscapeCodesEnabled get() = configuration.adapter.userInterface.ansiEscapeCodes.enabled

    override fun boot() {
        sendLicenseNotice()
        sendBanner()
        if (!keystoreExists()) {
            setupBoot(runContext)
        } else {
            preLaunchMigrationLocator.detect().let { migrationRequest ->
                if (migrationRequest.required) migrationBoot(runContext, migrationRequest) else applicationBoot(runContext)
            }
        }
    }

    private fun keystoreExists() = keyStoreLocation.isNotEmpty() &&
        systemOperation.exists(
            systemOperation.resolvePath(
                keyStoreLocation.toDirectory(),
                ReadableConfiguration.KEYSTORE_FILENAME.toFileName(),
            ),
        )

    private fun sendBanner() {
        userInterfaceAdapterPort.sendLineBreak()
        userInterfaceAdapterPort.send(outputOf(shellOf(if (ansiEscapeCodesEnabled) coloredBanner() else plainBanner())))
        userInterfaceAdapterPort.send(outputOf(shellOf("\t${javaClass.getPackage().implementationVersion}"), NEST), ageHint())
        userInterfaceAdapterPort.sendLineBreak()
        userInterfaceAdapterPort.send(outputOf(shellOf(SLOGAN), SPECIAL))
        userInterfaceAdapterPort.sendLineBreak()
    }

    private fun ageHint(): Output {
        manifestAttributes()?.getValue("Build-Date")?.also { buildDate ->
            ChronoUnit.DAYS.between(
                LocalDate.parse(buildDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")),
                LocalDate.now(),
            ).let { age ->
                when {
                    age <= 7 -> "This version is up to date." to DEFAULT
                    age <= 29 -> "This version is $age days old. Perhaps there's a newer version?" to DEFAULT
                    age <= 89 -> "This version is $age days old. Perhaps there's a newer version?" to OPERATION_ABORTED
                    else -> "This version is outdated." to ERROR_MESSAGE
                }
            }.let { (message, formatting) -> return@ageHint outputOf(shellOf(" | $message"), formatting) }
        }
        return emptyOutput()
    }

    private fun sendLicenseNotice() {
        userInterfaceAdapterPort.sendLineBreak()
        userInterfaceAdapterPort.send(outputOf(shellOf(COPYRIGHT)))
        userInterfaceAdapterPort.send(outputOf(shellOf(LICENSE)))
    }

    private fun plainBanner() = byteArrayOf(
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2C, 0x5F, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5F, 0x20, 0x20, 0x20, 0x20, 0x3E, 0x27, 0x20, 0x29, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x5F, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7C, 0x20, 0x5F, 0x5F, 0x5F, 0x20, 0x5C, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7C, 0x20, 0x7C, 0x20, 0x20, 0x20, 0x28, 0x20, 0x28, 0x20, 0x5C, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x7C, 0x20, 0x7C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7C, 0x20, 0x7C, 0x5F, 0x2F, 0x20, 0x2F, 0x5F, 0x20, 0x5F, 0x20, 0x5F,
        0x5F, 0x5F, 0x20, 0x5F, 0x5F, 0x5F, 0x7C, 0x20, 0x7C, 0x5F, 0x5F, 0x20, 0x20, 0x5F, 0x20, 0x5F, 0x20, 0x5F, 0x5F, 0x20, 0x5F, 0x5F,
        0x7C, 0x20, 0x7C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7C, 0x20, 0x20, 0x5F, 0x5F, 0x2F, 0x20, 0x5F, 0x60, 0x20, 0x2F, 0x20, 0x5F, 0x5F,
        0x2F, 0x20, 0x5F, 0x5F, 0x7C, 0x20, 0x27, 0x5F, 0x20, 0x5C, 0x7C, 0x20, 0x7C, 0x20, 0x27, 0x5F, 0x5F, 0x2F, 0x20, 0x5F, 0x60, 0x20,
        0x7C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7C, 0x20, 0x7C, 0x20, 0x7C, 0x20, 0x28, 0x5F, 0x7C, 0x20, 0x5C, 0x5F, 0x5F, 0x20, 0x5C, 0x5F,
        0x5F, 0x20, 0x5C, 0x20, 0x7C, 0x5F, 0x29, 0x20, 0x7C, 0x20, 0x7C, 0x20, 0x7C, 0x20, 0x7C, 0x20, 0x28, 0x5F, 0x7C, 0x20, 0x7C, 0x0A,
        0x20, 0x20, 0x20, 0x20, 0x5C, 0x5F, 0x7C, 0x20, 0x20, 0x5C, 0x5F, 0x5F, 0x2C, 0x5F, 0x7C, 0x5F, 0x5F, 0x5F, 0x2F, 0x5F, 0x5F, 0x5F,
        0x2F, 0x5F, 0x2E, 0x5F, 0x5F, 0x2F, 0x7C, 0x5F, 0x7C, 0x5F, 0x7C, 0x20, 0x20, 0x5C, 0x5F, 0x5F, 0x2C, 0x5F, 0x7C, 0x0A,
    )

    private fun coloredBanner() = byteArrayOf(
        0x0a, 0x1b, 0x5b, 0x33, 0x38, 0x3b, 0x35, 0x3b, 0x33, 0x39, 0x6d, 0x09, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2c, 0x5f, 0x0a, 0x09, 0x5f, 0x5f, 0x5f,
        0x5f, 0x5f, 0x5f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5f, 0x20, 0x20, 0x20, 0x20, 0x1b,
        0x5b, 0x33, 0x38, 0x3b, 0x35, 0x3b, 0x32, 0x32, 0x30, 0x6d, 0x3e, 0x27, 0x20, 0x29, 0x1b, 0x5b, 0x33, 0x38, 0x3b, 0x35, 0x3b, 0x33,
        0x39, 0x6d, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5f, 0x20, 0x0a, 0x09, 0x7c, 0x20, 0x5f, 0x5f, 0x5f, 0x20, 0x5c, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x7c, 0x20, 0x20, 0x20, 0x1b, 0x5b, 0x33, 0x38, 0x3b, 0x35, 0x3b,
        0x31, 0x39, 0x36, 0x6d, 0x28, 0x20, 0x28, 0x20, 0x5c, 0x1b, 0x5b, 0x33, 0x38, 0x3b, 0x35, 0x3b, 0x33, 0x39, 0x6d, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x7c, 0x20, 0x7c, 0x0a, 0x09, 0x7c, 0x20, 0x7c, 0x5f, 0x2f, 0x20, 0x2f, 0x5f, 0x20, 0x5f, 0x20, 0x5f, 0x5f, 0x5f, 0x20,
        0x5f, 0x5f, 0x5f, 0x7c, 0x20, 0x7c, 0x5f, 0x5f, 0x20, 0x20, 0x5f, 0x20, 0x5f, 0x20, 0x5f, 0x5f, 0x20, 0x5f, 0x5f, 0x7c, 0x20, 0x7c,
        0x0a, 0x09, 0x7c, 0x20, 0x20, 0x5f, 0x5f, 0x2f, 0x20, 0x5f, 0x60, 0x20, 0x2f, 0x20, 0x5f, 0x5f, 0x2f, 0x20, 0x5f, 0x5f, 0x7c, 0x20,
        0x27, 0x5f, 0x20, 0x5c, 0x7c, 0x20, 0x7c, 0x20, 0x27, 0x5f, 0x5f, 0x2f, 0x20, 0x5f, 0x60, 0x20, 0x7c, 0x0a, 0x09, 0x7c, 0x20, 0x7c,
        0x20, 0x7c, 0x20, 0x28, 0x5f, 0x7c, 0x20, 0x5c, 0x5f, 0x5f, 0x20, 0x5c, 0x5f, 0x5f, 0x20, 0x5c, 0x20, 0x7c, 0x5f, 0x29, 0x20, 0x7c,
        0x20, 0x7c, 0x20, 0x7c, 0x20, 0x7c, 0x20, 0x28, 0x5f, 0x7c, 0x20, 0x7c, 0x0a, 0x09, 0x5c, 0x5f, 0x7c, 0x20, 0x20, 0x5c, 0x5f, 0x5f,
        0x2c, 0x5f, 0x7c, 0x5f, 0x5f, 0x5f, 0x2f, 0x5f, 0x5f, 0x5f, 0x2f, 0x5f, 0x2e, 0x5f, 0x5f, 0x2f, 0x7c, 0x5f, 0x7c, 0x5f, 0x7c, 0x20,
        0x20, 0x5c, 0x5f, 0x5f, 0x2c, 0x5f, 0x7c, 0x1b, 0x5b, 0x30, 0x6d, 0x0a,
    )
}

private fun manifestAttributes() = {}.javaClass.protectionDomain.codeSource.location
    ?.let { it.openStream().use { stream -> JarInputStream(stream).manifest } }?.mainAttributes