package lu.rewardflow.terminal.data.repository

import android.content.Context
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.squareup.moshi.Moshi
import dagger.hilt.android.qualifiers.ApplicationContext
import lu.rewardflow.terminal.data.db.dao.PendingTransactionDao
import lu.rewardflow.terminal.data.db.entity.PendingTransaction
import lu.rewardflow.terminal.data.model.EnrollRequest
import lu.rewardflow.terminal.data.model.PointsEarnRequest
import lu.rewardflow.terminal.data.model.StampRequest
import lu.rewardflow.terminal.data.sync.SyncWorker
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Single entrypoint for "queue an action that failed offline".
 *
 * The terminal screen calls one of [queueStamp] / [queuePointsEarn] /
 * [queueEnroll] after detecting an [java.io.IOException] mid-action.
 * The body is serialized to JSON, persisted via [PendingTransactionDao],
 * and a [SyncWorker] is enqueued under a network constraint so the
 * system runs it as soon as connectivity returns.
 *
 * Redemption flows are NOT queued here — the plan calls for them to
 * stay online-only since balances at redeem time can't be retro'd.
 */
@Singleton
class QueueRepository @Inject constructor(
    @ApplicationContext private val context: Context,
    private val dao: PendingTransactionDao,
    moshi: Moshi,
) {
    private val stampAdapter = moshi.adapter(StampRequest::class.java)
    private val earnAdapter = moshi.adapter(PointsEarnRequest::class.java)
    private val enrollAdapter = moshi.adapter(EnrollRequest::class.java)

    suspend fun queueStamp(request: StampRequest, staffPinId: Int) {
        dao.insert(
            PendingTransaction(
                type = SyncWorker.TYPE_STAMP,
                requestJson = stampAdapter.toJson(request),
                staffPinId = staffPinId,
            )
        )
        scheduleSync()
    }

    suspend fun queuePointsEarn(request: PointsEarnRequest, staffPinId: Int) {
        dao.insert(
            PendingTransaction(
                type = SyncWorker.TYPE_POINTS_EARN,
                requestJson = earnAdapter.toJson(request),
                staffPinId = staffPinId,
            )
        )
        scheduleSync()
    }

    suspend fun queueEnroll(request: EnrollRequest, staffPinId: Int) {
        dao.insert(
            PendingTransaction(
                type = SyncWorker.TYPE_ENROLL,
                requestJson = enrollAdapter.toJson(request),
                staffPinId = staffPinId,
            )
        )
        scheduleSync()
    }

    /** Kick the sync worker. Idempotent — KEEP policy means a queued or
     *  running request isn't displaced. The system runs us when the
     *  network constraint is satisfied. */
    fun scheduleSync() {
        val constraints = Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()

        val request = OneTimeWorkRequestBuilder<SyncWorker>()
            .setConstraints(constraints)
            .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
            .build()

        WorkManager.getInstance(context).enqueueUniqueWork(
            UNIQUE_SYNC_WORK_NAME,
            ExistingWorkPolicy.KEEP,
            request,
        )
    }

    companion object {
        const val UNIQUE_SYNC_WORK_NAME = "loyalty_sync"
    }
}
