///|
/// 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"

// --- 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
    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

    // Check for UTF-8 multi-byte sequence
    if (first & 0xe0) == 0xc0 {
      // 2-byte sequence
      let b = tui_read_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 = tui_read_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 = tui_read_byte()
        if b < 0 {
          break
        }
        buf[i] = b.to_byte()
        len = len + 1
      }
    }
    bytes_to_string(buf, len)
  }
}

///|
/// 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)
}

///|
/// 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_string().trim_end(chars="\n\r").to_string()
}

///|
/// 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] = { val: fn(_s) {  } }

///|
let keypress_running : Ref[Bool] = { val: false }

///|
let inside_listener_loop : Ref[Bool] = { val: 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 {
      sleep(10)
    }
  }
  inside_listener_loop.val = false
}

///|
/// 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 not(keypress_running.val) {
    return false
  }
  let key = read_key()
  if key.length() > 0 {
    (keypress_handler.val)(key)
    return true
  }
  false
}

///|
/// Get character display width (simplified: ASCII=1, CJK=2)
fn char_width_code(code : Int) -> Int {
  if code < 0x80 {
    1
  } else if code >= 0x1100 &&
    (
      code <= 0x115F ||
      code == 0x2329 ||
      code == 0x232A ||
      (code >= 0x2E80 && code <= 0x9FFF) ||
      (code >= 0xAC00 && code <= 0xD7A3) ||
      (code >= 0xF900 && code <= 0xFAFF) ||
      (code >= 0xFE10 && code <= 0xFE1F) ||
      (code >= 0xFE30 && code <= 0xFE6F) ||
      (code >= 0xFF00 && code <= 0xFF60) ||
      (code >= 0xFFE0 && code <= 0xFFE6)
    ) {
    2
  } else {
    1
  }
}

///|
/// Line info for scrolling
priv struct LineInfo {
  start_idx : Int
  chars : Array[Char]
}

///|
/// Wrap text into lines (considering newlines and max_width)
fn wrap_text_to_lines(buf : Array[Char], max_width : Int) -> Array[LineInfo] {
  let lines : Array[LineInfo] = []
  let mut current_chars : Array[Char] = []
  let mut current_width = 0
  let mut line_start_idx = 0
  for i, c in buf {
    let code = c.to_int()
    // Handle newline
    if code == 10 {
      lines.push({ start_idx: line_start_idx, chars: current_chars })
      current_chars = []
      current_width = 0
      line_start_idx = i + 1
      continue
    }
    let cw = char_width_code(code)
    // Wrap if needed
    if current_width + cw > max_width {
      lines.push({ start_idx: line_start_idx, chars: current_chars })
      current_chars = [c]
      current_width = cw
      line_start_idx = i
    } else {
      current_chars.push(c)
      current_width = current_width + cw
    }
  }
  lines.push({ start_idx: line_start_idx, chars: current_chars })
  lines
}

///|
/// Find cursor line index and column offset
fn find_cursor_line(
  lines : Array[LineInfo],
  cursor : Int
) -> (Int, Int) {
  for line_idx, line in lines {
    let line_end = line.start_idx + line.chars.length()
    if cursor <= line_end {
      let pos_in_line = cursor - line.start_idx
      let mut col_offset = 0
      for i = 0; i < pos_in_line && i < line.chars.length(); i = i + 1 {
        col_offset = col_offset + char_width_code(line.chars[i].to_int())
      }
      return (line_idx, col_offset)
    }
  }
  // Cursor at end
  let last_idx = lines.length() - 1
  let last_line = lines[last_idx]
  let mut col_offset = 0
  for c in last_line.chars {
    col_offset = col_offset + char_width_code(c.to_int())
  }
  (last_idx, col_offset)
}

///|
/// Render buffer with scroll support
fn render_inplace_buffer_scroll(
  buf : Array[Char],
  start_row : Int,
  start_col : Int,
  max_width : Int,
  max_height : Int,
  scroll_offset : Int
) -> Unit {
  let lines = wrap_text_to_lines(buf, max_width)
  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
  let has_more_above = scroll_offset > 0
  let has_more_below = lines.length() > scroll_offset + max_height
  for i = 0; i < max_height; i = i + 1 {
    let line_idx = scroll_offset + i
    if line_idx >= lines.length() {
      break
    }
    let line = lines[line_idx]
    output = output +
      "\u001b[" +
      (start_row + i).to_string() +
      ";" +
      start_col.to_string() +
      "H"
    // Show ellipsis for overflow
    if i == 0 && has_more_above {
      output = output + "…"
      // Show remaining chars that fit
      let mut w = 1
      for c in line.chars {
        let cw = char_width_code(c.to_int())
        if w + cw > max_width {
          break
        }
        output = output + c.to_string()
        w = w + cw
      }
    } else if i == max_height - 1 && has_more_below {
      // Show chars that fit, then ellipsis
      let mut w = 0
      for c in line.chars {
        let cw = char_width_code(c.to_int())
        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
pub fn start_inplace_input(
  row : Int,
  col : Int,
  width : Int,
  height : Int,
  multiline : Bool,
  initial : String,
  on_result : (InputResult) -> Unit,
  on_lines_change~ : ((Int) -> Int)? = None,
) -> Unit {
  // Ensure raw mode is enabled
  let was_raw = is_raw_mode()
  if not(was_raw) {
    enable_raw_mode()
  }

  // Initialize buffer with initial value
  let buf : Array[Char] = []
  for i = 0; i < initial.length(); i = i + 1 {
    buf.push(Int::unsafe_to_char(initial[i].to_int()))
  }

  // Cursor position within buffer
  let mut cursor_pos = buf.length()
  // Scroll offset for multiline
  let mut scroll_offset = 0
  // Current height (can change dynamically)
  let mut current_height = height
  // Base row is the bottom of input area (fixed)
  // Input grows upward from this position
  let base_row = row + height - 1

  // Helper to render and position cursor with scroll
  fn do_render() {
    let lines = wrap_text_to_lines(buf, width)
    let (cursor_line_idx, cursor_col_offset) = find_cursor_line(lines, cursor_pos)
    // 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(lines.length())
        if new_height != current_height {
          current_height = new_height
          scroll_offset = 0
        }
      }
      None => ()
    }
    // Calculate new start row
    let start_row = base_row - current_height + 1
    // Clear old area if height changed
    if old_height != current_height {
      // Clear the area that's no longer used
      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),
          )
        }
      }
    }
    // Adjust scroll to keep cursor visible
    if cursor_line_idx < scroll_offset {
      scroll_offset = cursor_line_idx
    } else if cursor_line_idx >= scroll_offset + current_height {
      scroll_offset = cursor_line_idx - current_height + 1
    }
    // Render with scroll (at calculated start row)
    render_inplace_buffer_scroll(buf, start_row, col, width, current_height, scroll_offset)
    // Position cursor
    let display_row = start_row + cursor_line_idx - scroll_offset
    let display_col = col + cursor_col_offset
    print_raw(
      "\u001b[" + display_row.to_string() + ";" + display_col.to_string() + "H",
    )
    print_raw(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

    // Check for escape sequences
    if key.length() > 1 && key[0].to_int() == 0x1b {
      if key == "\u001b[Z" {
        // Shift+Tab - confirm and go to previous
        done = true
        result = InputResult::TabPrev(buf_to_string(buf))
      } else if key == "\u001b[13;2u" ||
        key == "\u001b[27;2;13~" ||
        key == "\u001bOM" ||
        key == "\u001b\r" ||
        key == "\u001b\n" {
        // Shift+Enter or Alt+Enter - insert newline
        // ESC CR or ESC LF is Alt+Enter in many terminals
        buf.insert(cursor_pos, '\n')
        cursor_pos = cursor_pos + 1
        need_render = true
      } else if key == "\u001b[D" {
        // Left arrow - move cursor left
        if cursor_pos > 0 {
          cursor_pos = cursor_pos - 1
          need_render = true
        }
      } else if key == "\u001b[C" {
        // Right arrow - move cursor right
        if cursor_pos < buf.length() {
          cursor_pos = cursor_pos + 1
          need_render = true
        }
      } else if key == "\u001b[A" || key == "\u001b[B" {
        // Up/Down arrows - ignore for now (TODO: multiline navigation)
        ()
      } else if key == "\u001b[H" {
        // Home - move to start
        cursor_pos = 0
        need_render = true
      } else if key == "\u001b[F" {
        // End - move to end
        cursor_pos = buf.length()
        need_render = true
      }
    } else if key == "\u001b" {
      // Escape - cancel
      done = true
      result = InputResult::Cancelled
    } else if key.length() == 1 {
      let code = key[0].to_int()
      if code == 9 {
        // Tab - confirm and go to next
        done = true
        result = InputResult::TabNext(buf_to_string(buf))
      } else if code == 13 {
        // Enter (CR) - confirm (both single and multiline)
        done = true
        result = InputResult::Confirmed(buf_to_string(buf))
      } else if code == 10 {
        // Ctrl+J (LF) - newline in multiline, confirm in single line
        if multiline {
          buf.insert(cursor_pos, '\n')
          cursor_pos = cursor_pos + 1
          need_render = true
        } else {
          done = true
          result = InputResult::Confirmed(buf_to_string(buf))
        }
      } else if code == 4 {
        // Ctrl+D - also confirm
        done = true
        result = InputResult::Confirmed(buf_to_string(buf))
      } else if code == 3 {
        // Ctrl+C - cancel
        done = true
        result = InputResult::Cancelled
      } else if code == 127 || code == 8 {
        // Backspace - delete char before cursor
        if cursor_pos > 0 {
          let _ = buf.remove(cursor_pos - 1)
          cursor_pos = cursor_pos - 1
          need_render = true
        }
      } else if code == 1 {
        // Ctrl+A - move to start of current line
        let mut new_pos = 0
        for i = cursor_pos - 1; i >= 0; i = i - 1 {
          if buf[i] == '\n' {
            new_pos = i + 1
            break
          }
        }
        cursor_pos = new_pos
        need_render = true
      } else if code == 5 {
        // Ctrl+E - move to end of current line
        let mut new_pos = buf.length()
        for i = cursor_pos; i < buf.length(); i = i + 1 {
          if buf[i] == '\n' {
            new_pos = i
            break
          }
        }
        cursor_pos = new_pos
        need_render = true
      } else if code == 11 {
        // Ctrl+K - delete from cursor to end of current line
        let mut end_pos = buf.length()
        for i = cursor_pos; i < buf.length(); i = i + 1 {
          if buf[i] == '\n' {
            end_pos = i
            break
          }
        }
        while cursor_pos < end_pos && buf.length() > cursor_pos {
          let _ = buf.remove(cursor_pos)
          end_pos = end_pos - 1
        }
        need_render = true
      } else if code == 21 {
        // Ctrl+U - delete from start of current line to cursor
        let mut start_pos = 0
        for i = cursor_pos - 1; i >= 0; i = i - 1 {
          if buf[i] == '\n' {
            start_pos = i + 1
            break
          }
        }
        while cursor_pos > start_pos {
          let _ = buf.remove(start_pos)
          cursor_pos = cursor_pos - 1
        }
        need_render = true
      } else if code >= 32 {
        // Printable character - insert at cursor (no limit with scroll)
        let c = Int::unsafe_to_char(code)
        buf.insert(cursor_pos, c)
        cursor_pos = cursor_pos + 1
        need_render = true
      }
    } else {
      // Multi-byte UTF-8 character - insert at cursor
      // Use iterator to properly handle Unicode codepoints
      for c in key {
        buf.insert(cursor_pos, c)
        cursor_pos = cursor_pos + 1
      }
      need_render = true
    }
    if need_render {
      do_render()
    }
    if done {
      // Hide cursor before exiting
      print_raw(ansi_hide_cursor())
      // Restore raw mode state if we changed it
      if not(was_raw) {
        cleanup_stdin()
      }
      on_result(result)
      return
    }
  }
}

///|
/// Start inline input with cooked mode (stub)
pub fn start_inline_input_cooked(
  _field_name : String,
  _initial : String,
  _on_result : (InputResult) -> Unit,
) -> Unit {
  // Cooked mode input would need async context
  // This is a stub
}

// --- 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 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
}