package com.atridad.ascently.data.sync

import android.content.Context
import android.content.SharedPreferences
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import androidx.annotation.RequiresPermission
import androidx.core.content.edit
import com.atridad.ascently.data.format.BackupAttempt
import com.atridad.ascently.data.format.BackupClimbSession
import com.atridad.ascently.data.format.BackupGym
import com.atridad.ascently.data.format.BackupProblem
import com.atridad.ascently.data.format.ClimbDataBackup
import com.atridad.ascently.data.repository.ClimbRepository
import com.atridad.ascently.data.state.DataStateManager
import com.atridad.ascently.utils.AppLogger
import com.atridad.ascently.utils.DateFormatUtils
import com.atridad.ascently.utils.ImageNamingUtils
import com.atridad.ascently.utils.ImageUtils
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit

class AscentlySyncProvider(
    private val context: Context,
    private val repository: ClimbRepository,
) : SyncProvider {

    override val type: SyncProviderType = SyncProviderType.SERVER

    private val dataStateManager = DataStateManager(context)

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

    private val sharedPreferences: SharedPreferences =
        context.getSharedPreferences("sync_preferences", Context.MODE_PRIVATE)

    private val httpClient =
        OkHttpClient.Builder()
            .connectTimeout(45, TimeUnit.SECONDS)
            .readTimeout(90, TimeUnit.SECONDS)
            .writeTimeout(90, TimeUnit.SECONDS)
            .build()

    private val json = Json {
        prettyPrint = true
        ignoreUnknownKeys = true
        explicitNulls = false
        coerceInputValues = true
    }

    private val _isConnected = MutableStateFlow(false)
    override val isConnected: StateFlow<Boolean> = _isConnected.asStateFlow()

    private val _isConfigured = MutableStateFlow(false)
    override val isConfigured: StateFlow<Boolean> = _isConfigured.asStateFlow()

    private var isOfflineMode = false

    private object Keys {
        const val SERVER_URL = "server_url"
        const val AUTH_TOKEN = "auth_token"
        const val IS_CONNECTED = "is_connected"
        const val LAST_SYNC_TIME = "last_sync_time"
        const val OFFLINE_MODE = "offline_mode"
    }

    init {
        loadInitialState()
        updateConfiguredState()
    }

    private fun loadInitialState() {
        _isConnected.value = sharedPreferences.getBoolean(Keys.IS_CONNECTED, false)
        isOfflineMode = sharedPreferences.getBoolean(Keys.OFFLINE_MODE, false)
    }

    private fun updateConfiguredState() {
        _isConfigured.value = serverUrl.isNotBlank() && authToken.isNotBlank()
    }

    var serverUrl: String
        get() = sharedPreferences.getString(Keys.SERVER_URL, "") ?: ""
        set(value) {
            sharedPreferences.edit { putString(Keys.SERVER_URL, value) }
            updateConfiguredState()
            _isConnected.value = false
            sharedPreferences.edit { putBoolean(Keys.IS_CONNECTED, false) }
        }

    var authToken: String
        get() = sharedPreferences.getString(Keys.AUTH_TOKEN, "") ?: ""
        set(value) {
            sharedPreferences.edit { putString(Keys.AUTH_TOKEN, value) }
            updateConfiguredState()
            _isConnected.value = false
            sharedPreferences.edit { putBoolean(Keys.IS_CONNECTED, false) }
        }

    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
    private fun isNetworkAvailable(): Boolean {
        val connectivityManager =
            context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val network = connectivityManager.activeNetwork ?: return false
        val activeNetwork = connectivityManager.getNetworkCapabilities(network) ?: return false
        return when {
            activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
            activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
            else -> false
        }
    }

    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
    override suspend fun sync() {
        if (isOfflineMode) {
            AppLogger.d(TAG) { "Sync skipped: Offline mode is enabled." }
            return
        }
        if (!isNetworkAvailable()) {
            AppLogger.d(TAG) { "Sync skipped: No internet connection." }
            throw SyncException.NetworkError("No internet connection.")
        }
        if (!_isConfigured.value) {
            throw SyncException.NotConfigured
        }
        if (!_isConnected.value) {
            throw SyncException.NotConnected
        }

        val localBackup = createBackupFromRepository()
        val serverBackup = downloadData()

        val hasLocalData =
            localBackup.gyms.isNotEmpty() ||
                localBackup.problems.isNotEmpty() ||
                localBackup.sessions.isNotEmpty() ||
                localBackup.attempts.isNotEmpty()

        val hasServerData =
            serverBackup.gyms.isNotEmpty() ||
                serverBackup.problems.isNotEmpty() ||
                serverBackup.sessions.isNotEmpty() ||
                serverBackup.attempts.isNotEmpty()

        val lastSyncTimeStr = sharedPreferences.getString(Keys.LAST_SYNC_TIME, null)
        if (hasLocalData && hasServerData && lastSyncTimeStr != null) {
            AppLogger.d(TAG) { "Using delta sync for incremental updates" }
            performDeltaSync(lastSyncTimeStr)
        } else {
            when {
                !hasLocalData && hasServerData -> {
                    AppLogger.d(TAG) { "No local data found, performing full restore from server" }
                    val imagePathMapping = syncImagesFromServer(serverBackup)
                    importBackupToRepository(serverBackup, imagePathMapping)
                    AppLogger.d(TAG) { "Full restore completed" }
                }

                hasLocalData && !hasServerData -> {
                    AppLogger.d(TAG) { "No server data found, uploading local data to server" }
                    uploadData(localBackup)
                    syncImagesForBackup(localBackup)
                    AppLogger.d(TAG) { "Initial upload completed" }
                }

                hasLocalData && hasServerData -> {
                    AppLogger.d(TAG) { "Both local and server data exist, merging (server wins)" }
                    mergeDataSafely(serverBackup)
                    AppLogger.d(TAG) { "Merge completed" }
                }

                else -> {
                    AppLogger.d(TAG) { "No data to sync" }
                }
            }
        }

        val now = DateFormatUtils.nowISO8601()
        sharedPreferences.edit { putString(Keys.LAST_SYNC_TIME, now) }
    }

    override fun disconnect() {
        serverUrl = ""
        authToken = ""
        _isConnected.value = false
        sharedPreferences.edit {
            remove(Keys.LAST_SYNC_TIME)
            putBoolean(Keys.IS_CONNECTED, false)
        }
        updateConfiguredState()
    }

    private suspend fun performDeltaSync(lastSyncTimeStr: String) {
        AppLogger.d(TAG) { "Starting delta sync with lastSyncTime=$lastSyncTimeStr" }

        val lastSyncDate = parseISO8601(lastSyncTimeStr) ?: Date(0)

        val allGyms = repository.getAllGyms().first()
        val modifiedGyms =
            allGyms
                .filter { gym -> parseISO8601(gym.updatedAt)?.after(lastSyncDate) == true }
                .map { BackupGym.fromGym(it) }

        val allProblems = repository.getAllProblems().first()
        val modifiedProblems =
            allProblems
                .filter { problem ->
                    parseISO8601(problem.updatedAt)?.after(lastSyncDate) == true
                }
                .map { problem ->
                    val backupProblem = BackupProblem.fromProblem(problem)
                    val normalizedImagePaths =
                        problem.imagePaths.mapIndexed { index, _ ->
                            ImageNamingUtils.generateImageFilename(problem.id, index)
                        }
                    if (normalizedImagePaths.isNotEmpty()) {
                        backupProblem.copy(imagePaths = normalizedImagePaths)
                    } else {
                        backupProblem
                    }
                }

        val allSessions = repository.getAllSessions().first()
        val modifiedSessions =
            allSessions
                .filter { session ->
                    parseISO8601(session.updatedAt)?.after(lastSyncDate) == true
                }
                .map { BackupClimbSession.fromClimbSession(it) }

        val allAttempts = repository.getAllAttempts().first()
        val modifiedAttempts =
            allAttempts
                .filter { attempt ->
                    parseISO8601(attempt.createdAt)?.after(lastSyncDate) == true
                }
                .map { BackupAttempt.fromAttempt(it) }

        val allDeletions = repository.getDeletedItems()
        val modifiedDeletions =
            allDeletions.filter { item ->
                parseISO8601(item.deletedAt)?.after(lastSyncDate) == true
            }

        AppLogger.d(TAG) {
            "Delta sync sending: gyms=${modifiedGyms.size}, problems=${modifiedProblems.size}, sessions=${modifiedSessions.size}, attempts=${modifiedAttempts.size}, deletions=${modifiedDeletions.size}"
        }

        val deltaRequest =
            DeltaSyncRequest(
                lastSyncTime = lastSyncTimeStr,
                gyms = modifiedGyms,
                problems = modifiedProblems,
                sessions = modifiedSessions,
                attempts = modifiedAttempts,
                deletedItems = modifiedDeletions,
            )

        val requestBody =
            json.encodeToString(DeltaSyncRequest.serializer(), deltaRequest)
                .toRequestBody("application/json".toMediaType())

        val request =
            Request.Builder()
                .url("$serverUrl/sync/delta")
                .header("Authorization", "Bearer $authToken")
                .post(requestBody)
                .build()

        val deltaResponse =
            withContext(Dispatchers.IO) {
                try {
                    httpClient.newCall(request).execute().use { response ->
                        if (response.isSuccessful) {
                            val body = response.body?.string()
                            if (!body.isNullOrEmpty()) {
                                json.decodeFromString(DeltaSyncResponse.serializer(), body)
                            } else {
                                throw SyncException.InvalidResponse("Empty response body")
                            }
                        } else {
                            handleHttpError(response.code)
                        }
                    }
                } catch (e: IOException) {
                    throw SyncException.NetworkError(e.message ?: "Network error")
                }
            }

        AppLogger.d(TAG) {
            "Delta sync received: gyms=${deltaResponse.gyms.size}, " +
                "problems=${deltaResponse.problems.size}, sessions=${deltaResponse.sessions.size}, " +
                "attempts=${deltaResponse.attempts.size}, deletions=${deltaResponse.deletedItems.size}"
        }

        applyDeltaResponse(deltaResponse)
        syncModifiedImages(modifiedProblems)
    }

    private fun parseISO8601(dateString: String): Date? {
        return try {
            val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
            format.parse(dateString)
        } catch (_: Exception) {
            null
        }
    }

    private suspend fun applyDeltaResponse(response: DeltaSyncResponse) {
        // SyncService handles the "isSyncing" state to prevent recursive sync triggers
        // when the repository is modified during a sync operation.

        try {
            // Merge and apply deletions first to prevent resurrection
            val allDeletions = repository.getDeletedItems() + response.deletedItems
            val uniqueDeletions = allDeletions.distinctBy { "${it.type}:${it.id}" }

            AppLogger.d(TAG) { "Applying ${uniqueDeletions.size} deletion records before merging data" }
            applyDeletions(uniqueDeletions)

            // Build deleted item lookup set
            val deletedItemSet = uniqueDeletions.map { "${it.type}:${it.id}" }.toSet()

            // Download images for new/modified problems from server
            val imagePathMapping = mutableMapOf<String, String>()
            for (problem in response.problems) {
                if (deletedItemSet.contains("problem:${problem.id}")) {
                    continue
                }
                problem.imagePaths?.forEach { imagePath ->
                    val serverFilename = imagePath.substringAfterLast('/')
                    try {
                        val localImagePath = downloadImage(serverFilename)
                        if (localImagePath != null) {
                            imagePathMapping[imagePath] = localImagePath
                        }
                    } catch (e: Exception) {
                        AppLogger.w(TAG) { "Failed to download image $imagePath: ${e.message}" }
                    }
                }
            }

            // Merge gyms
            val existingGyms = repository.getAllGyms().first()
            for (backupGym in response.gyms) {
                if (deletedItemSet.contains("gym:${backupGym.id}")) {
                    continue
                }
                val existing = existingGyms.find { it.id == backupGym.id }
                if (existing == null || backupGym.updatedAt >= existing.updatedAt) {
                    val gym = backupGym.toGym()
                    if (existing != null) {
                        repository.updateGym(gym)
                    } else {
                        repository.insertGym(gym)
                    }
                }
            }

            // Merge problems
            val existingProblems = repository.getAllProblems().first()
            for (backupProblem in response.problems) {
                if (deletedItemSet.contains("problem:${backupProblem.id}")) {
                    continue
                }
                val updatedImagePaths =
                    backupProblem.imagePaths?.map { oldPath ->
                        imagePathMapping[oldPath] ?: oldPath
                    }
                val problemToMerge = backupProblem.copy(imagePaths = updatedImagePaths)
                val problem = problemToMerge.toProblem()

                val existing = existingProblems.find { it.id == backupProblem.id }
                if (existing == null || backupProblem.updatedAt >= existing.updatedAt) {
                    if (existing != null) {
                        repository.updateProblem(problem)
                    } else {
                        repository.insertProblem(problem)
                    }
                }
            }

            // Merge sessions
            val existingSessions = repository.getAllSessions().first()
            for (backupSession in response.sessions) {
                if (deletedItemSet.contains("session:${backupSession.id}")) {
                    continue
                }
                val session = backupSession.toClimbSession()
                val existing = existingSessions.find { it.id == backupSession.id }
                if (existing == null || backupSession.updatedAt >= existing.updatedAt) {
                    if (existing != null) {
                        repository.updateSession(session)
                    } else {
                        repository.insertSession(session)
                    }
                }
            }

            // Merge attempts
            val existingAttempts = repository.getAllAttempts().first()
            for (backupAttempt in response.attempts) {
                if (deletedItemSet.contains("attempt:${backupAttempt.id}")) {
                    continue
                }
                val attempt = backupAttempt.toAttempt()
                val existing = existingAttempts.find { it.id == backupAttempt.id }
                if (existing == null || backupAttempt.createdAt >= existing.createdAt) {
                    if (existing != null) {
                        repository.updateAttempt(attempt)
                    } else {
                        repository.insertAttempt(attempt)
                    }
                }
            }

            // Apply deletions again for safety
            applyDeletions(uniqueDeletions)

            // Update deletion records
            repository.clearDeletedItems()
            uniqueDeletions.forEach { repository.trackDeletion(it.id, it.type) }
        } catch (e: Exception) {
            AppLogger.e(TAG, e) { "Error applying delta response" }
            throw e
        }
    }

    private suspend fun applyDeletions(
        deletions: List<com.atridad.ascently.data.format.DeletedItem>,
    ) {
        val existingGyms = repository.getAllGyms().first()
        val existingProblems = repository.getAllProblems().first()
        val existingSessions = repository.getAllSessions().first()
        val existingAttempts = repository.getAllAttempts().first()

        for (item in deletions) {
            when (item.type) {
                "gym" -> {
                    existingGyms.find { it.id == item.id }?.let { repository.deleteGym(it) }
                }

                "problem" -> {
                    existingProblems.find { it.id == item.id }?.let { repository.deleteProblem(it) }
                }

                "session" -> {
                    existingSessions.find { it.id == item.id }?.let { repository.deleteSession(it) }
                }

                "attempt" -> {
                    existingAttempts.find { it.id == item.id }?.let { repository.deleteAttempt(it) }
                }
            }
        }
    }

    private suspend fun syncModifiedImages(modifiedProblems: List<BackupProblem>) {
        if (modifiedProblems.isEmpty()) return

        AppLogger.d(TAG) { "Syncing images for ${modifiedProblems.size} modified problems" }

        for (backupProblem in modifiedProblems) {
            backupProblem.imagePaths?.forEach { imagePath ->
                val filename = imagePath.substringAfterLast('/')
                uploadImage(imagePath, filename)
            }
        }
    }

    private suspend fun downloadData(): ClimbDataBackup {
        val request =
            Request.Builder()
                .url("$serverUrl/sync")
                .header("Authorization", "Bearer $authToken")
                .get()
                .build()

        return withContext(Dispatchers.IO) {
            try {
                httpClient.newCall(request).execute().use { response ->
                    if (response.isSuccessful) {
                        val body = response.body?.string()
                        if (!body.isNullOrEmpty()) {
                            json.decodeFromString(body)
                        } else {
                            ClimbDataBackup(
                                exportedAt = DateFormatUtils.nowISO8601(),
                                gyms = emptyList(),
                                problems = emptyList(),
                                sessions = emptyList(),
                                attempts = emptyList(),
                            )
                        }
                    } else {
                        handleHttpError(response.code)
                    }
                }
            } catch (e: IOException) {
                throw SyncException.NetworkError(e.message ?: "Network error")
            }
        }
    }

    private suspend fun uploadData(backup: ClimbDataBackup) {
        val requestBody =
            json.encodeToString(backup).toRequestBody("application/json".toMediaType())

        val request =
            Request.Builder()
                .url("$serverUrl/sync")
                .header("Authorization", "Bearer $authToken")
                .put(requestBody)
                .build()

        withContext(Dispatchers.IO) {
            try {
                httpClient.newCall(request).execute().use { response ->
                    if (!response.isSuccessful) {
                        handleHttpError(response.code)
                    }
                }
            } catch (e: IOException) {
                throw SyncException.NetworkError(e.message ?: "Network error")
            }
        }
    }

    private suspend fun syncImagesFromServer(backup: ClimbDataBackup): Map<String, String> {
        val imagePathMapping = mutableMapOf<String, String>()
        val totalImages = backup.problems.sumOf { it.imagePaths?.size ?: 0 }
        AppLogger.d(TAG) { "Starting image download from server for $totalImages images" }

        withContext(Dispatchers.IO) {
            backup.problems.forEach { problem ->
                problem.imagePaths?.forEach { imagePath ->
                    val serverFilename = imagePath.substringAfterLast('/')
                    try {
                        val localImagePath = downloadImage(serverFilename)
                        if (localImagePath != null) {
                            imagePathMapping[imagePath] = localImagePath
                        }
                    } catch (_: SyncException.ImageNotFound) {
                        AppLogger.w(TAG) { "Image not found on server: $imagePath" }
                    } catch (e: CancellationException) {
                        throw e
                    } catch (e: Exception) {
                        AppLogger.w(TAG) { "Failed to download image $imagePath: ${e.message}" }
                    }
                }
            }
        }
        return imagePathMapping
    }

    private suspend fun downloadImage(serverFilename: String): String? {
        val request =
            Request.Builder()
                .url("$serverUrl/images/download?filename=$serverFilename")
                .header("Authorization", "Bearer $authToken")
                .build()

        return withContext(Dispatchers.IO) {
            try {
                httpClient.newCall(request).execute().use { response ->
                    if (response.isSuccessful) {
                        response.body?.bytes()?.let {
                            ImageUtils.saveImageFromBytesWithFilename(context, it, serverFilename)
                        }
                    } else {
                        if (response.code == 404) throw SyncException.ImageNotFound
                        null
                    }
                }
            } catch (e: IOException) {
                AppLogger.e(TAG, e) { "Network error downloading image $serverFilename" }
                null
            }
        }
    }

    private suspend fun syncImagesForBackup(backup: ClimbDataBackup) {
        AppLogger.d(TAG) { "Starting image sync for backup with ${backup.problems.size} problems" }
        withContext(Dispatchers.IO) {
            backup.problems.forEach { problem ->
                problem.imagePaths?.forEach { localPath ->
                    val filename = localPath.substringAfterLast('/')
                    uploadImage(localPath, filename)
                }
            }
        }
    }

    private suspend fun uploadImage(localPath: String, filename: String) {
        val file = ImageUtils.getImageFile(context, localPath)
        if (!file.exists()) {
            AppLogger.w(TAG) { "Local image file not found, cannot upload: $localPath" }
            return
        }

        val requestBody = file.readBytes().toRequestBody("application/octet-stream".toMediaType())

        val request =
            Request.Builder()
                .url("$serverUrl/images/upload?filename=$filename")
                .header("Authorization", "Bearer $authToken")
                .post(requestBody)
                .build()

        withContext(Dispatchers.IO) {
            try {
                httpClient.newCall(request).execute().use { response ->
                    if (response.isSuccessful) {
                        AppLogger.d(TAG) { "Successfully uploaded image: $filename" }
                    } else {
                        AppLogger.w(TAG) {
                            "Failed to upload image $filename. Server responded with ${response.code}"
                        }
                    }
                }
            } catch (e: IOException) {
                AppLogger.e(TAG, e) { "Network error uploading image $filename" }
            }
        }
    }

    private suspend fun createBackupFromRepository(): ClimbDataBackup {
        return withContext(Dispatchers.Default) {
            ClimbDataBackup(
                exportedAt = dataStateManager.getLastModified(),
                gyms = repository.getAllGyms().first().map { BackupGym.fromGym(it) },
                problems =
                repository.getAllProblems().first().map { problem ->
                    val backupProblem = BackupProblem.fromProblem(problem)
                    val normalizedImagePaths =
                        problem.imagePaths.mapIndexed { index, _ ->
                            ImageNamingUtils.generateImageFilename(
                                problem.id,
                                index,
                            )
                        }
                    if (normalizedImagePaths.isNotEmpty()) {
                        backupProblem.copy(imagePaths = normalizedImagePaths)
                    } else {
                        backupProblem
                    }
                },
                sessions =
                repository.getAllSessions().first().map {
                    BackupClimbSession.fromClimbSession(it)
                },
                attempts =
                repository.getAllAttempts().first().map {
                    BackupAttempt.fromAttempt(it)
                },
                deletedItems = repository.getDeletedItems(),
            )
        }
    }

    private suspend fun importBackupToRepository(
        backup: ClimbDataBackup,
        imagePathMapping: Map<String, String>,
    ) {
        val gyms = backup.gyms.map { it.toGym() }
        val problems =
            backup.problems.map { backupProblem ->
                val imagePaths = backupProblem.imagePaths
                val updatedImagePaths =
                    imagePaths?.map { oldPath -> imagePathMapping[oldPath] ?: oldPath }
                backupProblem.copy(imagePaths = updatedImagePaths).toProblem()
            }
        val sessions = backup.sessions.map { it.toClimbSession() }
        val attempts = backup.attempts.map { it.toAttempt() }

        repository.resetAllData()

        gyms.forEach { repository.insertGymWithoutSync(it) }
        problems.forEach { repository.insertProblemWithoutSync(it) }
        sessions.forEach { repository.insertSessionWithoutSync(it) }
        attempts.forEach { repository.insertAttemptWithoutSync(it) }

        repository.clearDeletedItems()
    }

    private suspend fun mergeDataSafely(serverBackup: ClimbDataBackup) {
        AppLogger.d(TAG) { "Server data will overwrite local data. Performing full restore." }
        val imagePathMapping = syncImagesFromServer(serverBackup)
        importBackupToRepository(serverBackup, imagePathMapping)
    }

    private fun handleHttpError(code: Int): Nothing {
        when (code) {
            401 -> throw SyncException.Unauthorized
            in 500..599 -> throw SyncException.ServerError(code)
            else -> throw SyncException.InvalidResponse("HTTP error code: $code")
        }
    }

    override suspend fun testConnection() {
        if (!_isConfigured.value) {
            _isConnected.value = false
            throw SyncException.NotConfigured
        }

        val request =
            Request.Builder()
                .url("$serverUrl/sync")
                .header("Authorization", "Bearer $authToken")
                .head()
                .build()
        try {
            withContext(Dispatchers.IO) {
                httpClient.newCall(request).execute().use { response ->
                    _isConnected.value = response.isSuccessful || response.code == 405
                }
            }
            if (!_isConnected.value) {
                throw SyncException.NotConnected
            }
        } catch (e: Exception) {
            _isConnected.value = false
            throw SyncException.NetworkError(e.message ?: "Connection error")
        } finally {
            sharedPreferences.edit { putBoolean(Keys.IS_CONNECTED, _isConnected.value) }
        }
    }
}
