package com.fabledsword.minstrel.diagnostics

import android.os.Build
import com.fabledsword.minstrel.BuildConfig
import com.fabledsword.minstrel.api.endpoints.DiagnosticEventWire
import com.fabledsword.minstrel.api.endpoints.DiagnosticsApi
import com.fabledsword.minstrel.api.endpoints.DiagnosticsReportRequest
import com.fabledsword.minstrel.auth.AuthStore
import com.fabledsword.minstrel.cache.db.dao.DiagnosticEventDao
import com.fabledsword.minstrel.cache.db.entities.DiagnosticEventEntity
import com.fabledsword.minstrel.connectivity.NetworkStatusController
import com.fabledsword.minstrel.connectivity.ServerHealth
import com.fabledsword.minstrel.di.ApplicationScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.serialization.json.Json
import retrofit2.Retrofit
import retrofit2.create
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Drains the [DiagnosticEventDao] ring buffer to POST /api/diagnostics
 * (M9). Triggers mirror [com.fabledsword.minstrel.cache.mutations.MutationReplayer]:
 * a periodic tick, a sign-in (sessionCookie non-null), and a server-health
 * recovery to [ServerHealth.Healthy] — so a dead-zone's buffered events
 * upload the moment the link comes back.
 *
 * Deliberately NOT the MutationQueue: diagnostics are high-volume,
 * best-effort telemetry. A failed upload just retries on the next tick;
 * the server no-ops (204) when the account's debug flag is off, which the
 * uploader treats as success so the buffer drains either way.
 */
@Singleton
class DiagnosticsUploader @Inject constructor(
    private val dao: DiagnosticEventDao,
    private val authStore: AuthStore,
    private val json: Json,
    @ApplicationScope private val scope: CoroutineScope,
    networkStatus: NetworkStatusController,
    retrofit: Retrofit,
) {
    private val api: DiagnosticsApi = retrofit.create()
    private val mutex = Mutex()

    init {
        scope.launch {
            while (true) {
                delay(UPLOAD_INTERVAL_MS)
                drainSafe()
            }
        }
        scope.launch {
            authStore.sessionCookie.filterNotNull().distinctUntilChanged().collect { drainSafe() }
        }
        scope.launch {
            networkStatus.state
                .map { it == ServerHealth.Healthy }
                .distinctUntilChanged()
                .filter { it }
                .collect { drainSafe() }
        }
    }

    /** Coalesces concurrent triggers via tryLock — a drain in flight wins. */
    private suspend fun drainSafe() {
        val clientId = authStore.clientId.value
        // Signed out / no client id yet → nothing to do. Single guard keeps
        // the return count within the detekt gate.
        if (authStore.sessionCookie.value.isNullOrEmpty() || clientId == null) return
        if (!mutex.tryLock()) return
        try {
            drain(clientId)
        } finally {
            mutex.unlock()
        }
    }

    private suspend fun drain(clientId: String) {
        var more = true
        while (more) {
            val batch = runCatching { dao.takeBatch(BATCH_SIZE) }.getOrNull().orEmpty()
            val sent = batch.isNotEmpty() && runCatching { upload(clientId, batch) }.isSuccess
            if (sent) runCatching { dao.deleteByIds(batch.map { it.id }) }
            // Keep going only while a full batch sent cleanly; otherwise stop
            // (empty buffer, or a failure to retry on the next trigger).
            more = sent && batch.size >= BATCH_SIZE
        }
    }

    private suspend fun upload(clientId: String, batch: List<DiagnosticEventEntity>) {
        api.report(
            DiagnosticsReportRequest(
                clientId = clientId,
                appVersion = BuildConfig.VERSION_NAME,
                osVersion = "Android ${Build.VERSION.RELEASE} (${Build.MODEL})",
                events = batch.map { row ->
                    DiagnosticEventWire(
                        kind = row.kind,
                        occurredAt = row.occurredAtMillis,
                        payload = json.parseToJsonElement(row.payloadJson),
                    )
                },
            ),
        )
    }

    private companion object {
        const val UPLOAD_INTERVAL_MS = 60_000L
        const val BATCH_SIZE = 100
    }
}
