import Foundation
import Observation
import SessionCore

/// T-iOS-25 · Quick-reply data layer: chip model, pure immutable CRUD and the
/// UserDefaults-backed palette store — a behavior mirror of
/// `public/quick-reply.ts` (Chip / addChip / removeChip / reorderChip /
/// updateChip / loadPalette / savePalette).
///
/// Storage decision (plan §5.3 split): the palette is NON-SECRET UI prefs →
/// UserDefaults with an injectable suite (same pattern as
/// `UserDefaultsLastSessionStore`), never Keychain.

/// One sendable snippet (mirror of the web `Chip` interface).
struct QuickReplyChip: Sendable, Equatable, Codable, Identifiable {
    /// Stable identifier (built-ins use the `__` prefix, user chips `user_`).
    let id: String
    /// Raw bytes to send (without the Enter suffix).
    let text: String
    /// Display label on the chip button. Rendered via `Text(verbatim:)` only —
    /// the SwiftUI analogue of the web's textContent-never-innerHTML (SEC-L3).
    let label: String
    /// If true, Enter (`\r`) is appended to `text` when sending.
    let appendEnter: Bool
}

/// Pure palette operations — every function returns a NEW array and never
/// mutates its input (web quick-reply.ts CRUD, verbatim semantics).
enum QuickReplyPalette {
    /// The six built-in chips for Claude Code interaction, order and values
    /// mirroring web `BUILT_IN_CHIPS`. Esc bytes resolve through `KeyByteMap`
    /// — hand-writing an escape sequence in the App layer is a review finding.
    static let builtInChips: [QuickReplyChip] = [
        QuickReplyChip(id: "__yes", text: "yes", label: "yes", appendEnter: true),
        QuickReplyChip(id: "__continue", text: "continue", label: "continue",
                       appendEnter: true),
        QuickReplyChip(id: "__1", text: "1", label: "1", appendEnter: true),
        QuickReplyChip(id: "__2", text: "2", label: "2", appendEnter: true),
        QuickReplyChip(id: "__3", text: "3", label: "3", appendEnter: true),
        QuickReplyChip(id: "__esc", text: KeyByteMap.bytes(for: .esc), label: "Esc",
                       appendEnter: false),
    ]

    /// Prefix for user-created chip ids (web: `user_<ts>_<rand>`; iOS: UUID).
    static let userChipIdPrefix = "user_"

    /// The full byte string a chip tap sends: text plus — when `appendEnter` —
    /// the Enter byte from `KeyByteMap` (`\r`, 0x0D, NOT `\n`; CLAUDE.md gotcha).
    static func payload(for chip: QuickReplyChip) -> String {
        chip.text + (chip.appendEnter ? KeyByteMap.bytes(for: .enter) : "")
    }

    /// New array with `chip` appended (web `addChip`).
    static func adding(_ chips: [QuickReplyChip], _ chip: QuickReplyChip) -> [QuickReplyChip] {
        chips + [chip]
    }

    /// New array with the chip matching `id` removed (web `removeChip`).
    static func removing(_ chips: [QuickReplyChip], id: String) -> [QuickReplyChip] {
        chips.filter { $0.id != id }
    }

    /// New array with the chip at `fromIndex` moved to `toIndex` (web
    /// `reorderChip` splice semantics). Either index out of bounds → the input
    /// returned unchanged. Operates on a local copy — value semantics
    /// guarantee the caller's array is never touched.
    static func reordering(
        _ chips: [QuickReplyChip], fromIndex: Int, toIndex: Int
    ) -> [QuickReplyChip] {
        guard chips.indices.contains(fromIndex), chips.indices.contains(toIndex) else {
            return chips
        }
        var result = chips
        let moved = result.remove(at: fromIndex)
        result.insert(moved, at: toIndex)
        return result
    }

    /// New array where the chip with `id` has non-nil fields patched (web
    /// `updateChip`: shallow merge, id immutable). Unknown id → unchanged.
    static func updating(
        _ chips: [QuickReplyChip], id: String,
        text: String? = nil, label: String? = nil, appendEnter: Bool? = nil
    ) -> [QuickReplyChip] {
        chips.map { chip in
            guard chip.id == id else { return chip }
            return QuickReplyChip(
                id: chip.id,
                text: text ?? chip.text,
                label: label ?? chip.label,
                appendEnter: appendEnter ?? chip.appendEnter
            )
        }
    }
}

/// Per-item lossy decode wrapper (mirror of the web `filter(isValidChip)`):
/// one malformed entry is dropped, the well-formed rest survive — stored data
/// crosses a storage boundary and is never trusted (LastSessionStore pattern).
private struct LossyQuickReplyChip: Decodable {
    let chip: QuickReplyChip?

    init(from decoder: any Decoder) throws {
        chip = try? QuickReplyChip(from: decoder)
    }
}

/// Observable palette store: holds the user chips, persists every change to
/// the injected `UserDefaults` and exposes the render list (built-ins first,
/// then user chips — web render order).
@MainActor
@Observable
final class QuickReplyStore {
    /// Single UserDefaults key for the encoded user palette
    /// (web: localStorage `web-terminal:quick-reply-palette`).
    static let paletteDefaultsKey = "quickReplyPalette"

    private(set) var userChips: [QuickReplyChip]

    @ObservationIgnored private let defaults: UserDefaults

    /// Built-in chips first, then the user palette (web `render()` order).
    var allChips: [QuickReplyChip] {
        QuickReplyPalette.builtInChips + userChips
    }

    init(defaults: UserDefaults = .standard) {
        self.defaults = defaults
        userChips = Self.loadPalette(from: defaults)
    }

    /// Add a new phrase (web editor `onSaveClick` semantics): text/label are
    /// trimmed, empty text is a no-op (returns false), empty label defaults to
    /// the text, the id is minted with the `user_` prefix.
    @discardableResult
    func addChip(text: String, label: String, appendEnter: Bool) -> Bool {
        let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmedText.isEmpty else { return false }
        let trimmedLabel = label.trimmingCharacters(in: .whitespacesAndNewlines)
        let chip = QuickReplyChip(
            id: QuickReplyPalette.userChipIdPrefix + UUID().uuidString,
            text: trimmedText,
            label: trimmedLabel.isEmpty ? trimmedText : trimmedLabel,
            appendEnter: appendEnter
        )
        setUserChips(QuickReplyPalette.adding(userChips, chip))
        return true
    }

    func removeChip(id: String) {
        setUserChips(QuickReplyPalette.removing(userChips, id: id))
    }

    func moveChip(fromIndex: Int, toIndex: Int) {
        setUserChips(QuickReplyPalette.reordering(
            userChips, fromIndex: fromIndex, toIndex: toIndex
        ))
    }

    /// SwiftUI `List.onMove` adapter (IndexSet + insert-before destination on
    /// the ORIGINAL array — `Array.move` implements that convention).
    func moveChips(fromOffsets: IndexSet, toOffset: Int) {
        var next = userChips
        next.move(fromOffsets: fromOffsets, toOffset: toOffset)
        setUserChips(next)
    }

    func updateChip(
        id: String, text: String? = nil, label: String? = nil, appendEnter: Bool? = nil
    ) {
        setUserChips(QuickReplyPalette.updating(
            userChips, id: id, text: text, label: label, appendEnter: appendEnter
        ))
    }

    /// Load the persisted palette. Missing key, non-JSON data or a non-array
    /// root → `[]`; malformed items are dropped per-entry. Never throws
    /// (web `loadPalette` contract).
    static func loadPalette(from defaults: UserDefaults) -> [QuickReplyChip] {
        guard let data = defaults.data(forKey: paletteDefaultsKey) else { return [] }
        guard let lossy = try? JSONDecoder().decode([LossyQuickReplyChip].self, from: data)
        else { return [] }
        return lossy.compactMap(\.chip)
    }

    // MARK: - Private

    private func setUserChips(_ chips: [QuickReplyChip]) {
        userChips = chips
        persist(chips)
    }

    /// Best-effort save (web `savePalette` contract: storage failure never
    /// throws or crashes — the in-memory palette stays authoritative for the
    /// session). Encoding a `[QuickReplyChip]` cannot practically fail.
    private func persist(_ chips: [QuickReplyChip]) {
        guard let data = try? JSONEncoder().encode(chips) else { return }
        defaults.set(data, forKey: Self.paletteDefaultsKey)
    }
}
