package chat.vojo.proxy.service

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.VpnService
import android.os.Build
import android.os.ParcelFileDescriptor
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import chat.vojo.proxy.MainActivity
import chat.vojo.proxy.R
import chat.vojo.proxy.core.AppSettings
import chat.vojo.proxy.core.ProxyConfig
import chat.vojo.proxy.core.Tun2Socks
import chat.vojo.proxy.core.formatBytes
import chat.vojo.proxy.core.formatUptime
import chat.vojo.proxy.core.socks.LocalSocks5Server
import chat.vojo.proxy.core.upstream.SocketProtector
import chat.vojo.proxy.core.upstream.Upstream
import chat.vojo.proxy.data.ConfigStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.net.DatagramSocket
import java.net.Socket
import java.util.concurrent.Executors

class ProxyVpnService : VpnService(), SocketProtector {

    // Every lifecycle transition (start/stop/restart/engine-exit) runs on this
    // single thread: no start/stop races, no blocking joins on the main thread.
    private val lifecycle = Executors.newSingleThreadExecutor { r -> Thread(r, "vojo-vpn-lifecycle") }
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    private var vpnInterface: ParcelFileDescriptor? = null
    private var server: LocalSocks5Server? = null
    private var engineThread: Thread? = null
    private var statsJob: Job? = null
    private var networkCallback: ConnectivityManager.NetworkCallback? = null
    private var lastNetwork: Network? = null

    // Mutated only on the lifecycle thread, but read from IO threads via
    // onUpstreamUnreachable, so it must be visible across threads.
    @Volatile private var running = false
    private var tornDown = true               // idempotency guard for shutdown()
    @Volatile private var stopRequested = false
    @Volatile private var engineGen = 0
    @Volatile private var lastUpstreamWarn = 0L

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when (intent?.action) {
            ACTION_STOP -> {
                lifecycle.execute {
                    shutdown(errorMessage = null)
                    stopSelf()
                }
                return START_NOT_STICKY
            }
            ACTION_RESTART -> {
                if (!goForeground()) return START_NOT_STICKY
                lifecycle.execute {
                    if (running) {
                        VpnState.event("Переподключение")
                        shutdown(errorMessage = null, keepNotification = true)
                    }
                    startTunnel()
                }
            }
            else -> {
                if (!goForeground()) return START_NOT_STICKY
                lifecycle.execute { startTunnel() }
            }
        }
        return START_STICKY
    }

    /** Promotes to foreground immediately (the 5-second FGS window starts at onStartCommand). */
    private fun goForeground(): Boolean {
        return try {
            startForegroundNotification(getString(R.string.notif_connecting))
            true
        } catch (e: Throwable) {
            // ForegroundServiceStartNotAllowedException on a background sticky
            // restart (Android 12+) must not crash the process. Tear down on the
            // lifecycle thread (an in-flight RESTART may still have a live engine)
            // and only then mark the tunnel stopped.
            Log.e(TAG, "startForeground rejected", e)
            lifecycle.execute {
                shutdown(errorMessage = null)
                VpnState.setStopped(null)
                stopSelf()
            }
            false
        }
    }

    private fun startTunnel() { // lifecycle thread
        if (running) return
        stopRequested = false
        tornDown = false
        lastUpstreamWarn = 0L
        val gen = ++engineGen
        // A new attempt supersedes any prior failure — clear its lingering notification.
        runCatching { getSystemService(NotificationManager::class.java)?.cancel(NOTIF_ERROR_ID) }
        VpnState.setConnecting()

        try {
            val state = runBlocking { ConfigStore(this@ProxyVpnService).current() }
            val config = state.activeConfig
            if (config == null) {
                fail(getString(R.string.error_no_server))
                return
            }
            VpnState.event("Подключение", "${config.name} · ${config.type.label}")
            updateNotification(getString(R.string.notif_connecting))

            val upstream = Upstream.create(config, this)
            val socks = LocalSocks5Server(upstream, ::onUpstreamUnreachable).also { it.start() }
            server = socks

            val pfd = establish(config, state.settings, socks.port)
            vpnInterface = pfd
            VpnState.event("Интерфейс поднят", "$TUN_IPV4 · mtu ${state.settings.mtu}")
            VpnState.event("SOCKS5-мост", "127.0.0.1:${socks.port}")
            VpnState.event("DNS через туннель", state.settings.dns)

            val hevConfig = buildEngineConfig(socks.port, state.settings)
            val fd = pfd.fd
            engineThread = Thread({
                val ret = Tun2Socks.nativeStart(hevConfig, fd)
                Log.i(TAG, "engine returned $ret")
                lifecycle.execute { onEngineExit(gen, ret) }
            }, "vojo-tun2socks").apply { start() }

            running = true
            VpnState.setConnected(VpnState.Applied(config.id, state.settings))
            VpnState.event("Подключено", "${config.name} · ${config.type.label}", VpnState.TunnelEvent.Tone.OK)
            updateNotification(getString(R.string.notif_connected, config.name))
            startStatsLoop()
            registerNetworkCallback()
        } catch (e: Throwable) {
            Log.e(TAG, "failed to start tunnel", e)
            fail(e.message ?: "Не удалось запустить туннель")
        }
    }

    /** Throttled warning surfaced to the journal when upstream connects fail. */
    private fun onUpstreamUnreachable() {
        if (!running) return
        val now = System.currentTimeMillis()
        if (now - lastUpstreamWarn > 30_000) {
            lastUpstreamWarn = now
            VpnState.event("Прокси не отвечает", "проверьте сервер или сеть", VpnState.TunnelEvent.Tone.WARN)
        }
    }

    /** Engine thread exited on its own (not via a requested stop) — tear down loudly. */
    private fun onEngineExit(gen: Int, ret: Int) { // lifecycle thread
        if (gen != engineGen || stopRequested) return
        fail("Движок туннеля неожиданно завершился (код $ret)")
    }

    private fun establish(config: ProxyConfig, settings: AppSettings, socksPort: Int): ParcelFileDescriptor {
        val builder = Builder()
            .setSession("vojo · ${config.name}")
            .setMtu(settings.mtu)
            .addAddress(TUN_IPV4, 32)
            .addRoute("0.0.0.0", 0)
            .addDnsServer(settings.dns.ifBlank { "1.1.1.1" })
            .setBlocking(false)

        if (settings.ipv6) {
            builder.addAddress(TUN_IPV6, 128).addRoute("::", 0)
        }

        applyAppFilter(builder, settings.allowedApps)
        builder.setUnderlyingNetworks(activeNetworkArray())

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            builder.setMetered(false)
        }
        builder.setConfigureIntent(activityPendingIntent())

        return builder.establish() ?: throw IllegalStateException("VPN permission not granted")
    }

    /** Whitelist mode when apps are selected; otherwise route everything except ourselves. */
    private fun applyAppFilter(builder: Builder, allowed: Set<String>) {
        val self = packageName
        if (allowed.isNotEmpty()) {
            var added = 0
            for (pkg in allowed) {
                if (pkg == self) continue
                runCatching { builder.addAllowedApplication(pkg); added++ }
                    .onFailure { Log.w(TAG, "skip missing package $pkg") }
            }
            // All selected packages are gone: routing *everything* through the
            // proxy would silently violate the whitelist contract.
            if (added == 0) {
                throw IllegalStateException("Выбранные приложения не установлены — обновите список")
            }
        } else {
            runCatching { builder.addDisallowedApplication(self) }
        }
    }

    private fun buildEngineConfig(socksPort: Int, settings: AppSettings): String = buildString {
        append("tunnel:\n")
        append("  mtu: ${settings.mtu}\n")
        append("  ipv4: $TUN_IPV4\n")
        if (settings.ipv6) append("  ipv6: '$TUN_IPV6'\n")
        append("socks5:\n")
        append("  address: 127.0.0.1\n")
        append("  port: $socksPort\n")
        append("  udp: 'udp'\n")
        append("misc:\n")
        append("  connect-timeout: 8000\n")
        append("  log-level: warn\n")
    }

    private fun startStatsLoop() {
        statsJob?.cancel()
        statsJob = scope.launch {
            var tick = 0
            while (isActive) {
                runCatching {
                    val s = Tun2Socks.nativeStats()
                    if (s.size >= 4) {
                        VpnState.setStats(VpnState.Stats(txBytes = s[1], rxBytes = s[3]))
                    }
                }
                // Refresh the notification with live traffic once a minute.
                if (++tick % 30 == 0 && VpnState.status.value == VpnState.Status.CONNECTED) {
                    runCatching { updateNotification(trafficSummary()) }
                }
                delay(2000)
            }
        }
    }

    private fun trafficSummary(): String {
        val st = VpnState.stats.value
        val up = formatUptime(VpnState.connectedSince.value, System.currentTimeMillis())
        return "↑ ${formatBytes(st.txBytes)} · ↓ ${formatBytes(st.rxBytes)} · $up"
    }

    private fun registerNetworkCallback() {
        val cm = getSystemService(ConnectivityManager::class.java) ?: return
        lastNetwork = cm.activeNetwork
        val cb = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                runCatching { setUnderlyingNetworks(arrayOf(network)) }
                if (network != lastNetwork) {
                    lastNetwork = network
                    val caps = runCatching { cm.getNetworkCapabilities(network) }.getOrNull()
                    val name = when {
                        caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true -> "Wi-Fi"
                        caps?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> "мобильная сеть"
                        caps?.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) == true -> "Ethernet"
                        else -> "другая сеть"
                    }
                    VpnState.event("Сеть изменилась", name, VpnState.TunnelEvent.Tone.WARN)
                }
            }

            override fun onLost(network: Network) {
                runCatching { setUnderlyingNetworks(null) }
            }
        }
        networkCallback = cb
        runCatching { cm.registerDefaultNetworkCallback(cb) }
    }

    private fun activeNetworkArray(): Array<Network>? {
        val cm = getSystemService(ConnectivityManager::class.java) ?: return null
        return cm.activeNetwork?.let { arrayOf(it) }
    }

    // --- SocketProtector ---
    override fun protect(socket: Socket): Boolean = super<VpnService>.protect(socket)
    override fun protect(socket: DatagramSocket): Boolean = super<VpnService>.protect(socket)

    private fun fail(message: String) { // lifecycle thread
        VpnState.event("Ошибка", message, VpnState.TunnelEvent.Tone.ERROR)
        shutdown(errorMessage = message)
        stopSelf()
    }

    /**
     * Tears everything down on the lifecycle thread. A non-null [errorMessage]
     * keeps the ERROR state (and an explanatory notification) visible to the user.
     */
    private fun shutdown(errorMessage: String?, keepNotification: Boolean = false) { // lifecycle thread
        // Idempotent: ACTION_STOP tears down then stopSelf() → onDestroy() would
        // otherwise run a second teardown (and log a duplicate "Отключено").
        if (tornDown) return
        tornDown = true
        stopRequested = true
        engineGen++ // invalidate pending engine-exit callbacks
        VpnState.setStopping()

        statsJob?.cancel()
        statsJob = null

        networkCallback?.let { cb ->
            runCatching { getSystemService(ConnectivityManager::class.java)?.unregisterNetworkCallback(cb) }
        }
        networkCallback = null

        val engine = engineThread
        engineThread = null
        if (engine != null) {
            // nativeStop only signals the engine's event fd; the native `running`
            // flag and the tun fd aren't released until nativeStart() returns and
            // the thread body finishes. Wait for that here (re-kicking stop) so a
            // following reconnect can't hit the C double-start guard (ret == -2),
            // and so we never close the tun fd while the engine still reads it.
            var waited = 0L
            while (engine.isAlive && waited < ENGINE_JOIN_TIMEOUT_MS) {
                runCatching { Tun2Socks.nativeStop() }
                runCatching { engine.join(ENGINE_JOIN_STEP_MS) }
                waited += ENGINE_JOIN_STEP_MS
            }
            if (engine.isAlive) Log.w(TAG, "engine thread did not exit in time")
        }

        server?.stop()
        server = null

        // Only reclaim the tun fd once the engine has truly stopped touching it;
        // closing it out from under live native code is undefined behaviour.
        if (engine == null || !engine.isAlive) {
            runCatching { vpnInterface?.close() }
            vpnInterface = null
        } else {
            Log.w(TAG, "leaving tun fd open — engine still alive after stop")
        }

        running = false
        VpnState.setStopped(errorMessage)
        // Don't log "Отключено" mid-reconnect (keepNotification) — that path
        // logs "Переподключение" → "Подключение" → "Подключено" instead.
        if (errorMessage == null && !keepNotification && VpnState.events.value.isNotEmpty()) {
            VpnState.event("Отключено")
        }
        if (!keepNotification) {
            stopForeground(STOP_FOREGROUND_REMOVE)
            if (errorMessage != null) postErrorNotification(errorMessage)
        }
    }

    override fun onRevoke() {
        // User revoked VPN permission or another VPN took over.
        lifecycle.execute {
            VpnState.event("VPN отозван системой", null, VpnState.TunnelEvent.Tone.WARN)
            shutdown(errorMessage = null)
            stopSelf()
        }
    }

    override fun onDestroy() {
        // System killed the service without ACTION_STOP: still release the
        // engine, the SOCKS server and the tun fd.
        lifecycle.execute { shutdown(errorMessage = null) }
        lifecycle.shutdown()
        scope.cancel()
        super.onDestroy()
    }

    // --- Notification ---
    private fun startForegroundNotification(text: String) {
        ensureChannel()
        val notification = buildNotification(text)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            startForeground(NOTIF_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
        } else {
            startForeground(NOTIF_ID, notification)
        }
    }

    private fun updateNotification(text: String) {
        val nm = getSystemService(NotificationManager::class.java) ?: return
        nm.notify(NOTIF_ID, buildNotification(text))
    }

    private fun postErrorNotification(text: String) {
        val nm = getSystemService(NotificationManager::class.java) ?: return
        ensureChannel()
        val n = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle(getString(R.string.notif_error_title))
            .setContentText(text)
            .setStyle(NotificationCompat.BigTextStyle().bigText(text))
            .setSmallIcon(R.drawable.ic_stat_shield)
            .setAutoCancel(true)
            .setCategory(NotificationCompat.CATEGORY_ERROR)
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setContentIntent(activityPendingIntent())
            .build()
        nm.notify(NOTIF_ERROR_ID, n)
    }

    private fun buildNotification(text: String): Notification {
        val stopIntent = PendingIntent.getService(
            this, 1,
            Intent(this, ProxyVpnService::class.java).setAction(ACTION_STOP),
            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
        )
        return NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle(getString(R.string.app_name))
            .setContentText(text)
            .setSmallIcon(R.drawable.ic_stat_shield)
            .setOngoing(true)
            .setShowWhen(false)
            .setCategory(NotificationCompat.CATEGORY_SERVICE)
            .setPriority(NotificationCompat.PRIORITY_LOW)
            .setContentIntent(activityPendingIntent())
            .addAction(0, getString(R.string.action_disconnect), stopIntent)
            .build()
    }

    private fun activityPendingIntent(): PendingIntent = PendingIntent.getActivity(
        this, 0,
        Intent(this, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
        PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
    )

    private fun ensureChannel() {
        val nm = getSystemService(NotificationManager::class.java) ?: return
        if (nm.getNotificationChannel(CHANNEL_ID) == null) {
            nm.createNotificationChannel(
                NotificationChannel(CHANNEL_ID, getString(R.string.channel_name), NotificationManager.IMPORTANCE_LOW).apply {
                    setShowBadge(false)
                    description = getString(R.string.channel_desc)
                }
            )
        }
    }

    companion object {
        private const val TAG = "ProxyVpnService"
        const val ACTION_START = "chat.vojo.proxy.START"
        const val ACTION_STOP = "chat.vojo.proxy.STOP"
        const val ACTION_RESTART = "chat.vojo.proxy.RESTART"

        private const val CHANNEL_ID = "vpn_status"
        private const val NOTIF_ID = 0x7C5C
        private const val NOTIF_ERROR_ID = 0x7C5D

        // Bounded wait for the native engine thread to exit during teardown.
        private const val ENGINE_JOIN_TIMEOUT_MS = 5000L
        private const val ENGINE_JOIN_STEP_MS = 250L

        const val TUN_IPV4 = "198.18.0.1"
        private const val TUN_IPV6 = "fc00::1"

        fun start(context: Context) {
            val intent = Intent(context, ProxyVpnService::class.java).setAction(ACTION_START)
            ContextCompat.startForegroundService(context, intent)
        }

        fun restart(context: Context) {
            val intent = Intent(context, ProxyVpnService::class.java).setAction(ACTION_RESTART)
            ContextCompat.startForegroundService(context, intent)
        }

        fun stop(context: Context) {
            val intent = Intent(context, ProxyVpnService::class.java).setAction(ACTION_STOP)
            context.startService(intent)
        }
    }
}
