package com.correx.apps.server.config

import com.correx.core.config.ConfigHolder
import com.correx.core.config.CorrexConfig
import com.correx.core.config.CorrexConfigWriter
import com.correx.core.config.EditableConfig
import com.correx.core.config.EditableField
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption

/** Outcome of a config update. [Ok.restartRequired] lists changed keys that need a restart. */
sealed interface ConfigUpdateResult {
    data class Ok(val restartRequired: Set<String>) : ConfigUpdateResult
    data class Error(val message: String) : ConfigUpdateResult
}

/**
 * Validates, persists, and live-applies config edits. The flow on [apply] is: validate the patch
 * against [EditableConfig] (security-sensitive keys are rejected because they are absent from the
 * registry), atomically rewrite the TOML file, swap the [holder], then invoke [rebuild] so
 * config-derived services are reconstructed. Validation/persist failures are reported without
 * mutating the holder or calling [rebuild].
 *
 * Config is not event-sourced — it lives in TOML — so this service deliberately stays outside the
 * event log.
 */
class ConfigService(
    private val holder: ConfigHolder,
    private val configPath: Path,
    private val rebuild: (CorrexConfig) -> Unit,
) {

    /** Current editable fields paired with their rendered string values, for the protocol snapshot. */
    fun snapshot(): List<Pair<EditableField, String>> =
        EditableConfig.fields.map { it to it.getString(holder.get()) }

    fun apply(patch: Map<String, String>): ConfigUpdateResult {
        val newCfg = EditableConfig.apply(holder.get(), patch)
            .getOrElse { return ConfigUpdateResult.Error(it.message ?: "invalid config update") }

        return runCatching { persist(newCfg) }.fold(
            onSuccess = {
                holder.set(newCfg)
                rebuild(newCfg)
                ConfigUpdateResult.Ok(patch.keys intersect EditableConfig.restartRequiredKeys)
            },
            onFailure = { ConfigUpdateResult.Error("failed to persist config: ${it.message}") },
        )
    }

    /** Writes via a temp file + atomic move so a crash mid-write cannot leave a truncated config. */
    private fun persist(cfg: CorrexConfig) {
        val parent = configPath.toAbsolutePath().parent
        Files.createDirectories(parent)
        val tmp = Files.createTempFile(parent, "config", ".toml.tmp")
        Files.writeString(tmp, CorrexConfigWriter.write(cfg))
        runCatching {
            Files.move(tmp, configPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE)
        }.getOrElse {
            // Some filesystems reject ATOMIC_MOVE across boundaries; fall back to a plain replace.
            Files.move(tmp, configPath, StandardCopyOption.REPLACE_EXISTING)
        }
    }
}
