Skip to content

HistoryMetrics.kt

src/jvmMain/kotlin/afield/document/history/HistoryMetrics.kt · 659 lines · cited by What a document is on disk

package afield.document.history

import afield.data.table.div
import afield.data.table.get
import afield.data.table.minus
import afield.data.table.plus
import afield.data.table.rem
import afield.data.table.times
import afield.runtime.execution.plus
import afield.runtime.lifecycle.plus
import afield.runtime.lifecycle.rem
import afield.tools.docs.screenshot
import afield.ui.output.history

import afield.app.protocol.HistoryActivityStats
import afield.app.protocol.HistoryBoxActivity
import afield.app.protocol.HistoryDiffStats
import afield.app.protocol.HistoryScreenshotRef
import afield.app.protocol.payload
import afield.app.server.ReadOnlyDocuments
import afield.audio.synth.*
import afield.data.table.ref
import afield.document.graph.box
import afield.document.persistence.Edn
import afield.document.persistence.SavingContext
import afield.document.persistence.safeSave
import afield.graphics.vulkan.*
import afield.media.stream.*
import afield.media.video.*
import afield.runtime.execution.launches
import afield.runtime.lifecycle.map
import afield.tools.automation.FlightRecorder
import afield.tools.docs.format
import afield.ui.output.stats
import afield.ui.output.templates.filter
import afield.app.diagnostics.logging.SaveReloadLog
import afield.app.diagnostics.logging.lprintln
import java.io.File
import java.util.Base64

Per-version activity and screenshot records for a document, stored in its history_metrics.edn beside metadata.edn.

Each record is keyed by version index. A save adds the activity the flight recorder saw since the previous save (per-box counts included), a diff adds its edit statistics, and a captured screenshot adds a reference to its image file. The history browser reads these back through HistoryMetrics.activityByVersion to annotate its timeline.

object HistoryMetrics

Reads and writes a document's history_metrics.edn: activity per save, edit statistics per version, and screenshot references.

object HistoryMetrics {
    private const val METRICS_EDN = "history_metrics.edn"
    private val metricsLock = Any()

fun recordCurrentSave(documentPath: String, timestamp: Long = System.currentTimeMillis())

body — 92 lines, from line 59
    fun recordCurrentSave(documentPath: String, timestamp: Long = System.currentTimeMillis()) {
        val startedAt = System.nanoTime()
        var versionMs = 0L
        var summaryMs = 0L
        var lockWaitMs = 0L
        var loadMs = 0L
        var computeMs = 0L
        var saveMs = 0L
        var recordCount = 0
        val versionStartedAt = System.nanoTime()
        val versionIndex = currentVersionIndex(documentPath)
        versionMs = SaveReloadLog.elapsedMillis(versionStartedAt)
        if (versionIndex < 0) {
            SaveReloadLog.logTiming(
                "history-metrics-record-save",
                startedAt,
                "doc=$documentPath skipped=true reason=no-current-version versionMs=$versionMs"
            )
            return
        }

        val summaryStartedAt = System.nanoTime()
        val summary = FlightRecorder.activitySummaryForDocument(documentPath, untilMillis = timestamp)
        summaryMs = SaveReloadLog.elapsedMillis(summaryStartedAt)

        val lockStartedAt = System.nanoTime()
        synchronized(metricsLock) {
            lockWaitMs = SaveReloadLog.elapsedMillis(lockStartedAt)
            val loadStartedAt = System.nanoTime()
            val existing = loadRecords(documentPath).toMutableList()
            loadMs = SaveReloadLog.elapsedMillis(loadStartedAt)
            val computeStartedAt = System.nanoTime()
            val previous = existing
                .filter { it.versionIndex < versionIndex }
                .maxByOrNull { it.versionIndex }
                ?.activity
            val sameFieldLaunch = previous?.fieldLaunchId == summary.fieldLaunchId
            val previousByBox = previous?.perBox.orEmpty().associateBy { it.uid }

            val perBox = summary.perBox.map { box ->
                val previousBox = if (sameFieldLaunch) previousByBox[box.uid] else null
                HistoryBoxActivity(
                    uid = box.uid,
                    name = box.name,
                    executions = box.executions,
                    launches = box.launches,
                    executionDelta = (box.executions - (previousBox?.executions ?: 0)).coerceAtLeast(0),
                    launchDelta = (box.launches - (previousBox?.launches ?: 0)).coerceAtLeast(0)
                )
            }

            val activity = HistoryActivityStats(
                fieldLaunchId = summary.fieldLaunchId,
                fieldLaunchStartedAt = summary.fieldLaunchStartedAt,
                executions = summary.executions,
                launches = summary.launches,
                executionDelta = if (sameFieldLaunch) {
                    (summary.executions - previous.executions).coerceAtLeast(0)
                } else {
                    summary.executions
                },
                launchDelta = if (sameFieldLaunch) {
                    (summary.launches - previous.launches).coerceAtLeast(0)
                } else {
                    summary.launches
                },
                editedBoxes = 0,
                movedBoxes = 0,
                perBox = perBox,
                screenshots = screenshotRefs(documentPath, versionIndex, timestamp)
            )

            existing.removeAll { it.versionIndex == versionIndex }
            existing += MetricRecord(versionIndex, timestamp, activity)
            recordCount = existing.size
            computeMs = SaveReloadLog.elapsedMillis(computeStartedAt)
            val saveStartedAt = System.nanoTime()
            saveRecords(documentPath, existing.sortedBy { it.versionIndex })
            saveMs = SaveReloadLog.elapsedMillis(saveStartedAt)
        }
        val totalMs = SaveReloadLog.logTiming(
            "history-metrics-record-save",
            startedAt,
            "doc=$documentPath versionIndex=$versionIndex records=$recordCount versionMs=$versionMs summaryMs=$summaryMs lockWaitMs=$lockWaitMs loadMs=$loadMs computeMs=$computeMs saveMs=$saveMs"
        )
        if (totalMs >= 500L) {
            lprintln(
                "warning,history",
                "slow history metrics record save doc=$documentPath ms=$totalMs versionMs=$versionMs summaryMs=$summaryMs loadMs=$loadMs saveMs=$saveMs"
            )
        }
    }

fun recordEditStats(documentPath: String, versionIndex: Int, stats: HistoryDiffStats)

    fun recordEditStats(documentPath: String, versionIndex: Int, stats: HistoryDiffStats) {
        synchronized(metricsLock) {
            val records = loadRecords(documentPath).toMutableList()
            val index = records.indexOfFirst { it.versionIndex == versionIndex }
            if (index < 0) return
            val record = records[index]
            records[index] = record.copy(
                activity = record.activity.copy(
                    editedBoxes = stats.boxesEdited,
                    movedBoxes = stats.boxesMoved
                )
            )
            saveRecords(documentPath, records.sortedBy { it.versionIndex })
        }
    }

fun activityByVersion(documentPath: String): Map<Int, HistoryActivityStats>

    fun activityByVersion(documentPath: String): Map<Int, HistoryActivityStats> {
        val activity = loadRecords(documentPath).associate {
            it.versionIndex to it.activity.copy(
                screenshots = validScreenshotRefs(documentPath, it.activity.screenshots)
            )
        }.toMutableMap()
        diskScreenshotRefsByVersion(documentPath).forEach { (versionIndex, refs) ->
            val existing = activity[versionIndex] ?: HistoryActivityStats()
            activity[versionIndex] = existing.copy(
                screenshots = mergeScreenshotRefs(existing.screenshots + refs)
            )
        }
        return activity
    }

fun addScreenshotRef

body — 57 lines, from line 183
    fun addScreenshotRef(
        documentPath: String,
        versionIndex: Int,
        channel: String,
        path: String,
        timestamp: Long = System.currentTimeMillis(),
        width: Int = 0,
        height: Int = 0
    ) {
        val startedAt = System.nanoTime()
        var lockWaitMs = 0L
        var loadMs = 0L
        var saveMs = 0L
        var recordCount = 0
        val lockStartedAt = System.nanoTime()
        synchronized(metricsLock) {
            lockWaitMs = SaveReloadLog.elapsedMillis(lockStartedAt)
            val loadStartedAt = System.nanoTime()
            val records = loadRecords(documentPath).toMutableList()
            loadMs = SaveReloadLog.elapsedMillis(loadStartedAt)
            val index = records.indexOfFirst { it.versionIndex == versionIndex }
            val record = if (index >= 0) {
                records[index]
            } else {
                MetricRecord(
                    versionIndex = versionIndex,
                    timestamp = timestamp,
                    activity = HistoryActivityStats()
                )
            }
            val nextScreenshots = (record.activity.screenshots.filterNot {
                it.channel == channel && it.path == path
            } + HistoryScreenshotRef(channel, path, timestamp, width, height))
                .sortedWith(compareBy<HistoryScreenshotRef> { it.channel }.thenBy { it.timestamp })
            val nextRecord = record.copy(activity = record.activity.copy(screenshots = nextScreenshots))
            if (index >= 0) {
                records[index] = nextRecord
            } else {
                records += nextRecord
            }
            recordCount = records.size
            val saveStartedAt = System.nanoTime()
            saveRecords(documentPath, records.sortedBy { it.versionIndex })
            saveMs = SaveReloadLog.elapsedMillis(saveStartedAt)
        }
        val totalMs = SaveReloadLog.logTiming(
            "history-metrics-add-screenshot",
            startedAt,
            "doc=$documentPath versionIndex=$versionIndex channel=$channel path=$path records=$recordCount lockWaitMs=$lockWaitMs loadMs=$loadMs saveMs=$saveMs"
        )
        if (totalMs >= 500L) {
            lprintln(
                "warning,history",
                "slow history metrics screenshot doc=$documentPath ms=$totalMs channel=$channel loadMs=$loadMs saveMs=$saveMs"
            )
        }
    }

fun storeScreenshotDataUrl

body — 85 lines, from line 241
    fun storeScreenshotDataUrl(
        documentPath: String,
        channel: String,
        dataUrl: String,
        versionIndex: Int = currentVersionIndex(documentPath),
        width: Int = 0,
        height: Int = 0,
        timestamp: Long = System.currentTimeMillis()
    ): HistoryScreenshotRef? {
        if (refuseReadOnlyWrite(documentPath, "storeScreenshotDataUrl")) return null
        val startedAt = System.nanoTime()
        var decodeMs = 0L
        var writeMs = 0L
        var addRefMs = 0L
        if (versionIndex < 0) {
            SaveReloadLog.logTiming(
                "history-store-screenshot-data-url",
                startedAt,
                "doc=$documentPath channel=$channel skipped=true reason=no-current-version"
            )
            return null
        }
        val safeChannel = sanitizeChannel(channel)
        val base64 = dataUrl.substringAfter(",", dataUrl).trim()
        val decodeStartedAt = System.nanoTime()
        val bytes = runCatching { Base64.getDecoder().decode(base64) }.getOrElse {
            decodeMs = SaveReloadLog.elapsedMillis(decodeStartedAt)
            SaveReloadLog.logTiming(
                "history-store-screenshot-data-url",
                startedAt,
                "doc=$documentPath versionIndex=$versionIndex channel=$safeChannel ok=false error=decode-failed decodeMs=$decodeMs"
            )
            lprintln("warning,history", "Failed to decode screenshot for $documentPath: ${it.message}")
            return null
        }
        decodeMs = SaveReloadLog.elapsedMillis(decodeStartedAt)
        val extension = screenshotExtension(dataUrl, bytes)
        if (extension == null) {
            SaveReloadLog.logTiming(
                "history-store-screenshot-data-url",
                startedAt,
                "doc=$documentPath versionIndex=$versionIndex channel=$safeChannel ok=false error=invalid-screenshot decodeMs=$decodeMs bytes=${bytes.size}"
            )
            lprintln("warning,history", "Ignoring empty or invalid screenshot for $documentPath channel $safeChannel")
            return null
        }
        val root = File(documentPath, "history_screenshots/$safeChannel")
        root.mkdirs()
        val file = File(root, "v%05d_%d.$extension".format(versionIndex, timestamp))
        return runCatching {
            val writeStartedAt = System.nanoTime()
            file.writeBytes(bytes)
            writeMs = SaveReloadLog.elapsedMillis(writeStartedAt)
            val ref = HistoryScreenshotRef(
                channel = safeChannel,
                path = file.relativeTo(File(documentPath)).path,
                timestamp = timestamp,
                width = width,
                height = height
            )
            val addRefStartedAt = System.nanoTime()
            addScreenshotRef(documentPath, versionIndex, safeChannel, ref.path, timestamp, width, height)
            addRefMs = SaveReloadLog.elapsedMillis(addRefStartedAt)
            val totalMs = SaveReloadLog.logTiming(
                "history-store-screenshot-data-url",
                startedAt,
                "doc=$documentPath versionIndex=$versionIndex channel=$safeChannel ok=true extension=$extension bytes=${bytes.size} decodeMs=$decodeMs writeMs=$writeMs addRefMs=$addRefMs path=${ref.path}"
            )
            if (totalMs >= 500L) {
                lprintln(
                    "warning,history",
                    "slow data-url history screenshot doc=$documentPath channel=$safeChannel ms=$totalMs decodeMs=$decodeMs writeMs=$writeMs addRefMs=$addRefMs"
                )
            }
            ref
        }.getOrElse {
            SaveReloadLog.logTiming(
                "history-store-screenshot-data-url",
                startedAt,
                "doc=$documentPath versionIndex=$versionIndex channel=$safeChannel ok=false error=${it.message ?: it::class.simpleName} decodeMs=$decodeMs writeMs=$writeMs addRefMs=$addRefMs bytes=${bytes.size}"
            )
            lprintln("warning,history", "Failed to write screenshot for $documentPath: ${it.message}")
            null
        }
    }

fun reserveScreenshotFile

    fun reserveScreenshotFile(
        documentPath: String,
        versionIndex: Int = currentVersionIndex(documentPath),
        channel: String,
        timestamp: Long = System.currentTimeMillis(),
        width: Int = 0,
        height: Int = 0,
        extension: String = "png"
    ): Pair<File, HistoryScreenshotRef>? {
        if (refuseReadOnlyWrite(documentPath, "reserveScreenshotFile")) return null
        if (versionIndex < 0) return null
        val safeChannel = sanitizeChannel(channel)
        val safeExtension = sanitizeScreenshotExtension(extension)
        val root = File(documentPath, "history_screenshots/$safeChannel")
        root.mkdirs()
        val file = File(root, "v%05d_%d.$safeExtension".format(versionIndex, timestamp))
        val ref = HistoryScreenshotRef(
            channel = safeChannel,
            path = file.relativeTo(File(documentPath)).path,
            timestamp = timestamp,
            width = width,
            height = height
        )
        return file to ref
    }

fun addScreenshotRef

    fun addScreenshotRef(
        documentPath: String,
        versionIndex: Int,
        ref: HistoryScreenshotRef
    ) {
        addScreenshotRef(
            documentPath = documentPath,
            versionIndex = versionIndex,
            channel = ref.channel,
            path = ref.path,
            timestamp = ref.timestamp,
            width = ref.width,
            height = ref.height
        )
    }

    private data class MetricRecord(
        val versionIndex: Int,
        val timestamp: Long,
        val activity: HistoryActivityStats
    )

fun currentVersionIndex(documentPath: String): Int

body — 284 lines, from line 375
    fun currentVersionIndex(documentPath: String): Int {
        val startedAt = System.nanoTime()
        val docDir = File(documentPath)
        val backups = File(docDir, "backups").listFiles { file ->
            file.name.startsWith("metadata_backup_") && file.name.endsWith(".edn")
        }?.size ?: 0
        val result = if (File(docDir, SavingContext.METADATA_EDN).exists()) backups else backups - 1
        val elapsedMs = SaveReloadLog.logTiming(
            "history-current-version-index",
            startedAt,
            "doc=$documentPath backups=$backups result=$result",
            thresholdMs = 100L
        )
        if (elapsedMs >= 500L) {
            lprintln(
                "warning,history",
                "slow currentVersionIndex doc=$documentPath ms=$elapsedMs backups=$backups result=$result"
            )
        }
        return result
    }

    private fun sanitizeChannel(channel: String): String =
        channel
            .ifBlank { "screenshot" }
            .replace(Regex("""[^A-Za-z0-9_.-]+"""), "-")
            .trim('-')
            .ifBlank { "screenshot" }

    private fun looksLikePng(bytes: ByteArray): Boolean =
        bytes.size >= 8 &&
            bytes[0] == 0x89.toByte() &&
            bytes[1] == 0x50.toByte() &&
            bytes[2] == 0x4E.toByte() &&
            bytes[3] == 0x47.toByte() &&
            bytes[4] == 0x0D.toByte() &&
            bytes[5] == 0x0A.toByte() &&
            bytes[6] == 0x1A.toByte() &&
            bytes[7] == 0x0A.toByte()

    private fun looksLikeJpeg(bytes: ByteArray): Boolean =
        bytes.size >= 3 &&
            bytes[0] == 0xFF.toByte() &&
            bytes[1] == 0xD8.toByte() &&
            bytes[2] == 0xFF.toByte()

    private fun looksLikeScreenshot(bytes: ByteArray): Boolean =
        looksLikePng(bytes) || looksLikeJpeg(bytes)

    private fun screenshotExtension(dataUrl: String, bytes: ByteArray): String? {
        val prefix = dataUrl.substringBefore(",", "").lowercase()
        return when {
            prefix.contains("image/png") && looksLikePng(bytes) -> "png"
            prefix.contains("image/jpeg") && looksLikeJpeg(bytes) -> "jpg"
            prefix.contains("image/jpg") && looksLikeJpeg(bytes) -> "jpg"
            looksLikePng(bytes) -> "png"
            looksLikeJpeg(bytes) -> "jpg"
            else -> null
        }
    }

    private fun sanitizeScreenshotExtension(extension: String): String =
        when (extension.lowercase().trimStart('.')) {
            "jpg", "jpeg" -> "jpg"
            else -> "png"
        }

    private fun supportedScreenshotExtension(extension: String): Boolean =
        extension.lowercase() == "png" ||
            extension.lowercase() == "jpg" ||
            extension.lowercase() == "jpeg"

    private fun metricsFile(documentPath: String): File =
        File(documentPath, METRICS_EDN)

    private fun loadRecords(documentPath: String): List<MetricRecord> {
        val file = metricsFile(documentPath)
        if (!file.exists()) return emptyList()
        return runCatching {
            val root = Edn.read(file.readText(), Edn.CustomTypes()) as? Map<*, *> ?: return@runCatching emptyList()
            val records = root["records"] as? List<*> ?: return@runCatching emptyList()
            records.mapNotNull(::recordFromAny)
        }.getOrElse {
            lprintln("warning,history", "Failed to read history metrics for $documentPath: ${it.message}")
            emptyList()
        }
    }

    private fun saveRecords(documentPath: String, records: List<MetricRecord>) {
        if (refuseReadOnlyWrite(documentPath, "saveRecords")) return
        val payload = linkedMapOf<String, Any?>(
            "version" to 1,
            "records" to records.map(::recordToMap)
        )
        val file = metricsFile(documentPath)
        file.parentFile?.mkdirs()
        file.safeSave(Edn.write(payload, Edn.CustomTypes()), maxBackups = 10)
    }

    private fun refuseReadOnlyWrite(documentPath: String, producer: String): Boolean {
        if (!ReadOnlyDocuments.blocksWritesTo(documentPath)) return false
        ReadOnlyDocuments.refuseWrite("HistoryMetrics.$producer", documentPath)
        return true
    }

    private fun recordFromAny(value: Any?): MetricRecord? {
        val map = value as? Map<*, *> ?: return null
        val versionIndex = (map["versionIndex"] as? Number)?.toInt() ?: return null
        val timestamp = (map["timestamp"] as? Number)?.toLong() ?: 0L
        val activity = activityFromAny(map["activity"]) ?: HistoryActivityStats()
        return MetricRecord(versionIndex, timestamp, activity)
    }

    private fun recordToMap(record: MetricRecord): Map<String, Any?> =
        linkedMapOf(
            "versionIndex" to record.versionIndex,
            "timestamp" to record.timestamp,
            "activity" to activityToMap(record.activity)
        )

    private fun activityFromAny(value: Any?): HistoryActivityStats? {
        val map = value as? Map<*, *> ?: return null
        return HistoryActivityStats(
            fieldLaunchId = map["fieldLaunchId"]?.toString() ?: "",
            fieldLaunchStartedAt = (map["fieldLaunchStartedAt"] as? Number)?.toLong() ?: 0L,
            executions = (map["executions"] as? Number)?.toInt() ?: 0,
            launches = (map["launches"] as? Number)?.toInt() ?: 0,
            executionDelta = (map["executionDelta"] as? Number)?.toInt() ?: 0,
            launchDelta = (map["launchDelta"] as? Number)?.toInt() ?: 0,
            editedBoxes = (map["editedBoxes"] as? Number)?.toInt() ?: 0,
            movedBoxes = (map["movedBoxes"] as? Number)?.toInt() ?: 0,
            perBox = (map["perBox"] as? List<*>)?.mapNotNull(::boxActivityFromAny).orEmpty(),
            screenshots = (map["screenshots"] as? List<*>)?.mapNotNull(::screenshotFromAny).orEmpty()
        )
    }

    private fun activityToMap(activity: HistoryActivityStats): Map<String, Any?> =
        linkedMapOf(
            "fieldLaunchId" to activity.fieldLaunchId,
            "fieldLaunchStartedAt" to activity.fieldLaunchStartedAt,
            "executions" to activity.executions,
            "launches" to activity.launches,
            "executionDelta" to activity.executionDelta,
            "launchDelta" to activity.launchDelta,
            "editedBoxes" to activity.editedBoxes,
            "movedBoxes" to activity.movedBoxes,
            "perBox" to activity.perBox.map(::boxActivityToMap),
            "screenshots" to activity.screenshots.map(::screenshotToMap)
        )

    private fun boxActivityFromAny(value: Any?): HistoryBoxActivity? {
        val map = value as? Map<*, *> ?: return null
        val uid = map["uid"]?.toString()?.takeIf { it.isNotBlank() } ?: return null
        return HistoryBoxActivity(
            uid = uid,
            name = map["name"]?.toString() ?: "",
            executions = (map["executions"] as? Number)?.toInt() ?: 0,
            launches = (map["launches"] as? Number)?.toInt() ?: 0,
            executionDelta = (map["executionDelta"] as? Number)?.toInt() ?: 0,
            launchDelta = (map["launchDelta"] as? Number)?.toInt() ?: 0
        )
    }

    private fun boxActivityToMap(activity: HistoryBoxActivity): Map<String, Any?> =
        linkedMapOf(
            "uid" to activity.uid,
            "name" to activity.name,
            "executions" to activity.executions,
            "launches" to activity.launches,
            "executionDelta" to activity.executionDelta,
            "launchDelta" to activity.launchDelta
        )

    private fun screenshotFromAny(value: Any?): HistoryScreenshotRef? {
        val map = value as? Map<*, *> ?: return null
        val channel = map["channel"]?.toString()?.takeIf { it.isNotBlank() } ?: return null
        val path = map["path"]?.toString()?.takeIf { it.isNotBlank() } ?: return null
        return HistoryScreenshotRef(
            channel = channel,
            path = path,
            timestamp = (map["timestamp"] as? Number)?.toLong() ?: 0L,
            width = (map["width"] as? Number)?.toInt() ?: 0,
            height = (map["height"] as? Number)?.toInt() ?: 0
        )
    }

    private fun screenshotToMap(ref: HistoryScreenshotRef): Map<String, Any?> =
        linkedMapOf(
            "channel" to ref.channel,
            "path" to ref.path,
            "timestamp" to ref.timestamp,
            "width" to ref.width,
            "height" to ref.height
        )

    private fun validScreenshotRefs(
        documentPath: String,
        refs: List<HistoryScreenshotRef>
    ): List<HistoryScreenshotRef> {
        val docDir = File(documentPath)
        val root = File(docDir, "history_screenshots").canonicalFile
        return refs.filter { ref ->
            runCatching {
                val file = File(docDir, ref.path).canonicalFile
                file.path.startsWith(root.path + File.separator) &&
                    file.isFile &&
                    file.length() > 8 &&
                    file.inputStream().use { input ->
                        val header = ByteArray(8)
                        input.read(header) == 8 && looksLikeScreenshot(header)
                    }
            }.getOrDefault(false)
        }
    }

    private val screenshotFilePattern = Regex("""^v(\d+)_(\d+)\.(png|jpg|jpeg)$""", RegexOption.IGNORE_CASE)

    private fun diskScreenshotRefsByVersion(documentPath: String): Map<Int, List<HistoryScreenshotRef>> {
        val docDir = File(documentPath)
        val root = File(docDir, "history_screenshots")
        if (!root.exists()) return emptyMap()

        val refs = linkedMapOf<Int, MutableList<HistoryScreenshotRef>>()
        root.listFiles { file -> file.isDirectory }.orEmpty().forEach { channelDir ->
            channelDir.listFiles { file -> file.isFile && supportedScreenshotExtension(file.extension) }.orEmpty()
                .forEach { file ->
                    val match = screenshotFilePattern.matchEntire(file.name) ?: return@forEach
                    val versionIndex = match.groupValues[1].toIntOrNull() ?: return@forEach
                    val timestamp = match.groupValues[2].toLongOrNull() ?: file.lastModified()
                    if (!isValidScreenshotFile(root, file)) return@forEach
                    refs.getOrPut(versionIndex) { mutableListOf() } += HistoryScreenshotRef(
                        channel = channelDir.name,
                        path = file.relativeTo(docDir).path,
                        timestamp = timestamp
                    )
                }
        }
        return refs.mapValues { (_, value) -> mergeScreenshotRefs(value) }
    }

    private fun mergeScreenshotRefs(refs: List<HistoryScreenshotRef>): List<HistoryScreenshotRef> {
        val byKey = linkedMapOf<Pair<String, String>, HistoryScreenshotRef>()
        refs.forEach { ref ->
            byKey[ref.channel to ref.path] = ref
        }
        return byKey.values.sortedWith(compareBy<HistoryScreenshotRef> { it.channel }.thenBy { it.timestamp })
    }

    private fun isValidScreenshotFile(root: File, file: File): Boolean {
        return runCatching {
            val canonicalRoot = root.canonicalFile
            val canonicalFile = file.canonicalFile
            canonicalFile.path.startsWith(canonicalRoot.path + File.separator) &&
                canonicalFile.isFile &&
                supportedScreenshotExtension(canonicalFile.extension) &&
                canonicalFile.length() > 8 &&
                canonicalFile.inputStream().use { input ->
                    val header = ByteArray(8)
                    input.read(header) == 8 && looksLikeScreenshot(header)
                }
        }.getOrDefault(false)
    }

    private fun screenshotRefs(
        documentPath: String,
        versionIndex: Int,
        timestamp: Long
    ): List<HistoryScreenshotRef> {
        val root = File(documentPath, "history_screenshots")
        if (!root.exists()) return emptyList()
        return root.listFiles { file -> file.isDirectory }.orEmpty().flatMap { channelDir ->
            channelDir.listFiles { file ->
                file.isFile && supportedScreenshotExtension(file.extension) &&
                    file.name.startsWith(String.format("v%05d_", versionIndex))
            }.orEmpty().map { file ->
                HistoryScreenshotRef(
                    channel = channelDir.name,
                    path = file.relativeTo(File(documentPath)).path,
                    timestamp = timestamp
                )
            }
        }.sortedWith(compareBy<HistoryScreenshotRef> { it.channel }.thenBy { it.path })
    }
}