package com.sshproxy.vpn



import android.content.Intent

import android.Manifest

import android.content.BroadcastReceiver

import android.content.Context

import android.content.IntentFilter

import android.net.VpnService

import android.content.res.ColorStateList
import android.graphics.Typeface
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan

import android.os.Build

import android.os.Bundle
import android.view.View
import android.widget.Toast

import androidx.activity.result.contract.ActivityResultContracts

import androidx.appcompat.app.AppCompatActivity

import androidx.core.content.ContextCompat

import androidx.core.content.PermissionChecker

import com.sshproxy.vpn.databinding.ActivityMainBinding

import com.sshproxy.vpn.service.TunnelConfigParcelable

import com.sshproxy.vpn.service.TunnelService

import com.sshproxy.vpn.service.TunnelServiceHolder

import com.sshproxy.vpn.util.getParcelableExtraCompat

import com.sshproxy.vpn.util.ConnectionMessages

import com.sshproxy.vpn.util.CallsAvailability
import com.sshproxy.vpn.util.CallsAvailabilityStatus
import com.sshproxy.vpn.util.TrafficFormatter
import com.sshproxy.vpn.util.TunnelTransport

import com.sshproxy.vpn.vpn.ProxyVpnService



class MainActivity : AppCompatActivity() {



    private lateinit var binding: ActivityMainBinding

    private var pendingConfig: TunnelConfigParcelable? = null



    private val notificationPermissionLauncher = registerForActivityResult(

        ActivityResultContracts.RequestPermission()

    ) { /* уведомления нужны для foreground-сервиса в фоне */ }



    private val vpnPermissionLauncher = registerForActivityResult(

        ActivityResultContracts.StartActivityForResult()

    ) {

        if (it.resultCode == RESULT_OK) {

            pendingConfig?.let { cfg ->

                ContextCompat.startForegroundService(

                    this,

                    Intent(this, TunnelService::class.java).apply {

                        action = TunnelService.ACTION_START

                        putExtra(TunnelService.EXTRA_CONFIG, cfg)

                    }

                )

            }

        } else {

            Toast.makeText(this, R.string.status_vpn_denied, Toast.LENGTH_LONG).show()

            showStatus(TunnelService.State.ERROR, getString(R.string.status_vpn_denied))

        }

        pendingConfig = null

    }



    private val stateReceiver = object : BroadcastReceiver() {

        override fun onReceive(context: Context?, intent: Intent?) {

            if (isFinishing || isDestroyed) {

                return

            }

            when (intent?.action) {

                TunnelService.ACTION_NEED_VPN_PERMISSION -> {

                    pendingConfig = intent.getParcelableExtraCompat<TunnelConfigParcelable>(

                        TunnelService.EXTRA_CONFIG

                    )

                    val prepare = VpnService.prepare(this@MainActivity)

                    if (prepare != null) {

                        vpnPermissionLauncher.launch(prepare)

                    }

                }

                ProxyVpnService.ACTION_TRAFFIC_STATS -> {

                    val rx = intent.getLongExtra(ProxyVpnService.EXTRA_RX_BYTES, 0L)

                    val tx = intent.getLongExtra(ProxyVpnService.EXTRA_TX_BYTES, 0L)

                    updateTraffic(rx, tx)

                }

            }

        }

    }



    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)

        setContentView(binding.root)

        binding.connectButton.setOnClickListener { connect() }

        binding.disconnectButton.setOnClickListener { disconnect() }

        binding.tunnelSettingsButton.setOnClickListener {

            startActivity(Intent(this, TunnelSettingsActivity::class.java))

        }

        binding.appExclusionButton.setOnClickListener {

            startActivity(Intent(this, AppExclusionActivity::class.java))

        }



        showStatus(TunnelService.State.DISCONNECTED, getString(R.string.status_idle))

        styleConnectButton(true)

        styleDisconnectButton(false)



        TunnelServiceHolder.setStateListener { state, message, transport, callsStatus ->

            if (isFinishing || isDestroyed) {

                return@setStateListener

            }

            showStatus(state, message, transport, callsStatus)

            updateButtons(state)

            when (state) {

                TunnelService.State.CONNECTED ->

                    Toast.makeText(this, R.string.toast_connected, Toast.LENGTH_SHORT).show()

                TunnelService.State.ERROR ->

                    Toast.makeText(

                        this,

                        getString(R.string.toast_error, message),

                        Toast.LENGTH_LONG

                    ).show()

                TunnelService.State.DISCONNECTED ->

                    Toast.makeText(this, R.string.toast_disconnected, Toast.LENGTH_SHORT).show()

                else -> Unit

            }

        }



        requestNotificationPermissionIfNeeded()

    }



    override fun onDestroy() {

        TunnelServiceHolder.setStateListener(null)

        super.onDestroy()

    }



    private fun requestNotificationPermissionIfNeeded() {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {

            return

        }

        if (PermissionChecker.checkSelfPermission(

                this,

                Manifest.permission.POST_NOTIFICATIONS

            ) != PermissionChecker.PERMISSION_GRANTED

        ) {

            notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)

        }

    }



    override fun onStart() {

        super.onStart()

        val filter = IntentFilter().apply {

            addAction(TunnelService.ACTION_NEED_VPN_PERMISSION)

            addAction(ProxyVpnService.ACTION_TRAFFIC_STATS)

        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {

            registerReceiver(stateReceiver, filter, RECEIVER_NOT_EXPORTED)

        } else {

            @Suppress("UnspecifiedRegisterReceiverFlag")

            registerReceiver(stateReceiver, filter)

        }

    }



    override fun onStop() {

        unregisterReceiver(stateReceiver)

        super.onStop()

    }



    private fun connect() {

        val config = (application as TunnelApplication).prefs.load()

        if (!config.isValid()) {

            val message = getString(R.string.status_invalid_config)

            Toast.makeText(this, message, Toast.LENGTH_LONG).show()

            showStatus(TunnelService.State.ERROR, message)

            startActivity(Intent(this, TunnelSettingsActivity::class.java))

            return

        }

        showStatus(

            TunnelService.State.CONNECTING,

            getString(R.string.status_connecting_ssh, config.host, config.port),

            TunnelTransport.TCP

        )



        val prepare = VpnService.prepare(this)

        if (prepare != null) {

            pendingConfig = TunnelConfigParcelable.from(config)

            vpnPermissionLauncher.launch(prepare)

            return

        }



        ContextCompat.startForegroundService(

            this,

            Intent(this, TunnelService::class.java).apply {

                action = TunnelService.ACTION_START

                putExtra(TunnelService.EXTRA_CONFIG, TunnelConfigParcelable.from(config))

            }

        )

    }



    private fun disconnect() {

        showStatus(TunnelService.State.DISCONNECTED, getString(R.string.status_disconnected))

        startService(

            Intent(this, TunnelService::class.java).apply {

                action = TunnelService.ACTION_STOP

            }

        )

    }



    private fun updateButtons(state: TunnelService.State) {

        val busy = state == TunnelService.State.CONNECTED ||

            state == TunnelService.State.CONNECTING ||

            state == TunnelService.State.RECONNECTING

        binding.connectButton.isEnabled = !busy

        styleConnectButton(!busy)

        styleDisconnectButton(

            state == TunnelService.State.CONNECTED ||

                state == TunnelService.State.RECONNECTING

        )

    }



    private fun styleConnectButton(enabled: Boolean) {

        binding.connectButton.isEnabled = enabled

        binding.connectButton.alpha = if (enabled) 1f else 0.35f

    }



    private fun styleDisconnectButton(enabled: Boolean) {

        binding.disconnectButton.isEnabled = enabled

        binding.disconnectButton.alpha = if (enabled) 1f else 0.35f

        if (enabled) {

            binding.disconnectButton.setBackgroundResource(R.drawable.ios_icon_button_destructive_filled)

            binding.disconnectButton.imageTintList =

                ColorStateList.valueOf(ContextCompat.getColor(this, android.R.color.white))

        } else {

            binding.disconnectButton.setBackgroundResource(R.drawable.ios_icon_button_destructive)

            binding.disconnectButton.imageTintList =

                ColorStateList.valueOf(ContextCompat.getColor(this, R.color.ios_destructive))

        }

    }



    private fun showStatus(
        state: TunnelService.State,
        message: String,
        transport: TunnelTransport? = null,
        callsStatus: CallsAvailabilityStatus? = null
    ) {

        binding.statusLabel.text = ConnectionMessages.stateTitle(state)

        binding.statusText.text = message

        applyStatusMessageStyle(state)

        updateCallsStatusDisplay(callsStatus)

        updateConnectionDetailsDisplay(transport)

        val colorRes = when (state) {

            TunnelService.State.CONNECTED -> R.color.status_connected

            TunnelService.State.CONNECTING,

            TunnelService.State.RECONNECTING,

            TunnelService.State.NEED_VPN_PERMISSION -> R.color.status_connecting

            TunnelService.State.ERROR -> R.color.status_error

            TunnelService.State.DISCONNECTED -> R.color.status_disconnected

        }

        val color = ContextCompat.getColor(this, colorRes)

        binding.statusLabel.setTextColor(color)

        if (state != TunnelService.State.CONNECTED) {

            binding.trafficText.text = getString(R.string.traffic_stats_idle)

        }

    }



    private fun updateTraffic(rxBytes: Long, txBytes: Long) {

        binding.trafficText.text = getString(

            R.string.traffic_stats,

            TrafficFormatter.format(rxBytes),

            TrafficFormatter.format(txBytes)

        )

    }



    private fun applyStatusMessageStyle(state: TunnelService.State) {
        val colorRes = when (state) {
            TunnelService.State.CONNECTED -> R.color.ios_label_secondary
            TunnelService.State.CONNECTING,
            TunnelService.State.RECONNECTING,
            TunnelService.State.NEED_VPN_PERMISSION -> R.color.status_connecting
            TunnelService.State.ERROR -> R.color.status_error
            TunnelService.State.DISCONNECTED -> R.color.status_disconnected
        }
        binding.statusText.setTextColor(ContextCompat.getColor(this, colorRes))
        binding.statusText.setTypeface(
            binding.statusText.typeface,
            if (state == TunnelService.State.ERROR) Typeface.BOLD else Typeface.NORMAL
        )
    }

    private fun updateCallsStatusDisplay(callsStatus: CallsAvailabilityStatus?) {
        if (callsStatus == null) {
            binding.callsStatusText.visibility = View.GONE
            binding.callsStatusText.text = ""
            return
        }

        binding.callsStatusText.visibility = View.VISIBLE
        val value = callsLabel(callsStatus)
        val prefix = getString(R.string.calls_status_prefix)
        val line = prefix + value
        val secondary = ContextCompat.getColor(this, R.color.ios_label_secondary)
        val valueColor = callsValueColor(callsStatus)

        val spannable = SpannableString(line)
        spannable.setSpan(
            ForegroundColorSpan(secondary),
            0,
            prefix.length,
            SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
        )
        spannable.setSpan(
            ForegroundColorSpan(valueColor),
            prefix.length,
            line.length,
            SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
        )
        if (callsStatus.availability == CallsAvailability.AVAILABLE ||
            callsStatus.availability == CallsAvailability.UNAVAILABLE
        ) {
            spannable.setSpan(
                StyleSpan(Typeface.BOLD),
                prefix.length,
                line.length,
                SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
            )
        }
        binding.callsStatusText.text = spannable
    }

    private fun callsValueColor(callsStatus: CallsAvailabilityStatus): Int {
        val colorRes = when (callsStatus.availability) {
            CallsAvailability.NOT_READY -> R.color.ios_label_secondary
            CallsAvailability.CHECKING -> R.color.calls_status_checking
            CallsAvailability.AVAILABLE -> R.color.calls_status_available
            CallsAvailability.UNAVAILABLE -> R.color.calls_status_unavailable
        }
        return ContextCompat.getColor(this, colorRes)
    }

    private fun updateConnectionDetailsDisplay(transport: TunnelTransport?) {
        if (transport == null) {
            binding.transportText.visibility = View.GONE
            binding.transportText.text = ""
            return
        }
        binding.transportText.text = getString(R.string.connection_transport_type, transport.label())
        binding.transportText.visibility = View.VISIBLE
    }

    private fun callsLabel(callsStatus: CallsAvailabilityStatus): String = when (callsStatus.availability) {
        CallsAvailability.NOT_READY -> getString(R.string.calls_status_not_ready)
        CallsAvailability.CHECKING -> getString(R.string.calls_status_checking)
        CallsAvailability.AVAILABLE -> getString(R.string.calls_status_available)
        CallsAvailability.UNAVAILABLE -> {
            val reason = callsStatus.unavailableReason?.trim().orEmpty()
            if (reason.isEmpty()) {
                getString(R.string.calls_status_unavailable)
            } else {
                getString(R.string.calls_status_unavailable_reason, reason)
            }
        }
    }

    private fun resetTrafficDisplay() {

        binding.trafficText.text = getString(R.string.traffic_stats_idle)

        updateConnectionDetailsDisplay(null)
        updateCallsStatusDisplay(null)
    }

}


