import { useMemo } from 'react';
import { Marked, Renderer } from 'marked';
import DOMPurify from 'dompurify';
import { linkifyOutputPathsInEscapedHtml, OUTPUT_PATH_REGEX } from './output-path-detect';

// Local Marked instance so we don't fight FilePreview's global `marked.use()`
// renderer config. Both modules render markdown but with different needs
// (FilePreview handles mermaid + image base URL; activity messages are short
// untrusted LLM output that needs sanitization).
const renderer = new Renderer();
renderer.link = function ({ href, title, text }: { href: string; title?: string | null; text: string }) {
  const titleAttr = title ? ` title="${String(title).replace(/"/g, '&quot;')}"` : '';
  const safeHref = String(href ?? '');
  // Markdown link whose destination is an `output/...` workspace
  // path: emit the data-output-path anchor so the preview pane opens
  // instead of navigating. Visible text stays as the markdown link
  // label. Test() against a /g regex must reset lastIndex afterward
  // or the next call may skip a match.
  const isOutputHref = OUTPUT_PATH_REGEX.test(safeHref);
  OUTPUT_PATH_REGEX.lastIndex = 0;
  if (isOutputHref) {
    return `<a class="output-path-link" data-output-path="${safeHref.replace(/"/g, '&quot;')}"${titleAttr} role="button" tabindex="0">${text}</a>`;
  }
  return `<a href="${safeHref}"${titleAttr} target="_blank" rel="noopener noreferrer">${text}</a>`;
};
// Bare paths inside paragraph / list / blockquote text bodies. Marked
// hands us pre-escaped text here, so the linkifier runs on safe HTML.
//
// CRITICAL: text tokens carry nested inline children (strong, em,
// codespan, link) in `tokens`. If we ignore them, `**bold**` and
// `` `code` `` are silently dropped to literal asterisks/backticks.
// Only the LEAF text node (no nested tokens) should be linkified.
renderer.text = function ({ tokens, text }: { tokens?: unknown[]; text: string }) {
  if (tokens && tokens.length > 0) {
    const self = this as unknown as { parser: { parseInline(tokens: unknown[]): string } };
    return self.parser.parseInline(tokens);
  }
  return linkifyOutputPathsInEscapedHtml(text);
};
// Inline code `output/foo.md` (single-backtick spans). Fenced code
// blocks go through `renderer.code` which we deliberately don't
// override — fenced code is typically a sample / example, and turning
// `output/...` lines inside it into clickable buttons is almost
// always wrong (the path may not actually exist on disk).
renderer.codespan = function ({ text }: { text: string }) {
  return `<code>${linkifyOutputPathsInEscapedHtml(text)}</code>`;
};

const md = new Marked({ gfm: true, breaks: true, renderer });

// Compact prose preset. Tailwind Typography's prose-sm slaps a
// `:where(p) { margin: 1.14em }` rule (~16px) on every paragraph,
// plus `line-height: 1.71`, which is too airy for chat / activity /
// timeline cards. The `:where()` part keeps Typography's specificity
// at 0,1,0 (.prose), so a plain `[&_p]:my-1` arbitrary variant
// (.parent p, specificity 0,1,1) should already beat it — but to
// remove any doubt we ALSO use Tailwind's `!` important prefix.
// Result: paragraphs ~4px apart, lines tight, headings reasonable.
const COMPACT_PROSE = [
  'prose prose-sm prose-slate dark:prose-invert max-w-none break-words min-w-0',
  // overflow-wrap: anywhere ensures unbreakable strings (long URLs, hashes,
  // file paths) wrap mid-token. `break-words` alone only honors word
  // boundaries which leaves them poking out of the container.
  '[&_a]:[overflow-wrap:anywhere] [&_a]:break-all',
  '[&_code]:[overflow-wrap:anywhere]',
  // Fenced code blocks: contain horizontal scroll inside the pre element.
  '[&_pre]:max-w-full [&_pre]:overflow-x-auto',
  '[&_p]:!my-1 [&_p]:!leading-snug',
  '[&_li]:!my-0 [&_li]:!leading-snug',
  '[&_ul]:!my-1 [&_ol]:!my-1',
  '[&_h1]:!my-1.5 [&_h1]:!leading-snug',
  '[&_h2]:!my-1.5 [&_h2]:!leading-snug',
  '[&_h3]:!my-1 [&_h3]:!leading-snug',
  '[&_h4]:!my-1 [&_h4]:!leading-snug',
  '[&_pre]:!my-1 [&_pre]:!leading-snug',
  '[&_blockquote]:!my-1 [&_blockquote]:!leading-snug',
  '[&_p:first-child]:!mt-0 [&_p:last-child]:!mb-0',
  '[&_h1:first-child]:!mt-0 [&_h2:first-child]:!mt-0 [&_h3:first-child]:!mt-0',
].join(' ');

interface MarkdownTextProps {
  text: string;
  /** Tailwind classes for the outer wrapper. If omitted, the compact prose preset is used. */
  className?: string;
}

/**
 * Renders Markdown safely. Used for agent intermediate messages where the
 * source string may include fenced code, lists, links, tables, etc.
 *
 * Sanitization: marked output is run through DOMPurify before injection.
 * The local Marked instance overrides the link renderer to force target=_blank
 * + rel=noopener,noreferrer for every link.
 *
 * For plain log lines like `[piece-runner] foo=bar`, render the string directly
 * instead — see ActivityEventCard for the per-kind switch.
 */
export function MarkdownText({ text, className }: MarkdownTextProps): JSX.Element | null {
  const html = useMemo(() => {
    if (!text) return '';
    const parsed = md.parse(text, { async: false });
    const raw = typeof parsed === 'string' ? parsed : '';
    // ADD_ATTR includes `data-output-path` so the OutputPreviewProvider's
    // click delegation can read it back. DOMPurify allows `data-*` by
    // default, but being explicit guards against config drift.
    return DOMPurify.sanitize(raw, { ADD_ATTR: ['target', 'rel', 'data-output-path', 'role', 'tabindex'] });
  }, [text]);

  if (!html) return null;
  // IMPORTANT: nest an inner <div> so the rendered <p>/<ul>/... are NOT direct
  // children of the prose-classed wrapper. Tailwind Typography's prose-sm
  // selectors are `.prose-sm > p`, `.prose-sm > ul` etc. — they only target
  // direct children. With <p> as a direct child, prose-sm slaps on
  // `margin-top: 1.14em` (~16px) between paragraphs which feels airy in
  // narrow chat / activity cards.
  //
  // The result bubble (`MarkdownPreview` in FilePreview) already nests this
  // way (its own div + MarkdownSegment wrapper), so its <p> never receives
  // those margins and looks compact. Matching that DOM shape here lines us
  // up perfectly with the result bubble's spacing.
  return (
    <div className={className ?? COMPACT_PROSE}>
      <div dangerouslySetInnerHTML={{ __html: html }} />
    </div>
  );
}
