import type React from 'react';
import { memo, useMemo } from 'react';
import { sanitizeTerminalText } from '../../utils/terminal';
import { type InlinePart, type MarkdownBlock, parseDocument, shortDomain } from '../markdown/parse';
import { terminalCellWidth, truncateTerminalText } from '../text';
import { colors } from '../theme';

const MAX_RENDERED_BLOCKS = 1_000;
const MAX_RENDERED_CODE_LINES = 300;
const MAX_RENDERED_TABLE_ROWS = 100;

function renderInlinePart(part: InlinePart, key: string, hintLabels?: Map<number, string>, compact = false) {
  const content = sanitizeTerminalText(part.content);
  switch (part.type) {
    case 'bold':
      return (
        <span key={key} fg={colors.textPrimary}>
          <strong>{content}</strong>
        </span>
      );
    case 'italic':
      return (
        <span key={key} fg={colors.textPrimary}>
          <em>{content}</em>
        </span>
      );
    case 'strikethrough':
      return (
        <span key={key} fg={colors.textMuted}>
          {content}
        </span>
      );
    case 'code':
      return (
        <span key={key} fg={colors.codeText}>
          {compact ? null : '`'}
          {content}
          {compact ? null : '`'}
        </span>
      );
    case 'link': {
      const linkText = content;
      const url = sanitizeTerminalText(part.url ?? '');
      const domain = shortDomain(url);
      const textIsUrl = linkText === url || linkText === domain || url.includes(linkText);
      const label = part.linkIndex !== undefined ? hintLabels?.get(part.linkIndex) : undefined;
      return (
        <span key={key}>
          {label ? (
            <span bg={colors.accentYellow} fg={colors.bg}>
              <strong>{` ${label} `}</strong>
            </span>
          ) : null}
          {label ? <span> </span> : null}
          <span fg={colors.accentBlue}>
            <u>{linkText}</u>
          </span>
          {!compact && !textIsUrl ? <span fg={colors.textMuted}>{` [${domain}]`}</span> : null}
        </span>
      );
    }
    default:
      return (
        <span key={key} fg={colors.textPrimary}>
          {content}
        </span>
      );
  }
}

function InlineContent({ parts, hintLabels }: { parts: InlinePart[]; hintLabels?: Map<number, string> }) {
  return <text wrapMode="char">{parts.map((part, i) => renderInlinePart(part, `${i}-${part.type}`, hintLabels))}</text>;
}

function InlineSpans({ parts, hintLabels }: { parts: InlinePart[]; hintLabels?: Map<number, string> }) {
  return parts.map((part, i) => renderInlinePart(part, `${i}-${part.type}`, hintLabels));
}

function inlineCellWidth(parts: InlinePart[], hintLabels?: Map<number, string>): number {
  return parts.reduce((width, part) => {
    const label = part.type === 'link' && part.linkIndex !== undefined ? hintLabels?.get(part.linkIndex) : undefined;
    return width + terminalCellWidth(sanitizeTerminalText(part.content)) + (label ? terminalCellWidth(label) + 3 : 0);
  }, 0);
}

function renderInlineCell(
  parts: InlinePart[],
  width: number,
  key: string,
  hintLabels?: Map<number, string>,
): React.ReactNode {
  const fitted: InlinePart[] = [];
  let used = 0;
  for (const part of parts) {
    const label = part.type === 'link' && part.linkIndex !== undefined ? hintLabels?.get(part.linkIndex) : undefined;
    const labelWidth = label ? terminalCellWidth(label) + 3 : 0;
    const remaining = Math.max(0, width - used);
    if (labelWidth > remaining) break;
    const content = sanitizeTerminalText(part.content);
    const visibleContent = truncateTerminalText(content, remaining - labelWidth);
    if (!visibleContent && labelWidth === 0) break;
    fitted.push({ ...part, content: visibleContent });
    used += labelWidth + terminalCellWidth(visibleContent);
    if (visibleContent !== content) break;
  }

  return (
    <span key={key}>
      {fitted.map((part, index) => renderInlinePart(part, `${key}-${index}`, hintLabels, true))}
      {' '.repeat(Math.max(0, width - used))}
    </span>
  );
}

export interface MarkdownRendererProps {
  content: string;
  width?: number;
  /** When set, links whose linkIndex appears as a key get a `[label]` prefix. */
  hintLabels?: Map<number, string>;
  /**
   * Pre-parsed blocks. When provided, the renderer reuses them instead of
   * re-parsing `content`, so callers that also need the parsed link list can
   * parse the document a single time and share the result.
   */
  blocks?: MarkdownBlock[];
}

export const MarkdownRenderer = memo(function MarkdownRenderer({
  content,
  width: availableWidth,
  hintLabels,
  blocks: blocksProp,
}: MarkdownRendererProps) {
  const blocks = useMemo(() => blocksProp ?? parseDocument(content).blocks, [blocksProp, content]);
  const hasFiniteExplicitWidth = typeof availableWidth === 'number' && Number.isFinite(availableWidth);
  const safeWidth =
    availableWidth === undefined ? 60 : hasFiniteExplicitWidth ? Math.max(0, Math.floor(availableWidth)) : 0;
  const hrWidth = Math.min(safeWidth, 60);

  const elements: React.ReactNode[] = [];
  let idx = 0;

  for (const block of blocks.slice(0, MAX_RENDERED_BLOCKS)) {
    const key = `md-${idx++}`;

    switch (block.type) {
      case 'h1': {
        const parts = (block.inlineParts ?? [{ type: 'text', content: block.content } satisfies InlinePart]).map(
          (part) => ({ ...part, content: part.content.toUpperCase() }),
        );
        elements.push(
          <box key={key} marginTop={1} marginBottom={1}>
            <text wrapMode="char">
              <span fg={colors.accentBlue}>
                <strong>
                  {'═══ '}
                  <InlineSpans parts={parts} hintLabels={hintLabels} />
                  {' ═══'}
                </strong>
              </span>
            </text>
          </box>,
        );
        break;
      }

      case 'h2': {
        const parts = block.inlineParts ?? [{ type: 'text', content: block.content } satisfies InlinePart];
        elements.push(
          <box key={key} marginTop={1} marginBottom={1}>
            <text wrapMode="char">
              <span fg={colors.accentBlue}>
                <strong>
                  {'── '}
                  <InlineSpans parts={parts} hintLabels={hintLabels} />
                  {' ──'}
                </strong>
              </span>
            </text>
          </box>,
        );
        break;
      }

      case 'h3': {
        const parts = block.inlineParts ?? [{ type: 'text', content: block.content } satisfies InlinePart];
        elements.push(
          <box key={key} marginTop={1}>
            <text wrapMode="char">
              <span fg={colors.accentBlue}>
                <strong>
                  <InlineSpans parts={parts} hintLabels={hintLabels} />
                </strong>
              </span>
            </text>
          </box>,
        );
        break;
      }

      case 'h4': {
        const parts = block.inlineParts ?? [{ type: 'text', content: block.content } satisfies InlinePart];
        elements.push(
          <box key={key} marginTop={1}>
            <text wrapMode="char">
              <span fg={colors.accentPurple}>
                <strong>
                  <InlineSpans parts={parts} hintLabels={hintLabels} />
                </strong>
              </span>
            </text>
          </box>,
        );
        break;
      }

      case 'h5': {
        const parts = block.inlineParts ?? [{ type: 'text', content: block.content } satisfies InlinePart];
        elements.push(
          <box key={key} marginTop={1}>
            <text wrapMode="char">
              <span fg={colors.accentPurple}>
                <em>
                  <InlineSpans parts={parts} hintLabels={hintLabels} />
                </em>
              </span>
            </text>
          </box>,
        );
        break;
      }

      case 'h6': {
        const parts = block.inlineParts ?? [{ type: 'text', content: block.content } satisfies InlinePart];
        elements.push(
          <box key={key} marginTop={1}>
            <text wrapMode="char">
              <span fg={colors.textSecondary}>
                <em>
                  <InlineSpans parts={parts} hintLabels={hintLabels} />
                </em>
              </span>
            </text>
          </box>,
        );
        break;
      }

      case 'blockquote': {
        const bqLines = block.blockquoteLines ?? [];
        const lineKeyCounts = new Map<string, number>();
        elements.push(
          <box key={key} marginLeft={1} marginTop={1} marginBottom={1} flexDirection="column">
            {bqLines.map((parts) => {
              // Keep keys stable without copying an entire untrusted quote
              // line into the React key. A short signature plus occurrence
              // count handles duplicate lines while bounding key size.
              const signature = parts
                .map((part) => `${part.type}:${part.content.slice(0, 64)}`)
                .join('|')
                .slice(0, 256);
              const seen = lineKeyCounts.get(signature) ?? 0;
              lineKeyCounts.set(signature, seen + 1);
              return (
                <box key={`${key}-bq-${signature}-${seen}`} flexDirection="row">
                  <text fg={colors.accentYellow}>{'┃ '}</text>
                  <text fg={colors.textSecondary} wrapMode="char">
                    <em>
                      <InlineSpans parts={parts} hintLabels={hintLabels} />
                    </em>
                  </text>
                </box>
              );
            })}
          </box>,
        );
        break;
      }

      case 'ulist': {
        const parts = block.inlineParts ?? [];
        elements.push(
          <box key={key} paddingLeft={(block.level ?? 0) * 2 + 2}>
            <text wrapMode="char">
              <span fg={colors.accentYellow}>{'• '}</span>
              <InlineSpans parts={parts} hintLabels={hintLabels} />
            </text>
          </box>,
        );
        break;
      }

      case 'olist': {
        const parts = block.inlineParts ?? [];
        elements.push(
          <box key={key} paddingLeft={(block.level ?? 0) * 2 + 2}>
            <text wrapMode="char">
              <span fg={colors.accentYellow}>{`${block.number ?? '·'}. `}</span>
              <InlineSpans parts={parts} hintLabels={hintLabels} />
            </text>
          </box>,
        );
        break;
      }

      case 'codeblock': {
        const codeLines = sanitizeTerminalText(block.content, { preserveNewlines: true }).split('\n');
        const showLines = codeLines.length > 3;
        elements.push(
          <box key={key} marginTop={1} marginBottom={1} flexDirection="column">
            {block.language ? (
              <box paddingLeft={1}>
                <text fg={colors.textMuted} wrapMode="char">{`  ${block.language}`}</text>
              </box>
            ) : null}
            {codeLines.slice(0, MAX_RENDERED_CODE_LINES).map((line, lineIndex) => {
              const lineKey = `${key}-line-${lineIndex}`;
              return (
                <box key={lineKey} paddingLeft={1}>
                  <text fg={colors.codeText} wrapMode="char">
                    {showLines ? `${String(lineIndex + 1).padStart(3, ' ')} ` : ''}
                    {line}
                  </text>
                </box>
              );
            })}
            {codeLines.length > MAX_RENDERED_CODE_LINES ? (
              <box paddingLeft={1}>
                <text
                  fg={colors.textMuted}
                  wrapMode="char"
                >{`… ${codeLines.length - MAX_RENDERED_CODE_LINES} code lines omitted`}</text>
              </box>
            ) : null}
          </box>,
        );
        break;
      }

      case 'hr':
        elements.push(
          <box key={key} marginTop={1} marginBottom={1}>
            <text fg={colors.textMuted}>{'─'.repeat(hrWidth)}</text>
          </box>,
        );
        break;

      case 'image':
        elements.push(
          <box key={key} paddingLeft={1}>
            <text wrapMode="char">
              <span fg={colors.textMuted}>{'🖼  '}</span>
              <span fg={colors.textSecondary}>
                <em>{sanitizeTerminalText(block.content)}</em>
              </span>
            </text>
          </box>,
        );
        break;

      case 'table':
        elements.push(renderTable(block, key, safeWidth, hintLabels));
        break;

      case 'blank':
        elements.push(<box key={key} height={1} />);
        break;

      case 'paragraph': {
        const parts = block.inlineParts ?? [];
        elements.push(
          <box key={key}>
            <InlineContent parts={parts} hintLabels={hintLabels} />
          </box>,
        );
        break;
      }
    }
  }

  if (blocks.length > MAX_RENDERED_BLOCKS) {
    elements.push(
      <text
        key="md-omitted-blocks"
        fg={colors.textMuted}
        wrapMode="char"
      >{`… ${blocks.length - MAX_RENDERED_BLOCKS} blocks omitted`}</text>,
    );
  }

  return (
    <box flexDirection="column" width={hasFiniteExplicitWidth && safeWidth > 0 ? safeWidth : undefined}>
      {elements}
    </box>
  );
});

function renderTable(
  block: MarkdownBlock,
  key: string,
  availableWidth: number | undefined,
  hintLabels?: Map<number, string>,
) {
  const rows = block.rows ?? [];
  if (rows.length === 0) return null;
  const colCount = Math.max(...rows.map((r) => r.length));
  if (colCount === 0) return null;

  // A table needs at least one character per visible column plus one space
  // between columns. On narrow panes, show as many leading columns as fit
  // rather than creating negative padding or overflowing the pane.
  const tableWidth = Math.max(0, Math.floor(availableWidth ?? 80));
  if (tableWidth === 0) return null;
  const visibleColCount = Math.min(colCount, Math.max(1, Math.floor((tableWidth + 1) / 2)));
  const gapsWidth = visibleColCount - 1;
  const colWidths = Array.from({ length: visibleColCount }, () => 1);
  const naturalWidths = Array.from({ length: visibleColCount }, () => 1);
  for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
    const row = rows[rowIndex] ?? [];
    for (let column = 0; column < visibleColCount; column++) {
      const parts = block.tableInlineParts?.[rowIndex]?.[column] ?? [
        { type: 'text', content: row[column] ?? '' } satisfies InlinePart,
      ];
      naturalWidths[column] = Math.max(naturalWidths[column] ?? 1, inlineCellWidth(parts, hintLabels));
    }
  }
  let remainingWidth = tableWidth - visibleColCount - gapsWidth;

  // Natural widths are measured in one pass. The bounded allocation below
  // avoids repeatedly scanning every row for every terminal cell.
  while (remainingWidth > 0) {
    let expanded = false;
    for (let c = 0; c < visibleColCount && remainingWidth > 0; c++) {
      const naturalWidth = naturalWidths[c] ?? 1;
      if ((colWidths[c] ?? 0) < naturalWidth) {
        colWidths[c] = (colWidths[c] ?? 0) + 1;
        remainingWidth--;
        expanded = true;
      }
    }
    if (!expanded) break;
  }

  const headerRow = rows[0];
  const dataRows = rows.slice(1, MAX_RENDERED_TABLE_ROWS + 1);

  return (
    <box key={key} marginTop={1} marginBottom={1} flexDirection="column">
      {headerRow ? (
        <text>
          {headerRow.slice(0, visibleColCount).map((cell, ci) => {
            const headerKey = `${key}-h-${ci}`;
            return (
              <span key={headerKey}>
                <span fg={colors.accentBlue}>
                  <strong>
                    {renderInlineCell(
                      block.tableInlineParts?.[0]?.[ci] ?? [{ type: 'text', content: cell }],
                      colWidths[ci] ?? 1,
                      headerKey,
                      hintLabels,
                    )}
                  </strong>
                </span>
                {ci < visibleColCount - 1 ? ' ' : null}
              </span>
            );
          })}
        </text>
      ) : null}
      <text fg={colors.textMuted}>{colWidths.map((w) => '─'.repeat(w)).join(' ')}</text>
      {dataRows.map((row, dataRowIndex) => {
        const rowKey = `${key}-r-${dataRowIndex}`;
        return (
          <text key={rowKey}>
            {row.slice(0, visibleColCount).map((cell, ci) => {
              const cellKey = `${rowKey}-c-${ci}`;
              return (
                <span key={cellKey}>
                  <span fg={colors.textPrimary}>
                    {renderInlineCell(
                      block.tableInlineParts?.[dataRowIndex + 1]?.[ci] ?? [{ type: 'text', content: cell }],
                      colWidths[ci] ?? 1,
                      cellKey,
                      hintLabels,
                    )}
                  </span>
                  {ci < visibleColCount - 1 ? ' ' : null}
                </span>
              );
            })}
          </text>
        );
      })}
      {block.omittedColumns ? <text fg={colors.textMuted}>{`… ${block.omittedColumns} columns omitted`}</text> : null}
      {block.omittedRows || rows.length - 1 > MAX_RENDERED_TABLE_ROWS ? (
        <text
          fg={colors.textMuted}
        >{`… ${(block.omittedRows ?? 0) + Math.max(0, rows.length - 1 - MAX_RENDERED_TABLE_ROWS)} table rows omitted`}</text>
      ) : null}
    </box>
  );
}
