package pl.firmatpp.kierowca.data.sync

import android.content.Context
import com.google.gson.Gson
import java.io.IOException
import java.time.LocalDate
import pl.firmatpp.kierowca.data.DriverRepository
import pl.firmatpp.kierowca.data.model.BootstrapResponse
import pl.firmatpp.kierowca.data.model.DriverRouteDto
import pl.firmatpp.kierowca.data.model.RouteResponse
import pl.firmatpp.kierowca.data.model.SyncScopeDto
import pl.firmatpp.kierowca.data.model.SyncStateResponse
import pl.firmatpp.kierowca.data.upload.DriverDatabase
import retrofit2.HttpException

class DriverSyncRepository(
    context: Context,
    private val repository: DriverRepository = DriverRepository(context),
    private val dao: DriverCacheDao = DriverDatabase.get(context).driverCacheDao(),
    private val gson: Gson = Gson(),
) {
    suspend fun bootstrap(date: String?): CachedValue<BootstrapResponse> {
        val requestedDate = date ?: currentDateFallback()

        return runCatching {
            val response = repository.bootstrap(date)
            val selectedDate = response.driverAppSettings?.selectedDate ?: requestedDate
            val routeState = response.syncState?.scopes?.firstOrNull { it.scope == SCOPE_ROUTES }
            dao.upsertBootstrap(
                DriverBootstrapCacheEntity(
                    date = selectedDate,
                    payloadJson = gson.toJson(response),
                    checksum = routeState?.checksum,
                    version = routeState?.version,
                    syncedAtEpochMillis = System.currentTimeMillis(),
                ),
            )
            saveSyncStates(response.syncState)
            CachedValue(response, stale = false, syncedAtEpochMillis = System.currentTimeMillis())
        }.getOrElse { throwable ->
            cachedBootstrap(requestedDate, throwable)
        }
    }

    suspend fun route(routeId: String): CachedValue<RouteResponse> =
        runCatching {
            val response = repository.route(routeId)
            val routeState = response.syncState?.scopes?.firstOrNull { it.scope == SCOPE_ROUTE_DETAIL }
            dao.upsertRoute(
                DriverRouteCacheEntity(
                    routeId = routeId,
                    payloadJson = gson.toJson(response),
                    checksum = routeState?.checksum,
                    version = routeState?.version,
                    syncedAtEpochMillis = System.currentTimeMillis(),
                ),
            )
            saveSyncStates(response.syncState)
            CachedValue(response, stale = false, syncedAtEpochMillis = System.currentTimeMillis())
        }.getOrElse { throwable ->
            cachedRoute(routeId, throwable)
        }

    suspend fun bootstrapFromCache(date: String?): CachedValue<BootstrapResponse>? {
        val cached = dao.bootstrap(date ?: currentDateFallback()) ?: return null
        return CachedValue(
            value = gson.fromJson(cached.payloadJson, BootstrapResponse::class.java),
            stale = true,
            syncedAtEpochMillis = cached.syncedAtEpochMillis,
        )
    }

    suspend fun hasBootstrapCache(date: String?): Boolean =
        dao.bootstrap(date ?: currentDateFallback()) != null

    suspend fun routeFromCache(routeId: String): CachedValue<RouteResponse>? {
        val cached = dao.route(routeId) ?: return null
        return CachedValue(
            value = gson.fromJson(cached.payloadJson, RouteResponse::class.java),
            stale = true,
            syncedAtEpochMillis = cached.syncedAtEpochMillis,
        )
    }

    suspend fun cacheConfirmedRoute(route: DriverRouteDto, date: String?) {
        val syncedAt = System.currentTimeMillis()
        dao.upsertRoute(
            DriverRouteCacheEntity(
                routeId = route.id,
                payloadJson = gson.toJson(RouteResponse(route = route)),
                checksum = null,
                version = null,
                syncedAtEpochMillis = syncedAt,
            ),
        )

        (routeVisibleDates(route) + listOfNotNull(date)).distinct().forEach { selectedDate ->
            val cachedBootstrap = dao.bootstrap(selectedDate) ?: return@forEach
            val response = gson.fromJson(cachedBootstrap.payloadJson, BootstrapResponse::class.java)
            val updatedRoutes = response.routes.today.map { item -> if (item.id == route.id) route else item }
            dao.upsertBootstrap(
                cachedBootstrap.copy(
                    payloadJson = gson.toJson(response.copy(routes = response.routes.copy(today = updatedRoutes))),
                    checksum = null,
                    version = null,
                    syncedAtEpochMillis = syncedAt,
                ),
            )
        }
    }

    suspend fun fetchSyncState(date: String?, routeId: String?): SyncStateResponse =
        repository.syncState(date, routeId)

    suspend fun shouldRefresh(scope: SyncScopeDto): Boolean {
        val local = dao.syncState(scope.scope, scope.date, scope.routeId)
        return local == null || local.checksum != scope.checksum || local.version < scope.version
    }

    suspend fun saveSyncStates(response: SyncStateResponse?) {
        response?.scopes.orEmpty().forEach { scope ->
            saveSyncScope(scope)
        }
    }

    suspend fun saveSyncScope(scope: SyncScopeDto) {
        dao.upsertSyncState(
            DriverSyncStateEntity(
                key = syncStateKey(scope.scope, scope.date, scope.routeId),
                scope = scope.scope,
                date = scope.date,
                routeId = scope.routeId,
                checksum = scope.checksum,
                version = scope.version,
                computedAt = scope.computedAt,
                syncedAtEpochMillis = System.currentTimeMillis(),
            ),
        )
    }

    suspend fun clearCache() {
        dao.clearBootstrap()
        dao.clearRoutes()
        dao.clearSyncStates()
    }

    private suspend fun cachedBootstrap(date: String, throwable: Throwable): CachedValue<BootstrapResponse> {
        val cached = dao.bootstrap(date)
        if (cached != null && throwable.isReadFallbackAllowed()) {
            return CachedValue(
                gson.fromJson(cached.payloadJson, BootstrapResponse::class.java),
                stale = true,
                syncedAtEpochMillis = cached.syncedAtEpochMillis,
            )
        }
        throw throwable
    }

    private suspend fun cachedRoute(routeId: String, throwable: Throwable): CachedValue<RouteResponse> {
        val cached = dao.route(routeId)
        if (cached != null && throwable.isReadFallbackAllowed()) {
            return CachedValue(
                gson.fromJson(cached.payloadJson, RouteResponse::class.java),
                stale = true,
                syncedAtEpochMillis = cached.syncedAtEpochMillis,
            )
        }
        throw throwable
    }

    private fun Throwable.isReadFallbackAllowed(): Boolean =
        this is IOException || (this is HttpException && code() in 500..599)

    private fun currentDateFallback(): String = java.time.LocalDate.now().toString()

    private fun routeVisibleDates(route: DriverRouteDto): List<String> {
        val start = route.routeDate?.let { runCatching { LocalDate.parse(it) }.getOrNull() } ?: return emptyList()
        val end = route.unloadingDate
            ?.let { runCatching { LocalDate.parse(it) }.getOrNull() }
            ?.takeUnless { it.isBefore(start) }
            ?: start

        return generateSequence(start) { date -> date.plusDays(1).takeIf { !it.isAfter(end) } }
            .map(LocalDate::toString)
            .toList()
    }

    companion object {
        const val SCOPE_ROUTES = "routes"
        const val SCOPE_ROUTE_DETAIL = "route_detail"
        const val SCOPE_SETTINGS = "settings"
        const val SCOPE_DISPATCH_SHEET = "dispatch_sheet"
        const val SCOPE_LEAVE_REQUESTS = "leave_requests"
        const val SCOPE_LEAVE_CALENDAR = "leave_calendar"
        const val SCOPE_SESSION = "session"
    }
}
