package expo.modules.rebreakprotection.filter

import java.io.File
import java.io.RandomAccessFile
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.channels.FileChannel
import java.util.concurrent.atomic.AtomicReference

/**
 * Memory-mapped binary Hash-Liste — sortierte 64-bit big-endian UInt64s.
 *
 * Datei-Format identisch zu iOS und Server: jeder Hash 8 Bytes, sortiert.
 * Reader macht Binary-Search → O(log n) lookup.
 *
 * Reload-Strategie: bei jedem load() wird neu gemappt. Aufrufer muss load()
 * triggern (z.B. nach syncBlocklist).
 *
 * Thread-safe: AtomicReference auf das aktuelle Snapshot — Lesen ist lock-free.
 */
class HashList(private val file: File) {

    private data class Snapshot(val buffer: ByteBuffer?, val count: Int)

    private val state = AtomicReference(Snapshot(null, 0))

    /**
     * Lädt blocklist.bin neu (mmap). Idempotent — kann zur Laufzeit mehrfach
     * aufgerufen werden, alte mmap wird vom GC eingesammelt.
     */
    @Synchronized
    fun load() {
        if (!file.exists()) {
            state.set(Snapshot(null, 0))
            return
        }
        try {
            RandomAccessFile(file, "r").use { raf ->
                val ch = raf.channel
                val size = ch.size()
                if (size <= 0 || size % 8L != 0L) {
                    state.set(Snapshot(null, 0))
                    return
                }
                val buf = ch.map(FileChannel.MapMode.READ_ONLY, 0, size)
                    .order(ByteOrder.BIG_ENDIAN)
                state.set(Snapshot(buf, (size / 8L).toInt()))
            }
        } catch (e: Exception) {
            state.set(Snapshot(null, 0))
        }
    }

    /** Aktuelle Anzahl der Hashes (zum Logging). */
    fun count(): Int = state.get().count

    /** True wenn die Datei aktuell als blocklist gemmapt ist. */
    fun isLoaded(): Boolean = state.get().buffer != null

    /**
     * Binary-Search auf sortierten 64-bit Hashes. O(log n).
     * Long-Vergleich ist hier UNSIGNED-semantisch — die Datei ist als unsigned
     * sortiert. Wir machen unsigned-Comparison via Long.compareUnsigned.
     */
    fun contains(hash: Long): Boolean {
        val snap = state.get()
        val buf = snap.buffer ?: return false
        val count = snap.count
        if (count == 0) return false

        var lo = 0
        var hi = count - 1
        while (lo <= hi) {
            val mid = (lo + hi) ushr 1
            val v = buf.getLong(mid * 8)
            val cmp = java.lang.Long.compareUnsigned(v, hash)
            when {
                cmp == 0 -> return true
                cmp < 0 -> lo = mid + 1
                else -> hi = mid - 1
            }
        }
        return false
    }

    /**
     * Subdomain-Match (max 5 Iterationen, parallel zu iOS).
     * Für `evil.shop.bet365.com` testen wir der Reihe nach:
     *   evil.shop.bet365.com → shop.bet365.com → bet365.com → com (TLD wird nie matchen).
     */
    fun matchesAnySuffix(host: String): Boolean {
        var current = DomainHasher.normalize(host)
        var iter = 0
        while (iter < 5) {
            if (contains(DomainHasher.hash(current))) return true
            val dot = current.indexOf('.')
            if (dot < 0) return false
            current = current.substring(dot + 1)
            if (!current.contains('.')) return false
            iter++
        }
        return false
    }
}
