package com.atridad.ascently.data.sync

import android.content.Context
import android.content.SharedPreferences
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.util.Log
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import java.io.IOException
import java.time.Instant
import java.util.concurrent.TimeUnit

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

    override val type = SyncProviderType.SERVER

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

    private val sharedPreferences: SharedPreferences =
        context.getSharedPreferences("ascently_prefs", 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
        encodeDefaults = true
        isLenient = 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()

    var isOfflineMode: Boolean
        get() = sharedPreferences.getBoolean(Keys.OFFLINE_MODE, false)
        set(value) = sharedPreferences.edit().putBoolean(Keys.OFFLINE_MODE, value).apply()

    object Keys {
        const val SERVER_URL = "sync_server_url"
        const val AUTH_TOKEN = "sync_auth_token"
        const val IS_CONNECTED = "is_connected"
        const val LAST_SYNC_TIME = "last_sync_time"
        const val OFFLINE_MODE = "offline_mode"
    }

    init {
        loadInitialState()
    }

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

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

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

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

    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
            activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
            else -> false
        }
    }

    override suspend fun sync() = withContext(Dispatchers.IO) {
        if (isOfflineMode) {
            AppLogger.i(TAG) { "Sync skipped: Offline mode is enabled." }
            return@withContext
        }

        if (!isConfigured.value) {
            throw SyncException.NotConfigured
        }

        if (!isNetworkAvailable()) {
            throw SyncException.NotConnected
        }

        val lastSyncTimeStr = sharedPreferences.getString(Keys.LAST_SYNC_TIME, null)

        if (lastSyncTimeStr != null) {
            AppLogger.i(TAG) { "Last sync time found ($lastSyncTimeStr), attempting delta sync" }
            try {
                performDeltaSync()
                sharedPreferences.edit()
                    .putString(Keys.LAST_SYNC_TIME, DateFormatUtils.nowISO8601())
                    .apply()
                return@withContext
            } catch (e: Exception) {
                AppLogger.e(TAG, e) { "Delta sync failed, falling back to full sync check: ${e.message}" }
            }
        }

        val localBackup = createBackupFromRepository()
        val serverBackup = try {
            downloadData()
        } catch (e: Exception) {
            AppLogger.e(TAG, e) { "Failed to download data: ${e.message}" }
            throw e
        }

        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()

        if (!hasLocalData && hasServerData) {
            AppLogger.i(TAG) { "Performing full restore from server" }
            AppLogger.i(TAG) { "Syncing images from server first..." }
            val imagePathMapping = syncImagesFromServer(serverBackup)

            AppLogger.i(TAG) { "Importing data after images..." }
            importBackupToRepository(serverBackup, imagePathMapping)
            AppLogger.i(TAG) { "Full restore completed" }
        } else if (hasLocalData && !hasServerData) {
            AppLogger.i(TAG) { "Uploading local data to server" }
            uploadData(localBackup)
            AppLogger.i(TAG) { "Uploading local images to server..." }
            syncImagesToServer()
            AppLogger.i(TAG) { "Initial upload completed" }
        } else if (hasLocalData && hasServerData) {
            AppLogger.i(TAG) { "Merging local and server data" }
            mergeDataSafely(localBackup, serverBackup)
            AppLogger.i(TAG) { "Safe merge completed" }
        } else {
            AppLogger.i(TAG) { "No data to sync" }
        }

        sharedPreferences.edit()
            .putString(Keys.LAST_SYNC_TIME, DateFormatUtils.nowISO8601())
            .apply()
    }

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

    private suspend fun performDeltaSync() {
        val lastSyncTimeStr = sharedPreferences.getString(Keys.LAST_SYNC_TIME, null)
            ?: throw SyncException.General("No last sync time for delta sync")

        val lastSyncDate = parseISO8601(lastSyncTimeStr) ?: Instant.EPOCH

        val allGyms = repository.getAllGymsSync()
        val modifiedGyms = allGyms.filter {
            (parseISO8601(it.updatedAt) ?: Instant.MIN).isAfter(lastSyncDate)
        }.map { BackupGym.fromGym(it) }.toMutableList()

        val allProblems = repository.getAllProblemsSync()
        val modifiedProblems = allProblems.filter {
            (parseISO8601(it.updatedAt) ?: Instant.MIN).isAfter(lastSyncDate)
        }.map { problem ->
            val backupProblem = BackupProblem.fromProblem(problem)
             if (!problem.imagePaths.isEmpty()) {
                val normalizedPaths = problem.imagePaths.mapIndexed { index, _ ->
                     "${problem.id}_${index}.jpg"
                }
                backupProblem.withUpdatedImagePaths(normalizedPaths)
            } else {
                backupProblem
            }
        }.toMutableList()

        val allSessions = repository.getAllSessionsSync()
        val modifiedSessions = allSessions.filter {
            (parseISO8601(it.updatedAt) ?: Instant.MIN).isAfter(lastSyncDate)
        }.map { BackupClimbSession.fromClimbSession(it) }.toMutableList()

        val allAttempts = repository.getAllAttemptsSync()
        val modifiedAttempts = allAttempts.filter {
            val updated = it.updatedAt
            (parseISO8601(updated) ?: Instant.MIN).isAfter(lastSyncDate)
        }.map { BackupAttempt.fromAttempt(it) }.toMutableList()

        val deletedItems = repository.getDeletedItems().filter {
            val deletedAt = parseISO8601(it.deletedAt) ?: Instant.MIN
            deletedAt.isAfter(lastSyncDate)
        }

        for (item in deletedItems) {
            when (item.type) {
                "gym" -> modifiedGyms.add(BackupGym.createTombstone(item.id, item.deletedAt))
                "problem" -> modifiedProblems.add(BackupProblem.createTombstone(item.id, item.deletedAt))
                "session" -> modifiedSessions.add(BackupClimbSession.createTombstone(item.id, item.deletedAt))
                "attempt" -> modifiedAttempts.add(BackupAttempt.createTombstone(item.id, item.deletedAt))
            }
        }

        AppLogger.i(TAG) { "Delta Sync: Sending gyms=${modifiedGyms.size}, problems=${modifiedProblems.size}, sessions=${modifiedSessions.size}, attempts=${modifiedAttempts.size}" }

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

        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 response = withContext(Dispatchers.IO) {
            httpClient.newCall(request).execute().use { response ->
                if (!response.isSuccessful) {
                    if (response.code == 401) throw SyncException.Unauthorized
                    throw SyncException.ServerError(response.code)
                }
                val body = response.body?.string() ?: throw SyncException.InvalidResponse("Empty response")
                json.decodeFromString<DeltaSyncResponse>(body)
            }
        }

        if (response.requestFullSync) {
            throw SyncException.General("Server requested full sync")
        }

        AppLogger.i(TAG) { "Delta Sync: Received gyms=${response.gyms.size}, problems=${response.problems.size}, sessions=${response.sessions.size}, attempts=${response.attempts.size}" }

        applyDeltaResponse(response)
        syncModifiedImages(modifiedProblems)

        repository.clearDeletedItems()

        sharedPreferences.edit()
            .putString(Keys.LAST_SYNC_TIME, response.serverTime)
            .apply()
    }

    private fun parseISO8601(dateStr: String?): Instant? {
        if (dateStr == null) return null
        return try {
            Instant.parse(dateStr)
        } catch (e: Exception) {
            null
        }
    }

    private suspend fun applyDeltaResponse(response: DeltaSyncResponse) {
        val imagePathMapping = mutableMapOf<String, String>()
        for (problem in response.problems) {
            if (problem.isDeleted) continue

            val paths = problem.imagePaths ?: continue
            for ((index, path) in paths.withIndex()) {
                val serverFilename = path.substringAfterLast('/')
                try {
                    val imageData = downloadImage(serverFilename)
                    val localFilename = "${problem.id}_${index}.jpg"
                    saveImageLocally(localFilename, imageData)
                    imagePathMapping[serverFilename] = localFilename
                } catch (e: Exception) {
                    AppLogger.e(TAG, e) { "Failed to download image $serverFilename: ${e.message}" }
                }
            }
        }

        for (gym in response.gyms) {
            if (gym.isDeleted) {
                val serverUpdate = parseISO8601(gym.updatedAt) ?: Instant.MAX
                val existing = repository.getGymById(gym.id)
                if (existing != null) {
                    val localUpdate = parseISO8601(existing.updatedAt) ?: Instant.MIN
                    if (serverUpdate.isAfter(localUpdate) || serverUpdate == localUpdate) {
                         repository.deleteGym(existing)
                    }
                }
                continue
            }

            val existing = repository.getGymById(gym.id)
            val serverUpdate = parseISO8601(gym.updatedAt) ?: Instant.MIN
            if (existing != null) {
                val localUpdate = parseISO8601(existing.updatedAt) ?: Instant.MIN
                if (serverUpdate.isAfter(localUpdate)) {
                    repository.updateGym(gym.toGym())
                }
            } else {
                repository.insertGym(gym.toGym())
            }
        }

        for (problem in response.problems) {
            if (problem.isDeleted) {
                val serverUpdate = parseISO8601(problem.updatedAt) ?: Instant.MAX
                val existing = repository.getProblemById(problem.id)
                if (existing != null) {
                    val localUpdate = parseISO8601(existing.updatedAt) ?: Instant.MIN
                    if (serverUpdate.isAfter(localUpdate) || serverUpdate == localUpdate) {
                         repository.deleteProblem(existing)
                    }
                }
                continue
            }

            var problemToMerge = problem
            if (imagePathMapping.isNotEmpty() && !problem.imagePaths.isNullOrEmpty()) {
                val updatedPaths = problem.imagePaths.map { imagePathMapping[it.substringAfterLast('/')] ?: it }
                problemToMerge = problem.withUpdatedImagePaths(updatedPaths)
            }

            val existing = repository.getProblemById(problem.id)
            val serverUpdate = parseISO8601(problem.updatedAt) ?: Instant.MIN
            if (existing != null) {
                val localUpdate = parseISO8601(existing.updatedAt) ?: Instant.MIN
                if (serverUpdate.isAfter(localUpdate)) {
                    repository.updateProblem(problemToMerge.toProblem())
                }
            } else {
                repository.insertProblem(problemToMerge.toProblem())
            }
        }

        for (session in response.sessions) {
            if (session.isDeleted) {
                val serverUpdate = parseISO8601(session.updatedAt) ?: Instant.MAX
                val existing = repository.getSessionById(session.id)
                if (existing != null) {
                    val localUpdate = parseISO8601(existing.updatedAt) ?: Instant.MIN
                    if (serverUpdate.isAfter(localUpdate) || serverUpdate == localUpdate) {
                         repository.deleteSession(existing)
                    }
                }
                continue
            }

            val existing = repository.getSessionById(session.id)
            val serverUpdate = parseISO8601(session.updatedAt) ?: Instant.MIN
            if (existing != null) {
                val localUpdate = parseISO8601(existing.updatedAt) ?: Instant.MIN
                if (serverUpdate.isAfter(localUpdate)) {
                    repository.updateSession(session.toClimbSession())
                }
            } else {
                repository.insertSession(session.toClimbSession())
            }
        }

        for (attempt in response.attempts) {
            if (attempt.isDeleted) {
                val serverUpdate = parseISO8601(attempt.updatedAt ?: attempt.createdAt) ?: Instant.MAX
                val existing = repository.getAttemptById(attempt.id)
                if (existing != null) {
                    val localUpdate = parseISO8601(existing.updatedAt) ?: Instant.MIN
                    if (serverUpdate.isAfter(localUpdate) || serverUpdate == localUpdate) {
                         repository.deleteAttempt(existing)
                    }
                }
                continue
            }

            val existing = repository.getAttemptById(attempt.id)
            val serverUpdate = parseISO8601(attempt.updatedAt ?: attempt.createdAt) ?: Instant.MIN
            if (existing != null) {
                val localUpdate = parseISO8601(existing.updatedAt) ?: Instant.MIN
                if (serverUpdate.isAfter(localUpdate)) {
                    repository.updateAttempt(attempt.toAttempt())
                }
            } else {
                repository.insertAttempt(attempt.toAttempt())
            }
        }
    }

    private suspend fun syncModifiedImages(modifiedProblems: List<BackupProblem>) {
         for (problem in modifiedProblems) {
             if (problem.isDeleted) continue
             val paths = problem.imagePaths ?: continue
             for (path in paths) {
                 val localFile = getLocalImageFile(path)
                 if (localFile != null && localFile.exists()) {
                     try {
                         uploadImage(path, localFile.readBytes())
                     } catch (e: Exception) {
                         AppLogger.e(TAG, e) { "Failed to upload image $path: ${e.message}" }
                     }
                 }
             }
         }
    }

    private fun getLocalImageFile(filename: String): File? {
        val dir = context.getExternalFilesDir("images") ?: return null
        return File(dir, filename)
    }

    private fun saveImageLocally(filename: String, data: ByteArray) {
        val dir = context.getExternalFilesDir("images") ?: return
        if (!dir.exists()) dir.mkdirs()
        File(dir, filename).writeBytes(data)
    }

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

        return withContext(Dispatchers.IO) {
            httpClient.newCall(request).execute().use { response ->
                if (!response.isSuccessful) {
                    if (response.code == 401) throw SyncException.Unauthorized
                    throw SyncException.ServerError(response.code)
                }
                val body = response.body?.string() ?: throw SyncException.InvalidResponse("Empty body")
                json.decodeFromString<ClimbDataBackup>(body)
            }
        }
    }

    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) {
            httpClient.newCall(request).execute().use { response ->
                if (!response.isSuccessful) {
                    if (response.code == 401) throw SyncException.Unauthorized
                    throw SyncException.ServerError(response.code)
                }
            }
        }
    }

    private suspend fun syncImagesFromServer(backup: ClimbDataBackup): Map<String, String> {
        val mapping = mutableMapOf<String, String>()
        for (problem in backup.problems) {
            if (problem.isDeleted) continue
            val paths = problem.imagePaths ?: continue
            for ((index, path) in paths.withIndex()) {
                val serverFilename = path.substringAfterLast('/')
                 try {
                    val imageData = downloadImage(serverFilename)
                    val localFilename = "${problem.id}_${index}.jpg"
                    saveImageLocally(localFilename, imageData)
                    mapping[serverFilename] = localFilename
                } catch (e: Exception) {
                    Log.e(TAG, "Failed to sync image $serverFilename", e)
                }
            }
        }
        return mapping
    }

    private suspend fun syncImagesToServer() {
        val problems = repository.getAllProblemsSync()
        for (problem in problems) {
             for (path in problem.imagePaths) {
                 val file = getLocalImageFile(path)
                 if (file != null && file.exists()) {
                      try {
                          uploadImage(path, file.readBytes())
                      } catch (e: Exception) {
                           Log.e(TAG, "Failed to upload image $path", e)
                      }
                 }
             }
         }
    }

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

        return withContext(Dispatchers.IO) {
            httpClient.newCall(request).execute().use { response ->
                if (!response.isSuccessful) {
                    if (response.code == 404) throw SyncException.ImageNotFound
                    throw SyncException.ServerError(response.code)
                }
                response.body?.bytes() ?: throw SyncException.InvalidResponse("Empty image body")
            }
        }
    }

    private suspend fun uploadImage(filename: String, data: ByteArray) {
        val requestBody = data.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) {
            httpClient.newCall(request).execute().use { response ->
                if (!response.isSuccessful) {
                     throw SyncException.ServerError(response.code)
                }
            }
        }
    }

    private suspend fun createBackupFromRepository(): ClimbDataBackup {
        val gyms = repository.getAllGymsSync().map { BackupGym.fromGym(it) }
        val problems = repository.getAllProblemsSync().map { BackupProblem.fromProblem(it) }
        val sessions = repository.getAllSessionsSync().map { BackupClimbSession.fromClimbSession(it) }
        val attempts = repository.getAllAttemptsSync().map { BackupAttempt.fromAttempt(it) }

        return ClimbDataBackup(
            exportedAt = DateFormatUtils.nowISO8601(),
            gyms = gyms,
            problems = problems,
            sessions = sessions,
            attempts = attempts
        )
    }

    private suspend fun importBackupToRepository(backup: ClimbDataBackup, imagePathMapping: Map<String, String>) {
        for (gym in backup.gyms) {
            if (!gym.isDeleted) repository.insertGym(gym.toGym())
        }

        for (problem in backup.problems) {
            if (!problem.isDeleted) {
                var p = problem
                if (imagePathMapping.isNotEmpty() && !problem.imagePaths.isNullOrEmpty()) {
                    val updatedPaths = problem.imagePaths.map { imagePathMapping[it.substringAfterLast('/')] ?: it }
                    p = problem.withUpdatedImagePaths(updatedPaths)
                }
                repository.insertProblem(p.toProblem())
            }
        }

        for (session in backup.sessions) {
            if (!session.isDeleted) repository.insertSession(session.toClimbSession())
        }

        for (attempt in backup.attempts) {
            if (!attempt.isDeleted) repository.insertAttempt(attempt.toAttempt())
        }
    }

    private suspend fun mergeDataSafely(local: ClimbDataBackup, server: ClimbDataBackup) {
        val deltaResponse = DeltaSyncResponse(
            serverTime = server.exportedAt,
            gyms = server.gyms,
            problems = server.problems,
            sessions = server.sessions,
            attempts = server.attempts,
        )
        applyDeltaResponse(deltaResponse)
    }

    override suspend fun testConnection() {
        if (!isConfigured.value) throw SyncException.NotConfigured

        val request = Request.Builder()
            .url("$serverUrl/health")
            .apply {
                if (authToken.isNotBlank()) header("Authorization", "Bearer $authToken")
            }
            .head() // Using HEAD as originally, or GET
            .build()

        try {
            withContext(Dispatchers.IO) {
                httpClient.newCall(request).execute().use { response ->
                    if (!response.isSuccessful && response.code != 405) {
                         throw SyncException.ServerError(response.code)
                    }
                    _isConnected.value = true
                    sharedPreferences.edit().putBoolean(Keys.IS_CONNECTED, true).apply()
                }
            }
        } catch (e: Exception) {
            _isConnected.value = false
            throw e
        }
    }
}
