package com.correx.infrastructure.artifactscas

import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.ArtifactId
import com.correx.core.utils.TypeId
import com.correx.infrastructure.artifactscas.compact.CompactionReport
import com.correx.infrastructure.artifactscas.compact.Compactor
import com.correx.infrastructure.artifactscas.compact.LivenessScanner
import com.correx.infrastructure.artifactscas.config.CasConfig
import com.correx.infrastructure.artifactscas.evict.EvictionReport
import com.correx.infrastructure.artifactscas.evict.Evictor
import com.correx.infrastructure.artifactscas.index.ArtifactIndex
import com.correx.infrastructure.artifactscas.recovery.TailScanReport
import com.correx.infrastructure.artifactscas.recovery.TailScanner
import com.correx.infrastructure.artifactscas.segment.SegmentLayout
import com.correx.infrastructure.artifactscas.segment.SegmentReader
import com.correx.infrastructure.artifactscas.segment.SegmentWriter
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.nio.file.Files
import kotlin.io.path.listDirectoryEntries

class CasArtifactStore(
    private val config: CasConfig,
    private val index: ArtifactIndex,
) : ArtifactStore, AutoCloseable {
    private val segmentsDir = config.rootDir.resolve("segments").also { Files.createDirectories(it) }
    private val writer = SegmentWriter(segmentsDir, config.maxSegmentBytes)
    private val reader = SegmentReader()
    private val mutex = Mutex()
    private val maintenanceMutex = Mutex()

    override suspend fun put(bytes: ByteArray): ArtifactId {
        val hash = SegmentLayout.blake3(bytes)
        return mutex.withLock {
            val existing = index.lookup(hash)
            if (existing == null) {
                val loc = writer.append(bytes, hash)
                index.insert(hash, loc)
            }
            TypeId(hash.toHex())
        }
    }

    override suspend fun get(id: ArtifactId): ByteArray? {
        val hash = id.value.hexToBytes()
        val loc = mutex.withLock { index.lookup(hash) } ?: return null
        return reader.read(loc, segmentsDir)
    }

    override suspend fun flushBefore(commit: suspend () -> Unit) {
        mutex.withLock {
            writer.fsync()
            index.flush()
            commit()
        }
    }

    override fun close() {
        writer.close()
        index.close()
    }

    suspend fun recover(): TailScanReport? = TailScanner(segmentsDir, index).recover()

    fun compactor(eventStore: EventStore): Compactor =
        Compactor(config, index, LivenessScanner(eventStore, index), storeMutex = mutex)

    // Run synchronously; safe to call alongside puts (acquires storeMutex briefly when swapping segments).
    // maintenanceMutex makes compact and evict mutually exclusive so they cannot race on liveness/segment files.
    suspend fun compact(eventStore: EventStore): CompactionReport =
        maintenanceMutex.withLock { compactor(eventStore).compact() }

    suspend fun evict(eventStore: EventStore): EvictionReport =
        maintenanceMutex.withLock {
            val evictor = Evictor(config, index, LivenessScanner(eventStore, index))
            mutex.withLock {
                val activeId = segmentsDir.listDirectoryEntries("*.seg")
                    .mapNotNull { runCatching { it.fileName.toString().removeSuffix(".seg").toLong() }.getOrNull() }
                    .maxOrNull() ?: return@withLock EvictionReport(emptyList(), 0L)
                evictor.evict(activeId)
            }
        }

    companion object {
        suspend fun open(config: CasConfig, index: ArtifactIndex): CasArtifactStore {
            val segmentsDir = config.rootDir.resolve("segments").also { Files.createDirectories(it) }
            TailScanner(segmentsDir, index).recover()
            return CasArtifactStore(config, index)
        }
    }
}

private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }

private const val HEX_RADIX = 16
private const val HEX_CHARS_PER_BYTE = 2
private const val HEX_NIBBLE_BITS = 4

private fun String.hexToBytes(): ByteArray {
    require(length % HEX_CHARS_PER_BYTE == 0) { "hex string must have even length" }
    return ByteArray(length / HEX_CHARS_PER_BYTE) { i ->
        val hi = Character.digit(this[HEX_CHARS_PER_BYTE * i], HEX_RADIX)
        val lo = Character.digit(this[HEX_CHARS_PER_BYTE * i + 1], HEX_RADIX)
        ((hi shl HEX_NIBBLE_BITS) + lo).toByte()
    }
}
