package wang.yaojia.webterm.components

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.add
import kotlinx.serialization.json.addJsonObject
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.put
import java.util.UUID

/**
 * # QuickReplyChip (A25) — one entry of the editable quick-reply palette.
 *
 * A canned snippet the user can tap to send while a gate is held. [id] is a stable
 * identity used for edit/remove/reorder (survives a [text] change); [text] is the
 * exact string sent VERBATIM as input via `TerminalSessionController.sendInput`
 * (plan §1 quick-reply row / §5 A25). No trailing byte is added — the palette owner
 * types whatever they want submitted.
 */
public data class QuickReplyChip(
    val id: String,
    val text: String,
)

/**
 * Sensible starter palette, used the first time the store is read (no stored blob
 * yet). Ids are stable so a default chip can still be edited/removed/reordered by id.
 * Once the user mutates the palette (including deleting every chip → an empty list),
 * their choice is persisted and these defaults are never re-seeded.
 */
public object QuickReplyDefaults {
    public val chips: List<QuickReplyChip> = listOf(
        QuickReplyChip("qr-continue", "continue"),
        QuickReplyChip("qr-yes", "yes"),
        QuickReplyChip("qr-no", "no"),
        QuickReplyChip("qr-1", "1"),
        QuickReplyChip("qr-2", "2"),
    )
}

/**
 * Frozen CRUD contract for the editable quick-reply palette (plan §5 A25). Every
 * mutation returns the NEW list (immutable style, mirrors [HostStore]
 * [wang.yaojia.webterm.hostregistry.HostStore]); nothing is mutated in place.
 * Cross-device sync is explicitly OUT of scope — each device's store is independent
 * (plan §1 "Cross-device palette sync" DEFERRED).
 *
 * Implementations: [DataStoreQuickReplyStore] (real, Preferences-DataStore-backed)
 * and [InMemoryQuickReplyStore] (in-`main` double for JVM tests / previews).
 */
public interface QuickReplyStore {
    /** The palette in display order (defaults on first read; the user's list thereafter). */
    public suspend fun loadAll(): List<QuickReplyChip>

    /**
     * Append a new chip carrying [text] (a fresh id is minted). Blank [text] is an
     * explicit no-op (returns the list unchanged) — a boundary guard so the palette
     * never accumulates empty chips. Non-blank [text] is stored VERBATIM (leading /
     * trailing whitespace preserved). Returns the new list.
     */
    public suspend fun add(text: String): List<QuickReplyChip>

    /**
     * Replace the [text] of the chip with [id] (position preserved). Unknown [id] or
     * blank [text] → no-op (unchanged list). Returns the new list.
     */
    public suspend fun edit(id: String, text: String): List<QuickReplyChip>

    /** Remove the chip with [id]. Unknown [id] is a no-op. Returns the new list. */
    public suspend fun remove(id: String): List<QuickReplyChip>

    /**
     * Move the chip at [fromIndex] to [toIndex] (drag-reorder). Out-of-range indices
     * are a no-op (returns the unchanged list). Returns the new list.
     */
    public suspend fun move(fromIndex: Int, toIndex: Int): List<QuickReplyChip>
}

// ── Pure collection transforms shared by all QuickReplyStore implementations (DRY) ──
// Never mutate the receiver — always return a fresh list. `internal` so both stores
// and the same-module unit tests use them without widening the public surface.

/** Append a chip with [id]/[text], unless [text] is blank (boundary guard → no-op). */
internal fun List<QuickReplyChip>.adding(text: String, id: String): List<QuickReplyChip> =
    if (text.isBlank()) this else this + QuickReplyChip(id, text)

/** Replace the [text] of the chip with [id] (position kept). Blank text / unknown id → no-op. */
internal fun List<QuickReplyChip>.editing(id: String, text: String): List<QuickReplyChip> =
    if (text.isBlank()) this else map { if (it.id == id) it.copy(text = text) else it }

/** A copy without the chip whose id equals [id] (unknown id → an unchanged copy). */
internal fun List<QuickReplyChip>.removingId(id: String): List<QuickReplyChip> =
    filter { it.id != id }

/** Immutable move [from] → [to]; out-of-range (either end) is a no-op. */
internal fun List<QuickReplyChip>.moving(from: Int, to: Int): List<QuickReplyChip> {
    if (from !in indices || to !in indices || from == to) return this
    val next = toMutableList()
    next.add(to, next.removeAt(from))
    return next
}

/**
 * Pure JSON codec for the persisted palette (JVM-testable, no Context). A chip list
 * ⇆ a JSON array of `{"id","text"}` objects. Encode is total; decode is defensive —
 * the blob is untrusted at rest, so a corrupt array or a chip missing a string
 * id/text is dropped rather than crashing. Uses the kotlinx JSON element API (no
 * `@Serializable` codegen → no serialization compiler plugin needed on :app; the
 * runtime lib is on the classpath via `:wire-protocol`'s `api` dependency).
 */
internal object QuickReplyCodec {
    private val json = Json { ignoreUnknownKeys = true; isLenient = true }

    fun encode(chips: List<QuickReplyChip>): String =
        buildJsonArray {
            chips.forEach { chip ->
                addJsonObject {
                    put("id", chip.id)
                    put("text", chip.text)
                }
            }
        }.toString()

    fun decode(raw: String?): List<QuickReplyChip> {
        if (raw.isNullOrBlank()) return emptyList()
        val array = try {
            json.parseToJsonElement(raw) as? JsonArray ?: return emptyList()
        } catch (_: Exception) {
            return emptyList() // corrupt blob at rest → start clean, never crash
        }
        return array.mapNotNull { element ->
            val obj = element as? JsonObject ?: return@mapNotNull null
            val id = obj["id"].asStringOrNull()
            val text = obj["text"].asStringOrNull()
            if (id.isNullOrEmpty() || text.isNullOrEmpty()) null else QuickReplyChip(id, text)
        }
    }

    private fun kotlinx.serialization.json.JsonElement?.asStringOrNull(): String? =
        (this as? JsonPrimitive)?.takeIf { it.isString }?.content
}

/**
 * Preferences-DataStore-backed [QuickReplyStore]. The whole palette is ONE JSON
 * string under [PALETTE_KEY], serialized by [QuickReplyCodec]; writes are an atomic
 * read-modify-write via [DataStore.edit], reusing the shared immutable transforms.
 *
 * Key-absent (first run) reads/seeds [QuickReplyDefaults]; once any write lands, the
 * stored list wins — including an empty list, so "delete all" is respected. The
 * [DataStore] is injected (constructed from a Context in :app DI), so this class has
 * no Android-framework surface of its own and is exercised in JVM tests via an
 * in-memory `DataStore<Preferences>` double.
 */
public class DataStoreQuickReplyStore(
    private val dataStore: DataStore<Preferences>,
    private val newId: () -> String = { UUID.randomUUID().toString() },
) : QuickReplyStore {

    override suspend fun loadAll(): List<QuickReplyChip> {
        val raw = dataStore.data.first()[PALETTE_KEY]
        return if (raw == null) QuickReplyDefaults.chips else QuickReplyCodec.decode(raw)
    }

    override suspend fun add(text: String): List<QuickReplyChip> =
        writeTransform { it.adding(text, newId()) }

    override suspend fun edit(id: String, text: String): List<QuickReplyChip> =
        writeTransform { it.editing(id, text) }

    override suspend fun remove(id: String): List<QuickReplyChip> =
        writeTransform { it.removingId(id) }

    override suspend fun move(fromIndex: Int, toIndex: Int): List<QuickReplyChip> =
        writeTransform { it.moving(fromIndex, toIndex) }

    private suspend fun writeTransform(
        transform: (List<QuickReplyChip>) -> List<QuickReplyChip>,
    ): List<QuickReplyChip> {
        lateinit var updated: List<QuickReplyChip>
        dataStore.edit { prefs ->
            val raw = prefs[PALETTE_KEY]
            val current = if (raw == null) QuickReplyDefaults.chips else QuickReplyCodec.decode(raw)
            updated = transform(current)
            prefs[PALETTE_KEY] = QuickReplyCodec.encode(updated)
        }
        return updated
    }

    private companion object {
        val PALETTE_KEY = stringPreferencesKey("quickReplyPalette")
    }
}

/**
 * In-memory [QuickReplyStore] — lives in `main` (not `test`) so it doubles for the
 * DataStore store in JVM CRUD tests AND backs Compose previews. Seeded with
 * [QuickReplyDefaults] by default. A [Mutex] serializes the read-modify-write so
 * concurrent callers can't interleave; state is replaced wholesale on every change.
 */
public class InMemoryQuickReplyStore(
    initial: List<QuickReplyChip> = QuickReplyDefaults.chips,
    private val newId: () -> String = { UUID.randomUUID().toString() },
) : QuickReplyStore {
    private val mutex = Mutex()
    private var chips: List<QuickReplyChip> = initial.toList()

    override suspend fun loadAll(): List<QuickReplyChip> = mutex.withLock { chips }

    override suspend fun add(text: String): List<QuickReplyChip> =
        write { it.adding(text, newId()) }

    override suspend fun edit(id: String, text: String): List<QuickReplyChip> =
        write { it.editing(id, text) }

    override suspend fun remove(id: String): List<QuickReplyChip> =
        write { it.removingId(id) }

    override suspend fun move(fromIndex: Int, toIndex: Int): List<QuickReplyChip> =
        write { it.moving(fromIndex, toIndex) }

    private suspend fun write(
        transform: (List<QuickReplyChip>) -> List<QuickReplyChip>,
    ): List<QuickReplyChip> = mutex.withLock {
        val updated = transform(chips)
        chips = updated
        updated
    }
}
