import { type ReactNode, useMemo } from 'react';
import Markdown, { defaultUrlTransform } from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
import rehypeKatex from 'rehype-katex';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';

import { createCodeBlockPre } from '../../../components/markdown/CodeBlock';
import { OPENHUMAN_LINK_EVENT } from '../../../components/OpenhumanLinkModal';
import { parseMarkdownTable } from '../../../utils/agentMessageBubbles';
import { hasLatexContent, normalizeLatexDelimiters } from '../../../utils/latex';
import { openUrl } from '../../../utils/openUrl';
import { openWorkspacePath } from '../../../utils/tauriCommands/workspacePaths';
import { parseWorkspaceHref } from '../../../utils/workspaceLinks';
import {
  type AgentBubblePosition,
  getAgentBubbleChrome,
  isAllowedExternalHref,
  parseBubbleSegments,
} from '../utils/format';

const GFM_REMARK_PLUGINS = [remarkGfm];
const MATH_REMARK_PLUGINS = [remarkGfm, remarkMath];
// rehype-highlight must come before rehypeKatex so code blocks inside math
// environments are not double-processed.
const HIGHLIGHT_REHYPE_PLUGINS = [rehypeHighlight];
const MATH_REHYPE_PLUGINS = [rehypeHighlight, rehypeKatex];
type ParsedMarkdownTable = NonNullable<ReturnType<typeof parseMarkdownTable>>;

/**
 * Pill rendered below an agent bubble for each
 * `<openhuman-link path="...">label</openhuman-link>` tag the agent
 * emits. Click dispatches an `OPENHUMAN_LINK_EVENT` window event that
 * `OpenhumanLinkModal` listens for, so the chat stays in view.
 */
function OpenhumanLinkPill({ path, label }: { path: string; label: string }) {
  return (
    <button
      type="button"
      onClick={() =>
        window.dispatchEvent(new CustomEvent(OPENHUMAN_LINK_EVENT, { detail: { path } }))
      }
      className="inline-flex items-center gap-1 rounded-full border border-primary-200 bg-primary-50 px-3 py-1 text-xs font-medium text-primary-700 transition-colors hover:bg-primary-100">
      {label}
      <svg className="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor">
        <path
          strokeLinecap="round"
          strokeLinejoin="round"
          strokeWidth={2}
          d="M5 12h14M13 6l6 6-6 6"
        />
      </svg>
    </button>
  );
}

function transformMarkdownUrl(url: string): string {
  return parseWorkspaceHref(url) ? url : defaultUrlTransform(url);
}

function MarkdownAnchor({ href, children }: { href?: string; children?: ReactNode }) {
  return (
    <a
      href={href}
      onClick={e => {
        e.preventDefault();
        const workspaceTarget = parseWorkspaceHref(href);
        if (workspaceTarget) {
          void openWorkspacePath(workspaceTarget.path).catch(err => {
            console.error('workspace open failed:', err);
          });
          return;
        }
        if (!href || !isAllowedExternalHref(href)) return;
        void openUrl(href).catch(() => {
          // Ignore launcher errors from OS URL handler failures.
        });
      }}
      className="cursor-pointer underline">
      {children}
    </a>
  );
}

export function BubbleMarkdown({
  content,
  tone = 'agent',
}: {
  content: string;
  tone?: 'agent' | 'user';
}) {
  const proseTone =
    tone === 'user'
      ? 'prose-invert prose-p:text-white prose-li:text-white prose-a:text-white prose-code:text-white prose-strong:text-white prose-headings:text-white [&_li::marker]:text-white/85'
      : 'dark:prose-invert prose-a:text-primary-500 prose-code:text-primary-700 dark:prose-code:text-primary-300 prose-headings:text-sm [&_li::marker]:text-stone-700 dark:[&_li::marker]:text-neutral-300';

  const hasMath = hasLatexContent(content);
  const rendered = hasMath ? normalizeLatexDelimiters(content) : content;

  // Memoize the `pre` override so it stays reference-stable across re-renders
  // that don't change tone (avoids remounting code blocks on every keystroke).
  const markdownComponents = useMemo(
    () => ({ a: MarkdownAnchor, pre: createCodeBlockPre(tone) }),
    [tone]
  );

  return (
    // prose-pre:my-2 and prose-pre:rounded-lg are kept to style the outer
    // wrapper div emitted by CodeBlock.tsx (it is not a <pre> itself, but
    // Tailwind prose targets the <pre> inside it via the child selector).
    // prose-pre:bg-* classes are intentionally removed: CodeBlock owns the
    // background colours on both the header bar and the code body.
    <div
      className={`text-sm prose prose-sm max-w-none prose-p:my-1 prose-pre:my-0 prose-code:text-xs prose-headings:font-semibold prose-ul:my-0 prose-ol:my-0 prose-li:my-0 ${proseTone} [&_ul]:my-0 [&_ol]:my-0 [&_ul]:pl-0 [&_ol]:pl-0 [&_ul]:list-inside [&_ol]:list-inside [&_li]:my-0 [&_li]:pl-0 [&_li_p]:inline [&_li_p]:m-0`}>
      <Markdown
        urlTransform={transformMarkdownUrl}
        components={markdownComponents}
        remarkPlugins={hasMath ? MATH_REMARK_PLUGINS : GFM_REMARK_PLUGINS}
        rehypePlugins={hasMath ? MATH_REHYPE_PLUGINS : HIGHLIGHT_REHYPE_PLUGINS}>
        {rendered}
      </Markdown>
    </div>
  );
}

export function TableCellMarkdown({ content }: { content: string }) {
  const hasMath = hasLatexContent(content);
  const rendered = hasMath ? normalizeLatexDelimiters(content) : content;
  // Table cells are compact — we add rehypeHighlight for token colouring but
  // intentionally skip the CodeBlock chrome (no header bar / copy button) to
  // avoid layout disruption inside narrow table cells.
  return (
    <div className="prose prose-sm dark:prose-invert max-w-none text-sm text-content-secondary prose-p:my-0 prose-ul:my-0 prose-ol:my-0 prose-li:my-0 prose-code:text-xs prose-code:text-primary-700 dark:prose-code:text-primary-300 prose-a:text-primary-500 prose-strong:text-stone-900 dark:prose-strong:text-neutral-100 prose-headings:text-sm prose-headings:font-semibold [&_li::marker]:text-stone-700 dark:[&_li::marker]:text-neutral-300 [&_ul]:my-0 [&_ol]:my-0 [&_ul]:pl-0 [&_ol]:pl-0 [&_ul]:list-inside [&_ol]:list-inside [&_li]:pl-0 [&_li_p]:inline [&_li_p]:m-0">
      <Markdown
        urlTransform={transformMarkdownUrl}
        components={{ a: MarkdownAnchor }}
        remarkPlugins={hasMath ? MATH_REMARK_PLUGINS : GFM_REMARK_PLUGINS}
        rehypePlugins={hasMath ? MATH_REHYPE_PLUGINS : HIGHLIGHT_REHYPE_PLUGINS}>
        {rendered}
      </Markdown>
    </div>
  );
}

function AgentMarkdownTable({
  table,
  className,
}: {
  table: ParsedMarkdownTable;
  className: string;
}) {
  return (
    <div className={className}>
      <div className="overflow-x-auto">
        <table className="w-max min-w-full border-collapse text-left text-sm text-content">
          <thead className="bg-surface-subtle dark:bg-surface-muted/90">
            <tr>
              {table.headers.map(header => (
                <th
                  key={header}
                  className="max-w-[25vw] border-b border-line px-4 py-2.5 text-xs font-semibold uppercase tracking-[0.08em] text-content-muted">
                  {header}
                </th>
              ))}
            </tr>
          </thead>
          <tbody>
            {table.rows.map((row, rowIndex) => (
              <tr
                key={`${rowIndex}:${row.join('|')}`}
                className="odd:bg-white dark:odd:bg-neutral-900 even:bg-stone-50 dark:even:bg-neutral-800/60">
                {row.map((cell, cellIndex) => (
                  <td
                    key={`${rowIndex}:${cellIndex}:${cell}`}
                    className="max-w-[25vw] border-t border-line px-4 py-3 align-top text-sm text-content-secondary">
                    <TableCellMarkdown content={cell} />
                  </td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

export function AgentMessageBubble({
  content,
  position = 'single',
}: {
  content: string;
  position?: AgentBubblePosition;
}) {
  const segments = parseBubbleSegments(content);
  const textContent = segments
    .filter(s => s.kind === 'text')
    .map(s => s.text)
    .join('')
    .trim();
  const linkSegments = segments.filter(
    (s): s is Extract<typeof s, { kind: 'link' }> => s.kind === 'link'
  );

  const table = parseMarkdownTable(textContent);
  const bubbleChrome = getAgentBubbleChrome(position);

  if (table) {
    return (
      <AgentMarkdownTable
        table={table}
        className={`w-full max-w-full overflow-hidden border border-line bg-surface/90 shadow-sm ${bubbleChrome}`}
      />
    );
  }

  return (
    <>
      {textContent && (
        <div
          className={`bg-surface-strong dark:bg-surface-muted/80 px-4 py-2.5 text-content ${bubbleChrome}`}>
          <BubbleMarkdown content={textContent} />
        </div>
      )}
      {linkSegments.length > 0 && (
        <div className="mt-2 flex flex-wrap gap-2">
          {linkSegments.map((segment, idx) => (
            <OpenhumanLinkPill
              key={`pill-${idx}-${segment.path}`}
              path={segment.path}
              label={segment.label}
            />
          ))}
        </div>
      )}
    </>
  );
}

export function AgentMessageText({ content }: { content: string }) {
  const segments = parseBubbleSegments(content);
  const textContent = segments
    .filter(s => s.kind === 'text')
    .map(s => s.text)
    .join('')
    .trim();
  const linkSegments = segments.filter(
    (s): s is Extract<typeof s, { kind: 'link' }> => s.kind === 'link'
  );
  const table = parseMarkdownTable(textContent);

  return (
    <div className="w-full min-w-0 px-1 py-1 text-content" data-testid="agent-message-text">
      {table ? (
        <AgentMarkdownTable table={table} className="w-full max-w-full overflow-hidden" />
      ) : (
        textContent && <BubbleMarkdown content={textContent} />
      )}
      {linkSegments.length > 0 && (
        <div className="mt-2 flex flex-wrap gap-2">
          {linkSegments.map((segment, idx) => (
            <OpenhumanLinkPill
              key={`pill-${idx}-${segment.path}`}
              path={segment.path}
              label={segment.label}
            />
          ))}
        </div>
      )}
    </div>
  );
}
