package com.doctate.watch.domain

import android.util.Log
import java.io.File
import java.nio.file.Files
import java.nio.file.StandardCopyOption
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

/**
 * Disk-backed [CaseStore]. One JSON file per case under [dir]; in-memory
 * map serialised by [mutex] so concurrent merges from the sync worker and
 * the UI can't collide on a write.
 *
 * Atomic writes via temp-file + `Files.move(... ATOMIC_MOVE, REPLACE_EXISTING)`
 * — same trick as `case_store.rs::write_marker_file`. Readers either see
 * the previous version or the new one, never a partial blob.
 *
 * Bootstrap is synchronous (constructor body): the watch can render the
 * cached list before any coroutine has run. Empty/missing dir is fine —
 * the store starts empty and creates the dir lazily on first write.
 */
class DiskCaseStore(private val dir: File) : CaseStore {

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

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

    init {
        bootstrap()
    }

    private fun bootstrap() {
        if (!dir.exists()) {
            dir.mkdirs()
            return
        }
        val files = dir.listFiles { f -> f.isFile && f.name.endsWith(".json") } ?: return
        for (f in files) {
            try {
                val marker = CaseMarker.fromJson(f.readText(Charsets.UTF_8))
                state[marker.caseId] = marker
            } catch (e: Exception) {
                // A single unreadable file shouldn't poison the store.
                Log.w(TAG, "skipping unreadable marker ${f.name}: ${e.message}")
            }
        }
        broadcast()
    }

    override suspend fun createLocal(now: Long): String {
        val id = CaseId.new()
        val nowRfc = epochMsToRfc(now)
        val marker = CaseMarker(
            caseId = id,
            createdAtRfc = nowRfc,
            lastActivityAtRfc = nowRfc,
            syncedToServer = false,
            oneliner = null,
        )
        mutex.withLock {
            writeMarkerFile(marker)
            state[id] = marker
            broadcast()
        }
        Log.i(TAG, "createLocal id=${id.take(8)}")
        return id
    }

    override suspend fun markActivity(caseId: String, now: Long) {
        val nowRfc = epochMsToRfc(now)
        mutex.withLock {
            val existing = state[caseId]
            val updated = existing?.copy(lastActivityAtRfc = nowRfc)
                ?: CaseMarker(
                    caseId = caseId,
                    createdAtRfc = nowRfc,
                    lastActivityAtRfc = nowRfc,
                    syncedToServer = false,
                    oneliner = null,
                )
            writeMarkerFile(updated)
            state[caseId] = updated
            broadcast()
        }
    }

    override suspend fun markSynced(caseId: String) {
        mutex.withLock {
            val existing = state[caseId] ?: return
            if (existing.syncedToServer) return
            val updated = existing.copy(syncedToServer = true)
            writeMarkerFile(updated)
            state[caseId] = updated
            broadcast()
        }
        Log.i(TAG, "markSynced id=${caseId.take(8)}")
    }

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

    override suspend fun mergeServerSnapshot(response: OnelinersResponse) {
        mutex.withLock {
            for (entry in response.oneliners) {
                val serverActivityRfc = entry.lastRecordingAt
                    ?: entry.updatedAt
                    ?: entry.createdAt

                val existing = state[entry.caseId]
                val updated = if (existing != null) {
                    // A locally-set Manual oneliner sticks against non-Manual server overrides.
                    val keepLocalManual = existing.oneliner is OnelinerState.Manual &&
                        entry.oneliner !is OnelinerState.Manual
                    existing.copy(
                        syncedToServer = true,
                        oneliner = if (keepLocalManual) existing.oneliner else entry.oneliner,
                        lastActivityAtRfc = if (serverActivityRfc > existing.lastActivityAtRfc) {
                            serverActivityRfc
                        } else existing.lastActivityAtRfc,
                    )
                } else {
                    CaseMarker(
                        caseId = entry.caseId,
                        createdAtRfc = entry.createdAt,
                        lastActivityAtRfc = serverActivityRfc,
                        syncedToServer = true,
                        oneliner = entry.oneliner,
                    )
                }
                writeMarkerFile(updated)
                state[entry.caseId] = updated
            }
            broadcast()
        }
    }

    override suspend fun reconcileWithServerSnapshot(response: OnelinersResponse): Int {
        // Window anchor: as_of - window_hours, lexicographically comparable to RFC3339.
        val asOfMs = Instant.parse(response.asOf).toEpochMilli()
        val windowStartMs = asOfMs - response.windowHours * 3_600_000L
        val windowStartRfc = epochMsToRfc(windowStartMs)
        val serverIds = response.oneliners.map { it.caseId }.toHashSet()

        var removed = 0
        mutex.withLock {
            val toRemove = state.values.filter { m ->
                m.syncedToServer &&
                    m.lastActivityAtRfc >= windowStartRfc &&
                    m.caseId !in serverIds
            }.map { it.caseId }
            for (id in toRemove) {
                deleteMarkerFile(id)
                state.remove(id)
                Log.i(TAG, "marker removed (reconcile: missing from server snapshot) id=${id.take(8)}")
                removed++
            }
            if (removed > 0) broadcast()
        }
        return removed
    }

    override suspend fun cleanupStale(cutoffMs: Long): Int {
        val cutoffRfc = epochMsToRfc(cutoffMs)
        var removed = 0
        mutex.withLock {
            val toRemove = state.values.filter { m ->
                m.syncedToServer && m.lastActivityAtRfc < cutoffRfc
            }.map { it.caseId }
            for (id in toRemove) {
                deleteMarkerFile(id)
                state.remove(id)
                Log.i(TAG, "stale marker removed id=${id.take(8)} cutoff=$cutoffRfc")
                removed++
            }
            if (removed > 0) broadcast()
        }
        return removed
    }

    override suspend fun clearAll() {
        mutex.withLock {
            val files = dir.listFiles { f -> f.name.endsWith(".json") } ?: emptyArray()
            for (f in files) {
                if (!f.delete()) Log.w(TAG, "failed to delete marker ${f.name} during clearAll")
            }
            state.clear()
            broadcast()
        }
    }

    private fun broadcast() {
        // Sort newest-first so .firstOrNull() yields the "current case".
        val list = state.values
            .map { it.toEntry() }
            .sortedByDescending { it.lastActivityAt }
        _flow.value = list
    }

    private fun writeMarkerFile(marker: CaseMarker) {
        if (!dir.exists()) dir.mkdirs()
        val finalPath = File(dir, "${marker.caseId}.json")
        val tmpPath = File(dir, "${marker.caseId}.json.tmp")
        tmpPath.writeText(marker.toJson(), Charsets.UTF_8)
        Files.move(
            tmpPath.toPath(),
            finalPath.toPath(),
            StandardCopyOption.REPLACE_EXISTING,
            StandardCopyOption.ATOMIC_MOVE,
        )
    }

    private fun deleteMarkerFile(caseId: String) {
        val f = File(dir, "$caseId.json")
        if (f.exists() && !f.delete()) {
            Log.w(TAG, "failed to delete marker $caseId.json")
        }
    }

    companion object {
        private const val TAG = "DiskCaseStore"

        /** Epoch-ms → RFC3339 UTC string with `Z` suffix (no offset). */
        fun epochMsToRfc(ms: Long): String = Instant.ofEpochMilli(ms).toString()
    }
}
