import React, { useRef } from 'react';
import stripAnsi from 'strip-ansi';
import { Messages } from '../components/Messages.js';
import { KeybindingProvider } from '../keybindings/KeybindingContext.js';
import { loadKeybindingsSyncWithWarnings } from '../keybindings/loadUserBindings.js';
import type { KeybindingContextName } from '../keybindings/types.js';
import { AppStateProvider } from '../state/AppState.js';
import type { Tools } from '../Tool.js';
import type { Message } from '../types/message.js';
import { renderToAnsiString } from './staticRender.js';

/**
 * Minimal keybinding provider for static/headless renders.
 * Provides keybinding context without the ChordInterceptor (which uses useInput
 * and would hang in headless renders with no stdin).
 */
function StaticKeybindingProvider({ children }: { children: React.ReactNode }): React.ReactNode {
  const { bindings } = loadKeybindingsSyncWithWarnings();
  const pendingChordRef = useRef(null);
  const handlerRegistryRef = useRef(new Map());
  const activeContexts = useRef(new Set<KeybindingContextName>()).current;

  return (
    <KeybindingProvider
      bindings={bindings}
      pendingChordRef={pendingChordRef}
      pendingChord={null}
      setPendingChord={() => {}}
      activeContexts={activeContexts}
      registerActiveContext={() => {}}
      unregisterActiveContext={() => {}}
      handlerRegistryRef={handlerRegistryRef}
    >
      {children}
    </KeybindingProvider>
  );
}

// Upper-bound how many NormalizedMessages a Message can produce.
// normalizeMessages splits one Message with N content blocks into N
// NormalizedMessages — 1:1 with block count. String content = 1 block.
// AttachmentMessage etc. have no .message and normalize to ≤1.
function normalizedUpperBound(m: Message): number {
  if (!('message' in m)) return 1;
  const c = m.message!.content;
  return Array.isArray(c) ? c.length : 1;
}

/**
 * Streams rendered messages in chunks, ANSI codes preserved. Each chunk is a
 * fresh renderToAnsiString — yoga layout tree + Ink's screen buffer are sized
 * to the tallest CHUNK instead of the full session. Measured (Mar 2026,
 * 538-msg session): −55% plateau RSS vs a single full render. The sink owns
 * the output — write to stdout for `[` dump-to-scrollback, appendFile for `v`.
 *
 * Messages.renderRange slices AFTER normalize→group→collapse, so tool-call
 * grouping stays correct across chunk seams; buildMessageLookups runs on
 * the full normalized array so tool_use↔tool_result resolves regardless of
 * which chunk each landed in.
 */
export async function streamRenderedMessages(
  messages: Message[],
  tools: Tools,
  sink: (ansiChunk: string) => void | Promise<void>,
  {
    columns,
    verbose = false,
    chunkSize = 40,
    onProgress,
  }: {
    columns?: number;
    verbose?: boolean;
    chunkSize?: number;
    onProgress?: (rendered: number) => void;
  } = {},
): Promise<void> {
  const renderChunk = (range: readonly [number, number]) =>
    renderToAnsiString(
      <AppStateProvider>
        <StaticKeybindingProvider>
          <Messages
            messages={messages}
            tools={tools}
            commands={[]}
            verbose={verbose}
            toolJSX={null}
            toolUseConfirmQueue={[]}
            inProgressToolUseIDs={new Set()}
            isMessageSelectorVisible={false}
            conversationId="export"
            screen="prompt"
            streamingToolUses={[]}
            showAllInTranscript={true}
            isLoading={false}
            renderRange={range}
          />
        </StaticKeybindingProvider>
      </AppStateProvider>,
      columns,
    );

  // renderRange indexes into the post-collapse array whose length we can't
  // see from here — normalize splits each Message into one NormalizedMessage
  // per content block (unbounded per message), collapse merges some back.
  // Ceiling is the exact normalize output count + chunkSize so the loop
  // always reaches the empty slice where break fires (collapse only shrinks).
  let ceiling = chunkSize;
  for (const m of messages) ceiling += normalizedUpperBound(m);
  for (let offset = 0; offset < ceiling; offset += chunkSize) {
    const ansi = await renderChunk([offset, offset + chunkSize]);
    if (stripAnsi(ansi).trim() === '') break;
    await sink(ansi);
    onProgress?.(offset + chunkSize);
  }
}

/**
 * Renders messages to a plain text string suitable for export.
 * Uses the same React rendering logic as the interactive UI.
 */
export async function renderMessagesToPlainText(
  messages: Message[],
  tools: Tools = [],
  columns?: number,
): Promise<string> {
  const parts: string[] = [];
  await streamRenderedMessages(messages, tools, chunk => void parts.push(stripAnsi(chunk)), { columns });
  return parts.join('');
}
