package com.doctate.watch.testfixtures

import com.doctate.watch.domain.CaseEntry
import com.doctate.watch.domain.CaseId
import com.doctate.watch.domain.CaseStore
import com.doctate.watch.domain.OnelinerState
import com.doctate.watch.domain.OnelinersResponse
import com.doctate.watch.domain.isManual
import java.time.Instant
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

/**
 * Thread-safe in-memory [CaseStore] for tests that need a store but not its
 * disk-IO. Mirrors the behaviour of `DiskCaseStore` (sort order, manual-stick
 * rule, reconcile window semantics) so contract tests can swap them.
 */
class InMemoryCaseStore : CaseStore {
    private data class Entry(
        val caseId: String,
        val createdAt: Long,
        val lastActivityAt: Long,
        val syncedToServer: Boolean,
        val oneliner: OnelinerState?,
    )

    private val state = LinkedHashMap<String, Entry>()
    private val mutex = Mutex()
    private val _flow = MutableStateFlow<List<CaseEntry>>(emptyList())

    override val casesFlow: StateFlow<List<CaseEntry>> = _flow.asStateFlow()

    override suspend fun createLocal(now: Long): String {
        val id = CaseId.new()
        mutex.withLock {
            state[id] = Entry(id, now, now, syncedToServer = false, oneliner = null)
            broadcast()
        }
        return id
    }

    override suspend fun markActivity(caseId: String, now: Long) {
        mutex.withLock {
            val existing = state[caseId]
            state[caseId] = existing?.copy(lastActivityAt = now)
                ?: Entry(caseId, now, now, syncedToServer = false, oneliner = null)
            broadcast()
        }
    }

    override suspend fun markSynced(caseId: String) {
        mutex.withLock {
            val existing = state[caseId] ?: return
            if (existing.syncedToServer) return
            state[caseId] = existing.copy(syncedToServer = true)
            broadcast()
        }
    }

    override suspend fun setOnelinerLocal(caseId: String, state: OnelinerState) {
        mutex.withLock {
            val existing = this.state[caseId] ?: return
            this.state[caseId] = existing.copy(oneliner = state)
            broadcast()
        }
    }

    override suspend fun mergeServerSnapshot(response: OnelinersResponse) {
        mutex.withLock {
            for (entry in response.oneliners) {
                val serverActivityMs = (entry.lastRecordingAt ?: entry.updatedAt ?: entry.createdAt)
                    .toEpochMs()
                val existing = state[entry.caseId]
                state[entry.caseId] = if (existing != null) {
                    val keepLocalManual = existing.oneliner.isManual() &&
                        entry.oneliner !is OnelinerState.Manual
                    existing.copy(
                        syncedToServer = true,
                        oneliner = if (keepLocalManual) existing.oneliner else entry.oneliner,
                        lastActivityAt = maxOf(existing.lastActivityAt, serverActivityMs),
                    )
                } else {
                    Entry(
                        caseId = entry.caseId,
                        createdAt = entry.createdAt.toEpochMs(),
                        lastActivityAt = serverActivityMs,
                        syncedToServer = true,
                        oneliner = entry.oneliner,
                    )
                }
            }
            broadcast()
        }
    }

    override suspend fun reconcileWithServerSnapshot(response: OnelinersResponse): Int {
        val asOfMs = response.asOf.toEpochMs()
        val windowStartMs = asOfMs - response.windowHours * 3_600_000L
        val serverIds = response.oneliners.map { it.caseId }.toHashSet()
        var removed = 0
        mutex.withLock {
            val toRemove = state.values.filter { e ->
                e.syncedToServer &&
                    e.lastActivityAt >= windowStartMs &&
                    e.caseId !in serverIds
            }.map { it.caseId }
            for (id in toRemove) {
                state.remove(id)
                removed++
            }
            if (removed > 0) broadcast()
        }
        return removed
    }

    override suspend fun cleanupStale(cutoffMs: Long): Int {
        var removed = 0
        mutex.withLock {
            val toRemove = state.values.filter { it.syncedToServer && it.lastActivityAt < cutoffMs }
                .map { it.caseId }
            for (id in toRemove) {
                state.remove(id)
                removed++
            }
            if (removed > 0) broadcast()
        }
        return removed
    }

    override suspend fun clearAll() {
        mutex.withLock {
            state.clear()
            broadcast()
        }
    }

    private fun broadcast() {
        _flow.value = state.values
            .sortedByDescending { it.lastActivityAt }
            .map { CaseEntry(it.caseId, it.createdAt, it.lastActivityAt, it.oneliner, it.syncedToServer) }
    }

    private fun String.toEpochMs(): Long = Instant.parse(this).toEpochMilli()
}
