/*
 * 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.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.VpnService
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.Log
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import org.strongswan.android.data.VpnProfileDataSource
import org.strongswan.android.logic.VpnStateService
import org.strongswan.android.ui.VpnProfileControlActivity
import java.util.concurrent.atomic.AtomicBoolean

/**
 * Trusted networks model: a trusted Wi-Fi means the VPN should be down, everything else
 * (cellular or untrusted Wi-Fi) means the VPN should be up.
 *
 * The manager disconnects any active session on a trusted Wi-Fi so that
 * leaving the network (cellular / untrusted Wi-Fi) can start the tunnel again.
 */
class AutoConnectManager private constructor(
	private val context: Context,
) : VpnStateService.VpnStateListener
{
	data class Config(
		val enabled: Boolean = false,
		val profileUuid: String? = null,
		val trustedSsids: Set<String> = emptySet(),
	)

	enum class NetworkKind
	{
		TRUSTED_WIFI,
		UNTRUSTED_WIFI,
		UNKNOWN_WIFI,
		CELLULAR,
	}

	enum class Action
	{
		CONNECT,
		DISCONNECT,
		IGNORE,
	}

	private val handler = Handler(Looper.getMainLooper())
	private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
	private val pendingEvaluate = AtomicBoolean(false)
	private val running = AtomicBoolean(false)

	private var service: VpnStateService? = null
	private var config = Config()
	private var currentNetwork: NetworkKind? = null
	/* marks the session this manager started itself */
	private val autoSession = AtomicBoolean(false)

	private val monitor = NetworkMonitor(context, { config.trustedSsids }, ::onNetworkChanged)
	private val lifecycleBound = AtomicBoolean(false)

	private val appLifecycleObserver = object : DefaultLifecycleObserver
	{
		override fun onStart(owner: LifecycleOwner)
		{
			if (running.get())
			{
				monitor.forceEvaluate()
			}
		}
	}

	private val connection = object : ServiceConnection
	{
		override fun onServiceConnected(name: ComponentName?, binder: IBinder?)
		{
			service = (binder as VpnStateService.LocalBinder).service
			service?.registerListener(this@AutoConnectManager)
			evaluate()
		}

		override fun onServiceDisconnected(name: ComponentName?)
		{
			service = null
		}
	}

	fun start()
	{
		config = loadConfig()
		if (!config.enabled)
		{
			return
		}
		if (!running.compareAndSet(false, true))
		{
			monitor.forceEvaluate()
			return
		}
		context.bindService(
			Intent(context, VpnStateService::class.java), connection, Service.BIND_AUTO_CREATE
		)
		monitor.start()
		bindAppLifecycle()
	}

	fun stop()
	{
		unbindAppLifecycle()
		monitor.stop()
		if (!running.compareAndSet(true, false))
		{
			return
		}
		handler.removeCallbacksAndMessages(null)
		service?.unregisterListener(this)
		try
		{
			context.unbindService(connection)
		}
		catch (_: IllegalArgumentException)
		{
		}
		service = null
	}

	fun reloadConfig()
	{
		config = loadConfig()
		if (config.enabled)
		{
			start()
		}
		else
		{
			stop()
			autoSession.set(false)
		}
	}

	val currentConfig: Config
		get()
		{
			if (!running.get())
			{
				config = loadConfig()
			}
			return config
		}

	fun updateConfig(transform: (Config) -> Config)
	{
		val updated = transform(config)
		saveConfig(updated)
		config = updated
		if (updated.enabled)
		{
			start()
			currentNetwork = monitor.currentKind()
			evaluate()
		}
		else
		{
			stop()
			autoSession.set(false)
		}
	}

	/**
	 * Called by [NetworkMonitor] when the active network changed (already debounced).
	 */
	fun onNetworkChanged(kind: NetworkKind?)
	{
		if (!running.get())
		{
			return
		}
		currentNetwork = kind
		evaluate()
	}

	override fun stateChanged()
	{
		val state = service?.state ?: return
		if (state == VpnStateService.State.DISABLED)
		{
			autoSession.set(false)
		}
	}

	/**
	 * Pure decision function, unit-testable.
	 */
	private fun evaluate(config: Config, network: NetworkKind?, vpnActive: Boolean, autoSession: Boolean): Action =
		evaluateDecision(config, network, vpnActive, autoSession)

	private fun evaluate()
	{
		if (!running.get())
		{
			return
		}
		val svc = service ?: return
		val state = svc.state
		val vpnActive = state == VpnStateService.State.CONNECTED || state == VpnStateService.State.CONNECTING
		when (evaluate(config, currentNetwork, vpnActive, autoSession.get()))
		{
			Action.CONNECT -> connectProfile()
			Action.DISCONNECT ->
			{
				Log.i(TAG, "Trusted network detected, disconnecting")
				autoSession.set(false)
				svc.disconnect()
			}
			Action.IGNORE ->
			{
			}
		}
	}

	private fun connectProfile()
	{
		val uuid = config.profileUuid ?: return
		if (!pendingEvaluate.compareAndSet(false, true))
		{
			return
		}
		handler.postDelayed({
			pendingEvaluate.set(false)
			val svc = service
			if (svc != null && running.get())
			{
				val state = svc.state
				val vpnActive = state == VpnStateService.State.CONNECTED ||
					state == VpnStateService.State.CONNECTING
				if (evaluate(config, currentNetwork, vpnActive, autoSession.get()) == Action.CONNECT)
				{
					Log.i(TAG, "Untrusted network detected, starting auto session")
					autoSession.set(true)
					startVpn(svc, uuid)
				}
			}
		}, CONNECT_DELAY_MS)
	}

	private fun startVpn(svc: VpnStateService, uuid: String)
	{
		val needsConsent = try
		{
			VpnService.prepare(context) != null
		}
		catch (e: Exception)
		{
			Log.e(TAG, "VpnService.prepare failed", e)
			true
		}
		if (!needsConsent)
		{
			val extras = Bundle()
			extras.putString(VpnProfileDataSource.KEY_UUID, uuid)
			svc.connect(extras, true)
			return
		}
		val intent = Intent(context, VpnProfileControlActivity::class.java)
		intent.action = VpnProfileControlActivity.START_PROFILE
		intent.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_UUID, uuid)
		intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
		try
		{
			context.startActivity(intent)
		}
		catch (e: Exception)
		{
			Log.e(TAG, "Failed to start profile control activity", e)
			autoSession.set(false)
		}
	}

	private fun loadConfig(): Config
	{
		val json = prefs.getString(KEY_CONFIG, null) ?: return Config()
		return try
		{
			val obj = JSONObject(json)
			Config(
				enabled = obj.optBoolean("enabled", false),
				profileUuid = if (obj.isNull("profileUuid")) null else obj.optString("profileUuid").takeIf { it.isNotBlank() },
				trustedSsids = obj.optJSONArray("trustedSsids")?.let { arr ->
					(0 until arr.length()).mapNotNull { i ->
						arr.optString(i)?.takeIf { it.isNotBlank() }
					}.toSet()
				} ?: emptySet(),
			)
		}
		catch (_: JSONException)
		{
			Config()
		}
	}

	private fun saveConfig(config: Config)
	{
		val obj = JSONObject()
		obj.put("enabled", config.enabled)
		obj.put("profileUuid", config.profileUuid ?: JSONObject.NULL)
		obj.put("trustedSsids", JSONArray(config.trustedSsids.sorted()))
		prefs.edit().putString(KEY_CONFIG, obj.toString()).apply()
	}

	private fun bindAppLifecycle()
	{
		if (!lifecycleBound.compareAndSet(false, true))
		{
			return
		}
		ProcessLifecycleOwner.get().lifecycle.addObserver(appLifecycleObserver)
	}

	private fun unbindAppLifecycle()
	{
		if (!lifecycleBound.compareAndSet(true, false))
		{
			return
		}
		ProcessLifecycleOwner.get().lifecycle.removeObserver(appLifecycleObserver)
	}

	companion object
	{
		private const val TAG = "AutoConnectManager"
		private const val PREFS_NAME = "one.shx.strongswan.ext.AUTO_CONNECT"
		private const val KEY_CONFIG = "config"
		private const val CONNECT_DELAY_MS = 3000L

		/**
		 * Pure decision function: trusted Wi-Fi means VPN down, everything else
		 * means VPN up.
		 */
		fun evaluateDecision(
			config: Config,
			network: NetworkKind?,
			vpnActive: Boolean,
			autoSession: Boolean,
		): Action
		{
			if (!config.enabled || config.profileUuid == null)
			{
				return Action.IGNORE
			}
			return when (network)
			{
				null -> Action.IGNORE
				NetworkKind.TRUSTED_WIFI -> if (vpnActive) Action.DISCONNECT else Action.IGNORE
				/* SSID is redacted in background until location is readable. Keep an
				 * existing tunnel (do not treat unknown as untrusted) and still
				 * auto-start if the VPN is down. */
				NetworkKind.UNKNOWN_WIFI -> if (!vpnActive) Action.CONNECT else Action.IGNORE
				NetworkKind.UNTRUSTED_WIFI, NetworkKind.CELLULAR -> if (!vpnActive) Action.CONNECT else Action.IGNORE
			}
		}

		@Volatile
		private var instance: AutoConnectManager? = null

		@JvmStatic
		fun getInstance(context: Context): AutoConnectManager =
			instance ?: synchronized(this) {
				instance ?: AutoConnectManager(context.applicationContext).also { instance = it }
			}
	}
}
