/**
 * Motion bodies, rendered as markdown.
 *
 * A template body is a document — a discovery guide, a playbook, a narrative —
 * and the app had nowhere to render one: Piggy's transcript uses Streamdown,
 * which is built for a half-finished token stream in a 22rem dock, not for a
 * page of authored prose. This is the page-width counterpart, on plain
 * `react-markdown` with GFM for the tables and task lists the content uses.
 *
 * Every element is styled from the map below. There is no
 * `@tailwindcss/typography` in this repo and one is deliberately not being
 * added for this, so there is no `prose` to fall back on and an element absent
 * from the map renders with bare browser defaults.
 *
 * `rehypePlugins` is deliberately empty. Without `rehype-raw`, react-markdown
 * does not render embedded HTML at all, and its default `urlTransform` already
 * drops `javascript:` and other non-navigational protocols — so the safe
 * behaviour here is the behaviour of adding nothing. Template bodies are
 * author-written but they are also promoted out of engagement artefacts that
 * anyone with `motion:write` can edit, so they are treated as untrusted.
 */
import { isValidElement, type ComponentProps, type CSSProperties, type ReactNode } from 'react';
import ReactMarkdown, { type Components } from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { ArrowUpRight } from 'lucide-react';
import { cn } from '@/components/ui';
import { CodeBlock } from './CodeBlock';

/** Fenced blocks carry their language as `language-<name>` on the `code` element. */
const LANGUAGE_CLASS = /language-([\w-]+)/;

export function Markdown({ content, className }: { content: string; className?: string }) {
  return (
    <div
      className={cn(
        // Block rhythm lives on the container rather than on each element, so
        // the spacing between a heading and the paragraph under it does not
        // depend on which of the two carries the margin.
        'min-w-0 space-y-4 break-words text-sm leading-6 text-fg [&>*:first-child]:pt-0',
        className,
      )}
    >
      <ReactMarkdown remarkPlugins={[remarkGfm]} components={MARKDOWN_COMPONENTS}>
        {content}
      </ReactMarkdown>
    </div>
  );
}

const MARKDOWN_COMPONENTS: Components = {
  p: ({ children }) => <p className="leading-6">{children}</p>,

  /*
   * Headings buy their air with padding, not margin — the container's
   * `space-y-4` sets the gap below, and a margin above would be collapsed
   * against it inconsistently. The scale stays close to body size: these are
   * section headings inside a card, not the page's own title.
   */
  h1: ({ children }) => <h1 className="pt-4 text-xl font-semibold tracking-tight">{children}</h1>,
  h2: ({ children }) => <h2 className="pt-4 text-lg font-semibold tracking-tight">{children}</h2>,
  h3: ({ children }) => <h3 className="pt-3 text-base font-semibold">{children}</h3>,
  h4: ({ children }) => <h4 className="pt-2 text-sm font-semibold">{children}</h4>,
  h5: ({ children }) => <h5 className="pt-2 text-sm font-medium text-muted">{children}</h5>,
  h6: ({ children }) => (
    <h6 className="pt-2 text-xs font-medium uppercase tracking-wide text-muted">{children}</h6>
  ),

  ul: ({ children }) => <ul className="list-disc space-y-1.5 pl-5 marker:text-muted">{children}</ul>,
  ol: ({ children }) => <ol className="list-decimal space-y-1.5 pl-5 marker:text-muted">{children}</ol>,
  // A nested list is the first *element* child of its item even when prose
  // precedes it, so the parent's `space-y` never reaches it.
  li: ({ children }) => <li className="leading-6 [&>ol]:mt-1.5 [&>ul]:mt-1.5">{children}</li>,

  strong: ({ children }) => <strong className="font-semibold text-fg">{children}</strong>,
  em: ({ children }) => <em className="italic">{children}</em>,
  del: ({ children }) => <del className="text-muted line-through">{children}</del>,
  a: MarkdownLink,

  blockquote: ({ children }) => (
    <blockquote className="border-l-2 border-border pl-4 text-muted [&>*+*]:mt-2">
      {children}
    </blockquote>
  ),
  hr: () => <hr className="border-border" />,

  img: ({ src, alt }) => (
    // `referrerPolicy` so an image URL that arrived with a promoted artefact
    // cannot use the referer to learn which template the reader had open.
    <img
      src={typeof src === 'string' ? src : undefined}
      alt={alt ?? ''}
      loading="lazy"
      referrerPolicy="no-referrer"
      className="max-w-full rounded-lg border border-border"
    />
  ),

  /*
   * The fence chrome is built entirely in `pre`, which never renders the
   * `code` element react-markdown handed it — it reads the language and the
   * text off it instead. That is what makes the `code` entry below reachable
   * only for inline code: react-markdown 10 stopped passing an `inline` flag,
   * and the usual replacement — guessing from the `language-` class — gets a
   * fenced block with no language wrong every time.
   */
  pre: ({ children }) => <CodeFence>{children}</CodeFence>,
  code: ({ children }) => (
    <code className="rounded border border-border bg-surface-2 px-1 py-0.5 font-mono text-[0.85em]">
      {children}
    </code>
  ),

  table: ({ children }) => (
    // Without this the widest table on the page sets the width of the page,
    // and every route scrolls sideways on a phone.
    <div className="scroll-x rounded-lg border border-border">
      {/* `w-max min-w-full`: fill the box when the table is narrow, spill into
          the scroller rather than squash the columns when it is not. */}
      <table className="w-max min-w-full border-collapse text-left text-[13px] leading-5">
        {children}
      </table>
    </div>
  ),
  thead: ({ children }) => <thead className="border-b border-border bg-surface-2">{children}</thead>,
  tbody: ({ children }) => <tbody className="divide-y divide-border">{children}</tbody>,
  // A row highlight is what lets you keep your place across a table that is
  // wider than the pane and has been scrolled sideways.
  tr: ({ children }) => <tr className="transition-colors hover:bg-surface-2">{children}</tr>,
  th: ({ children, style }) => (
    <th
      className="whitespace-nowrap px-3 py-2 align-bottom font-medium text-muted"
      style={alignStyle(style)}
    >
      {children}
    </th>
  ),
  // No `nums` here, deliberately: tabular figures on every cell render an
  // author's prose column in the digit-width of a ledger.
  td: ({ children, style }) => (
    <td className="px-3 py-2 align-top" style={alignStyle(style)}>
      {children}
    </td>
  ),
};

/**
 * Links in a template body point out of PIG — a vendor's docs, a paper, a
 * customer's status page — so they open in a new tab rather than navigating a
 * workspace someone has unsaved edits in, carry no referer, and wear a marker
 * glyph so a plausible phrase cannot pass itself off as internal navigation.
 */
function MarkdownLink({ href, children }: ComponentProps<'a'>) {
  return (
    <a
      href={href}
      target="_blank"
      rel="noreferrer noopener"
      title={href}
      className="font-medium text-info underline decoration-border underline-offset-2 hover:decoration-info"
    >
      {children}
      <ArrowUpRight className="ml-0.5 inline size-3 align-[-0.1em]" aria-hidden />
    </a>
  );
}

/**
 * A fenced code block, built from the `code` element rather than around it.
 *
 * The chrome — the language caption and the copy button — comes from
 * `CodeBlock`, so a mermaid fence inside a promoted body offers the same copy
 * action a mermaid field does in `FieldsView`. They are the same thing to the
 * person reading them.
 */
function CodeFence({ children }: { children: ReactNode }) {
  const element = isValidElement<{ className?: string; children?: ReactNode }>(children)
    ? children
    : null;
  const language = LANGUAGE_CLASS.exec(element?.props.className ?? '')?.[1];
  const text = codeText(element ? element.props.children : children);

  return <CodeBlock source={text} language={language} />;
}

/**
 * GFM column alignment — the `---:` in a delimiter row — is the one piece of
 * element styling the markdown itself owns, and a currency column that
 * silently reverts to the left is the difference between a readable table and
 * a wall. It reaches the cell as `style.textAlign`; only that property is
 * taken, so nothing else an author writes can style the page.
 */
function alignStyle(style: CSSProperties | undefined): CSSProperties | undefined {
  const value = style?.textAlign;
  return value === 'right' || value === 'center' || value === 'left' ? { textAlign: value } : undefined;
}

/** The fence body reaches us as React children, normally one text node deep. */
function codeText(children: ReactNode): string {
  if (typeof children === 'string') return children;
  if (Array.isArray(children)) return (children as ReactNode[]).map(codeText).join('');
  if (isValidElement<{ children?: ReactNode }>(children)) return codeText(children.props.children);
  return '';
}
