package com.correx.infrastructure.persistence

import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.serialization.JsonEventSerializer
import com.correx.core.events.serialization.eventJson
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.CausationId
import com.correx.core.events.types.CorrelationId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.infrastructure.persistence.util.JDBCHelper.transaction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.datetime.Instant
import org.slf4j.LoggerFactory
import java.sql.Connection
import java.sql.ResultSet
import java.util.concurrent.*

private val log = LoggerFactory.getLogger(SqliteEventStore::class.java)

class SqliteEventStore(
    private val connection: Connection,
    private val jsonSerializer: JsonEventSerializer = JsonEventSerializer(eventJson),
    private val artifactStore: ArtifactStore,
) : EventStore {
    private val appendMutex = Mutex()
    private val subscriptions = ConcurrentHashMap<SessionId, MutableSharedFlow<StoredEvent>>()
    private val globalFlow = MutableSharedFlow<StoredEvent>(
        replay = 0,
        extraBufferCapacity = 1024,
        onBufferOverflow = BufferOverflow.SUSPEND,
    )

    init {
        connection.createStatement().use { stmt ->
            stmt.execute("PRAGMA journal_mode=WAL;")
            stmt.execute("PRAGMA busy_timeout=5000;")
            stmt.execute("PRAGMA synchronous=NORMAL;")
            stmt.execute(
                """
                CREATE TABLE IF NOT EXISTS events (
                    event_id TEXT PRIMARY KEY,
                    session_id TEXT NOT NULL,
                    sequence INTEGER NOT NULL,
                    session_sequence INTEGER NOT NULL,
                    timestamp TEXT NOT NULL,
                    schema_version INTEGER NOT NULL,
                    causation_id TEXT,
                    correlation_id TEXT,
                    payload TEXT NOT NULL
                );
                """.trimIndent(),
            )
            stmt.execute(
                "CREATE UNIQUE INDEX IF NOT EXISTS events_sequence_uq ON events(sequence);"
            )
            stmt.execute(
                "CREATE UNIQUE INDEX IF NOT EXISTS events_session_seq_uq ON events(session_id, session_sequence);"
            )
        }
    }

    override suspend fun append(event: NewEvent): StoredEvent {
        var stored: StoredEvent? = null
        appendMutex.withLock {
            artifactStore.flushBefore {
                withContext(Dispatchers.IO) {
                    stored = withConnection {
                        connection.transaction {
                            val existing = findByEventId(event.metadata.eventId)
                            check(existing == null) { "duplicate event_id: ${event.metadata.eventId}" }
                            val (globalSeqVal, sessionSeqVal) = insertAssigningSequences(event)
                            StoredEvent(
                                metadata = event.metadata,
                                sequence = globalSeqVal,
                                sessionSequence = sessionSeqVal,
                                payload = event.payload,
                            )
                        }
                    }
                }
            }
        }
        val result = checkNotNull(stored)
        subscriptions[event.metadata.sessionId]?.let { warnIfDropped(it.tryEmit(result), result) }
        globalFlow.emit(result)
        return result
    }

    override suspend fun appendAll(events: List<NewEvent>): List<StoredEvent> {
        if (events.isEmpty()) return emptyList()
        val sessionId = events.first().metadata.sessionId
        var stored: List<StoredEvent> = emptyList()
        appendMutex.withLock {
            artifactStore.flushBefore {
                withContext(Dispatchers.IO) {
                    stored = withConnection {
                        connection.transaction {
                            events.map { event ->
                                val existing = findByEventId(event.metadata.eventId)
                                check(existing == null) { "duplicate event_id: ${event.metadata.eventId}" }
                                val (globalSeqVal, sessionSeqVal) = insertAssigningSequences(event)
                                StoredEvent(
                                    metadata = event.metadata,
                                    sequence = globalSeqVal,
                                    sessionSequence = sessionSeqVal,
                                    payload = event.payload,
                                )
                            }
                        }
                    }
                }
            }
        }
        val flow = subscriptions[sessionId]
        if (flow != null) stored.forEach { warnIfDropped(flow.tryEmit(it), it) }
        stored.forEach { globalFlow.emit(it) }
        return stored
    }

    override fun read(sessionId: SessionId): List<StoredEvent> = withConnection {
        connection.prepareStatement(
            """
            SELECT * FROM events
            WHERE session_id = ?
            ORDER BY session_sequence ASC
            """.trimIndent(),
        ).use { ps ->
            ps.setString(1, sessionId.value)
            ps.executeQuery().use { rs ->
                buildList {
                    while (rs.next()) {
                        add(rs.toStoredEvent(jsonSerializer))
                    }
                }
            }
        }
    }

    override fun readFrom(sessionId: SessionId, fromSequence: Long): List<StoredEvent> = withConnection {
        connection.prepareStatement(
            """
            SELECT * FROM events
            WHERE session_id = ?
              AND session_sequence > ?
            ORDER BY session_sequence ASC
            """.trimIndent(),
        ).use { ps ->
            ps.setString(1, sessionId.value)
            ps.setLong(2, fromSequence)
            ps.executeQuery().use { rs ->
                buildList {
                    while (rs.next()) {
                        add(rs.toStoredEvent(jsonSerializer))
                    }
                }
            }
        }
    }

    override fun lastSequence(sessionId: SessionId): Long? = withConnection {
        connection.prepareStatement(
            "SELECT MAX(session_sequence) FROM events WHERE session_id = ?"
        ).use { ps ->
            ps.setString(1, sessionId.value)
            ps.executeQuery().use { rs ->
                if (rs.next()) rs.getLong(1).takeIf { !rs.wasNull() }
                else null
            }
        }
    }

    override fun subscribe(sessionId: SessionId): Flow<StoredEvent> =
        subscriptions.computeIfAbsent(sessionId) { MutableSharedFlow(replay = 0, extraBufferCapacity = 64) }

    override fun subscribeAll(): Flow<StoredEvent> = globalFlow.asSharedFlow()

    override suspend fun lastGlobalSequence(): Long =
        withContext(Dispatchers.IO) {
            withConnection {
                connection.prepareStatement(
                    "SELECT COALESCE(MAX(sequence), 0) FROM events"
                ).use { ps ->
                    ps.executeQuery().use { rs ->
                        rs.next()
                        rs.getLong(1)
                    }
                }
            }
        }

    override fun allEvents(): Sequence<StoredEvent> = withConnection {
        connection.prepareStatement(
            """
            SELECT * FROM events
            ORDER BY session_id ASC, sequence ASC
            """.trimIndent(),
        ).use { ps ->
            ps.executeQuery().use { rs ->
                buildList {
                    while (rs.next()) {
                        add(rs.toStoredEvent(jsonSerializer))
                    }
                }
            }
        }.asSequence()
    }

    override fun allSessionIds(): Set<SessionId> = withConnection {
        mutableSetOf<SessionId>().apply {
            connection.prepareStatement("SELECT DISTINCT session_id FROM events").use { ps ->
                ps.executeQuery().use { rs ->
                    while (rs.next()) add(SessionId(rs.getString("session_id")))
                }
            }
        }
    }

    // ---------- helpers ----------

    /**
     * Serializes all JDBC access to the shared [connection] on a single JVM monitor. Reads run on the
     * caller thread while append's transaction runs on Dispatchers.IO; the coroutine [appendMutex] only
     * excludes append-vs-append, so without this a read could hit the connection mid-transaction
     * (SQLite JDBC is not thread-safe → corrupted tx state). ponytail: store-wide lock, one session at a
     * time is fine here; shard per-connection only if read throughput ever matters.
     */
    private inline fun <T> withConnection(block: () -> T): T = synchronized(connection) { block() }

    /**
     * A per-session subscription's buffer (extraBufferCapacity 64) is full and this event was dropped.
     * The event is durably persisted (the drop is only on the live SharedFlow), but a lagging in-process
     * collector — e.g. LiveArtifactRepository — silently diverges from the log. Surface it instead of
     * swallowing the false return. ponytail: log-only; give that collector a bounded rebuild-on-lag if
     * divergence ever bites in practice.
     */
    private fun warnIfDropped(emitted: Boolean, event: StoredEvent) {
        if (!emitted) {
            log.warn(
                "dropped live subscription emit for session {} seq {} ({}): buffer full, collector lagging",
                event.metadata.sessionId.value,
                event.sequence,
                event.payload::class.simpleName,
            )
        }
    }

    /**
     * Inserts [event], letting SQLite assign both the global `sequence` and the per-session
     * `session_sequence` as `MAX+1` subqueries evaluated inside the INSERT itself, and RETURNs the
     * assigned values. SQLite serializes writers (one write transaction at a time), so the subqueries
     * are atomic against other processes on the same DB — closing the CLI+server MAX+1 race that the
     * old read-then-insert had (the unique index on `sequence` is the backstop). Within one appendAll
     * transaction, each INSERT sees the prior rows, so a batch increments correctly.
     */
    @Suppress("MagicNumber")
    private fun insertAssigningSequences(event: NewEvent): Pair<Long, Long> =
        connection.prepareStatement(
            """
            INSERT INTO events (
                event_id,
                session_id,
                sequence,
                session_sequence,
                timestamp,
                schema_version,
                causation_id,
                correlation_id,
                payload
            ) VALUES (
                ?, ?,
                (SELECT COALESCE(MAX(sequence), 0) + 1 FROM events),
                (SELECT COALESCE(MAX(session_sequence), 0) + 1 FROM events WHERE session_id = ?),
                ?, ?, ?, ?, ?
            )
            RETURNING sequence, session_sequence
            """.trimIndent(),
        ).use { ps ->
            ps.setString(1, event.metadata.eventId.value)
            ps.setString(2, event.metadata.sessionId.value)
            ps.setString(3, event.metadata.sessionId.value)
            ps.setString(4, event.metadata.timestamp.toString())
            ps.setInt(5, event.metadata.schemaVersion)
            ps.setString(6, event.metadata.causationId?.value)
            ps.setString(7, event.metadata.correlationId?.value)
            ps.setString(8, jsonSerializer.serialize(event.payload))
            ps.executeQuery().use { rs ->
                rs.next()
                rs.getLong("sequence") to rs.getLong("session_sequence")
            }
        }

    private fun findByEventId(eventId: EventId): StoredEvent? =
        connection.prepareStatement(
            """
            SELECT * FROM events
            WHERE event_id = ?
            """.trimIndent(),
        ).use { ps ->
            ps.setString(1, eventId.value)
            ps.executeQuery().use { rs ->
                if (rs.next()) rs.toStoredEvent(jsonSerializer) else null
            }
        }
}

private fun ResultSet.toStoredEvent(jsonSerializer: JsonEventSerializer): StoredEvent =
    StoredEvent(
        metadata = EventMetadata(
            eventId = EventId(getString("event_id")),
            sessionId = SessionId(getString("session_id")),
            timestamp = Instant.parse(getString("timestamp")),
            schemaVersion = getInt("schema_version"),
            causationId = getString("causation_id")?.let { CausationId(it) },
            correlationId = getString("correlation_id")?.let { CorrelationId(it) },
        ),
        sequence = getLong("sequence"),
        sessionSequence = getLong("session_sequence"),
        payload = jsonSerializer.deserialize(getString("payload")),
    )
