/**
 * Markdown rendering component for ink-terminal.
 *
 * Adapted from Claude Code's Markdown.tsx. Key patterns extracted:
 * 1. LRU token cache (500 entries, hash-keyed, MRU promotion)
 * 2. hasMarkdownSyntax fast-path optimization (skip lexer for plain text)
 * 3. Stable/unstable prefix splitting for streaming mode
 *
 * Simplified vs Claude Code:
 * - No useSettings/useTheme — standalone component
 * - No syntax highlighting (code blocks render as dim text)
 * - No MarkdownTable React component — tables rendered inline as ANSI strings
 * - No stripPromptXMLTags — no Claude-specific preprocessing
 * - No React compiler runtime (_c) — uses standard React.memo/useMemo
 */

import { marked, type Token } from 'marked'
import React, { useMemo, useRef } from 'react'
import { Ansi } from '../react/Ansi.js'
import Box from '../react/components/Box.js'
import { formatToken } from './format-token.js'
import { LRUTokenCache } from './token-cache.js'

// ---------------------------------------------------------------------------
// Marked configuration
// ---------------------------------------------------------------------------

let markedConfigured = false

function configureMarked(): void {
	if (markedConfigured) return
	markedConfigured = true

	// Disable strikethrough parsing — the model often uses ~ for "approximate"
	// (e.g., ~100) and rarely intends actual strikethrough formatting.
	marked.use({
		tokenizer: {
			del() {
				return undefined
			},
		},
	})
}

// ---------------------------------------------------------------------------
// Token cache + fast-path
// ---------------------------------------------------------------------------

const tokenCache = new LRUTokenCache<Token[]>(500)

// Single regex: matches any MD marker or ordered-list start (N. at line start).
// One pass instead of many indexOf scans.
const MD_SYNTAX_RE = /[#*`|[>\-_~]|\n\n|^\d+\. |\n\d+\. /

/**
 * Fast check for markdown syntax presence. If none is found, skip the
 * ~3ms marked.lexer call entirely — render as a single paragraph. Covers
 * the majority of short responses that are plain sentences.
 */
function hasMarkdownSyntax(s: string): boolean {
	// Sample first 500 chars — if markdown exists it's usually early (headers,
	// code fence, list). Long outputs are mostly plain text tails.
	return MD_SYNTAX_RE.test(s.length > 500 ? s.slice(0, 500) : s)
}

/**
 * Parse markdown content into tokens, with LRU caching and fast-path
 * for plain text.
 */
function cachedLexer(content: string): Token[] {
	// Fast path: plain text with no markdown syntax -> single paragraph token.
	if (!hasMarkdownSyntax(content)) {
		return [
			{
				type: 'paragraph',
				raw: content,
				text: content,
				tokens: [{ type: 'text', raw: content, text: content }],
			} as Token,
		]
	}

	const hit = tokenCache.get(content)
	if (hit) return hit

	const tokens = marked.lexer(content)
	tokenCache.set(content, tokens)
	return tokens
}

// ---------------------------------------------------------------------------
// Component props
// ---------------------------------------------------------------------------

type MarkdownProps = {
	children: string
	/** When true, render all text content as dim */
	dimColor?: boolean
	/** When true, use streaming mode with stable/unstable prefix splitting */
	streaming?: boolean
}

// ---------------------------------------------------------------------------
// Public components
// ---------------------------------------------------------------------------

/**
 * Renders markdown content as styled ANSI text inside an ink-terminal layout.
 *
 * Basic usage:
 * ```tsx
 * <Markdown>{markdownString}</Markdown>
 * ```
 *
 * Streaming usage (re-parses only the unstable tail):
 * ```tsx
 * <Markdown streaming>{partialMarkdownString}</Markdown>
 * ```
 */
export function Markdown({
	children,
	dimColor,
	streaming,
}: MarkdownProps): React.ReactNode {
	if (streaming) {
		return <StreamingMarkdown dimColor={dimColor}>{children}</StreamingMarkdown>
	}
	return <MarkdownBody dimColor={dimColor}>{children}</MarkdownBody>
}

// ---------------------------------------------------------------------------
// Internal: static markdown rendering
// ---------------------------------------------------------------------------

const MarkdownBody = React.memo(function MarkdownBody({
	children,
	dimColor,
}: {
	children: string
	dimColor?: boolean
}): React.ReactNode {
	configureMarked()

	const rendered = useMemo(() => {
		const tokens = cachedLexer(children)
		return tokens.map(token => formatToken(token)).join('').trim()
	}, [children])

	if (!rendered) return null

	return (
		<Box flexDirection="column" gap={1}>
			<Ansi dimColor={dimColor}>{rendered}</Ansi>
		</Box>
	)
})

// ---------------------------------------------------------------------------
// Internal: streaming markdown rendering
// ---------------------------------------------------------------------------

/**
 * Renders markdown during streaming by splitting at the last top-level block
 * boundary: everything before is stable (memoized, never re-parsed), only the
 * final block is re-parsed per delta.
 *
 * marked.lexer() correctly handles unclosed code fences as a single token,
 * so block boundaries are always safe.
 *
 * The stable boundary only advances (monotonic), so ref mutation during render
 * is idempotent and safe under StrictMode double-rendering.
 */
function StreamingMarkdown({
	children,
	dimColor,
}: {
	children: string
	dimColor?: boolean
}): React.ReactNode {
	configureMarked()

	const stablePrefixRef = useRef('')

	// Reset if text was replaced (defensive; normally unmount handles this)
	if (!children.startsWith(stablePrefixRef.current)) {
		stablePrefixRef.current = ''
	}

	// Lex only from current boundary — O(unstable length), not O(full text)
	const boundary = stablePrefixRef.current.length
	const tokens = marked.lexer(children.substring(boundary))

	// Last non-space token is the growing block; everything before is final
	let lastContentIdx = tokens.length - 1
	while (lastContentIdx >= 0 && tokens[lastContentIdx]!.type === 'space') {
		lastContentIdx--
	}
	let advance = 0
	for (let i = 0; i < lastContentIdx; i++) {
		advance += tokens[i]!.raw.length
	}
	if (advance > 0) {
		stablePrefixRef.current = children.substring(0, boundary + advance)
	}

	const stablePrefix = stablePrefixRef.current
	const unstableSuffix = children.substring(stablePrefix.length)

	// stablePrefix is memoized inside <MarkdownBody> via React.memo + useMemo
	// so it never re-parses as the unstable suffix grows
	return (
		<Box flexDirection="column" gap={1}>
			{stablePrefix && (
				<MarkdownBody dimColor={dimColor}>{stablePrefix}</MarkdownBody>
			)}
			{unstableSuffix && (
				<MarkdownBody dimColor={dimColor}>{unstableSuffix}</MarkdownBody>
			)}
		</Box>
	)
}
