import { createStore } from "solid-js/store"
import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js"
import { useRenderer, useTerminalDimensions } from "@opentui/solid"
import type { ScrollBoxRenderable, TextareaRenderable } from "@opentui/core"
import open from "open"
import { selectedForeground, tint, useTheme } from "../../context/theme"
import type { FormFormInfo, FormValue } from "@opencode-ai/sdk/v2"
import type { FormInfo } from "../../context/data"
import { useSDK } from "../../context/sdk"
import { SplitBorder } from "../../ui/border"
import { useTuiConfig } from "../../config"
import { useBindings, useOpencodeModeStack } from "../../keymap"

const FORM_MODE = "form"

type Field = FormFormInfo["fields"][number]

function fieldLabel(field: Field) {
  return field.title ?? field.key
}

function truncate(label: string, max: number) {
  return label.length > max ? label.slice(0, max - 1).trimEnd() + "…" : label
}

function validateText(field: Field, text: string): string | undefined {
  if (field.type !== "string") return
  if (field.minLength !== undefined && text.length < field.minLength)
    return `Must be at least ${field.minLength} characters`
  if (field.maxLength !== undefined && text.length > field.maxLength)
    return `Must be at most ${field.maxLength} characters`
  if (field.pattern !== undefined) {
    try {
      if (!new RegExp(field.pattern).test(text)) return `Must match pattern: ${field.pattern}`
    } catch {
      return `Invalid pattern: ${field.pattern}`
    }
  }
  if (field.format === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text)) return "Expected an email address"
  if (field.format === "uri") {
    try {
      new URL(text)
    } catch {
      return "Expected a URL"
    }
  }
  if (field.format === "date") {
    const date = new Date(`${text}T00:00:00.000Z`)
    if (!/^\d{4}-\d{2}-\d{2}$/.test(text) || Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== text)
      return "Expected a date (YYYY-MM-DD)"
  }
  if (field.format === "date-time" && Number.isNaN(new Date(text).getTime())) return "Expected a date and time"
}

function validateSelection(field: Field, value: FormValue | undefined) {
  if (field.type !== "multiselect" || value === undefined) return
  if (!Array.isArray(value)) return "Expected selections"
  if (field.required && value.length === 0) return "Select at least one option"
  if (field.minItems !== undefined && value.length < field.minItems) return `Select at least ${field.minItems}`
  if (field.maxItems !== undefined && value.length > field.maxItems) return `Select at most ${field.maxItems}`
}

function validateValue(field: Field, value: FormValue | undefined) {
  if (value === undefined) return field.required ? "Answer required" : undefined
  if (field.required && (value === "" || (Array.isArray(value) && value.length === 0))) {
    return field.type === "multiselect" ? "Select at least one option" : "Answer required"
  }
  if (field.type === "string") {
    if (typeof value !== "string") return "Expected text"
    const invalid = validateText(field, value)
    if (invalid) return invalid
    if (field.options && !field.custom && !field.options.some((option) => option.value === value)) {
      return "Select an available option"
    }
    return
  }
  if (field.type === "number" || field.type === "integer") {
    if (typeof value !== "number" || !Number.isFinite(value)) return "Expected a number"
    if (field.type === "integer" && !Number.isInteger(value)) return "Expected an integer"
    if (typeof field.minimum === "number" && value < field.minimum) return `Must be at least ${field.minimum}`
    if (typeof field.maximum === "number" && value > field.maximum) return `Must be at most ${field.maximum}`
    return
  }
  if (field.type === "boolean") return typeof value === "boolean" ? undefined : "Expected yes or no"
  const invalid = validateSelection(field, value)
  if (invalid) return invalid
  if (
    Array.isArray(value) &&
    !field.custom &&
    value.some((item) => !field.options.some((option) => option.value === item))
  ) {
    return "Select only available options"
  }
}

function fieldRows(field: Field): { value: FormValue; label: string; description?: string }[] {
  if (field.type === "boolean")
    return [
      { value: true, label: "Yes" },
      { value: false, label: "No" },
    ]
  if (field.type === "multiselect" || (field.type === "string" && field.options))
    return (field.options ?? []).map((option) => ({
      value: option.value,
      label: option.label,
      description: option.description,
    }))
  return []
}

function selectedRow(field: Field | undefined, value: FormValue | undefined) {
  if (!field || value === undefined || Array.isArray(value)) return 0
  const rows = fieldRows(field)
  const index = rows.findIndex((row) => row.value === value)
  if (index !== -1) return index
  if (typeof value === "string" && field.type === "string" && field.options && field.custom) return rows.length
  return 0
}

function customDefault(field: Field) {
  if (field.type !== "string" || !field.options || !field.custom || typeof field.default !== "string") return
  if (!field.options.some((option) => option.value === field.default)) return field.default
}

function display(field: Field, value: FormValue | undefined) {
  if (value === undefined) return ""
  const label = (item: string | number | boolean) =>
    fieldRows(field).find((row) => row.value === item)?.label ?? String(item)
  if (Array.isArray(value)) return value.length === 0 ? "(none)" : value.map(label).join(", ")
  return label(value)
}

export function FormPrompt(props: { form: FormInfo }) {
  return props.form.mode === "url" ? <UrlPrompt form={props.form} /> : <FieldsPrompt form={props.form} />
}

function UrlPrompt(props: { form: FormInfo & { mode: "url" } }) {
  const sdk = useSDK()
  const { theme } = useTheme()
  const modeStack = useOpencodeModeStack()
  const message = createMemo(() => {
    const value = props.form.metadata?.["message"]
    return typeof value === "string" ? value : undefined
  })

  onMount(() => onCleanup(modeStack.push(FORM_MODE)))

  useBindings(() => ({
    mode: FORM_MODE,
    enabled: true,
    commands: [
      {
        name: "app.exit",
        title: "Dismiss form",
        category: "Form",
        run() {
          void sdk.api.form.cancel({ sessionID: props.form.sessionID, formID: props.form.id })
        },
      },
    ],
    bindings: [
      {
        key: "return",
        desc: "Open link",
        group: "Form",
        cmd: () => {
          void open(props.form.url)
        },
      },
      {
        key: "escape",
        desc: "Dismiss form",
        group: "Form",
        cmd: () => {
          void sdk.api.form.cancel({ sessionID: props.form.sessionID, formID: props.form.id })
        },
      },
    ],
  }))

  return (
    <box
      backgroundColor={theme.backgroundPanel}
      border={["left"]}
      borderColor={theme.accent}
      customBorderChars={SplitBorder.customBorderChars}
    >
      <box gap={1} paddingLeft={2} paddingRight={3} paddingTop={1} paddingBottom={1}>
        <text fg={theme.text}>{props.form.title ?? "Input requested"}</text>
        <Show when={message()}>
          <text fg={theme.textMuted}>{message()}</text>
        </Show>
        <text fg={theme.secondary}>{props.form.url}</text>
      </box>
      <box flexDirection="row" flexShrink={0} gap={2} paddingLeft={2} paddingRight={3} paddingBottom={1}>
        <text fg={theme.text}>
          enter <span style={{ fg: theme.textMuted }}>open link</span>
        </text>
        <text fg={theme.text}>
          esc <span style={{ fg: theme.textMuted }}>dismiss</span>
        </text>
      </box>
    </box>
  )
}

function FieldsPrompt(props: { form: FormInfo & { mode: "form" } }) {
  const sdk = useSDK()
  const { theme } = useTheme()
  const renderer = useRenderer()
  const dimensions = useTerminalDimensions()
  const tuiConfig = useTuiConfig()
  const modeStack = useOpencodeModeStack()

  const [tabHover, setTabHover] = createSignal<number | "confirm" | null>(null)
  const [store, setStore] = createStore({
    tab: 0,
    answers: Object.fromEntries(
      props.form.fields.flatMap((field) => (field.default === undefined ? [] : [[field.key, field.default]])),
    ) as Record<string, FormValue | undefined>,
    custom: Object.fromEntries(
      props.form.fields.flatMap((field) => {
        const value = customDefault(field)
        return value === undefined ? [] : [[field.key, value]]
      }),
    ) as Record<string, string>,
    selected: selectedRow(props.form.fields[0], props.form.fields[0]?.default),
    editing: false,
    error: "",
  })

  let textarea: TextareaRenderable | undefined
  let review: ScrollBoxRenderable | undefined

  const fields = createMemo(() => {
    const answers: Record<string, FormValue | undefined> = {}
    return props.form.fields.filter((field) => {
      const active = (field.when ?? []).every((when) => {
        const value = answers[when.key]
        if (value === undefined) return false
        const hit = Array.isArray(value) ? value.some((item) => item === when.value) : value === when.value
        return when.op === "eq" ? hit : !hit
      })
      if (active) answers[field.key] = store.answers[field.key]
      return active
    })
  })
  const single = createMemo(() => {
    const list = fields()
    if (props.form.fields.length !== 1) return false
    if (list.length !== 1) return false
    const field = list[0]!
    return field.type === "boolean" || (field.type === "string" && field.options !== undefined)
  })
  const tabs = createMemo(() => (single() ? 1 : fields().length + 1))
  const tabbed = createMemo(() => {
    const width = fields().reduce((sum, item) => sum + truncate(fieldLabel(item), 24).length + 3, "Confirm".length + 3)
    return width <= dimensions().width - 8
  })
  const answered = createMemo(
    () =>
      fields().filter((item) => {
        const value = store.answers[item.key]
        return value !== undefined
      }).length,
  )
  const field = createMemo(() => fields()[Math.min(store.tab, fields().length - 1)])
  const confirm = createMemo(() => !single() && store.tab >= fields().length)
  const rows = createMemo(() => {
    const current = field()
    if (!current) return []
    const configured = fieldRows(current)
    const value = store.answers[current.key]
    if (current.type !== "multiselect" || !Array.isArray(value)) return configured
    const known = new Set(configured.map((row) => row.value))
    return [
      ...configured,
      ...value.filter((item) => !known.has(item)).map((item) => ({ value: item, label: item, description: undefined })),
    ]
  })
  const textual = createMemo(() => {
    if (confirm()) return false
    const current = field()
    if (!current) return false
    if (current.type === "number" || current.type === "integer") return true
    return current.type === "string" && current.options === undefined
  })
  const custom = createMemo(() => {
    const current = field()
    if (!current) return false
    if (current.type === "string" && current.options !== undefined) return current.custom === true
    if (current.type === "multiselect") return current.custom === true
    return false
  })
  const multi = createMemo(() => field()?.type === "multiselect")
  const placeholder = createMemo(() => {
    const current = field()
    if (current?.type === "string") {
      if (current.placeholder) return current.placeholder
      if (current.format === "email") return "name@example.com"
      if (current.format === "uri") return "https://example.com"
      if (current.format === "date") return "YYYY-MM-DD"
      if (current.format === "date-time") return "YYYY-MM-DDTHH:MM:SSZ"
    }
    if (current?.type === "number" || current?.type === "integer") {
      const minimum = typeof current.minimum === "number" ? current.minimum : undefined
      const maximum = typeof current.maximum === "number" ? current.maximum : undefined
      if (minimum !== undefined && maximum !== undefined) return `${minimum}-${maximum}`
      if (minimum !== undefined) return `at least ${minimum}`
      if (maximum !== undefined) return `at most ${maximum}`
    }
    return "Type your answer"
  })
  const other = createMemo(() => custom() && store.selected === rows().length)
  const input = createMemo(() => store.custom[field()?.key ?? ""] ?? "")
  const customPicked = createMemo(() => {
    const value = input()
    if (!value) return false
    const answer = store.answers[field()?.key ?? ""]
    if (Array.isArray(answer)) return answer.includes(value)
    return answer === value
  })

  function answer(key: string, value: FormValue | undefined) {
    setStore("answers", { ...store.answers, [key]: value })
    setStore("error", "")
  }

  function replySingle(field: Field, value: FormValue) {
    sdk.api.form
      .reply({
        sessionID: props.form.sessionID,
        formID: props.form.id,
        answer: { [field.key]: value },
      })
      .catch((error: unknown) => {
        setStore(
          "error",
          typeof error === "object" && error !== null && "message" in error && typeof error.message === "string"
            ? error.message
            : "Invalid answer",
        )
      })
  }

  function pick(value: FormValue, customValue?: string) {
    const current = field()
    if (!current) return
    const invalid = validateValue(current, value)
    if (invalid) {
      setStore("error", invalid)
      return
    }
    answer(current.key, value)
    if (customValue !== undefined) setStore("custom", { ...store.custom, [current.key]: customValue })
    if (single()) {
      replySingle(current, value)
      return
    }
    selectTab(store.tab + 1)
  }

  function toggle(value: string) {
    const current = field()
    if (!current) return
    const existing = store.answers[current.key]
    const list = Array.isArray(existing) ? [...existing] : []
    const index = list.indexOf(value)
    if (index === -1) list.push(value)
    if (index !== -1) list.splice(index, 1)
    answer(current.key, list)
  }

  function validateCurrent() {
    if (confirm()) return true
    const current = field()
    if (!current) return true
    const invalid = validateValue(current, store.answers[current.key])
    if (!invalid) return true
    setStore("error", invalid)
    return false
  }

  function selectTab(index: number) {
    if (!confirm() && index > store.tab && !validateCurrent()) return
    const next = fields()[index]
    setStore("tab", index)
    setStore("selected", selectedRow(next, next ? store.answers[next.key] : undefined))
    setStore("editing", false)
    setStore("error", "")
  }

  function selectOption() {
    if (other()) {
      if (!multi()) {
        setStore("editing", true)
        return
      }
      const value = input()
      if (value && customPicked()) {
        toggle(value)
        return
      }
      setStore("editing", true)
      return
    }
    const row = rows()[store.selected]
    if (!row) return
    if (multi()) {
      toggle(String(row.value))
      return
    }
    pick(row.value)
  }

  function commitInput(text: string) {
    const current = field()
    if (!current) return false
    const isTextual = textual()
    const isMulti = multi()
    if (!text) {
      const previous = store.custom[current.key]
      const existing = store.answers[current.key]
      const values = Array.isArray(existing) ? existing.filter((value) => value !== previous) : []
      const value = !isTextual && isMulti && Array.isArray(existing) ? values : undefined
      const invalid = validateValue(current, value)
      if (invalid) {
        setStore("error", invalid)
        return false
      }
      answer(current.key, value)
      setStore("custom", { ...store.custom, [current.key]: "" })
      setStore("editing", false)
      return true
    }

    if (isTextual && (current.type === "number" || current.type === "integer")) {
      const value = Number(text)
      const invalid = validateValue(current, value)
      if (invalid) {
        setStore("error", invalid)
        return false
      }
      answer(current.key, value)
    }

    if (isTextual && current.type === "string") {
      const invalid = validateValue(current, text)
      if (invalid) {
        setStore("error", invalid)
        return false
      }
      answer(current.key, text)
    }

    if (!isTextual && isMulti) {
      const previous = store.custom[current.key]
      const existing = store.answers[current.key]
      const values = Array.isArray(existing) ? [...existing] : []
      if (previous) {
        const index = values.indexOf(previous)
        if (index !== -1) values.splice(index, 1)
      }
      if (!values.includes(text)) values.push(text)
      answer(current.key, values)
    }

    if (!isTextual && !isMulti) {
      const invalid = validateValue(current, text)
      if (invalid) {
        setStore("error", invalid)
        return false
      }
      answer(current.key, text)
    }

    const configured = current.type === "string" && current.options?.some((option) => option.value === text)
    setStore("custom", { ...store.custom, [current.key]: isMulti || configured ? "" : text })
    setStore("editing", false)
    return true
  }

  function submitInput(text: string, direction: 1 | -1 = 1) {
    if (!commitInput(text)) {
      if (direction === -1) selectTab((store.tab + direction + tabs()) % tabs())
      return
    }
    if (!single()) selectTab((store.tab + direction + tabs()) % tabs())
  }

  function selectTabFromMouse(target?: Field) {
    const targetIndex = () => {
      const index = target ? fields().findIndex((field) => field.key === target.key) : fields().length
      return index === -1 ? fields().length : index
    }
    const move = () => selectTab(targetIndex())
    if (!textual() && !store.editing) {
      move()
      return
    }
    if (!commitInput(textarea?.plainText?.trim() ?? "")) {
      if (targetIndex() < store.tab) move()
      return
    }
    move()
  }

  onMount(() => onCleanup(modeStack.push(FORM_MODE)))

  useBindings(() => ({
    mode: FORM_MODE,
    enabled: (store.editing || textual()) && !confirm(),
    commands: [
      {
        name: "prompt.clear",
        title: "Clear answer edit",
        category: "Form",
        run() {
          const text = textarea?.plainText ?? ""
          if (!text) {
            setStore("editing", false)
            return
          }
          textarea?.setText("")
        },
      },
    ],
    bindings: [
      {
        key: "escape",
        desc: "Cancel answer edit",
        group: "Form",
        cmd: () => {
          if (textual()) {
            void sdk.api.form.cancel({ sessionID: props.form.sessionID, formID: props.form.id })
            return
          }
          setStore("editing", false)
        },
      },
      ...tuiConfig.keybinds.get("prompt.clear"),
      {
        key: "tab",
        desc: "Next field",
        group: "Form",
        cmd: () => {
          const text = textarea?.plainText?.trim() ?? ""
          submitInput(text)
        },
      },
      {
        key: "shift+tab",
        desc: "Previous field",
        group: "Form",
        cmd: () => {
          const text = textarea?.plainText?.trim() ?? ""
          submitInput(text, -1)
        },
      },
      {
        key: "return",
        desc: "Submit answer edit",
        group: "Form",
        cmd: () => {
          const text = textarea?.plainText?.trim() ?? ""
          const current = field()
          if (!current) return
          if (textual()) {
            submitInput(text)
            return
          }
          const wasMulti = multi()
          if (!commitInput(text) || wasMulti || !text) return
          if (single()) {
            replySingle(current, text)
            return
          }
          selectTab(store.tab + 1)
        },
      },
    ],
  }))

  useBindings(() => {
    const total = rows().length + (custom() ? 1 : 0)
    const max = Math.min(total, 9)

    return {
      mode: FORM_MODE,
      enabled: !store.editing && !textual(),
      commands: [
        {
          name: "app.exit",
          title: "Dismiss form",
          category: "Form",
          run() {
            void sdk.api.form.cancel({ sessionID: props.form.sessionID, formID: props.form.id })
          },
        },
      ],
      bindings: [
        {
          key: "left",
          desc: "Previous field",
          group: "Form",
          cmd: () => selectTab((store.tab - 1 + tabs()) % tabs()),
        },
        {
          key: "h",
          desc: "Previous field",
          group: "Form",
          cmd: () => selectTab((store.tab - 1 + tabs()) % tabs()),
        },
        { key: "right", desc: "Next field", group: "Form", cmd: () => selectTab((store.tab + 1) % tabs()) },
        { key: "l", desc: "Next field", group: "Form", cmd: () => selectTab((store.tab + 1) % tabs()) },
        {
          key: "tab",
          desc: "Next field",
          group: "Form",
          cmd: () => selectTab((store.tab + 1) % tabs()),
        },
        {
          key: "shift+tab",
          desc: "Previous field",
          group: "Form",
          cmd: () => selectTab((store.tab - 1 + tabs()) % tabs()),
        },
        ...(confirm()
          ? [
              {
                key: "return",
                desc: "Submit form",
                group: "Form",
                cmd: () => {
                  const invalid = fields().find((field) => validateValue(field, store.answers[field.key]))
                  if (invalid) {
                    setStore("error", validateValue(invalid, store.answers[invalid.key]) ?? "Invalid answer")
                    return
                  }
                  sdk.api.form
                    .reply({
                      sessionID: props.form.sessionID,
                      formID: props.form.id,
                      answer: Object.fromEntries(
                        fields().flatMap((field) => {
                          const value = store.answers[field.key]
                          return value === undefined ? [] : [[field.key, value] as const]
                        }),
                      ),
                    })
                    .catch((error: unknown) => {
                      setStore(
                        "error",
                        typeof error === "object" &&
                          error !== null &&
                          "message" in error &&
                          typeof error.message === "string"
                          ? error.message
                          : "Invalid answer",
                      )
                    })
                },
              },
              {
                key: "escape",
                desc: "Dismiss form",
                group: "Form",
                cmd: () => {
                  void sdk.api.form.cancel({ sessionID: props.form.sessionID, formID: props.form.id })
                },
              },
              { key: "up", desc: "Scroll review", group: "Form", cmd: () => review?.scrollBy(-1) },
              { key: "k", desc: "Scroll review", group: "Form", cmd: () => review?.scrollBy(-1) },
              { key: "down", desc: "Scroll review", group: "Form", cmd: () => review?.scrollBy(1) },
              { key: "j", desc: "Scroll review", group: "Form", cmd: () => review?.scrollBy(1) },
              ...tuiConfig.keybinds.get("app.exit"),
            ]
          : [
              ...Array.from({ length: max }, (_, index) => ({
                key: String(index + 1),
                desc: `Select answer ${index + 1}`,
                group: "Form",
                cmd: () => {
                  setStore("selected", index)
                  selectOption()
                },
              })),
              {
                key: "up",
                desc: "Previous answer",
                group: "Form",
                cmd: () => setStore("selected", (store.selected - 1 + total) % total),
              },
              {
                key: "k",
                desc: "Previous answer",
                group: "Form",
                cmd: () => setStore("selected", (store.selected - 1 + total) % total),
              },
              {
                key: "down",
                desc: "Next answer",
                group: "Form",
                cmd: () => setStore("selected", (store.selected + 1) % total),
              },
              {
                key: "j",
                desc: "Next answer",
                group: "Form",
                cmd: () => setStore("selected", (store.selected + 1) % total),
              },
              { key: "return", desc: "Select answer", group: "Form", cmd: () => selectOption() },
              {
                key: "escape",
                desc: "Dismiss form",
                group: "Form",
                cmd: () => {
                  void sdk.api.form.cancel({ sessionID: props.form.sessionID, formID: props.form.id })
                },
              },
              ...tuiConfig.keybinds.get("app.exit"),
            ]),
      ],
    }
  })

  return (
    <box
      backgroundColor={theme.backgroundPanel}
      border={["left"]}
      borderColor={theme.accent}
      customBorderChars={SplitBorder.customBorderChars}
    >
      <box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
        <Show when={props.form.title}>
          <box paddingLeft={1}>
            <text fg={theme.textMuted}>{props.form.title}</text>
          </box>
        </Show>
        <Show when={!single() && !tabbed()}>
          <box flexDirection="row" gap={1} paddingLeft={1}>
            <text fg={theme.textMuted}>
              {confirm() ? "Review" : `Field ${Math.min(store.tab, fields().length - 1) + 1} of ${fields().length}`}
            </text>
            <text fg={theme.textMuted}>
              · {answered()}/{fields().length} answered
            </text>
          </box>
        </Show>
        <Show when={!single() && tabbed()}>
          <box flexDirection="row" gap={1} paddingLeft={1}>
            <For each={fields()}>
              {(item, index) => {
                const isTab = () => index() === store.tab
                const isAnswered = () => store.answers[item.key] !== undefined
                return (
                  <box
                    paddingLeft={1}
                    paddingRight={1}
                    backgroundColor={
                      isTab() ? theme.accent : tabHover() === index() ? theme.backgroundElement : theme.backgroundPanel
                    }
                    onMouseOver={() => setTabHover(index())}
                    onMouseOut={() => setTabHover(null)}
                    onMouseUp={() => {
                      if (renderer.getSelection()?.getSelectedText()) return
                      selectTabFromMouse(item)
                    }}
                  >
                    <text
                      fg={
                        isTab() ? selectedForeground(theme, theme.accent) : isAnswered() ? theme.text : theme.textMuted
                      }
                    >
                      {truncate(fieldLabel(item), 24)}
                    </text>
                  </box>
                )
              }}
            </For>
            <box
              paddingLeft={1}
              paddingRight={1}
              backgroundColor={
                confirm() ? theme.accent : tabHover() === "confirm" ? theme.backgroundElement : theme.backgroundPanel
              }
              onMouseOver={() => setTabHover("confirm")}
              onMouseOut={() => setTabHover(null)}
              onMouseUp={() => {
                if (renderer.getSelection()?.getSelectedText()) return
                selectTabFromMouse()
              }}
            >
              <text fg={confirm() ? selectedForeground(theme, theme.accent) : theme.textMuted}>Confirm</text>
            </box>
          </box>
        </Show>

        <Show when={!confirm() && field()}>
          <box paddingLeft={1} gap={1}>
            <box>
              <text fg={theme.text}>
                {field()!.description ?? fieldLabel(field()!)}
                {field()!.required ? " (required)" : ""}
                {multi() ? " (select all that apply)" : ""}
              </text>
            </box>
            <Show when={textual() ? field()!.key : undefined} keyed>
              <box paddingLeft={1}>
                <textarea
                  ref={(val: TextareaRenderable) => {
                    textarea = val
                    val.traits = { status: "ANSWER" }
                    queueMicrotask(() => {
                      val.focus()
                      val.gotoLineEnd()
                    })
                  }}
                  initialValue={input() || display(field()!, store.answers[field()!.key])}
                  placeholder={placeholder()}
                  placeholderColor={theme.textMuted}
                  minHeight={1}
                  maxHeight={6}
                  textColor={theme.text}
                  focusedTextColor={theme.text}
                  cursorColor={theme.primary}
                />
              </box>
            </Show>
            <Show when={!textual()}>
              <box>
                <For each={rows()}>
                  {(row, i) => {
                    const active = () => i() === store.selected
                    const picked = () => {
                      const value = store.answers[field()?.key ?? ""]
                      if (Array.isArray(value)) return value.includes(String(row.value))
                      return value === row.value
                    }
                    return (
                      <box
                        onMouseOver={() => setStore("selected", i())}
                        onMouseDown={() => setStore("selected", i())}
                        onMouseUp={() => {
                          if (renderer.getSelection()?.getSelectedText()) return
                          selectOption()
                        }}
                      >
                        <box flexDirection="row">
                          <box backgroundColor={active() ? theme.backgroundElement : undefined} paddingRight={1}>
                            <text fg={active() ? tint(theme.textMuted, theme.secondary, 0.6) : theme.textMuted}>
                              {`${i() + 1}.`}
                            </text>
                          </box>
                          <box backgroundColor={active() ? theme.backgroundElement : undefined}>
                            <text fg={active() ? theme.secondary : picked() ? theme.success : theme.text}>
                              {multi() ? `[${picked() ? "✓" : " "}] ${row.label}` : row.label}
                            </text>
                          </box>
                          <Show when={!multi()}>
                            <text fg={theme.success}>{picked() ? " ✓" : ""}</text>
                          </Show>
                        </box>
                        <Show when={row.description}>
                          <box paddingLeft={3}>
                            <text fg={theme.textMuted}>{row.description}</text>
                          </box>
                        </Show>
                      </box>
                    )
                  }}
                </For>
                <Show when={custom()}>
                  <box
                    onMouseOver={() => setStore("selected", rows().length)}
                    onMouseDown={() => setStore("selected", rows().length)}
                    onMouseUp={() => {
                      if (renderer.getSelection()?.getSelectedText()) return
                      selectOption()
                    }}
                  >
                    <box flexDirection="row">
                      <box backgroundColor={other() ? theme.backgroundElement : undefined} paddingRight={1}>
                        <text fg={other() ? tint(theme.textMuted, theme.secondary, 0.6) : theme.textMuted}>
                          {`${rows().length + 1}.`}
                        </text>
                      </box>
                      <box backgroundColor={other() ? theme.backgroundElement : undefined}>
                        <text fg={other() ? theme.secondary : customPicked() ? theme.success : theme.text}>
                          {multi() ? `[${customPicked() ? "✓" : " "}] Type your own answer` : "Type your own answer"}
                        </text>
                      </box>
                      <Show when={!multi()}>
                        <text fg={theme.success}>{customPicked() ? " ✓" : ""}</text>
                      </Show>
                    </box>
                    <Show when={store.editing}>
                      <box paddingLeft={3}>
                        <textarea
                          ref={(val: TextareaRenderable) => {
                            textarea = val
                            val.traits = { status: "ANSWER" }
                            queueMicrotask(() => {
                              val.focus()
                              val.gotoLineEnd()
                            })
                          }}
                          initialValue={input()}
                          placeholder="Type your own answer"
                          placeholderColor={theme.textMuted}
                          minHeight={1}
                          maxHeight={6}
                          textColor={theme.text}
                          focusedTextColor={theme.text}
                          cursorColor={theme.primary}
                        />
                      </box>
                    </Show>
                    <Show when={!store.editing && input()}>
                      <box paddingLeft={3}>
                        <text fg={theme.textMuted}>{input()}</text>
                      </box>
                    </Show>
                  </box>
                </Show>
              </box>
            </Show>
          </box>
        </Show>

        <Show when={confirm()}>
          <Show when={tabbed()}>
            <box paddingLeft={1}>
              <text fg={theme.text}>Review</text>
            </box>
          </Show>
          <scrollbox
            maxHeight={Math.min(fields().length, Math.max(3, dimensions().height - 14))}
            scrollbarOptions={{ visible: false }}
            ref={(r: ScrollBoxRenderable) => (review = r)}
          >
            <For each={fields()}>
              {(item) => {
                const value = () => display(item, store.answers[item.key])
                const answered = () => {
                  const value = store.answers[item.key]
                  return value !== undefined
                }
                const missing = () => !answered() && item.required === true
                const invalid = () => validateValue(item, store.answers[item.key])
                return (
                  <box paddingLeft={1}>
                    <text>
                      <span style={{ fg: theme.textMuted }}>{truncate(fieldLabel(item), 40)}:</span>{" "}
                      <span
                        style={{ fg: invalid() || missing() ? theme.error : answered() ? theme.text : theme.textMuted }}
                      >
                        {invalid() ?? (answered() ? value() : missing() ? "(required)" : "(not answered)")}
                      </span>
                    </text>
                  </box>
                )
              }}
            </For>
          </scrollbox>
        </Show>
      </box>
      <box
        flexDirection="row"
        flexShrink={0}
        gap={1}
        paddingLeft={2}
        paddingRight={3}
        paddingBottom={1}
        justifyContent="space-between"
      >
        <box flexDirection="row" gap={2}>
          <Show when={!single()}>
            <text fg={theme.text}>
              {"⇆"} <span style={{ fg: theme.textMuted }}>tab</span>
            </text>
          </Show>
          <Show when={!confirm() && !textual()}>
            <text fg={theme.text}>
              {"↑↓"} <span style={{ fg: theme.textMuted }}>select</span>
            </text>
          </Show>
          <Show when={confirm()}>
            <text fg={theme.text}>
              {"↑↓"} <span style={{ fg: theme.textMuted }}>scroll</span>
            </text>
          </Show>
          <text fg={theme.text}>
            enter{" "}
            <span style={{ fg: theme.textMuted }}>
              {confirm() ? "submit" : multi() ? "toggle" : single() ? "submit" : "confirm"}
            </span>
          </text>
          <text fg={theme.text}>
            esc <span style={{ fg: theme.textMuted }}>dismiss</span>
          </text>
        </box>
        <Show when={store.error}>
          <text fg={theme.error}>{store.error}</text>
        </Show>
      </box>
    </box>
  )
}
