package net.hivetechnology.o20010_06

import android.app.Application
import android.net.VpnService
import android.util.Log
import androidx.annotation.StringRes
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import net.hivetechnology.o20010_06.api.MobileVpnApi
import net.hivetechnology.o20010_06.model.SessionCredentials
import net.hivetechnology.o20010_06.model.SessionStatus
import net.hivetechnology.o20010_06.storage.SecureSessionStore
import net.hivetechnology.o20010_06.vpn.HtVpnService
import net.hivetechnology.o20010_06.vpn.VpnState

data class MainUiState(
    val phase: String = "idle",
    val title: String = "",
    val detail: String = "",
    val deviceName: String = "",
    val transport: String? = null,
    val loading: Boolean = false,
    val needsDisclosure: Boolean = false,
    val needsPermission: Boolean = false,
    val error: String? = null,
)

class MainViewModel(application: Application) : AndroidViewModel(application) {
    private val api = MobileVpnApi(fallbackDeviceName = text(R.string.generic_device))
    private val store = SecureSessionStore(application)
    private val mutableState = MutableStateFlow(initialState())
    val state = mutableState.asStateFlow()
    private var credentials: SessionCredentials? = store.load()
    private var pendingTicket: String? = null
    private var pendingPortalReturnUrl: String? = null
    private var portalReturnUrl = mobileVpnPortalDestination(credentials?.portalReturnUrl)

    init {
        viewModelScope.launch {
            VpnState.current.collect { vpn ->
                if (vpn.status == "idle") return@collect
                val current = credentials
                if (vpn.status == "failed" && vpn.errorCode != null && current != null) {
                    reportServiceFailure(current, vpn.errorCode, vpn.detail)
                }
                if (vpn.status in setOf("stopped", "failed")) credentials = null
                mutableState.value = mutableState.value.copy(
                    phase = vpn.status,
                    title = titleFor(vpn.status),
                    detail = vpn.detail.ifBlank { detailFor(vpn.status) },
                    deviceName = vpn.deviceName.ifBlank { current?.deviceName.orEmpty() },
                    transport = vpn.transport,
                    loading = vpn.status in setOf("waiting-runtime", "connecting", "reconnecting"),
                    needsDisclosure = false,
                    needsPermission = false,
                    error = if (vpn.status == "failed") vpn.detail else null,
                )
            }
        }
        credentials?.let { current ->
            if (VpnService.prepare(application) == null) {
                mutableState.value = MainUiState(
                    phase = "reconnecting",
                    title = text(R.string.restoring_title),
                    detail = text(R.string.restoring_detail),
                    deviceName = current.deviceName,
                    loading = true,
                )
                startVpnService(current)
            } else {
                mutableState.value = MainUiState(
                    phase = "awaiting-disclosure",
                    title = text(R.string.before_continuing),
                    detail = text(R.string.vpn_disclosure),
                    deviceName = current.deviceName,
                    needsDisclosure = true,
                )
            }
        }
    }

    fun acceptHandoff(ticket: String, portalReturnUrl: String?) {
        if (!ticket.matches(Regex("^[A-Za-z0-9_-]{40,128}$"))) {
            mutableState.value = errorState(text(R.string.invalid_vpn_link))
            return
        }
        if (credentials != null || pendingTicket != null || mutableState.value.phase == "redeeming") {
            mutableState.value = mutableState.value.copy(error = text(R.string.active_vpn_conflict))
            return
        }
        pendingTicket = ticket
        pendingPortalReturnUrl = portalReturnUrl
        this.portalReturnUrl = mobileVpnPortalDestination(portalReturnUrl)
        mutableState.value = MainUiState(
            phase = "privacy-consent",
            title = text(R.string.before_continuing),
            detail = text(R.string.vpn_disclosure),
            needsDisclosure = true,
        )
    }

    fun acceptVpnDisclosure() {
        val ticket = pendingTicket
        if (ticket != null) {
            val returnUrl = pendingPortalReturnUrl
            pendingTicket = null
            pendingPortalReturnUrl = null
            redeem(ticket, returnUrl)
            return
        }
        val current = credentials ?: return
        requestSystemVpnPermission(current)
    }

    private fun redeem(ticket: String, returnUrl: String?) {
        mutableState.value = MainUiState(
            phase = "redeeming",
            title = text(R.string.verifying_request),
            detail = text(R.string.secure_portal_connection),
            loading = true,
        )
        viewModelScope.launch {
            runCatching {
                api.redeem(ticket, BuildConfig.VERSION_NAME, store.installationId, returnUrl)
            }.onSuccess { result ->
                credentials = result.credentials
                portalReturnUrl = mobileVpnPortalDestination(result.credentials.portalReturnUrl)
                store.save(result.credentials)
                runCatching { api.update(result.credentials, SessionStatus.AWAITING_PERMISSION) }
                requestSystemVpnPermission(result.credentials)
            }.onFailure { error ->
                mutableState.value = errorState(error.message ?: text(R.string.request_open_failed))
            }
        }
    }

    private fun requestSystemVpnPermission(current: SessionCredentials) {
        mutableState.value = MainUiState(
            phase = "awaiting-permission",
            title = text(R.string.authorize_vpn),
            detail = text(R.string.android_permission_detail, current.deviceName),
            deviceName = current.deviceName,
            needsPermission = true,
        )
    }

    fun cancelVpnDisclosure() {
        if (pendingTicket != null) {
            pendingTicket = null
            pendingPortalReturnUrl = null
            mutableState.value = MainUiState(
                phase = "cancelled",
                title = text(R.string.vpn_not_started),
                detail = text(R.string.request_cancelled),
            )
            return
        }
        val current = credentials ?: return
        viewModelScope.launch {
            runCatching { api.update(current, SessionStatus.STOPPED) }
            runCatching { api.stop(current) }
            store.clear()
            credentials = null
            mutableState.value = MainUiState(
                phase = "stopped",
                title = text(R.string.vpn_not_started),
                detail = text(R.string.request_cancelled),
            )
        }
    }

    fun permissionHandled(granted: Boolean) {
        mutableState.value = mutableState.value.copy(needsPermission = false)
        val current = credentials ?: return
        if (granted) {
            mutableState.value = MainUiState(
                "waiting-runtime",
                text(R.string.preparing_vpn),
                text(R.string.waiting_for_runtime),
                current.deviceName,
                loading = true,
            )
            startVpnService(current)
        } else {
            viewModelScope.launch {
                runCatching { api.update(current, SessionStatus.STOPPED) }
                runCatching { api.stop(current) }
                store.clear()
                credentials = null
                mutableState.value = errorState(text(R.string.vpn_permission_cancelled))
            }
        }
    }

    fun vpnAuthorizationUnavailable() {
        mutableState.value = mutableState.value.copy(needsPermission = false)
        val current = credentials ?: return
        val detail = text(R.string.vpn_permission_not_applied)
        store.clear()
        credentials = null
        mutableState.value = failedState(detail, current.deviceName)
        reportServiceFailure(current, "ERR_ANDROID_VPN_PERMISSION_NOT_APPLIED", detail)
    }

    fun disconnect() {
        HtVpnService.stop(getApplication())
    }

    fun portalDestination(): String = mobileVpnPortalDestination(portalReturnUrl)

    fun portalOpenFailed() {
        mutableState.value = mutableState.value.copy(error = text(R.string.portal_open_failed))
    }

    private fun startVpnService(current: SessionCredentials) {
        HtVpnService.start(getApplication()).onFailure { error ->
            val detail = text(
                R.string.vpn_service_start_failed,
                error.localizedMessage?.takeIf { it.isNotBlank() } ?: error.javaClass.simpleName,
            )
            Log.e(TAG, "Unable to start Android VPN service", error)
            store.clear()
            credentials = null
            mutableState.value = failedState(detail, current.deviceName)
            reportServiceFailure(current, "ERR_ANDROID_VPN_SERVICE_START", detail)
        }
    }

    private fun reportServiceFailure(current: SessionCredentials, errorCode: String, detail: String) {
        viewModelScope.launch {
            runCatching {
                api.update(
                    current,
                    SessionStatus.FAILED,
                    errorCode = errorCode,
                    errorMessage = detail.take(400),
                )
            }
        }
    }

    private fun titleFor(status: String): String = when (status) {
        "waiting-runtime" -> text(R.string.preparing_vpn)
        "connecting" -> text(R.string.connecting_to_device)
        "connected" -> text(R.string.vpn_connected)
        "reconnecting" -> text(R.string.vpn_reconnecting)
        "stopped" -> text(R.string.vpn_disconnected)
        "failed" -> text(R.string.connection_failed)
        else -> text(R.string.app_name)
    }

    private fun detailFor(status: String): String = when (status) {
        "connected" -> text(R.string.connected_detail)
        "reconnecting" -> text(R.string.reconnecting_detail)
        "stopped" -> text(R.string.stopped_detail)
        else -> ""
    }

    fun showStorePreview(mode: String) {
        if (!BuildConfig.DEBUG) return
        val device = text(R.string.preview_device_name)
        mutableState.value = when (mode) {
            "connecting" -> MainUiState(
                phase = "connecting",
                title = text(R.string.connecting_to_device),
                detail = text(R.string.checking_runtime_connection),
                deviceName = device,
                loading = true,
            )
            "connected" -> MainUiState(
                phase = "connected",
                title = text(R.string.vpn_connected),
                detail = text(R.string.connected_detail),
                deviceName = device,
            )
            else -> initialState()
        }
    }

    private fun initialState() = MainUiState(
        title = text(R.string.initial_title),
        detail = text(R.string.initial_detail),
    )

    private fun errorState(message: String) = failedState(message)

    private fun failedState(message: String, deviceName: String = "") = MainUiState(
        phase = "failed",
        title = text(R.string.connection_failed),
        detail = message,
        deviceName = deviceName,
        error = message,
    )

    private fun text(@StringRes id: Int, vararg arguments: Any): String =
        getApplication<Application>().getString(id, *arguments)

    private companion object {
        const val TAG = "MainViewModel"
    }
}
