/**
 * Piggy's answer, rendered as markdown.
 *
 * The model writes GFM — renewal tables, bolded figures, numbered next steps
 * and the occasional SQL block. The transcript used to print that verbatim, so
 * everyone read `**Renewal:**` and pipe-delimited soup.
 *
 * Every element is styled from the map below rather than left to Streamdown's
 * own look. Streamdown ships Tailwind class names inside its compiled output,
 * and PIG's Tailwind only scans `src/**`, so those class names are never
 * emitted into the stylesheet — anything not overridden here would render with
 * bare browser defaults. There is no `@tailwindcss/typography` in this repo
 * either, so there is no `prose` to fall back on.
 *
 * `rehypePlugins` is deliberately not passed. Streamdown's default chain is
 * rehype-raw → rehype-sanitize → rehype-harden, and supplying our own would
 * silently replace it — dropping the sanitiser that keeps a model-authored
 * `javascript:` href or a stray `<script>` out of the DOM.
 */
import type { ComponentProps, CSSProperties, ReactNode } from 'react';
import { isValidElement } from 'react';
import { ArrowUpRight } from 'lucide-react';
import { Streamdown, type Components, type ExtraProps } from 'streamdown';
import { Label, cn } from '@/components/ui';

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

export function PiggyResponse({ content, className }: { content: string; className?: string }) {
  return (
    <Streamdown
      // Half a table or an unclosed `**` arrives on nearly every frame while
      // the answer streams. Without this the transcript flashes raw pipes and
      // asterisks between tokens.
      parseIncompleteMarkdown
      // Streamdown's own copy/download overlays for tables and code blocks are
      // styled with class names this build never emits, so they would land as
      // unstyled buttons floating over the answer.
      controls={false}
      components={MARKDOWN_COMPONENTS}
      className={cn(
        // Block rhythm lives here rather than on each element: Streamdown's
        // root already sets `space-y-*`, whose `> * + *` rule outranks any
        // margin utility a child could carry.
        'space-y-3 break-words text-sm leading-6 [&>*:first-child]:pt-0',
        className,
      )}
    >
      {content}
    </Streamdown>
  );
}

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

  /*
   * Headings buy their extra air with padding, not margin — see the note on
   * `space-y-3` above. The scale is compressed against the ordinary chat type:
   * this renders in a 22rem dock as often as on a full page, and a document
   * h1 at that width reads as a shout.
   */
  h1: ({ children }) => <h1 className="pt-2 text-base font-semibold tracking-tight">{children}</h1>,
  // h1 and h2 share the section-heading step deliberately. The scale has one
  // size for "this owns the block below it", and `##` is what a model reaches
  // for first — set a step down from `#` it read as a bolded sentence rather
  // than as the heading of the table under it.
  h2: ({ children }) => <h2 className="pt-2 text-base font-semibold">{children}</h2>,
  h3: ({ children }) => <h3 className="pt-1 text-sm font-semibold">{children}</h3>,
  h4: ({ children }) => <h4 className="pt-1 text-sm font-medium">{children}</h4>,
  h5: ({ children }) => <h5 className="pt-1 text-sm font-medium text-muted">{children}</h5>,
  h6: ({ children }) => (
    <h6 className="pt-1 text-xs font-medium uppercase tracking-[0.06em] text-muted">{children}</h6>
  ),

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

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

  blockquote: ({ children }) => (
    <blockquote className="border-l-2 border-border pl-3 text-muted">{children}</blockquote>
  ),
  hr: () => <hr className="border-border" />,

  img: ({ src, alt }) => (
    // `referrerPolicy` so a model-authored image URL cannot use the referer to
    // learn which PIG record the reader had open when it loaded.
    <img
      src={typeof src === 'string' ? src : undefined}
      alt={alt ?? ''}
      loading="lazy"
      referrerPolicy="no-referrer"
      className="max-w-full rounded-lg border border-border"
    />
  ),

  /*
   * There is no `pre` entry, and that is deliberate: Streamdown's `pre` is not
   * a wrapper but a marker that tags its `code` child with `data-block`, which
   * is how the pair below is told apart. Replacing it would break that
   * contract, so the whole fenced-block chrome — the `pre` included — is built
   * by `CodeFence`, and `inlineCode` takes the inline case.
   */
  code: CodeFence,
  inlineCode: ({ children }) => (
    <code className="rounded border border-border bg-surface-2 px-1 py-0.5 font-mono text-[0.85em]">
      {children}
    </code>
  ),

  table: ({ children }) => (
    // The dock is 22rem wide and a renewals table is not. `scroll-x` keeps the
    // overflow inside this box — with momentum and overscroll containment, so
    // swiping a table on a phone does not drag the transcript with it.
    <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-sm 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>,
  // The column heading is the product's one micro label, so a renewals table in
  // an answer and a renewals table on /contracts are read at the same size.
  th: ({ children, style, align }) => (
    <Label as="th" className="whitespace-nowrap px-3 py-2 align-bottom" style={alignStyle(style, align)}>
      {children}
    </Label>
  ),
  td: ({ children, style, align }) => (
    <td className="nums px-3 py-2 align-top" style={alignStyle(style, align)}>
      {children}
    </td>
  ),
};

/**
 * Every link here was written by the model, not by PIG, so it is treated as
 * outbound and untrusted: a new tab (nothing in a chat should navigate the
 * workspace away), no `opener` handle back to us, no referer leaking the
 * record the reader was on, and a marker glyph so a plausible-looking phrase
 * cannot pass itself off as internal navigation.
 */
function MarkdownLink({ href, children }: ComponentProps<'a'> & ExtraProps) {
  return (
    <a
      href={href}
      target="_blank"
      rel="noreferrer noopener nofollow"
      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. Only ever reached for fenced blocks: supplying
 * `inlineCode` alongside `code` is what makes Streamdown route the two cases
 * apart, so there is no inline branch to guard here.
 */
function CodeFence({ className, children }: ComponentProps<'code'> & ExtraProps) {
  const language = LANGUAGE_CLASS.exec(className ?? '')?.[1];
  return (
    <div className="overflow-hidden rounded-lg border border-border bg-surface-2">
      {language ? (
        <div className="border-b border-border px-3 py-1.5 font-mono text-xs lowercase text-muted">{language}</div>
      ) : null}
      <pre className="scroll-x p-3 text-xs leading-5">
        <code className="font-mono">{codeText(children)}</code>
      </pre>
    </div>
  );
}

/**
 * GFM column alignment — the `---:` in a delimiter row — is the one piece of
 * element styling the markdown itself owns, and a `$1.86/GPU-hr` column that
 * silently reverts to the left is the difference between a readable table and
 * a wall. Markdown carries it as the legacy `align` attribute, which the hast
 * to JSX conversion may hand over already translated into `style.textAlign` —
 * so both are read, and the winner becomes a real inline style. Left as a bare
 * attribute it is only a user-agent presentational hint, which the cell's own
 * `text-left` class outranks.
 *
 * Only the alignment is taken, and these two cells are the only components
 * here that forward a style at all. The sanitiser upstream already strips
 * author `style`, and this keeps that true even if it ever stops.
 */
function alignStyle(style: CSSProperties | undefined, align: string | undefined): CSSProperties | undefined {
  const value = style?.textAlign ?? align;
  return value === 'right' || value === 'center' || value === 'left' ? { textAlign: value } : undefined;
}

/** The fence body reaches us as React children, one text node deep on a
 *  complete block but occasionally nested while the block is still arriving. */
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 '';
}
