package de.krisenvorrat.app.data.repository

import de.krisenvorrat.app.data.db.dao.ItemDao
import de.krisenvorrat.app.data.db.dao.PendingSyncOpDao
import de.krisenvorrat.app.data.db.entity.ItemEntity
import de.krisenvorrat.app.data.db.entity.PendingSyncOpEntity
import de.krisenvorrat.app.data.sync.WebSocketClient
import de.krisenvorrat.app.data.sync.WebSocketEvent
import de.krisenvorrat.app.di.ApplicationScope
import de.krisenvorrat.app.domain.model.SettingsKeys
import de.krisenvorrat.app.domain.model.SyncError
import de.krisenvorrat.app.domain.repository.ItemRepository
import de.krisenvorrat.app.domain.repository.SettingsRepository
import de.krisenvorrat.app.domain.repository.SyncService
import de.krisenvorrat.shared.model.ItemDto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import java.time.LocalDate
import java.util.UUID
import javax.inject.Inject

internal class ItemRepositoryImpl @Inject constructor(
    private val dao: ItemDao,
    private val syncService: SyncService,
    private val pendingSyncOpDao: PendingSyncOpDao,
    private val settingsRepository: SettingsRepository,
    private val webSocketClient: WebSocketClient,
    @ApplicationScope private val scope: CoroutineScope
) : ItemRepository {

    init {
        scope.launch {
            webSocketClient.events.collect { event ->
                if (event is WebSocketEvent.Connected) {
                    drainQueue()
                }
            }
        }
    }

    override fun getAll(): Flow<List<ItemEntity>> = dao.getAll()

    override suspend fun getById(id: String): ItemEntity? =
        withContext(Dispatchers.IO) { dao.getById(id) }

    override suspend fun insert(item: ItemEntity) {
        withContext(Dispatchers.IO) { dao.insert(item) }
        scope.launch { attemptPatch(item.id, item.toDto()) }
    }

    override suspend fun update(item: ItemEntity) {
        withContext(Dispatchers.IO) { dao.update(item) }
        scope.launch { attemptPatch(item.id, item.toDto()) }
    }

    override suspend fun delete(item: ItemEntity) {
        withContext(Dispatchers.IO) {
            pendingSyncOpDao.deleteByItemId(item.id)
            dao.delete(item)
        }
        scope.launch { attemptDelete(item.id) }
    }

    override fun getByCategory(categoryId: Int): Flow<List<ItemEntity>> =
        dao.getByCategory(categoryId)

    override fun getByLocation(locationId: Int): Flow<List<ItemEntity>> =
        dao.getByLocation(locationId)

    override fun getExpiringSoon(daysUntil: Int): Flow<List<ItemEntity>> =
        dao.getExpiringSoonByCutoff(LocalDate.now().plusDays(daysUntil.toLong()))

    override suspend fun countByCategoryId(categoryId: Int): Int =
        withContext(Dispatchers.IO) { dao.countByCategoryId(categoryId) }

    override suspend fun reassignCategory(fromId: Int, toId: Int) =
        withContext(Dispatchers.IO) { dao.updateCategoryId(fromId, toId) }

    override suspend fun countByLocationId(locationId: Int): Int =
        withContext(Dispatchers.IO) { dao.countByLocationId(locationId) }

    override suspend fun reassignLocation(fromId: Int, toId: Int) =
        withContext(Dispatchers.IO) { dao.updateLocationId(fromId, toId) }

    override suspend fun getLastUsedLocationId(): Int? =
        withContext(Dispatchers.IO) { dao.getLastUsedLocationId() }

    private suspend fun attemptPatch(itemId: String, item: ItemDto) {
        val token = settingsRepository.getValue(SettingsKeys.AUTH_ACCESS_TOKEN)
        if (token.isNullOrBlank()) return
        val result = syncService.patchItem(itemId, item)
        val error = result.exceptionOrNull()
        if (error is SyncError.Timeout || error is SyncError.ConnectionError || error is SyncError.Unknown) {
            pendingSyncOpDao.insert(
                PendingSyncOpEntity(
                    id = UUID.randomUUID().toString(),
                    itemId = itemId,
                    operation = "PATCH",
                    payload = Json.encodeToString(ItemDto.serializer(), item),
                    createdAt = System.currentTimeMillis()
                )
            )
        }
    }

    private suspend fun attemptDelete(itemId: String) {
        val token = settingsRepository.getValue(SettingsKeys.AUTH_ACCESS_TOKEN)
        if (token.isNullOrBlank()) return
        val result = syncService.deleteItem(itemId)
        val error = result.exceptionOrNull()
        if (error is SyncError.Timeout || error is SyncError.ConnectionError || error is SyncError.Unknown) {
            pendingSyncOpDao.insert(
                PendingSyncOpEntity(
                    id = UUID.randomUUID().toString(),
                    itemId = itemId,
                    operation = "DELETE",
                    payload = "",
                    createdAt = System.currentTimeMillis()
                )
            )
        }
    }

    private suspend fun drainQueue() {
        val ops = pendingSyncOpDao.getAll()
        for (op in ops) {
            val result = when (op.operation) {
                "PATCH" -> {
                    val item = runCatching {
                        Json.decodeFromString(ItemDto.serializer(), op.payload)
                    }.getOrNull()
                    if (item == null) {
                        pendingSyncOpDao.deleteById(op.id)
                        continue
                    }
                    syncService.patchItem(op.itemId, item)
                }
                "DELETE" -> syncService.deleteItem(op.itemId)
                else -> Result.failure(IllegalStateException("Unknown operation: ${op.operation}"))
            }
            if (result.isSuccess) {
                pendingSyncOpDao.deleteById(op.id)
            }
        }
    }

    private fun ItemEntity.toDto() = ItemDto(
        id = id,
        name = name,
        categoryId = categoryId,
        quantity = quantity,
        unit = unit,
        unitPrice = unitPrice,
        kcalPerKg = kcalPerKg,
        expiryDate = expiryDate?.toString(),
        locationId = locationId,
        notes = notes,
        lastUpdated = lastUpdated
    )
}

