///|
/// I/O implementation for native target using C FFI
/// Provides raw mode, terminal size, and character-by-character input

// --- C FFI declarations ---

///|
extern "C" fn tui_enable_raw_mode() -> Int = "tui_enable_raw_mode"

///|
extern "C" fn tui_disable_raw_mode() -> Int = "tui_disable_raw_mode"

///|
extern "C" fn tui_is_raw_mode() -> Int = "tui_is_raw_mode"

///|
extern "C" fn tui_get_terminal_cols() -> Int = "tui_get_terminal_cols"

///|
extern "C" fn tui_get_terminal_rows() -> Int = "tui_get_terminal_rows"

///|
extern "C" fn tui_read_byte() -> Int = "tui_read_byte"

///|
#borrow(buf)
extern "C" fn tui_write_bytes_ffi(buf : FixedArray[Byte], len : Int) = "tui_write_bytes"

///|
extern "C" fn tui_flush() = "tui_flush"

///|
extern "C" fn tui_is_tty() -> Int = "tui_is_tty"

///|
extern "C" fn tui_sleep_ms(ms : Int) = "tui_sleep_ms"

///|
extern "C" fn tui_get_time_ms() -> Int = "tui_get_time_ms"

// --- Helper functions ---

///|
/// Decode UTF-8 bytes to a MoonBit String (UTF-16)
fn bytes_to_string(buf : FixedArray[Byte], len : Int) -> String {
  let chars : Array[Char] = []
  let mut i = 0
  while i < len {
    let b0 = buf[i].to_int()
    if b0 < 0x80 {
      // ASCII (single byte)
      chars.push(b0.unsafe_to_char())
      i = i + 1
    } else if (b0 & 0xE0) == 0xC0 && i + 1 < len {
      // 2-byte UTF-8 sequence
      let b1 = buf[i + 1].to_int()
      let code = ((b0 & 0x1F) << 6) | (b1 & 0x3F)
      chars.push(code.unsafe_to_char())
      i = i + 2
    } else if (b0 & 0xF0) == 0xE0 && i + 2 < len {
      // 3-byte UTF-8 sequence
      let b1 = buf[i + 1].to_int()
      let b2 = buf[i + 2].to_int()
      let code = ((b0 & 0x0F) << 12) | ((b1 & 0x3F) << 6) | (b2 & 0x3F)
      chars.push(code.unsafe_to_char())
      i = i + 3
    } else if (b0 & 0xF8) == 0xF0 && i + 3 < len {
      // 4-byte UTF-8 sequence (outside BMP, needs surrogate pair)
      let b1 = buf[i + 1].to_int()
      let b2 = buf[i + 2].to_int()
      let b3 = buf[i + 3].to_int()
      let code = ((b0 & 0x07) << 18) |
        ((b1 & 0x3F) << 12) |
        ((b2 & 0x3F) << 6) |
        (b3 & 0x3F)
      // Encode as UTF-16 surrogate pair
      let adjusted = code - 0x10000
      let high = 0xD800 + (adjusted >> 10)
      let low = 0xDC00 + (adjusted & 0x3FF)
      chars.push(high.unsafe_to_char())
      chars.push(low.unsafe_to_char())
      i = i + 4
    } else {
      // Invalid or unexpected byte, skip
      i = i + 1
    }
  }

  // Convert Array[Char] to String
  chars.iter().map(fn(c) { c.to_string() }).fold(init="", fn(a, b) { a + b })
}

// --- Public API ---

///|
/// Enable raw mode for character-by-character input
pub fn enable_raw_mode() -> Unit {
  let _ = tui_enable_raw_mode()
}

///|
/// Disable raw mode and restore terminal settings
pub fn cleanup_stdin() -> Unit {
  let _ = tui_disable_raw_mode()
}

///|
/// Check if terminal is in raw mode
pub fn is_raw_mode() -> Bool {
  tui_is_raw_mode() != 0
}

///|
/// Get terminal size (columns, rows)
pub fn get_terminal_size() -> (Int, Int) {
  let cols = tui_get_terminal_cols()
  let rows = tui_get_terminal_rows()
  (cols, rows)
}

///|
/// Read a single key from stdin (raw mode)
/// Returns the key as a string (may be escape sequence)
pub fn read_key() -> String {
  let first = tui_read_byte()
  if first < 0 {
    return ""
  }

  // Check for escape sequence
  if first == 0x1b {
    // Try to read more bytes for escape sequence
    let buf = FixedArray::make(16, b'\x00')
    buf[0] = first.to_byte()
    let mut len = 1

    // Read second byte
    let second = tui_read_byte()
    if second < 0 {
      // Just ESC
      return bytes_to_string(buf, len)
    }
    buf[1] = second.to_byte()
    len = 2

    // Check if it's a CSI sequence (ESC [)
    if second == 0x5b {
      // CSI sequence - read until final byte (0x40-0x7E, excluding '[')
      for i = 2; i < 16; i = i + 1 {
        let b = tui_read_byte()
        if b < 0 {
          break
        }
        buf[i] = b.to_byte()
        len = len + 1
        // Final byte of CSI is in 0x40-0x7E range (letters and some symbols)
        // But not '[' (0x5B) and not intermediate/parameter bytes (0x20-0x3F)
        if b >= 0x40 && b <= 0x7e && b != 0x5b {
          break
        }
      }
    } else if second == 0x4f {
      // SS3 sequence (ESC O) - read one more byte
      let b = tui_read_byte()
      if b >= 0 {
        buf[2] = b.to_byte()
        len = 3
      }
    }
    // Other escape sequences: just return what we have

    // Convert to string
    return bytes_to_string(buf, len)
  } else {
    // Single byte character
    let buf = FixedArray::make(4, b'\x00')
    buf[0] = first.to_byte()
    let mut len = 1

    // Helper to read a continuation byte with retry
    // Returns the byte value, or -1 if still not available after retries
    fn read_continuation_byte() -> Int {
      // First try without delay
      let b = tui_read_byte()
      if b >= 0 {
        return b
      }
      // Retry a few times with small delays (IME may send bytes with slight delay)
      for retry = 0; retry < 5; retry = retry + 1 {
        tui_sleep_ms(1) // 1ms delay
        let b = tui_read_byte()
        if b >= 0 {
          return b
        }
      }
      -1
    }

    // Check for UTF-8 multi-byte sequence
    if (first & 0xe0) == 0xc0 {
      // 2-byte sequence
      let b = read_continuation_byte()
      if b >= 0 {
        buf[1] = b.to_byte()
        len = 2
      }
    } else if (first & 0xf0) == 0xe0 {
      // 3-byte sequence
      for i = 1; i < 3; i = i + 1 {
        let b = read_continuation_byte()
        if b < 0 {
          break
        }
        buf[i] = b.to_byte()
        len = len + 1
      }
    } else if (first & 0xf8) == 0xf0 {
      // 4-byte sequence
      for i = 1; i < 4; i = i + 1 {
        let b = read_continuation_byte()
        if b < 0 {
          break
        }
        buf[i] = b.to_byte()
        len = len + 1
      }
    }
    bytes_to_string(buf, len)
  }
}

///|
/// Detect Shift+Enter or Alt+Enter escape sequences
fn is_shift_enter_sequence(key : String) -> Bool {
  key == "\u001b[13;2u" ||
  key == "\u001b[27;2;13~" ||
  key == "\u001bOM" ||
  key == "\u001b\r" ||
  key == "\u001b\n"
}

///|
/// Parse a raw key string into a KeyEvent
fn parse_raw_key_to_event(key : String) -> @events.KeyEvent {
  // Check for escape sequences first
  if key.length() > 1 && key[0].to_int() == 0x1b {
    // Escape sequences
    if key == "\u001b[Z" {
      return @events.KeyEvent::Special(
        @events.SpecialKey::BackTab,
        @events.KeyModifier::Shift,
      )
    } else if is_shift_enter_sequence(key) {
      return @events.KeyEvent::Special(
        @events.SpecialKey::Enter,
        @events.KeyModifier::Shift,
      )
    } else if key == "\u001b[D" {
      return @events.KeyEvent::Special(
        @events.SpecialKey::Left,
        @events.KeyModifier::None,
      )
    } else if key == "\u001b[C" {
      return @events.KeyEvent::Special(
        @events.SpecialKey::Right,
        @events.KeyModifier::None,
      )
    } else if key == "\u001b[A" {
      return @events.KeyEvent::Special(
        @events.SpecialKey::Up,
        @events.KeyModifier::None,
      )
    } else if key == "\u001b[B" {
      return @events.KeyEvent::Special(
        @events.SpecialKey::Down,
        @events.KeyModifier::None,
      )
    } else if key == "\u001b[H" || key == "\u001b[1~" {
      return @events.KeyEvent::Special(
        @events.SpecialKey::Home,
        @events.KeyModifier::None,
      )
    } else if key == "\u001b[F" || key == "\u001b[4~" {
      return @events.KeyEvent::Special(
        @events.SpecialKey::End,
        @events.KeyModifier::None,
      )
    } else if key == "\u001b[3~" {
      return @events.KeyEvent::Special(
        @events.SpecialKey::Delete,
        @events.KeyModifier::None,
      )
    } else {
      // Unknown escape sequence
      return @events.KeyEvent::Char('\u001b', @events.KeyModifier::None)
    }
  } else if key == "\u001b" {
    // Just Escape
    return @events.KeyEvent::Special(
      @events.SpecialKey::Escape,
      @events.KeyModifier::None,
    )
  } else if key.length() == 1 {
    let code = key[0].to_int()
    // Control characters
    if code == 9 {
      return @events.KeyEvent::Special(
        @events.SpecialKey::Tab,
        @events.KeyModifier::None,
      )
    } else if code == 13 {
      return @events.KeyEvent::Special(
        @events.SpecialKey::Enter,
        @events.KeyModifier::None,
      )
    } else if code == 10 {
      // Ctrl+J (LF)
      return @events.KeyEvent::Char('j', @events.KeyModifier::Ctrl)
    } else if code == 127 || code == 8 {
      return @events.KeyEvent::Special(
        @events.SpecialKey::Backspace,
        @events.KeyModifier::None,
      )
    } else if code == 1 {
      return @events.KeyEvent::Char('a', @events.KeyModifier::Ctrl)
    } else if code == 3 {
      return @events.KeyEvent::Char('c', @events.KeyModifier::Ctrl)
    } else if code == 4 {
      return @events.KeyEvent::Char('d', @events.KeyModifier::Ctrl)
    } else if code == 5 {
      return @events.KeyEvent::Char('e', @events.KeyModifier::Ctrl)
    } else if code == 11 {
      return @events.KeyEvent::Char('k', @events.KeyModifier::Ctrl)
    } else if code == 21 {
      return @events.KeyEvent::Char('u', @events.KeyModifier::Ctrl)
    } else if code >= 32 {
      // Printable character
      let c = Int::unsafe_to_char(code)
      return @events.KeyEvent::Char(c, @events.KeyModifier::None)
    } else {
      // Other control character
      return @events.KeyEvent::Char(
        Int::unsafe_to_char(code),
        @events.KeyModifier::Ctrl,
      )
    }
  } else {
    // Multi-byte UTF-8 character
    let chars = key.iter()
    match chars.next() {
      Some(c) => @events.KeyEvent::Char(c, @events.KeyModifier::None)
      None => @events.KeyEvent::Char(' ', @events.KeyModifier::None)
    }
  }
}

///|
/// Encode a Unicode code point to UTF-8 bytes
fn encode_utf8(code : Int, buf : FixedArray[Byte], pos : Int) -> Int {
  if code < 0x80 {
    buf[pos] = code.to_byte()
    1
  } else if code < 0x800 {
    buf[pos] = (0xC0 | (code >> 6)).to_byte()
    buf[pos + 1] = (0x80 | (code & 0x3F)).to_byte()
    2
  } else if code < 0x10000 {
    buf[pos] = (0xE0 | (code >> 12)).to_byte()
    buf[pos + 1] = (0x80 | ((code >> 6) & 0x3F)).to_byte()
    buf[pos + 2] = (0x80 | (code & 0x3F)).to_byte()
    3
  } else {
    buf[pos] = (0xF0 | (code >> 18)).to_byte()
    buf[pos + 1] = (0x80 | ((code >> 12) & 0x3F)).to_byte()
    buf[pos + 2] = (0x80 | ((code >> 6) & 0x3F)).to_byte()
    buf[pos + 3] = (0x80 | (code & 0x3F)).to_byte()
    4
  }
}

///|
/// Print string to stdout without newline
pub fn print_raw(s : String) -> Unit {
  // Convert UTF-16 string to UTF-8 bytes
  let str_len = s.length()
  // Max 4 bytes per code point
  let max_size = str_len * 4
  let buf = FixedArray::make(max_size, b'\x00')
  let mut pos = 0
  let mut i = 0
  while i < str_len {
    let unit = s[i].to_int()

    // Check for surrogate pair (for characters outside BMP)
    if unit >= 0xD800 && unit <= 0xDBFF && i + 1 < str_len {
      let unit2 = s[i + 1].to_int()
      if unit2 >= 0xDC00 && unit2 <= 0xDFFF {
        // Decode surrogate pair to code point
        let code = 0x10000 + ((unit - 0xD800) << 10) + (unit2 - 0xDC00)
        pos = pos + encode_utf8(code, buf, pos)
        i = i + 2
        continue
      }
    }

    // Regular BMP character
    pos = pos + encode_utf8(unit, buf, pos)
    i = i + 1
  }
  tui_write_bytes_ffi(buf, pos)
  tui_flush()
}

///|
/// Sleep for milliseconds
pub fn sleep(ms : Int) -> Unit {
  tui_sleep_ms(ms)
}

///|
/// Get current time in milliseconds (monotonic)
pub fn get_time_ms() -> Int {
  tui_get_time_ms()
}

///|
/// Check if stdin is a TTY
pub fn is_tty() -> Bool {
  tui_is_tty() != 0
}

///|
/// Debug print to stderr
pub fn debug_stderr(s : String) -> Unit {
  println("[DEBUG] " + s)
}

///|
/// Debug stdin state
pub fn debug_stdin_state(_label : String) -> Unit {
  // No-op for native
}

// --- Async versions (using moonbitlang/async) ---

///|
/// Read a line from stdin (line-buffered, cooked mode)
/// Returns the line without trailing newline
pub async fn read_line() -> String {
  let buf = FixedArray::make(1024, b'\x00')
  let n = @stdio.stdin.read(buf, offset=0, max_len=1024)
  if n <= 0 {
    return ""
  }
  let bytes = Bytes::from_array(buf)
  let s = bytes.to_unchecked_string()
  let view = s[:n]
  view.to_owned().trim_end(chars="\n\r").to_owned()
}

///|
/// Print string to stdout without newline (async version)
pub async fn print_raw_async(s : String) -> Unit {
  @stdio.stdout.write(s)
}

///|
/// Sleep for milliseconds (async version)
pub async fn sleep_async(ms : Int) -> Unit {
  @async.sleep(ms)
}

// --- Input session stubs (would need more complex implementation) ---

///|
/// Keypress handler storage
let keypress_handler : Ref[(String) -> Unit] = Ref(fn(_s) {  })

///|
/// Idle handler storage (called when no key input)
let idle_handler : Ref[() -> Unit] = Ref(fn() {  })

///|
let keypress_running : Ref[Bool] = Ref(false)

///|
let inside_listener_loop : Ref[Bool] = Ref(false)

///|
/// Start keypress listener (raw mode) - blocks until stop_keypress_listener is called
/// This provides the same interface as JS where the listener runs until stopped
/// If called from within a handler (e.g., from restore_tui), just updates the handler
pub fn start_keypress_listener(handler : (String) -> Unit) -> Unit {
  keypress_handler.val = handler
  keypress_running.val = true
  enable_raw_mode()

  // If called from within a handler, just update the handler reference
  // The original loop will continue with the new handler
  if inside_listener_loop.val {
    return
  }
  inside_listener_loop.val = true

  // Run polling loop internally - this blocks until stop_keypress_listener is called
  while keypress_running.val {
    let key = read_key()
    if key.length() > 0 {
      (keypress_handler.val)(key)
    } else {
      // Call idle handler when no key input
      (idle_handler.val)()
      sleep(10)
    }
  }
  inside_listener_loop.val = false
}

///|
/// Set idle handler (called periodically when no key input)
pub fn set_idle_handler(handler : () -> Unit) -> Unit {
  idle_handler.val = handler
}

///|
/// Stop keypress listener
pub fn stop_keypress_listener() -> Unit {
  keypress_running.val = false
  cleanup_stdin()
}

///|
/// Poll for keypress (call this in a loop) - for manual polling if needed
pub fn poll_keypress() -> Bool {
  if !keypress_running.val {
    return false
  }
  let key = read_key()
  if key.length() > 0 {
    (keypress_handler.val)(key)
    return true
  }
  false
}

///|
/// Render display state with scroll support
fn render_display_state(
  state : @input.DisplayState,
  start_row : Int,
  start_col : Int,
  max_width : Int,
  max_height : Int,
) -> Unit {
  let mut output = ""
  // Clear area
  for r = 0; r < max_height; r = r + 1 {
    output = output +
      "\u001b[" +
      (start_row + r).to_string() +
      ";" +
      start_col.to_string() +
      "H"
    for w = 0; w < max_width; w = w + 1 {
      output = output + " "
    }
  }
  // Draw visible lines
  for i = 0; i < max_height; i = i + 1 {
    let line_idx = state.scroll_offset + i
    if line_idx >= state.lines.length() {
      break
    }
    let line = state.lines[line_idx]
    output = output +
      "\u001b[" +
      (start_row + i).to_string() +
      ";" +
      start_col.to_string() +
      "H"
    // Show ellipsis for overflow
    if i == 0 && state.has_more_above {
      output = output + "…"
      // Show remaining chars that fit
      let mut w = 1
      for c in line.chars {
        let cw = @core.char_display_width(c)
        if w + cw > max_width {
          break
        }
        output = output + c.to_string()
        w = w + cw
      }
    } else if i == max_height - 1 && state.has_more_below {
      // Show chars that fit, then ellipsis
      let mut w = 0
      for c in line.chars {
        let cw = @core.char_display_width(c)
        if w + cw > max_width - 1 {
          break
        }
        output = output + c.to_string()
        w = w + cw
      }
      output = output + "…"
    } else {
      for c in line.chars {
        output = output + c.to_string()
      }
    }
  }
  print_raw(output)
}

///|
/// Helper to convert buffer to string
fn buf_to_string(buf : Array[Char]) -> String {
  buf.iter().map(fn(c) { c.to_string() }).fold(init="", fn(a, b) { a + b })
}

///|
/// Start inplace input (native implementation with raw mode editing)
/// Blocks until user confirms, cancels, or tabs
/// on_completion: Optional callback (text, cursor_pos) -> completion_suffix
///                If provided and returns Some, Tab inserts the completion
pub fn start_inplace_input(
  row : Int,
  col : Int,
  width : Int,
  height : Int,
  multiline : Bool,
  initial : String,
  on_result : (InputResult) -> Unit,
  confirm_on_shift_enter? : Bool = false,
  esc_cancels? : Bool = true,
  on_lines_change? : ((Int) -> Int)? = None,
  on_completion? : ((String, Int) -> String?)? = None,
) -> Unit {
  // Ensure raw mode is enabled
  let was_raw = is_raw_mode()
  if !was_raw {
    enable_raw_mode()
  }

  // Create controller with configuration
  let config = @input.TextInputConfig::new(
    width,
    height,
    multiline~,
    confirm_on_shift_enter~,
    esc_cancels~,
  )
  let controller = @input.TextInputController::new(config, initial)

  // Current height (can change dynamically)
  let mut current_height = height
  // Base row is the bottom of input area (fixed)
  let base_row = row + height - 1

  // Helper to render and position cursor with scroll
  fn do_render() {
    let state = controller.get_display_state()
    // Calculate current start row (grows upward)
    let old_height = current_height
    let old_start_row = base_row - old_height + 1
    // Update height if callback provided
    match on_lines_change {
      Some(callback) => {
        let new_height = callback(state.total_lines)
        if new_height != current_height {
          current_height = new_height
          controller.resize(width, new_height)
        }
      }
      None => ()
    }
    // Calculate new start row
    let start_row = base_row - current_height + 1
    // Clear old area if height changed
    if old_height != current_height {
      if current_height < old_height {
        // Shrinking: clear above
        for r = old_start_row; r < start_row; r = r + 1 {
          print_raw(
            "\u001b[" +
            r.to_string() +
            ";" +
            col.to_string() +
            "H" +
            " ".repeat(width),
          )
        }
      } else {
        // Growing: clear new area above
        for r = start_row; r < old_start_row; r = r + 1 {
          print_raw(
            "\u001b[" +
            r.to_string() +
            ";" +
            col.to_string() +
            "H" +
            " ".repeat(width),
          )
        }
      }
    }
    // Get updated state after potential resize
    let state = controller.get_display_state()
    // Render with scroll (at calculated start row)
    render_display_state(state, start_row, col, width, current_height)
    // Position cursor
    let display_row = start_row + state.cursor_line - state.scroll_offset
    let display_col = col + state.cursor_col
    print_raw(
      "\u001b[" + display_row.to_string() + ";" + display_col.to_string() + "H",
    )
    print_raw(@render.ansi_show_cursor())
  }

  // Initial render
  do_render()

  // Edit loop
  for ;; {
    let key = read_key()
    if key.length() == 0 {
      sleep(10)
      continue
    }
    let mut need_render = false
    let mut done = false
    let mut result : InputResult = InputResult::Cancelled

    // Handle multi-character printable input (IME) before normal parsing
    // parse_raw_key_to_event only handles single characters for printable input
    let action = if key.length() > 1 && key[0].to_int() != 0x1b {
      // Not an escape sequence - check if all chars are printable
      let chars = key.to_array()
      let mut all_printable = true
      for c in chars {
        let code = c.to_int()
        if !((code >= 32 && code < 127) || code >= 128) {
          all_printable = false
          break
        }
      }
      if all_printable {
        // Multi-character printable input (IME) - insert entire string
        @input.EditAction::Insert(key)
      } else {
        // Parse normally
        let event = parse_raw_key_to_event(key)
        @input.interpret_key(event, config)
      }
    } else {
      // Single character or escape sequence - parse normally
      let event = parse_raw_key_to_event(key)
      @input.interpret_key(event, config)
    }

    // Handle special actions that require custom logic
    match action {
      @input.EditAction::Tab => {
        // Tab - try completion first, then confirm and go to next
        let mut handled_by_completion = false
        match on_completion {
          Some(provider) => {
            let text = controller.get_text()
            let completion_result = provider(text, controller.get_cursor_pos())
            match completion_result {
              Some(suffix) =>
                if suffix.length() > 0 {
                  let _ = controller.apply_action(
                    @input.EditAction::Insert(suffix),
                  )
                  need_render = true
                  handled_by_completion = true
                }
              None => ()
            }
          }
          None => ()
        }
        if !handled_by_completion {
          done = true
          result = InputResult::TabNext(controller.get_text())
        }
      }
      @input.EditAction::BackTab => {
        done = true
        result = InputResult::TabPrev(controller.get_text())
      }
      @input.EditAction::Confirm => {
        done = true
        result = InputResult::Confirmed(controller.get_text())
      }
      @input.EditAction::Cancel => {
        done = true
        result = InputResult::Cancelled
      }
      @input.EditAction::ForceQuit => {
        done = true
        result = InputResult::ForceQuit
      }
      @input.EditAction::None => ()
      _ => {
        // Apply edit action to controller
        let handled = controller.apply_action(action)
        if handled {
          need_render = true
        }
      }
    }

    // Handle Ctrl+D (confirm) - not captured by interpret_key
    if key.length() == 1 && key[0].to_int() == 4 {
      done = true
      result = InputResult::Confirmed(controller.get_text())
    }
    if need_render {
      do_render()
    }
    if done {
      // Hide cursor before exiting
      print_raw(@render.ansi_hide_cursor())
      // Restore raw mode state if we changed it
      if !was_raw {
        cleanup_stdin()
      }
      on_result(result)
      return
    }
  }
}

///|
/// Start inline input with cooked mode (native implementation)
pub fn start_inline_input_cooked(
  field_name : String,
  initial : String,
  on_result : (InputResult) -> Unit,
  confirm_on_shift_enter? : Bool = false,
  esc_cancels? : Bool = true,
) -> Unit {
  // Ensure raw mode is enabled for predictable key handling
  let was_raw = is_raw_mode()
  if !was_raw {
    enable_raw_mode()
  }
  let _ = field_name
  print_raw(@render.ansi_show_cursor())
  let mut start_row = 1
  let mut start_col = 1
  let mut can_render = false
  let mut rendered_lines = 1
  let mut max_rows = 1
  let mut max_cols = 1
  let reserve_rows = 1
  fn parse_cursor_pos(resp : String) -> (Int, Int)? {
    if !resp.has_prefix("\u001b[") || !resp.has_suffix("R") {
      return None
    }
    if resp.length() < 4 {
      return None
    }
    let body = resp[2:resp.length() - 1]
    let mut row = 0
    let mut col = 0
    let mut seen_sep = false
    for c in body {
      if c == ';' {
        if seen_sep {
          return None
        }
        seen_sep = true
        continue
      }
      if !c.is_ascii_digit() {
        return None
      }
      let digit = c.to_int() - '0'.to_int()
      if !seen_sep {
        row = row * 10 + digit
      } else {
        col = col * 10 + digit
      }
    }
    if !seen_sep {
      return None
    }
    Some((row, col))
  }

  // Buffer to store any user input that was read while getting cursor position
  let pending_input : Array[String] = []
  fn read_cursor_pos_fn() -> (Int, Int)? {
    print_raw("\u001b[6n")
    // Try to read cursor position response, but save any user input
    for attempt = 0; attempt < 10; attempt = attempt + 1 {
      let resp = read_key()
      if resp.length() == 0 {
        sleep(5)
        continue
      }
      match parse_cursor_pos(resp) {
        Some(pos) => return Some(pos)
        None => {
          // Not a cursor position response - it's user input, save it
          pending_input.push(resp)
          // Keep trying for a bit in case cursor response comes later
          continue
        }
      }
    }
    None
  }

  match read_cursor_pos_fn() {
    Some((row, col)) => {
      start_row = row
      start_col = col
      can_render = true
      let (cols, rows) = get_terminal_size()
      let available = rows - start_row + 1 - reserve_rows
      max_rows = if available > 0 { available } else { 1 }
      let available_cols = cols - start_col + 1
      max_cols = if available_cols > 0 { available_cols } else { 1 }
    }
    None => ()
  }

  // Create controller with configuration
  // For cooked mode, we use multiline mode with confirm_on_shift_enter logic
  let config = @input.TextInputConfig::new(
    max_cols,
    max_rows,
    multiline=true,
    confirm_on_shift_enter~,
    esc_cancels~,
  )
  let controller = @input.TextInputController::new(config, initial)
  fn render_all() -> Bool {
    if !can_render {
      return false
    }
    let state = controller.get_display_state()
    let max_visible = if max_rows > 0 { max_rows } else { 1 }
    let start_idx = state.scroll_offset
    let end_idx = if start_idx + max_visible < state.lines.length() {
      start_idx + max_visible
    } else {
      state.lines.length()
    }
    let visible_len = end_idx - start_idx
    let clear_lines = if rendered_lines < max_rows {
      rendered_lines
    } else {
      max_rows
    }
    for i = 0; i < clear_lines; i = i + 1 {
      let row = start_row + i
      print_raw("\u001b[" + row.to_string() + ";" + start_col.to_string() + "H")
      print_raw("\u001b[K")
    }
    for i = start_idx; i < end_idx; i = i + 1 {
      let row = start_row + (i - start_idx)
      print_raw("\u001b[" + row.to_string() + ";" + start_col.to_string() + "H")
      print_raw(buf_to_string(state.lines[i].chars))
    }
    rendered_lines = if visible_len > 0 { visible_len } else { 1 }
    let mut row_offset = state.cursor_line - state.scroll_offset
    if row_offset < 0 {
      row_offset = 0
    }
    if row_offset >= max_visible {
      row_offset = max_visible - 1
    }
    let display_row = start_row + row_offset
    let display_col = start_col + state.cursor_col
    print_raw(
      "\u001b[" + display_row.to_string() + ";" + display_col.to_string() + "H",
    )
    print_raw(@render.ansi_show_cursor())
    true
  }

  if !render_all() && controller.get_text().length() > 0 {
    print_raw(controller.get_text())
  }

  // Process any pending input that was captured during cursor position query
  for pending in pending_input {
    // Handle multi-character printable input (IME)
    let action = if pending.length() > 1 && pending[0].to_int() != 0x1b {
      let chars = pending.to_array()
      let mut all_printable = true
      for c in chars {
        let code = c.to_int()
        if !((code >= 32 && code < 127) || code >= 128) {
          all_printable = false
          break
        }
      }
      if all_printable {
        @input.EditAction::Insert(pending)
      } else {
        let event = parse_raw_key_to_event(pending)
        @input.interpret_key(event, config)
      }
    } else {
      let event = parse_raw_key_to_event(pending)
      @input.interpret_key(event, config)
    }
    match action {
      @input.EditAction::Confirm
      | @input.EditAction::Cancel
      | @input.EditAction::ForceQuit => ()
      _ => {
        let handled = controller.apply_action(action)
        if handled {
          let _ = render_all()
        }
      }
    }
  }
  for ;; {
    let key = read_key()
    if key.length() == 0 {
      sleep(10)
      continue
    }
    let mut need_render = false
    let mut done = false
    let mut result : InputResult = InputResult::Cancelled

    // Handle multi-character printable input (IME) before normal parsing
    let action = if key.length() > 1 && key[0].to_int() != 0x1b {
      // Not an escape sequence - check if all chars are printable
      let chars = key.to_array()
      let mut all_printable = true
      for c in chars {
        let code = c.to_int()
        if !((code >= 32 && code < 127) || code >= 128) {
          all_printable = false
          break
        }
      }
      if all_printable {
        @input.EditAction::Insert(key)
      } else {
        let event = parse_raw_key_to_event(key)
        @input.interpret_key(event, config)
      }
    } else {
      let event = parse_raw_key_to_event(key)
      @input.interpret_key(event, config)
    }

    // Handle special actions
    match action {
      @input.EditAction::Confirm => {
        done = true
        let value = controller.get_text()
        result = InputResult::Confirmed(
          if value.length() == 0 {
            initial
          } else {
            value
          },
        )
      }
      @input.EditAction::Cancel => {
        done = true
        result = InputResult::Cancelled
      }
      @input.EditAction::ForceQuit => {
        done = true
        result = InputResult::ForceQuit
      }
      @input.EditAction::None => ()
      _ => {
        // Apply edit action to controller
        let handled = controller.apply_action(action)
        if handled {
          need_render = true
        }
      }
    }
    if need_render {
      let _ = render_all()
    }
    if done {
      if !was_raw {
        cleanup_stdin()
      }
      on_result(result)
      return
    }
  }
}

// --- Functions for JS compatibility ---

///|
/// Keep alive non-blocking (no-op for native, used in JS for event loop)
pub fn keep_alive_nonblocking(_ms : Int) -> Unit {
  // No-op for native - native uses blocking loop
}

///|
/// Start inline input (same as inplace input for native)
pub fn start_inline_input(
  row : Int,
  col : Int,
  width : Int,
  height : Int,
  multiline : Bool,
  initial : String,
  on_result : (InputResult) -> Unit,
) -> Unit {
  start_inplace_input(row, col, width, height, multiline, initial, on_result)
}

///|
/// Start render ticker (stub - native uses polling loop instead)
pub fn start_render_ticker(_ms : Int, _callback : () -> Unit) -> Int {
  // Native doesn't need a render ticker - it uses a polling loop
  0
}

///|
/// Stop render ticker (stub)
pub fn stop_render_ticker(_ticker_id : Int) -> Unit {
  // No-op for native
}

///|
/// Start resize listener (stub - native does not emit resize events)
pub fn start_resize_listener(_callback : (Int, Int) -> Unit) -> Unit {
  // No-op for native
}

///|
/// Stop resize listener (stub)
pub fn stop_resize_listener() -> Unit {
  // No-op for native
}

///|
/// Start input session with callback (wrapper for start_inplace_input)
pub fn start_input_session_cb(
  row : Int,
  col : Int,
  width : Int,
  height : Int,
  multiline : Bool,
  initial : String,
  on_result : (InputResult) -> Unit,
) -> Unit {
  start_inplace_input(row, col, width, height, multiline, initial, on_result)
}

///|
/// Abort input session (stub)
pub fn abort_input_session() -> Unit {
  // Native input is blocking, cannot be aborted externally
}

// --- Environment and Arguments ---

///|
/// Get command line arguments (not available in native mode)
pub fn get_args() -> Array[String] {
  // Native mode doesn't have easy access to argv
  []
}

///|
/// Get environment variable value (not available in native mode)
pub fn get_env(_name : String) -> String {
  // Native mode doesn't have easy access to getenv
  ""
}

///|
/// Check if running in headless mode (always false for native)
pub fn is_headless() -> Bool {
  false
}