/*
 * Copyright (C) 2026 SHX VPN
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 */

package org.strongswan.android.logic.autoconnect

import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.WifiInfo
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import java.util.concurrent.ConcurrentHashMap

/**
 * Watches underlying (non-VPN) networks. While a VPN is up,
 * [ConnectivityManager.getActiveNetwork] is the tunnel, so we must track
 * Wi-Fi / cellular from the callback instead.
 *
 * Wi-Fi is preferred over cellular when both are up. Events are debounced
 * because Wi-Fi ↔ mobile switches arrive as a cascade.
 *
 * SSID is cached from [ConnectivityManager.NetworkCallback] callbacks registered
 * with [ConnectivityManager.NetworkCallback.FLAG_INCLUDE_LOCATION_INFO] so it
 * stays readable while the app is in the background.
 */
class NetworkMonitor(
	private val context: Context,
	private val trustedSsidsProvider: () -> Set<String>,
	private val onNetworkChanged: (AutoConnectManager.NetworkKind?) -> Unit,
)
{
	private val handler = Handler(Looper.getMainLooper())
	private val networks = ConcurrentHashMap<Network, NetworkCapabilities>()
	private val ssids = ConcurrentHashMap<Network, String>()
	private var registered = false
	private var lastReported: AutoConnectManager.NetworkKind? = null
	private var ssidRetries = 0

	private val events = object
	{
		fun onAvailable(network: Network)
		{
			val cm = connectivityManager() ?: return
			cm.getNetworkCapabilities(network)?.let { remember(network, it) }
			scheduleEvaluate()
		}

		fun onLost(network: Network)
		{
			networks.remove(network)
			ssids.remove(network)
			scheduleEvaluate()
		}

		fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities)
		{
			if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN))
			{
				networks.remove(network)
				ssids.remove(network)
			}
			else
			{
				remember(network, capabilities)
			}
			scheduleEvaluate()
		}
	}

	private val callback: ConnectivityManager.NetworkCallback = createCallback()

	fun start()
	{
		if (registered)
		{
			return
		}
		val cm = connectivityManager() ?: return
		val request = NetworkRequest.Builder()
			.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
			.build()
		try
		{
			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
			{
				cm.registerNetworkCallback(request, callback, handler)
			}
			else
			{
				cm.registerNetworkCallback(request, callback)
			}
			registered = true
			snapshotExisting(cm)
			scheduleEvaluate()
		}
		catch (e: Exception)
		{
			Log.e(TAG, "Failed to register network callback", e)
		}
	}

	fun stop()
	{
		if (!registered)
		{
			return
		}
		handler.removeCallbacks(evaluateRunnable)
		try
		{
			connectivityManager()?.unregisterNetworkCallback(callback)
		}
		catch (_: Exception)
		{
		}
		registered = false
		networks.clear()
		ssids.clear()
		lastReported = null
		ssidRetries = 0
	}

	fun forceEvaluate()
	{
		lastReported = null
		ssidRetries = 0
		handler.removeCallbacks(evaluateRunnable)
		handler.post(evaluateRunnable)
	}

	fun currentKind(): AutoConnectManager.NetworkKind?
	{
		var wifiNetwork: Network? = null
		var wifiCaps: NetworkCapabilities? = null
		var cellular = false
		for ((network, caps) in networks)
		{
			if (caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN))
			{
				continue
			}
			if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
			{
				wifiNetwork = network
				wifiCaps = caps
			}
			else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR))
			{
				cellular = true
			}
		}
		if (wifiNetwork != null && wifiCaps != null)
		{
			val ssid = ssidFor(wifiNetwork, wifiCaps)
			val trusted = trustedSsidsProvider()
			Log.i(TAG, "Underlying Wi-Fi ssid=$ssid trusted=$trusted")
			if (ssid == null)
			{
				return AutoConnectManager.NetworkKind.UNKNOWN_WIFI
			}
			return if (trusted.any { it.equals(ssid, ignoreCase = true) })
			{
				AutoConnectManager.NetworkKind.TRUSTED_WIFI
			}
			else
			{
				AutoConnectManager.NetworkKind.UNTRUSTED_WIFI
			}
		}
		if (cellular)
		{
			return AutoConnectManager.NetworkKind.CELLULAR
		}
		return null
	}

	private fun remember(network: Network, caps: NetworkCapabilities)
	{
		networks[network] = caps
		SsidReader.fromWifiInfo(caps.transportInfo as? WifiInfo)?.let { ssids[network] = it }
	}

	private fun ssidFor(network: Network, caps: NetworkCapabilities): String?
	{
		ssids[network]?.let { return it }
		return SsidReader.from(context, caps)
	}

	private fun snapshotExisting(cm: ConnectivityManager)
	{
		try
		{
			for (network in cm.allNetworks)
			{
				val caps = cm.getNetworkCapabilities(network) ?: continue
				if (caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN))
				{
					continue
				}
				if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
					caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR))
				{
					remember(network, caps)
				}
			}
		}
		catch (e: Exception)
		{
			Log.w(TAG, "Failed to snapshot networks", e)
		}
	}

	private fun connectivityManager(): ConnectivityManager? =
		context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager

	private fun createCallback(): ConnectivityManager.NetworkCallback
	{
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
		{
			return object : ConnectivityManager.NetworkCallback(
				ConnectivityManager.NetworkCallback.FLAG_INCLUDE_LOCATION_INFO
			)
			{
				override fun onAvailable(network: Network) = events.onAvailable(network)

				override fun onLost(network: Network) = events.onLost(network)

				override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) =
					events.onCapabilitiesChanged(network, capabilities)
			}
		}
		return object : ConnectivityManager.NetworkCallback()
		{
			override fun onAvailable(network: Network) = events.onAvailable(network)

			override fun onLost(network: Network) = events.onLost(network)

			override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) =
				events.onCapabilitiesChanged(network, capabilities)
		}
	}

	private fun scheduleEvaluate()
	{
		handler.removeCallbacks(evaluateRunnable)
		handler.postDelayed(evaluateRunnable, DEBOUNCE_MS)
	}

	private fun evaluateNow()
	{
		val kind = currentKind()
		if (kind == AutoConnectManager.NetworkKind.UNKNOWN_WIFI && ssidRetries < SSID_RETRY_MAX)
		{
			ssidRetries++
			Log.i(TAG, "Wi-Fi SSID not readable yet, retry $ssidRetries/$SSID_RETRY_MAX")
			handler.postDelayed(evaluateRunnable, SSID_RETRY_MS)
			return
		}
		ssidRetries = 0
		if (kind != lastReported)
		{
			Log.i(TAG, "Network kind $lastReported -> $kind")
			lastReported = kind
			onNetworkChanged(kind)
		}
	}

	private val evaluateRunnable = Runnable { evaluateNow() }

	companion object
	{
		private const val TAG = "NetworkMonitor"
		private const val DEBOUNCE_MS = 1500L
		private const val SSID_RETRY_MS = 400L
		private const val SSID_RETRY_MAX = 8
	}
}
