package de.arxsilex.maptools.converter.desktop

import de.arxsilex.maptools.converter.core.GeoBounds
import de.arxsilex.maptools.converter.core.ElevationOutputFormat
import de.arxsilex.maptools.converter.core.InputFormat
import de.arxsilex.maptools.converter.core.MapFormatVersion
import de.arxsilex.maptools.converter.core.PoiSchemaVersion
import de.arxsilex.maptools.converter.core.elevation.ElevationMetadataReader
import java.nio.ByteBuffer
import java.nio.file.Files
import java.nio.file.Path
import java.sql.Connection
import java.sql.DriverManager
import org.mapsforge.map.reader.MapFile

internal object ExportOutputValidator {
    private val mapsforgeMagic = "mapsforge binary OSM".encodeToByteArray()
    private val sqliteMagic = byteArrayOf(
        0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66,
        0x6f, 0x72, 0x6d, 0x61, 0x74, 0x20, 0x33, 0x00,
    )

    fun validateElevation(path: Path, format: ElevationOutputFormat) {
        val inputFormat = when (format) {
            ElevationOutputFormat.GEOTIFF -> InputFormat.ELEVATION_GEOTIFF
            ElevationOutputFormat.ESRI_ASCII_GRID -> InputFormat.ELEVATION_ASC
            ElevationOutputFormat.SRTM_HGT -> InputFormat.ELEVATION_HGT
            ElevationOutputFormat.DTED -> InputFormat.ELEVATION_DTED
        }
        val files = if (format.tiled) {
            require(Files.isDirectory(path)) { "Das Höhendaten-Ausgabeverzeichnis fehlt: $path" }
            Files.list(path).use { entries ->
                entries.filter(Files::isRegularFile).toList()
            }
        } else {
            listOf(path)
        }
        require(files.isNotEmpty()) { "Die Höhendatenausgabe enthält keine Rasterdateien." }
        files.forEach { file ->
            require(Files.size(file) > 0L) { "Die Höhendatendatei ist leer: $file" }
            ElevationMetadataReader.read(file, inputFormat)
        }
    }

    fun validateMapsforgeMapHeader(path: Path, expectedFormat: MapFormatVersion) {
        val header = readHeader(path, mapsforgeMagic.size + 8)
        require(header.copyOfRange(0, mapsforgeMagic.size).contentEquals(mapsforgeMagic)) {
            "Die erzeugte .map-Datei besitzt keinen gültigen Mapsforge-Dateikopf."
        }
        val formatVersion = ByteBuffer.wrap(header, mapsforgeMagic.size + 4, 4).int
        require(formatVersion == expectedFormat.metadataVersion) {
            "Erwartet wurde Mapsforge-Kartenformat v${expectedFormat.metadataVersion}, erzeugt wurde v$formatVersion."
        }
    }

    fun validateMapsforgeMap(path: Path, expectedBounds: GeoBounds, expectedFormat: MapFormatVersion) {
        validateMapsforgeMapHeader(path, expectedFormat)
        val mapFile = MapFile(path.toFile())
        try {
            val info = mapFile.mapFileInfo
            require(info.fileVersion == expectedFormat.metadataVersion) {
                "Mapsforge-Reader meldet Formatversion ${info.fileVersion}."
            }
            require(info.fileSize == Files.size(path)) { "Mapsforge-Reader meldet eine abweichende Dateigröße." }
            val actual = info.boundingBox
            val tolerance = 0.000_001
            require(kotlin.math.abs(actual.minLongitude - expectedBounds.west) <= tolerance &&
                kotlin.math.abs(actual.minLatitude - expectedBounds.south) <= tolerance &&
                kotlin.math.abs(actual.maxLongitude - expectedBounds.east) <= tolerance &&
                kotlin.math.abs(actual.maxLatitude - expectedBounds.north) <= tolerance
            ) {
                "Die Mapsforge-Kartengrenzen stimmen nicht mit der Auswahl überein: $actual"
            }
            require(mapFile.startPosition() != null && mapFile.startZoomLevel() != null) {
                "Startposition oder Startzoom fehlen in der Mapsforge-Karte."
            }
        } finally {
            mapFile.close()
        }
    }

    fun validateMapsforgePoi(path: Path, expectedSchema: PoiSchemaVersion) {
        val header = readHeader(path, sqliteMagic.size)
        require(header.contentEquals(sqliteMagic)) {
            "Die erzeugte .poi-Datei besitzt keinen gültigen SQLite-Dateikopf."
        }

        try {
            Class.forName("org.sqlite.JDBC")
            val databaseUri = path.toAbsolutePath().toUri()
            DriverManager.getConnection("jdbc:sqlite:$databaseUri?mode=ro").use { connection ->
                require(connection.singleString("PRAGMA quick_check") == "ok") {
                    "Die SQLite-Integritätsprüfung der .poi-Datei ist fehlgeschlagen."
                }

                val tables = connection.queryStrings(
                    "SELECT name FROM sqlite_master WHERE type = 'table'",
                )
                val requiredTables = setOf(
                    "metadata",
                    "poi_categories",
                    "poi_category_map",
                    "poi_data",
                    "poi_index",
                ) + if (expectedSchema == PoiSchemaVersion.V4) setOf("poi_data_fts") else emptySet()
                require(tables.containsAll(requiredTables)) {
                    "Die .poi-Datei besitzt kein vollständiges Mapsforge-POI-v${expectedSchema.metadataVersion}-Schema."
                }

                val poiIndexColumns = connection.queryStrings("SELECT name FROM pragma_table_info('poi_index')")
                val requiredIndexColumns = when (expectedSchema) {
                    PoiSchemaVersion.V3 -> setOf("id", "lat", "lon")
                    PoiSchemaVersion.V4 -> setOf("id", "minLat", "maxLat", "minLon", "maxLon")
                }
                require(poiIndexColumns.containsAll(requiredIndexColumns)) {
                    "Der räumliche POI-Index entspricht nicht dem Mapsforge-POI-v${expectedSchema.metadataVersion}-Format."
                }
                require(connection.metadataValue("version") == expectedSchema.metadataVersion.toString()) {
                    "Die .poi-Datei besitzt nicht die erwartete Mapsforge-POI-Version ${expectedSchema.metadataVersion}."
                }
                require(connection.metadataValue("bounds")?.split(',')?.size == 4) {
                    "In der .poi-Datei fehlen gültige Kartengrenzen."
                }

                val poiCount = connection.singleLong("SELECT COUNT(*) FROM poi_data")
                val spatialCount = connection.singleLong("SELECT COUNT(*) FROM poi_index")
                require(poiCount == spatialCount) {
                    "POI-Daten und räumlicher Index sind unvollständig ($poiCount/$spatialCount)."
                }
                when (expectedSchema) {
                    PoiSchemaVersion.V3 -> require(
                        connection.singleLong(
                            "SELECT COUNT(*) FROM poi_index WHERE lat < -90 OR lat > 90 OR lon < -180 OR lon > 180",
                        ) == 0L,
                    ) {
                        "Der POI-v3-Index enthält ungültige Koordinaten."
                    }
                    PoiSchemaVersion.V4 -> {
                        require(
                            connection.singleLong(
                                "SELECT COUNT(*) FROM poi_index " +
                                    "WHERE minLat > maxLat OR minLon > maxLon",
                            ) == 0L,
                        ) {
                            "Der räumliche POI-Index enthält ungültige Koordinatengrenzen."
                        }
                        // Diese Abfrage zwingt SQLite, das FTS5-Modul tatsächlich zu laden.
                        connection.prepareStatement(
                            "SELECT COUNT(*) FROM poi_data_fts WHERE poi_data_fts MATCH ?",
                        ).use { statement ->
                            statement.setString(1, "\"maptoolsvalidatornomatch\"")
                            statement.executeQuery().use { result ->
                                require(result.next()) { "Der FTS5-Volltextindex kann nicht gelesen werden." }
                            }
                        }
                    }
                }
            }
        } catch (exception: ReflectiveOperationException) {
            throw IllegalStateException("Der SQLite-Treiber für die POI-Prüfung fehlt.", exception)
        } catch (exception: java.sql.SQLException) {
            throw IllegalArgumentException(
                "Die erzeugte .poi-Datei kann nicht als Mapsforge-POI-Datenbank gelesen werden: ${exception.message}",
                exception,
            )
        }
    }

    private fun Connection.queryStrings(sql: String): Set<String> =
        createStatement().use { statement ->
            statement.executeQuery(sql).use { result ->
                buildSet {
                    while (result.next()) add(result.getString(1))
                }
            }
        }

    private fun Connection.singleString(sql: String): String? =
        createStatement().use { statement ->
            statement.executeQuery(sql).use { result ->
                if (result.next()) result.getString(1) else null
            }
        }

    private fun Connection.singleLong(sql: String): Long =
        createStatement().use { statement ->
            statement.executeQuery(sql).use { result ->
                require(result.next()) { "Die Datenbankabfrage lieferte kein Ergebnis." }
                result.getLong(1)
            }
        }

    private fun Connection.metadataValue(name: String): String? =
        prepareStatement("SELECT value FROM metadata WHERE name = ?").use { statement ->
            statement.setString(1, name)
            statement.executeQuery().use { result ->
                if (result.next()) result.getString(1) else null
            }
        }

    private fun readHeader(path: Path, byteCount: Int): ByteArray {
        require(Files.isRegularFile(path) && Files.size(path) >= byteCount) {
            "Die Ausgabedatei fehlt oder ist unvollständig: $path"
        }
        return Files.newInputStream(path).use { input ->
            ByteArray(byteCount).also { bytes ->
                var offset = 0
                while (offset < bytes.size) {
                    val count = input.read(bytes, offset, bytes.size - offset)
                    require(count >= 0) { "Der Dateikopf ist unvollständig: $path" }
                    offset += count
                }
            }
        }
    }
}
