package de.arxsilex.maptools.converter.core.pbf

import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.IOException
import java.nio.file.AtomicMoveNotSupportedException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.security.DigestInputStream
import java.security.MessageDigest
import kotlin.io.path.fileSize
import kotlin.io.path.getLastModifiedTime

object PbfBlockIndexStore {
    private const val MAGIC = 0x4D544349
    private const val FORMAT_VERSION = 1
    private const val MAX_ENTRIES = 10_000_000

    fun write(index: PbfBlockIndex, destination: Path) {
        val normalizedDestination = destination.toAbsolutePath().normalize()
        val parent = normalizedDestination.parent
            ?: throw IOException("Für die Indexdatei fehlt ein Zielverzeichnis.")
        Files.createDirectories(parent)
        val temporary = Files.createTempFile(parent, normalizedDestination.fileName.toString(), ".tmp")
        try {
            DataOutputStream(BufferedOutputStream(Files.newOutputStream(temporary))).use { output ->
                output.writeInt(MAGIC)
                output.writeInt(FORMAT_VERSION)
                output.writeLong(index.sourceSize)
                output.writeLong(index.sourceLastModifiedMillis)
                output.writeUTF(index.sourceSha256)
                output.writeInt(index.entries.size)
                index.entries.forEach { entry ->
                    output.writeInt(entry.sequence)
                    output.writeLong(entry.fileOffset)
                    output.writeUTF(entry.type)
                    output.writeInt(entry.blobHeaderSize)
                    output.writeInt(entry.blobSize)
                    output.writeInt(entry.rawSize ?: -1)
                    output.writeByte(entry.compression.ordinal)
                }
            }
            try {
                Files.move(
                    temporary,
                    normalizedDestination,
                    StandardCopyOption.ATOMIC_MOVE,
                    StandardCopyOption.REPLACE_EXISTING,
                )
            } catch (_: AtomicMoveNotSupportedException) {
                Files.move(temporary, normalizedDestination, StandardCopyOption.REPLACE_EXISTING)
            }
        } finally {
            Files.deleteIfExists(temporary)
        }
    }

    fun read(source: Path): PbfBlockIndex =
        DataInputStream(BufferedInputStream(Files.newInputStream(source))).use { input ->
            if (input.readInt() != MAGIC) throw IOException("Die Datei ist kein MapTools-PBF-Index.")
            val version = input.readInt()
            if (version != FORMAT_VERSION) {
                throw IOException("Nicht unterstützte PBF-Indexversion $version.")
            }
            val sourceSize = input.readLong()
            val sourceLastModifiedMillis = input.readLong()
            val sourceSha256 = input.readUTF()
            if (!sourceSha256.matches(Regex("[0-9a-f]{64}"))) {
                throw IOException("Der PBF-Index enthält keinen gültigen SHA-256-Wert.")
            }
            val entryCount = input.readInt()
            if (entryCount !in 1..MAX_ENTRIES) {
                throw IOException("Ungültige Anzahl von PBF-Indexeinträgen: $entryCount.")
            }
            val compressions = PbfCompression.entries
            val entries = List(entryCount) {
                val sequence = input.readInt()
                val fileOffset = input.readLong()
                val type = input.readUTF()
                val blobHeaderSize = input.readInt()
                val blobSize = input.readInt()
                val rawSizeValue = input.readInt()
                val compressionOrdinal = input.readUnsignedByte()
                if (compressionOrdinal !in compressions.indices) {
                    throw IOException("Unbekannte Kompression im PBF-Index: $compressionOrdinal.")
                }
                PbfBlockIndexEntry(
                    sequence = sequence,
                    fileOffset = fileOffset,
                    type = type,
                    blobHeaderSize = blobHeaderSize,
                    blobSize = blobSize,
                    rawSize = rawSizeValue.takeIf { it >= 0 },
                    compression = compressions[compressionOrdinal],
                ).also { entry -> validateEntry(entry, it) }
            }
            PbfBlockIndex(sourceSize, sourceLastModifiedMillis, sourceSha256, entries)
        }

    fun matchesSource(index: PbfBlockIndex, source: Path, verifySha256: Boolean = false): Boolean {
        val normalizedSource = source.toAbsolutePath().normalize()
        if (!Files.isRegularFile(normalizedSource)) return false
        if (normalizedSource.fileSize() != index.sourceSize) return false
        if (normalizedSource.getLastModifiedTime().toMillis() != index.sourceLastModifiedMillis) return false
        return !verifySha256 || sha256(normalizedSource) == index.sourceSha256
    }

    private fun validateEntry(entry: PbfBlockIndexEntry, expectedSequence: Int) {
        if (entry.sequence != expectedSequence || entry.fileOffset < 0L ||
            entry.blobHeaderSize !in 1..64 * 1024 || entry.blobSize !in 1..32 * 1024 * 1024 ||
            entry.type.isBlank()
        ) {
            throw IOException("Ungültiger PBF-Indexeintrag $expectedSequence.")
        }
    }

    private fun sha256(path: Path): String {
        val digest = MessageDigest.getInstance("SHA-256")
        DigestInputStream(Files.newInputStream(path), digest).use { input ->
            val buffer = ByteArray(64 * 1024)
            while (input.read(buffer) >= 0) {
                // DigestInputStream aktualisiert den Hash bereits beim Lesen.
            }
        }
        return digest.digest().joinToString("") { byte ->
            (byte.toInt() and 0xFF).toString(16).padStart(2, '0')
        }
    }
}
