package com.sshproxy.vpn.service

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.VpnService
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.sshproxy.vpn.MainActivity
import com.sshproxy.vpn.R
import com.sshproxy.vpn.data.TunnelConfig
import com.sshproxy.vpn.ssh.CallsAvailabilityMonitor
import com.sshproxy.vpn.ssh.ConnectionHealthMonitor
import com.sshproxy.vpn.ssh.SocksAuthCredentials
import com.sshproxy.vpn.ssh.SshTunnelManager
import com.sshproxy.vpn.ssh.UdpRelayHealthCheck
import com.sshproxy.vpn.util.CallsAvailabilityStatus
import com.sshproxy.vpn.util.ConnectionMessages
import com.sshproxy.vpn.util.TunnelTransport
import com.sshproxy.vpn.util.getParcelableExtraCompat
import com.sshproxy.vpn.vpn.ProxyVpnService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class TunnelService : LifecycleService() {

    private var connectJob: Job? = null
    private val sshManager = SshTunnelManager()
    private val healthMonitor = ConnectionHealthMonitor(sshManager) { handleConnectionLost() }
    private val callsMonitor = CallsAvailabilityMonitor(
        sshManager = sshManager,
        remoteHost = { lastConfig?.remoteSocksHost ?: TunnelConfig.DEFAULT_REMOTE_SOCKS_HOST },
        remotePort = { lastConfig?.remoteSocksPort ?: TunnelConfig.DEFAULT_REMOTE_SOCKS_PORT },
        remoteCredentials = {
            lastConfig?.let {
                SocksAuthCredentials(it.remoteSocksUsername, it.remoteSocksPassword)
            } ?: SocksAuthCredentials.NONE
        },
        formatUnavailableReason = ::formatCallsUnavailableReason,
        onAvailabilityChanged = { status -> updateCallsAvailability(status) }
    )
    private var lastConfig: TunnelConfig? = null
    private var isTunnelActive = false
    private var userRequestedStop = false
    private var reconnectAttempt = 0
    private var lastBroadcastState: State = State.DISCONNECTED
    private var lastBroadcastMessage: String = ""
    private var lastBroadcastTransport: TunnelTransport? = null
    private var lastCallsStatus: CallsAvailabilityStatus? = null

    override fun onCreate() {
        super.onCreate()
        TunnelServiceHolder.sshManager = sshManager
        createNotificationChannel()

        val filter = IntentFilter(TunnelService.ACTION_VPN_UP)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            registerReceiver(vpnUpReceiver, filter, RECEIVER_NOT_EXPORTED)
        } else {
            @Suppress("UnspecifiedRegisterReceiverFlag")
            registerReceiver(vpnUpReceiver, filter)
        }
    }

    private val vpnUpReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            val host = lastConfig?.host.orEmpty().ifBlank { "сервер" }
            val message = getString(R.string.status_connected, host)
            isTunnelActive = true
            val notification = buildNotification(getString(R.string.notification_connected, host))
            startForeground(NOTIFICATION_ID, notification)
            reconnectAttempt = 0
            val intervalMs = lastConfig?.keepAliveIntervalSec?.times(1000L)
                ?: ConnectionHealthMonitor.DEFAULT_INTERVAL_MS
            healthMonitor.start(lifecycleScope, intervalMs)
            startCallsMonitor(intervalMs)
            val config = lastConfig
            if (config == null) {
                broadcastState(State.CONNECTED, message, TunnelTransport.UDP)
                return
            }
            lifecycleScope.launch {
                val calls = resolveCallsAvailability(config)
                broadcastState(State.CONNECTED, message, TunnelTransport.UDP, calls)
            }
        }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        super.onStartCommand(intent, flags, startId)

        when (intent?.action) {
            ACTION_STOP -> {
                userRequestedStop = true
                stopEverything()
                stopForeground(STOP_FOREGROUND_REMOVE)
                stopSelf()
                return START_NOT_STICKY
            }
            ACTION_START -> {
                userRequestedStop = false
                reconnectAttempt = 0
                val config = intent.getParcelableExtraCompat<TunnelConfigParcelable>(EXTRA_CONFIG)
                    ?.toConfig()
                if (config == null || !config.isValid()) {
                    startForeground(
                        NOTIFICATION_ID,
                        buildNotification(getString(R.string.notification_error, getString(R.string.status_invalid_config)))
                    )
                    broadcastState(State.ERROR, getString(R.string.status_invalid_config))
                    stopForeground(STOP_FOREGROUND_REMOVE)
                    stopSelf()
                    return START_NOT_STICKY
                }
                lastConfig = config
                startForeground(
                    NOTIFICATION_ID,
                    buildNotification(getString(R.string.notification_connecting))
                )
                connectJob?.cancel()
                connectJob = lifecycleScope.launch {
                    connect(config, isReconnect = false)
                }
            }
        }
        return START_STICKY
    }

    override fun onDestroy() {
        unregisterReceiver(vpnUpReceiver)
        if (isTunnelActive) {
            stopEverything()
        }
        TunnelServiceHolder.sshManager = null
        super.onDestroy()
    }

    override fun onTaskRemoved(rootIntent: Intent?) {
        // Туннель продолжает работать в фоне (stopWithTask=false).
        super.onTaskRemoved(rootIntent)
    }

    override fun onBind(intent: Intent): IBinder? = super.onBind(intent)

    private suspend fun connect(config: TunnelConfig, isReconnect: Boolean) {
        lastConfig = config
        if (isReconnect) {
            broadcastState(
                State.RECONNECTING,
                getString(R.string.status_reconnecting_ssh, config.host, config.port),
                TunnelTransport.TCP,
                CallsAvailabilityStatus.NOT_READY
            )
        } else {
            broadcastState(
                State.CONNECTING,
                getString(R.string.status_connecting_ssh, config.host, config.port),
                TunnelTransport.TCP,
                CallsAvailabilityStatus.NOT_READY
            )
        }
        TunnelServiceHolder.setNativeErrorListener { message ->
            if (lastConfig?.autoReconnect == true && !userRequestedStop) {
                handleConnectionLost()
                return@setNativeErrorListener
            }
            broadcastState(State.ERROR, message)
            updateNotification(getString(R.string.notification_error, message))
            stopEverything()
            stopForeground(STOP_FOREGROUND_REMOVE)
            stopSelf()
        }
        try {
            val socksPort = withContext(Dispatchers.IO) {
                sshManager.connect(config)
            }

            broadcastState(
                State.CONNECTING,
                getString(R.string.status_ssh_ok, config.host),
                TunnelTransport.TCP,
                CallsAvailabilityStatus.NOT_READY
            )
            val socksMessage = getString(
                R.string.status_socks_ok,
                socksPort,
                config.remoteSocksHost,
                config.remoteSocksPort
            )
            broadcastState(
                State.CONNECTING,
                socksMessage,
                TunnelTransport.UDP,
                CallsAvailabilityStatus.CHECKING
            )
            val callsStatus = resolveCallsAvailability(config)
            broadcastState(
                State.CONNECTING,
                socksMessage,
                TunnelTransport.UDP,
                callsStatus
            )

            val vpnIntent = VpnService.prepare(this)
            if (vpnIntent != null) {
                broadcastState(
                    State.NEED_VPN_PERMISSION,
                    getString(R.string.status_need_vpn),
                    TunnelTransport.UDP,
                    callsStatus
                )
                sendBroadcast(
                    Intent(ACTION_NEED_VPN_PERMISSION).setPackage(packageName)
                        .putExtra(EXTRA_CONFIG, TunnelConfigParcelable.from(config))
                )
                stopForeground(STOP_FOREGROUND_REMOVE)
                stopSelf()
                return
            }

            startVpn(socksPort)
            updateNotification(getString(R.string.notification_connecting))
            broadcastState(
                State.CONNECTING,
                getString(R.string.status_vpn_starting),
                TunnelTransport.UDP,
                callsStatus
            )
        } catch (e: Exception) {
            sshManager.disconnect()
            if (lastConfig?.autoReconnect == true && !userRequestedStop) {
                scheduleReconnect(config)
                return
            }
            val message = ConnectionMessages.mapError(e)
            broadcastState(State.ERROR, message)
            updateNotification(getString(R.string.notification_error, message))
            stopForeground(STOP_FOREGROUND_REMOVE)
            stopSelf()
        }
    }

    private fun scheduleReconnect(config: TunnelConfig) {
        reconnectAttempt++
        val delaySec = minOf(1 shl reconnectAttempt.coerceAtMost(5), MAX_RECONNECT_DELAY_SEC)
        val message = getString(R.string.status_reconnecting, reconnectAttempt, delaySec)
        broadcastState(State.RECONNECTING, message, TunnelTransport.TCP, CallsAvailabilityStatus.NOT_READY)
        updateNotification(getString(R.string.notification_reconnecting, reconnectAttempt))
        connectJob?.cancel()
        connectJob = lifecycleScope.launch {
            delay(delaySec * 1000L)
            if (userRequestedStop) {
                return@launch
            }
            connect(config, isReconnect = true)
        }
    }

    fun startVpnAfterPermission(config: TunnelConfig) {
        if (!sshManager.isConnected) {
            connectJob = lifecycleScope.launch {
                connect(config, isReconnect = false)
            }
            return
        }
        lifecycleScope.launch {
            try {
                val port = config.socksPort
                val callsStatus = resolveCallsAvailability(config)
                startVpn(port)
                val host = config.host.ifBlank { "сервер" }
                startForeground(
                    NOTIFICATION_ID,
                    buildNotification(getString(R.string.notification_connected, host))
                )
                broadcastState(
                    State.CONNECTING,
                    getString(R.string.status_vpn_starting),
                    TunnelTransport.UDP,
                    callsStatus
                )
            } catch (e: Exception) {
                val message = ConnectionMessages.mapError(e)
                broadcastState(State.ERROR, message)
                updateNotification(getString(R.string.notification_error, message))
                stopEverything()
                stopSelf()
            }
        }
    }

    private fun startVpn(socksPort: Int) {
        val vpnIntent = Intent(this, ProxyVpnService::class.java)
            .putExtra(ProxyVpnService.EXTRA_SOCKS_PORT, socksPort)
        startService(vpnIntent)
    }

    private fun handleConnectionLost() {
        if (!isTunnelActive && reconnectAttempt == 0) {
            return
        }
        healthMonitor.stop()
        callsMonitor.stop()
        val config = lastConfig
        val lostMessage = getString(R.string.status_connection_lost)

        if (config == null || !config.autoReconnect || userRequestedStop) {
            teardownTunnel()
            broadcastState(State.ERROR, lostMessage)
            updateNotification(getString(R.string.notification_error, lostMessage))
            lastConfig = null
            reconnectAttempt = 0
            stopForeground(STOP_FOREGROUND_REMOVE)
            stopSelf()
            return
        }

        broadcastState(State.RECONNECTING, lostMessage, TunnelTransport.UDP, CallsAvailabilityStatus.NOT_READY)
        teardownTunnel()
        scheduleReconnect(config)
    }

    private fun teardownTunnel() {
        healthMonitor.stop()
        callsMonitor.stop()
        isTunnelActive = false
        startService(
            Intent(this, ProxyVpnService::class.java).setAction(ProxyVpnService.ACTION_STOP)
        )
        sshManager.disconnect()
    }

    private fun stopEverything() {
        healthMonitor.stop()
        callsMonitor.stop()
        connectJob?.cancel()
        isTunnelActive = false
        reconnectAttempt = 0
        startService(
            Intent(this, ProxyVpnService::class.java).setAction(ProxyVpnService.ACTION_STOP)
        )
        sshManager.disconnect()
        lastConfig = null
        lastCallsStatus = null
        broadcastState(State.DISCONNECTED, getString(R.string.status_disconnected))
        updateNotification(getString(R.string.notification_disconnected))
    }

    private fun formatCallsUnavailableReason(result: UdpRelayHealthCheck.Result): String {
        val stageLabel = when (result.stage) {
            UdpRelayHealthCheck.Stage.TCP_CONNECT -> getString(R.string.calls_unavailable_tcp)
            UdpRelayHealthCheck.Stage.SOCKS_AUTH -> getString(R.string.calls_unavailable_socks)
            UdpRelayHealthCheck.Stage.FWD_UDP -> getString(R.string.calls_unavailable_fwd_udp)
            UdpRelayHealthCheck.Stage.UDP_PROBE -> getString(R.string.calls_unavailable_udp_probe)
        }
        val detail = mapCallsDetail(result.detail)
        return when {
            detail == null -> stageLabel
            result.stage == UdpRelayHealthCheck.Stage.SOCKS_AUTH -> detail
            else -> "$stageLabel ($detail)"
        }
    }

    private fun mapCallsDetail(detail: String?): String? {
        return when (detail?.trim()) {
            null, "" -> null
            "credentials required" -> getString(R.string.calls_unavailable_socks_need_login)
            "username/password rejected" -> getString(R.string.calls_unavailable_socks_bad_credentials)
            "SOCKS5 auth failed" -> getString(R.string.calls_unavailable_socks_need_login)
            else -> detail
        }
    }

    private suspend fun resolveCallsAvailability(config: TunnelConfig): CallsAvailabilityStatus {
        return withContext(Dispatchers.IO) {
            val result = sshManager.checkUdpRelay(
                remoteHost = config.remoteSocksHost,
                remotePort = config.remoteSocksPort,
                credentials = SocksAuthCredentials(
                    username = config.remoteSocksUsername,
                    password = config.remoteSocksPassword
                )
            )
            CallsAvailabilityStatus.fromCheckResult(result, ::formatCallsUnavailableReason)
        }
    }

    private fun startCallsMonitor(intervalMs: Long) {
        if (!isTunnelActive) {
            return
        }
        callsMonitor.start(lifecycleScope, intervalMs)
    }

    private fun updateCallsAvailability(status: CallsAvailabilityStatus) {
        if (!isTunnelActive) {
            return
        }
        if (status.hasSameDisplayAs(lastCallsStatus)) {
            return
        }
        lastCallsStatus = status
        broadcastState(
            lastBroadcastState,
            lastBroadcastMessage,
            lastBroadcastTransport,
            status
        )
    }

    private fun broadcastState(
        state: State,
        message: String,
        transport: TunnelTransport? = null,
        callsStatus: CallsAvailabilityStatus? = null
    ) {
        lastBroadcastState = state
        lastBroadcastMessage = message
        lastBroadcastTransport = transport
        if (callsStatus != null) {
            lastCallsStatus = callsStatus
        }
        TunnelServiceHolder.updateState(
            state,
            message,
            transport,
            lastCallsStatus
        )
        sendBroadcast(
            Intent(ACTION_STATE).setPackage(packageName)
                .putExtra(EXTRA_STATE, state.name)
                .putExtra(EXTRA_MESSAGE, message)
                .putExtra(EXTRA_TRANSPORT, transport?.name)
                .putExtra(EXTRA_CALLS_AVAILABILITY, lastCallsStatus?.availability?.name)
                .putExtra(EXTRA_CALLS_UNAVAILABLE_REASON, lastCallsStatus?.unavailableReason)
        )
    }

    private fun buildNotification(text: String): Notification {
        val openIntent = PendingIntent.getActivity(
            this,
            0,
            Intent(this, MainActivity::class.java),
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
        val stopIntent = PendingIntent.getService(
            this,
            1,
            Intent(this, TunnelService::class.java).setAction(ACTION_STOP),
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
        return NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle(getString(R.string.app_name))
            .setContentText(text)
            .setSmallIcon(R.drawable.ic_vpn)
            .setContentIntent(openIntent)
            .addAction(0, "Отключить", stopIntent)
            .setOngoing(true)
            .build()
    }

    private fun updateNotification(text: String) {
        val manager = getSystemService(NotificationManager::class.java)
        manager.notify(NOTIFICATION_ID, buildNotification(text))
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_ID,
                "SSH VPN",
                NotificationManager.IMPORTANCE_DEFAULT
            )
            channel.setShowBadge(false)
            getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
        }
    }

    enum class State {
        CONNECTING, CONNECTED, DISCONNECTED, ERROR, NEED_VPN_PERMISSION, RECONNECTING
    }

    companion object {
        const val ACTION_START = "com.sshproxy.vpn.START"
        const val ACTION_STOP = "com.sshproxy.vpn.STOP"
        const val ACTION_STATE = "com.sshproxy.vpn.STATE"
        const val ACTION_NEED_VPN_PERMISSION = "com.sshproxy.vpn.NEED_VPN"
        const val EXTRA_CONFIG = "config"
        const val EXTRA_STATE = "state"
        const val EXTRA_MESSAGE = "message"
        const val EXTRA_TRANSPORT = "transport"
        const val EXTRA_CALLS_AVAILABILITY = "calls_availability"
        const val EXTRA_CALLS_UNAVAILABLE_REASON = "calls_unavailable_reason"

        const val ACTION_VPN_UP = "com.sshproxy.vpn.VPN_UP"
        private const val CHANNEL_ID = "ssh_vpn"
        private const val NOTIFICATION_ID = 1001
        private const val MAX_RECONNECT_DELAY_SEC = 60
    }
}
