package com.correx.infrastructure.artifactscas.recovery

import com.correx.infrastructure.artifactscas.index.ArtifactIndex
import com.correx.infrastructure.artifactscas.segment.Location
import com.correx.infrastructure.artifactscas.segment.SegmentLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.RandomAccessFile
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.listDirectoryEntries

data class TailScanReport(
    val activeSegmentId: Long,
    val knownTailOffset: Long,
    val recoveredRecords: Int,
    val truncatedAt: Long?,
)

class TailScanner(
    private val segmentsDir: Path,
    private val index: ArtifactIndex,
) {
    suspend fun recover(): TailScanReport? = withContext(Dispatchers.IO) {
        if (!Files.exists(segmentsDir)) return@withContext null
        val active = segmentsDir.listDirectoryEntries("*.seg")
            .mapNotNull { p ->
                runCatching { p.fileName.toString().removeSuffix(".seg").toLong() to p }.getOrNull()
            }
            .maxByOrNull { it.first }
            ?: return@withContext null
        val (activeId, activePath) = active
        val knownTail = index.maxKnownTailFor(activeId)
        scanAndRecover(activeId, activePath, knownTail)
    }

    private suspend fun scanAndRecover(activeId: Long, path: Path, startOffset: Long): TailScanReport {
        var recovered = 0
        var truncatedAt: Long? = null
        withContext(Dispatchers.IO) {
            RandomAccessFile(path.toFile(), "rw").use { raf ->
                val fileLen = raf.length()
                var pos = startOffset
                while (pos < fileLen) {
                    val step = tryReadRecord(raf, pos, fileLen)
                    if (step == null) {
                        truncatedAt = pos
                        break
                    }
                    index.insert(step.hash, Location(activeId, pos, step.length))
                    recovered += 1
                    pos += SegmentLayout.HEADER_SIZE + step.length
                }
                // Truncate at first bad/partial record so writer resumes at a clean boundary.
                truncatedAt?.let { raf.setLength(it) }
            }
        }
        return TailScanReport(activeId, startOffset, recovered, truncatedAt)
    }

    private fun tryReadRecord(raf: RandomAccessFile, pos: Long, fileLen: Long): ValidRecord? =
        runCatching {
            require(fileLen - pos >= SegmentLayout.HEADER_SIZE)
            raf.seek(pos)
            val header = ByteArray(SegmentLayout.HEADER_SIZE)
            raf.readFully(header)
            val (length, crc, hash) = SegmentLayout.decodeHeader(header)
            require(length >= 0 && pos + SegmentLayout.HEADER_SIZE + length <= fileLen)
            val payload = ByteArray(length)
            raf.readFully(payload)
            require(SegmentLayout.crc32c(payload) == crc)
            require(SegmentLayout.blake3(payload).contentEquals(hash))
            ValidRecord(length, hash)
        }.getOrNull()

    private class ValidRecord(val length: Int, val hash: ByteArray)
}
