/**
 * public/quick-reply.ts — N-quickreply: quick-reply chips + saved-prompt palette.
 *
 * Feature: A3 (Walk-away Workbench — "Quick-Reply Chips + Saved Prompt Palette")
 *
 * Provides:
 *   - BUILT_IN_CHIPS  — pre-set chips for Claude Code's most common responses.
 *   - Immutable CRUD  — addChip / removeChip / reorderChip / updateChip.
 *   - Palette I/O     — loadPalette / savePalette (localStorage, never throw).
 *   - mountQuickReply — DOM mount: chips + inline editor.
 *
 * Security: SEC-L3 — all chip labels are set via textContent, never innerHTML,
 *   so a snippet label such as `<script>alert(1)</script>` appears as inert text.
 */

/** A single sendable snippet. */
export interface Chip {
  /** Stable identifier (built-in chips use a double-underscore prefix). */
  id: string
  /** Raw bytes to send (without the Enter suffix). */
  text: string
  /** Display label on the chip button. Set via textContent — never innerHTML. */
  label: string
  /** If true, \r is appended to `text` when sending. */
  appendEnter: boolean
}

// ── constants ─────────────────────────────────────────────────────────────────

/** localStorage key used to persist the user palette. */
export const PALETTE_KEY = 'web-terminal:quick-reply-palette'

/** The six built-in chips for Claude Code interaction. Immutable. */
export const BUILT_IN_CHIPS: readonly Chip[] = Object.freeze([
  { id: '__yes', text: 'yes', label: 'yes', appendEnter: true },
  { id: '__continue', text: 'continue', label: 'continue', appendEnter: true },
  { id: '__1', text: '1', label: '1', appendEnter: true },
  { id: '__2', text: '2', label: '2', appendEnter: true },
  { id: '__3', text: '3', label: '3', appendEnter: true },
  { id: '__esc', text: '\x1b', label: 'Esc', appendEnter: false },
])

// ── immutable CRUD ────────────────────────────────────────────────────────────

/** Return a new array with `chip` appended. Does not mutate `chips`. */
export function addChip(chips: readonly Chip[], chip: Chip): Chip[] {
  return [...chips, chip]
}

/** Return a new array with the chip matching `id` removed. Does not mutate. */
export function removeChip(chips: readonly Chip[], id: string): Chip[] {
  return chips.filter((c) => c.id !== id)
}

/**
 * Return a new array with the chip at `fromIndex` moved to `toIndex`.
 * Returns a shallow copy unchanged if either index is out of bounds.
 * Does not mutate `chips`.
 */
export function reorderChip(chips: readonly Chip[], fromIndex: number, toIndex: number): Chip[] {
  if (fromIndex < 0 || fromIndex >= chips.length) return [...chips]
  if (toIndex < 0 || toIndex >= chips.length) return [...chips]
  const result = [...chips]
  const [moved] = result.splice(fromIndex, 1)
  // moved is defined because fromIndex is valid (checked above)
  result.splice(toIndex, 0, moved as Chip)
  return result
}

/**
 * Return a new array where the chip with `id` is shallow-merged with `patch`.
 * The id field cannot be changed. Does not mutate `chips` or the chip objects.
 */
export function updateChip(
  chips: readonly Chip[],
  id: string,
  patch: Partial<Omit<Chip, 'id'>>,
): Chip[] {
  return chips.map((c) => (c.id === id ? { ...c, ...patch } : c))
}

// ── localStorage persistence ──────────────────────────────────────────────────

function isValidChip(item: unknown): item is Chip {
  if (item === null || typeof item !== 'object') return false
  const o = item as Record<string, unknown>
  return (
    typeof o['id'] === 'string' &&
    typeof o['text'] === 'string' &&
    typeof o['label'] === 'string' &&
    typeof o['appendEnter'] === 'boolean'
  )
}

/**
 * Load the user palette from localStorage.
 * Returns [] on missing key, bad JSON, or unexpected shape. Never throws.
 */
export function loadPalette(): Chip[] {
  try {
    const raw = localStorage.getItem(PALETTE_KEY)
    if (!raw) return []
    const parsed: unknown = JSON.parse(raw)
    if (!Array.isArray(parsed)) return []
    return parsed.filter(isValidChip)
  } catch {
    return []
  }
}

/**
 * Persist the user palette to localStorage.
 * Silently ignores storage errors (quota exceeded, private mode). Never throws.
 */
export function savePalette(chips: readonly Chip[]): void {
  try {
    localStorage.setItem(PALETTE_KEY, JSON.stringify(chips))
  } catch {
    // Quota exceeded or storage unavailable — best-effort, ignore.
  }
}

// ── DOM helpers ───────────────────────────────────────────────────────────────

/** Create a typed element with an optional CSS class and text content. */
function el<K extends keyof HTMLElementTagNameMap>(
  tag: K,
  cls?: string,
  text?: string,
): HTMLElementTagNameMap[K] {
  const node = document.createElement(tag)
  if (cls) node.className = cls
  if (text !== undefined) node.textContent = text // SEC-L3: textContent only
  return node
}

/** Remove all children of `parent`. */
function clearChildren(parent: HTMLElement): void {
  while (parent.firstChild) {
    parent.removeChild(parent.firstChild)
  }
}

// ── mountQuickReply ───────────────────────────────────────────────────────────

/** Options for mountQuickReply. */
export interface QuickReplyOpts {
  /** Called with the full byte string to inject into the terminal. */
  onSend: (data: string) => void
  /** W2: optional. When provided, the snippet editor gains a "Queue" button that
   *  enqueues the typed text as a follow-up prompt (fired on next idle) instead of
   *  sending it now. `appendEnter` mirrors the editor checkbox. */
  onQueue?: (text: string, appendEnter: boolean) => void
}

/** Returned by mountQuickReply to allow cleanup. */
export interface QuickReplyHandle {
  /** Remove all DOM content and event listeners created by mountQuickReply. */
  dispose: () => void
}

/**
 * Mount the quick-reply chip row + palette into `container`.
 *
 * Renders built-in chips, then user palette chips, then a `+` button that
 * opens an inline editor to add new snippets. Clicking a chip calls
 * `opts.onSend(text [+ '\r'])`.
 *
 * Labels are set via textContent — never innerHTML (SEC-L3).
 */
export function mountQuickReply(container: HTMLElement, opts: QuickReplyOpts): QuickReplyHandle {
  const { onSend, onQueue } = opts
  // Track disposers for all event listeners so dispose() is clean.
  const disposers: Array<() => void> = []

  function makeChipButton(chip: Chip): HTMLButtonElement {
    const btn = el('button', 'qr-chip')
    btn.textContent = chip.label // SEC-L3: textContent, never innerHTML
    btn.title = chip.appendEnter ? `${chip.text}⏎` : chip.text

    function handler() {
      onSend(chip.text + (chip.appendEnter ? '\r' : ''))
    }
    btn.addEventListener('click', handler)
    disposers.push(() => btn.removeEventListener('click', handler))
    return btn
  }

  function openEditor(): void {
    const editor = el('div', 'qr-editor')

    const textInput = document.createElement('input')
    textInput.type = 'text'
    textInput.className = 'qr-editor-text'
    textInput.placeholder = 'Text to send'

    const labelInput = document.createElement('input')
    labelInput.type = 'text'
    labelInput.className = 'qr-editor-label'
    labelInput.placeholder = 'Label (optional, defaults to text)'

    const enterWrap = el('label', 'qr-editor-enter-label')
    const enterCheck = document.createElement('input')
    enterCheck.type = 'checkbox'
    enterCheck.className = 'qr-editor-enter'
    enterCheck.checked = true
    enterWrap.appendChild(enterCheck)
    enterWrap.appendChild(document.createTextNode(' Append Enter'))

    const saveBtn = el('button', 'qr-editor-save', 'Save')
    const cancelBtn = el('button', 'qr-editor-cancel', 'Cancel')

    function onSaveClick() {
      const text = textInput.value.trim()
      if (!text) return // empty text — no-op, editor stays open
      const rawLabel = labelInput.value.trim()
      const label = rawLabel !== '' ? rawLabel : text
      const newChip: Chip = {
        id: `user_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`,
        text,
        label,
        appendEnter: enterCheck.checked,
      }
      savePalette(addChip(loadPalette(), newChip))
      editor.remove()
      render()
    }

    function onCancelClick() {
      editor.remove()
    }

    saveBtn.addEventListener('click', onSaveClick)
    cancelBtn.addEventListener('click', onCancelClick)

    editor.appendChild(textInput)
    editor.appendChild(labelInput)
    editor.appendChild(enterWrap)
    editor.appendChild(saveBtn)

    // W2: "Queue" defers the typed text as a follow-up prompt (fires on next
    // idle) rather than saving it as a chip. Shown only when a handler is wired.
    if (onQueue !== undefined) {
      const queueBtn = el('button', 'qr-editor-queue', 'Queue')
      queueBtn.title = 'Queue as a follow-up (runs when Claude next goes idle)'
      function onQueueClick() {
        const text = textInput.value.trim()
        if (!text) return // empty text — no-op, editor stays open
        onQueue?.(text, enterCheck.checked)
        editor.remove()
      }
      queueBtn.addEventListener('click', onQueueClick)
      editor.appendChild(queueBtn)
    }

    editor.appendChild(cancelBtn)

    container.appendChild(editor)
  }

  function render(): void {
    clearChildren(container)

    const row = el('div', 'qr-row')

    // Built-in chips first
    for (const chip of BUILT_IN_CHIPS) {
      row.appendChild(makeChipButton(chip))
    }

    // User palette chips
    for (const chip of loadPalette()) {
      row.appendChild(makeChipButton(chip))
    }

    // Add-snippet button
    const addBtn = el('button', 'qr-add-btn', '+')
    addBtn.title = 'Add custom snippet'
    addBtn.setAttribute('aria-label', 'Add custom snippet')

    function onAddClick() {
      openEditor()
    }
    addBtn.addEventListener('click', onAddClick)
    disposers.push(() => addBtn.removeEventListener('click', onAddClick))

    row.appendChild(addBtn)
    container.appendChild(row)
  }

  render()

  return {
    dispose() {
      for (const cleanup of disposers) cleanup()
      clearChildren(container)
    },
  }
}
