'use client';

import React, { memo } from 'react';
import ReactMarkdown from 'react-markdown';
import rehypeExternalLinks from 'rehype-external-links';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import remarkBreaks from 'remark-breaks';
import remarkEmoji from 'remark-emoji';
import remarkGfm from 'remark-gfm';
import remarkSmartypants from 'remark-smartypants';
import type { Components } from 'react-markdown';

import { useCollapsibleContent } from '../../hooks/useCollapsibleContent';
import type { MarkdownMessageProps } from './types';
import { buildSchema, buildUrlTransform } from './sanitize';
import { looksLikePlainProse } from './plainText';
import { createMarkdownComponents } from './components';
import { CollapseToggle } from './CollapseToggle';
import { applyPreprocess, buildLinkRulesComponent, collectProtocols } from './linkRules';

/** Body text size class for the plain-text / prose wrapper. */
const SIZE_TO_TEXT: Record<'xs' | 'sm' | 'base', string> = {
  xs: 'text-xs',
  sm: 'text-sm',
  base: 'text-base',
};

/** Tailwind-typography modifier per size level. */
const SIZE_TO_PROSE: Record<'xs' | 'sm' | 'base', string> = {
  xs: 'prose-xs',
  sm: 'prose-sm',
  base: 'prose-base',
};

/**
 * MarkdownMessage — chat-tuned markdown renderer.
 *
 * Features:
 * - GitHub Flavored Markdown (GFM) via remark-gfm
 * - Syntax-highlighted code blocks with Copy button
 * - Mermaid diagram rendering (` ```mermaid ` fence)
 * - Tables, lists, blockquotes scaled for chat density
 * - User vs assistant styling modes (`isUser`)
 * - Plain-text fast path: skips ReactMarkdown when content has no
 *   markdown syntax (cheaper render, preserves newlines via CSS)
 * - Optional collapsible "Read more..." for long messages
 *
 * Custom URL schemes (chat mentions, deep-links, custom file viewers)
 * are best handled with the declarative `linkRules` prop — see the
 * type definition in `./types.ts` and the storybook for examples.
 *
 * @example
 * ```tsx
 * <MarkdownMessage content="# Hello\n\nThis is **bold** text." />
 *
 * // User message styling
 * <MarkdownMessage content="Some content" isUser />
 *
 * // Custom URL scheme via linkRules
 * <MarkdownMessage
 *   content="Talk to [Vps-audi](cmdop://machine/abc-123)"
 *   linkRules={[machineMentionRule]}
 * />
 * ```
 *
 * Memoised: re-renders only when props change. `content` is the main
 * trigger — all other props (className, isUser, size, etc.) are
 * compared by value/reference. `onCollapseChange` is compared by
 * reference — callers should stabilise it with useCallback.
 */
function MarkdownMessageRaw({
  content,
  className = '',
  isUser = false,
  size,
  isCompact = false,
  plainText,
  customComponents,
  extraHrefProtocols,
  linkRules,
  collapsible = false,
  maxLength,
  maxLines,
  readMoreLabel = 'Read more...',
  showLessLabel = 'Show less',
  defaultExpanded = false,
  onCollapseChange,
  codeTheme = 'dark',
}: MarkdownMessageProps) {
  // Pre-process content through any rules that requested it. Done
  // before trim so a rule can rewrite multi-line shapes too.
  const preprocessed = React.useMemo(
    () => applyPreprocess(content, linkRules),
    [content, linkRules],
  );

  // Union of `extraHrefProtocols` and any `protocols` declared by rules.
  const effectiveProtocols = React.useMemo(
    () => collectProtocols(extraHrefProtocols, linkRules),
    [extraHrefProtocols, linkRules],
  );

  // Effective custom components: merge linkRules' synthesized `a`
  // renderer with any caller-provided `customComponents`. linkRules
  // wins when both target the same href (rule is the more specific
  // declarative claim by design).
  const effectiveCustomComponents = React.useMemo<Partial<Components> | undefined>(() => {
    if (!linkRules || linkRules.length === 0) return customComponents;
    const callerA = customComponents?.a;
    const aRenderer = buildLinkRulesComponent(linkRules, isUser, callerA);
    return { ...(customComponents ?? {}), a: aRenderer };
  }, [customComponents, linkRules, isUser]);

  const trimmedContent = preprocessed.trim();

  // Collapsible content logic — defaults kick in only when enabled.
  const collapsibleOptions = React.useMemo(() => {
    if (!collapsible) return {};
    return {
      maxLength: maxLength ?? 1000,
      maxLines: maxLines ?? 10,
      defaultExpanded,
    };
  }, [collapsible, maxLength, maxLines, defaultExpanded]);

  const { isCollapsed, toggleCollapsed, displayContent, shouldCollapse } =
    useCollapsibleContent(trimmedContent, collapsible ? collapsibleOptions : {});

  React.useEffect(() => {
    if (collapsible && shouldCollapse && onCollapseChange) {
      onCollapseChange(isCollapsed);
    }
  }, [isCollapsed, collapsible, shouldCollapse, onCollapseChange]);

  // `size` wins when set explicitly; otherwise fall back to the
  // legacy binary `isCompact` so existing callers keep working.
  const resolvedSize: 'xs' | 'sm' | 'base' = size ?? (isCompact ? 'xs' : 'sm');

  const components = React.useMemo(() => {
    const base = createMarkdownComponents(isUser, resolvedSize, codeTheme);
    return effectiveCustomComponents ? { ...base, ...effectiveCustomComponents } : base;
  }, [isUser, resolvedSize, codeTheme, effectiveCustomComponents]);

  const schema = React.useMemo(() => buildSchema(effectiveProtocols), [effectiveProtocols]);
  const urlTransform = React.useMemo(
    () => buildUrlTransform(effectiveProtocols),
    [effectiveProtocols],
  );

  const textSizeClass = SIZE_TO_TEXT[resolvedSize];
  const proseClass = SIZE_TO_PROSE[resolvedSize];

  // Resolve plain-vs-markdown branch:
  //   1. Caller-passed `plainText` wins outright (explicit beats clever).
  //   2. Caller-supplied non-`a` customComponents force the markdown
  //      pipeline — those overrides only matter on real markdown nodes.
  //   3. Otherwise auto-detect via `looksLikePlainProse` (short,
  //      single-paragraph, no markdown markers). This is the chat-bubble
  //      WhatsApp/Telegram heuristic — "would the human have written
  //      this in one keystroke?".
  const customComponentsBeyondLinks = React.useMemo(() => {
    if (!customComponents) return false;
    return Object.keys(customComponents).some((k) => k !== 'a');
  }, [customComponents]);
  const isPlainText = plainText !== undefined
    ? plainText
    : !customComponentsBeyondLinks && looksLikePlainProse(displayContent);

  if (isPlainText) {
    // <div> + whitespace-pre-wrap: respects newlines AND collapses
    // double spaces, which is what users mean when they hit Enter
    // twice. <span> would break flow inside a flex bubble.
    //
    // leading-snug (1.375), not leading-relaxed (1.625) — `pre-wrap`
    // makes every `\n` a hard line break, so the relaxed leading
    // turns multi-line user messages into airy ladders. snug matches
    // WhatsApp/Telegram bubble density.
    return (
      <div
        className={`${textSizeClass} font-normal antialiased leading-snug break-words whitespace-pre-wrap ${className}`}
      >
        {displayContent}
        {collapsible && shouldCollapse && (
          <>
            {isCollapsed && '... '}
            <CollapseToggle
              isCollapsed={isCollapsed}
              onClick={toggleCollapsed}
              readMoreLabel={readMoreLabel}
              showLessLabel={showLessLabel}
              isUser={isUser}
              isCompact={resolvedSize === 'xs'}
            />
          </>
        )}
      </div>
    );
  }

  return (
    <div className={className}>
      <div
        className={`
          prose ${proseClass} max-w-none break-words overflow-hidden ${textSizeClass} font-normal antialiased
          ${isUser ? 'prose-invert' : 'dark:prose-invert'}
          [&>*]:leading-relaxed
          [&>*:first-child]:mt-0 [&>*:last-child]:mb-0
          [&_p]:my-2
          [&_ul]:my-2 [&_ol]:my-2 [&_ul]:pl-5 [&_ol]:pl-5
          [&_li]:my-1 [&_li>p]:my-0
          [&_h1]:mt-4 [&_h1]:mb-2 [&_h1]:text-base [&_h1]:font-semibold
          [&_h2]:mt-3.5 [&_h2]:mb-1.5 [&_h2]:text-[15px] [&_h2]:font-semibold
          [&_h3]:mt-3 [&_h3]:mb-1 [&_h3]:text-sm [&_h3]:font-medium
          [&_h4]:mt-3 [&_h4]:mb-1 [&_h4]:text-sm [&_h4]:font-medium
        `}
        style={{
          // Inherit colors from parent — fixes issues with external
          // CSS variables overriding prose tokens.
          '--tw-prose-body': 'inherit',
          '--tw-prose-headings': 'inherit',
          '--tw-prose-bold': 'inherit',
          '--tw-prose-links': 'inherit',
          color: 'inherit',
        } as React.CSSProperties}
      >
        <ReactMarkdown
          // Remark plugin order is load-bearing:
          //   1. `remark-gfm` — tables, strikethrough, autolinks, task lists.
          //   2. `remark-breaks` — chat convention: single `\n` → `<br>`
          //      (ChatGPT / Slack / Discord / Linear). CommonMark's
          //      default would collapse those into one paragraph and
          //      LLM punchlines / poems / dialogue would render as a
          //      run-on line. Goes BEFORE smartypants so quotes that
          //      land at line breaks still get the curly treatment.
          //   3. `remark-smartypants` — typographic substitutions:
          //      "..." → …, -- → —, "x" → "x". Cheap "humanized"
          //      polish à la Medium / Substack.
          //   4. `remark-emoji` — `:smile:` → 😄 (GitHub / Linear
          //      shortcode style). Leaves Unicode emoji untouched.
          remarkPlugins={[remarkGfm, remarkBreaks, remarkSmartypants, remarkEmoji]}
          // rehype-raw parses inline HTML in the source; rehype-sanitize
          // (with our extended schema) runs after to keep XSS guards
          // (no scripts, no on* handlers, no javascript: urls).
          // rehype-external-links runs LAST so it tags every <a> that
          // sanitize let through — externals get target=_blank +
          // rel=noopener noreferrer in one pass, instead of every
          // custom `a` renderer reimplementing the rule.
          rehypePlugins={[
            rehypeRaw,
            [rehypeSanitize, schema],
            [rehypeExternalLinks, { target: '_blank', rel: ['noopener', 'noreferrer'] }],
          ]}
          components={components}
          // urlTransform runs in remark-rehype before sanitize. Without
          // overriding it, react-markdown's default strips `href` for
          // unknown schemes — making our extended sanitize whitelist
          // moot. Only set when the caller opted into extra protocols.
          urlTransform={urlTransform}
        >
          {displayContent}
        </ReactMarkdown>
      </div>
      {collapsible && shouldCollapse && (
        <CollapseToggle
          isCollapsed={isCollapsed}
          onClick={toggleCollapsed}
          readMoreLabel={readMoreLabel}
          showLessLabel={showLessLabel}
          isUser={isUser}
          isCompact={isCompact}
        />
      )}
    </div>
  );
};

export const MarkdownMessage = memo(MarkdownMessageRaw);

export default MarkdownMessage;
