package ru.sergey.familyvpn

import android.net.ConnectivityManager
import android.net.LinkProperties
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import java.net.NetworkInterface
import java.util.concurrent.CountDownLatch

/** No synchronous ConnectivityManager queries from callbacks, and no UI thread work. */
internal class AndroidNetworkMonitor(
    private val connectivity: ConnectivityManager,
    private val publish: (NetworkSnapshot?, Boolean) -> Unit,
    private val underlying: (NetworkSnapshot?) -> Unit
) {
    internal data class NetworkSnapshot(val network: Network, val path: NetworkPath)
    private val thread = HandlerThread("nyrvo-network").apply { start() }
    private val handler = Handler(thread.looper)
    private val state = NetworkPathState()
    @Volatile private var closed = false
    @Volatile var snapshot: NetworkSnapshot? = null; private set
    private var registered = false
    private var network: Network? = null
    private data class CapabilitiesSnapshot(val kind: Int, val metered: Boolean)
    private data class LinkSnapshot(val name: String, val addresses: List<String>, val dns: List<String>, val gateways: List<String>, val mtu: Int)
    private var capabilities: CapabilitiesSnapshot? = null
    private var properties: LinkSnapshot? = null
    private var epoch = 0L
    private var retries = 0
    private var scheduled: Runnable? = null

    private val callback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(value: Network) {
            if (closed) return
            if (network != value) { epoch++; retries = 0; capabilities = null; properties = null }
            network = value; state.available(value.networkHandle); schedule()
        }
        override fun onCapabilitiesChanged(value: Network, caps: NetworkCapabilities) {
            if (closed || network != value) return
            val expensive = !caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
            capabilities = CapabilitiesSnapshot(when {
                caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> 1
                caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> 2
                caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> 3; else -> 0
            }, expensive)
            state.capabilities(value.networkHandle, caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
                caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN), expensive)
            schedule()
        }
        override fun onLinkPropertiesChanged(value: Network, link: LinkProperties) {
            if (closed || network != value) return
            properties = LinkSnapshot(link.interfaceName.orEmpty(),
                link.linkAddresses.map { "${it.address.hostAddress?.substringBefore('%')}/${it.prefixLength}" },
                link.dnsServers.mapNotNull { it.hostAddress },
                link.routes.filter { it.isDefaultRoute }.mapNotNull { it.gateway?.hostAddress },
                if (Build.VERSION.SDK_INT >= 29) link.mtu else 0)
            schedule()
        }
        override fun onBlockedStatusChanged(value: Network, blocked: Boolean) {
            if (closed || network != value) return
            state.blocked(value.networkHandle, blocked); schedule()
        }
        override fun onLost(value: Network) {
            if (closed || network != value) return // an old Wi-Fi loss cannot erase the new mobile path
            epoch++; network = null; capabilities = null; properties = null
            state.lost(value.networkHandle); schedule()
        }
    }
    fun start() {
        if (closed || registered) return
        val request = NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
            .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
            .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN).build()
        // One lifecycle-bounded request, released by close; no repeated network
        // requests or timeout that would permanently stop monitoring while offline.
        registered = true
        if (Build.VERSION.SDK_INT >= 31) connectivity.registerBestMatchingNetworkCallback(request, callback, handler)
        // Handler overload is public since API26. A default callback would also
        // report our own VPN and lose its physical underlay on Android8/8.1.
        else connectivity.requestNetwork(request, callback, handler)
    }
    fun listenerChanged() {
        handler.post { if (!closed) { state.listenerChanged(); schedule() } }
    }
    fun reapplyUnderlying() { handler.post { if (!closed) underlying(snapshot) } }
    private fun schedule(delay: Long = 0) {
        scheduled?.let(handler::removeCallbacks)
        val generation = epoch
        scheduled = Runnable {
            if (closed || generation != epoch) return@Runnable
            val current = network; val link = properties; val caps = capabilities
            if (current != null && link != null && caps != null) {
                val nic = runCatching { NetworkInterface.getByName(link.name) }.getOrNull()
                state.properties(NetworkPath(current.networkHandle, link.name, nic?.index ?: -1, link.addresses,
                    link.dns, link.gateways,
                    link.mtu.takeIf { it > 0 } ?: runCatching { nic?.mtu?.takeIf { it > 0 } }.getOrNull() ?: 1500,
                    caps.kind, caps.metered))
            }
            if (state.waitingForProperties() && retries++ < 10) { schedule(100); return@Runnable }
            state.dispatch(expired = true)?.let { change ->
                val next = change.path?.let { path -> current?.takeIf { it.networkHandle == path.id }?.let { NetworkSnapshot(it, path) } }
                snapshot = next // publish immutable data before native getInterfaces() re-enters
                publish(next, change.resetSameInterface)
            }
        }.also { handler.postDelayed(it, delay) }
    }
    /** Called outside the service's gate, before closing native resources. getInterfaces never waits here. */
    fun close() {
        if (closed) return
        closed = true
        val finished = CountDownLatch(1)
        val finish = Runnable {
            try {
                scheduled?.let(handler::removeCallbacks); scheduled = null; epoch++
                if (registered) { runCatching { connectivity.unregisterNetworkCallback(callback) }; registered = false }
                state.close(); snapshot = null
            } finally { finished.countDown() }
        }
        if (Looper.myLooper() == handler.looper) finish.run()
        else { if (handler.post(finish)) finished.await() else finish.run() }
        thread.quitSafely()
    }
}
