import * as React from 'react';
import { useLayoutEffect } from 'react';
import { PassThrough } from 'stream';
import stripAnsi from 'strip-ansi';
import { wrappedRender as render, useApp } from '@anthropic/ink';

// This is a workaround for the fact that Ink doesn't support multiple <Static>
// components in the same render tree. Instead of using a <Static> we just render
// the component to a string and then print it to stdout

/**
 * Wrapper component that exits after rendering.
 * Uses useLayoutEffect to ensure we wait for React's commit phase to complete
 * before exiting. This is more robust than process.nextTick() for React 19's
 * async render cycle.
 */
function RenderOnceAndExit({ children }: { children: React.ReactNode }): React.ReactNode {
  const { exit } = useApp();

  // useLayoutEffect runs synchronously after React commits DOM mutations.
  // setTimeout(0) defers exit to allow Ink to flush output to the stream.
  useLayoutEffect(() => {
    const timer = setTimeout(exit, 0);
    return () => clearTimeout(timer);
  }, [exit]);

  return <>{children}</>;
}

// DEC synchronized update markers used by terminals
const SYNC_START = '\x1B[?2026h';
const SYNC_END = '\x1B[?2026l';

/**
 * Extracts content from the first complete frame in Ink's output.
 * Ink with non-TTY stdout outputs multiple frames, each wrapped in DEC synchronized
 * update sequences ([?2026h ... [?2026l). We only want the first frame's content.
 */
function extractFirstFrame(output: string): string {
  const startIndex = output.indexOf(SYNC_START);
  if (startIndex === -1) return output;

  const contentStart = startIndex + SYNC_START.length;
  const endIndex = output.indexOf(SYNC_END, contentStart);
  if (endIndex === -1) return output;

  return output.slice(contentStart, endIndex);
}

/**
 * Renders a React node to a string with ANSI escape codes (for terminal output).
 */
export async function renderToAnsiString(node: React.ReactNode, columns?: number): Promise<string> {
  let output = '';

  // Capture all writes. Set .columns so Ink (ink.tsx:~165) picks up a
  // chosen width instead of PassThrough's undefined → 80 fallback —
  // useful for rendering at terminal width for file dumps that should
  // match what the user sees on screen.
  const stream = new PassThrough();
  if (columns !== undefined) {
    (stream as unknown as { columns: number }).columns = columns;
  }
  stream.on('data', chunk => {
    output += chunk.toString();
  });

  // Render the component wrapped in RenderOnceAndExit
  // Non-TTY stdout (PassThrough) gives full-frame output instead of diffs
  const instance = await render(<RenderOnceAndExit>{node}</RenderOnceAndExit>, {
    stdout: stream as unknown as NodeJS.WriteStream,
    patchConsole: false,
  });

  // Wait for the component to exit naturally
  await instance.waitUntilExit();

  // Extract only the first frame's content to avoid duplication
  // (Ink outputs multiple frames in non-TTY mode)
  return extractFirstFrame(output);
}

/**
 * Renders a React node to a plain text string (ANSI codes stripped).
 */
export async function renderToString(node: React.ReactNode, columns?: number): Promise<string> {
  const output = await renderToAnsiString(node, columns);
  return stripAnsi(output);
}
