package pl.firmatpp.kierowca.data.upload

import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import java.util.UUID
import kotlinx.coroutines.delay
import pl.firmatpp.kierowca.data.ApiErrorKind
import pl.firmatpp.kierowca.data.ApiErrorMapper
import pl.firmatpp.kierowca.data.DriverRepository
import pl.firmatpp.kierowca.data.rethrowIfCancellation
import pl.firmatpp.kierowca.data.model.RoutePointBatchBody
import pl.firmatpp.kierowca.data.model.RoutePointDto
import pl.firmatpp.kierowca.diagnostics.AppDiagnostics
import pl.firmatpp.kierowca.data.sync.NetworkMonitor
import pl.firmatpp.kierowca.tracking.ActiveRouteTrackingService

class RoutePointWorker(
    appContext: Context,
    params: WorkerParameters,
) : CoroutineWorker(appContext, params) {
    private val dao = DriverDatabase.get(appContext).routePointDao()
    private val repository = DriverRepository(appContext)

    override suspend fun doWork(): Result {
        if (!NetworkMonitor(applicationContext).isCurrentlyValidated()) return Result.retry()
        val routeId = inputData.getString(KEY_ROUTE_ID) ?: return Result.failure()
        while (!isStopped) {
            val points = dao.pendingForRoute(routeId, BATCH_LIMIT)
            if (points.isEmpty()) return Result.success()

            val pointIds = points.map { it.clientPointId }
            dao.updateStatus(pointIds, RoutePointStatus.Syncing, null, attemptIncrement = 1)

            val outcome = runCatching {
                repository.storeRoutePoints(
                    routeId = routeId,
                    body = RoutePointBatchBody(
                        clientBatchId = UUID.randomUUID().toString(),
                        points = points.map { it.toDto() },
                    ),
                )
            }
            val response = outcome.getOrNull()
            if (response != null) {
                dao.delete(pointIds)
                if (response.arrived) ActiveRouteTrackingService.markArrived(applicationContext, routeId)
                if (dao.pendingCount(routeId) == 0) {
                    delay(750L)
                    if (dao.pendingCount(routeId) == 0) return Result.success()
                }
                continue
            }

            val throwable = outcome.exceptionOrNull() ?: return Result.retry()
            throwable.rethrowIfCancellation()
            val error = ApiErrorMapper.map(throwable)
            AppDiagnostics.reportNonFatal(
                throwable = throwable,
                operation = "route_point_worker",
                keys = mapOf(
                    "route_id" to routeId,
                    "batch_size" to points.size,
                    "api_error_kind" to error.kind.name,
                    "api_error_code" to error.code,
                    "api_status_code" to error.statusCode,
                    "retryable" to error.retryable,
                ),
            )
            if (shouldStopRouteTrackingAfterPointError(error.kind, error.code)) {
                ActiveRouteTrackingService.stop(applicationContext, routeId)
            }
            if (isInactiveRoutePointError(error.code)) {
                dao.delete(pointIds)
                continue
            }

            val status = if (error.retryable) RoutePointStatus.FailedRetryable else RoutePointStatus.FailedPermanent
            dao.updateStatus(pointIds, status, error.message, attemptIncrement = 0)
            return if (error.retryable && error.kind != ApiErrorKind.Auth) Result.retry() else Result.failure()
        }

        return Result.retry()
    }

    private fun RoutePointEntity.toDto(): RoutePointDto =
        RoutePointDto(
            clientPointId = clientPointId,
            trip_id = routeId,
            driver_id = driverId,
            lat = lat,
            lon = lon,
            accuracy = accuracy,
            speed = speed,
            bearing = bearing,
            timestamp = timestamp,
            battery = battery,
            charging = charging,
        )

    companion object {
        const val KEY_ROUTE_ID = "routeId"
        private const val BATCH_LIMIT = 100

        fun uniqueWorkName(routeId: String): String = "route-point-sync-$routeId"
    }
}

internal fun isInactiveRoutePointError(code: String?): Boolean = code == "ROUTE_POINTS_INVALID_STATUS"

internal fun shouldStopRouteTrackingAfterPointError(kind: ApiErrorKind, code: String?): Boolean =
    kind == ApiErrorKind.Auth || isInactiveRoutePointError(code)
