import React, { useCallback, useMemo, useRef, useState } from 'react';
import { Image, Linking, Pressable, ScrollView, Text, View } from 'react-native';
import Markdown, { MarkdownIt, type ASTNode, type RenderRule } from 'react-native-markdown-display';

import { markdownStyles } from './markdownStyles';
import { decodeFragment, extractTableData, getNodeText, hasWideContent, isSafeUrl, slugify } from './markdownTextUtils';
import { getTextDirection } from './messageUtils';
import { useMarkdownScroll } from './markdownScrollContext';
import { useSuperagentTheme } from '../../theme';
import type { SuperagentMarkdownRenderer, SuperagentMessage } from '../../types';

// CommonMark + GFM (tables, strikethrough, autolinks). `html: false` keeps raw
// HTML out, `linkify` makes bare http(s) URLs tappable like the old renderer.
const markdownItInstance = MarkdownIt({ breaks: false, html: false, linkify: true, typographer: false });

export function MarkdownText({
  content,
  isUser,
  message,
  onLongPress,
  renderMarkdown,
}: {
  content: string;
  isUser: boolean;
  message: SuperagentMessage;
  onLongPress?: () => void;
  renderMarkdown?: SuperagentMarkdownRenderer;
}) {
  // Subscribe to the color scheme so the memoized style/rules below re-resolve
  // through the themed proxy when the user toggles light/dark.
  const { scheme } = useSuperagentTheme();
  const scroller = useMarkdownScroll();

  // slug -> heading view, so an in-document anchor link can scroll to its target.
  const headingRefs = useRef<Map<string, View>>(new Map());
  const registerHeading = useCallback((slug: string, element: View | null) => {
    if (!slug) return;
    if (element) headingRefs.current.set(slug, element);
    else headingRefs.current.delete(slug);
  }, []);

  // GitHub-style disambiguation for repeated headings: the first "Setup" gets
  // `setup`, the next `setup-1`, etc., so anchor links resolve to distinct
  // targets. The counter is reset per render (below) so slugs stay stable and
  // ordinal, keeping headingRefs keyed on unique slugs.
  const slugCountsRef = useRef<Map<string, number>>(new Map());
  const resolveHeadingSlug = useCallback((node: ASTNode) => {
    const base = slugify(getNodeText(node));
    const seen = slugCountsRef.current.get(base) ?? 0;
    slugCountsRef.current.set(base, seen + 1);
    return seen === 0 ? base : `${base}-${seen}`;
  }, []);

  const handleLinkPress = useCallback((href: string) => {
    // In-document anchor (e.g. [jump](#setup)): scroll to the matching heading.
    // Slugify the (decoded) fragment so encoded hrefs like `#my%20heading`
    // resolve to the same slug headings are registered under.
    if (href.startsWith('#')) {
      const target = headingRefs.current.get(slugify(decodeFragment(href.slice(1))));
      if (target) scroller?.scrollViewIntoView(target);
      return;
    }
    // External link: open only safe http(s) URLs.
    if (isSafeUrl(href)) Linking.openURL(href).catch(() => {});
  }, [scroller]);

  const rules = useMemo(
    () => createMarkdownRules({ onLinkPress: handleLinkPress, onLongPress, registerHeading, resolveHeadingSlug }),
    [handleLinkPress, onLongPress, registerHeading, resolveHeadingSlug],
  );

  // Rebuild the style object whenever the scheme changes (new identity) so the
  // library re-reads the themed proxy instead of caching the first scheme.
  const style = useMemo(() => {
    const resolved = { ...markdownStyles } as Record<string, unknown>;
    // Align each message by the direction of its own text — RTL languages
    // (Hebrew/Arabic) right, LTR left — independent of the device locale. `body`
    // styles are inherited by the rendered text, so textAlign here reaches every
    // paragraph.
    const base = hasWideContent(content) ? resolved.bodyWide : resolved.body;
    const direction = getTextDirection(content);
    resolved.body = {
      ...(base as object),
      textAlign: direction === 'rtl' ? 'right' : 'left',
      writingDirection: direction,
    };
    return resolved;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [content, scheme]);

  if (renderMarkdown) {
    const Renderer = renderMarkdown;
    return <Renderer content={content} isUser={isUser} message={message} />;
  }

  if (!content || !content.trim()) return null;

  // Reset the heading-slug counter before the rules render this message's
  // headings (children render right after this body), so slugs are assigned
  // fresh and in document order on every render.
  slugCountsRef.current = new Map();

  return (
    <Markdown markdownit={markdownItInstance} mergeStyle rules={rules} style={style}>
      {content}
    </Markdown>
  );
}

// Override the few rules where we need native-only behavior the library's
// defaults don't give us: long-press passthrough, link + in-document anchor
// handling, heading anchors, horizontally scrollable code and tables, bold
// table headers, and images. The default image rule renders via
// react-native-fit-image, which throws "size props must be present none or both
// of width and height" on this RN version — so render with RN's <Image>.
function createMarkdownRules({
  onLinkPress,
  onLongPress,
  registerHeading,
  resolveHeadingSlug,
}: {
  onLinkPress: (href: string) => void;
  onLongPress?: () => void;
  registerHeading: (slug: string, element: View | null) => void;
  resolveHeadingSlug: (node: ASTNode) => string;
}): Record<string, RenderRule> {
  // Headings are wrapped in a measurable, ref'd View keyed by their (unique,
  // disambiguated) slug so anchor links can scroll to them. collapsable={false}
  // keeps Android from flattening the View away (which would null the ref).
  const heading = (level: number): RenderRule => (node, children, _parent, styles) => {
    const slug = resolveHeadingSlug(node);
    return (
      <View collapsable={false} key={node.key} ref={(element) => registerHeading(slug, element)}>
        <Text style={styles[`heading${level}`]}>{children}</Text>
      </View>
    );
  };

  return {
    // Skip onLongPress for text inside a link or blocklink, otherwise the leaf
    // <Text> becomes the touch responder and swallows the (block)link's onPress.
    text: (node, _children, parent, styles, inheritedStyles = {}) => {
      const insideLink = parent.some((ancestor) => ancestor.type === 'link' || ancestor.type === 'blocklink');
      return (
        <Text key={node.key} onLongPress={insideLink ? undefined : onLongPress} style={[inheritedStyles, styles.text]}>
          {node.content}
        </Text>
      );
    },

    heading1: heading(1),
    heading2: heading(2),
    heading3: heading(3),
    heading4: heading(4),
    heading5: heading(5),
    heading6: heading(6),

    link: (node, children, _parent, styles) => (
      <Text key={node.key} onPress={() => onLinkPress(node.attributes?.href ?? '')} style={styles.link}>
        {children}
      </Text>
    ),

    // The library rewrites links wrapping block content (e.g. a linked image)
    // to `blocklink`. Route it through the same handler so block links honor the
    // safe-URL / anchor policy instead of the library default, which would open
    // any scheme (it also re-wraps children in styles.image — we don't).
    blocklink: (node, children, _parent) => (
      <Pressable key={node.key} onPress={() => onLinkPress(node.attributes?.href ?? '')}>
        {children}
      </Pressable>
    ),

    // Fenced code scrolls horizontally (the host drops the bubble Pressable for
    // ``` / ~~~). Indented code (code_block) is NOT flagged by the host, so it
    // renders without horizontal scroll to avoid a scroll/long-press conflict.
    fence: (node, _children, _parent, styles) => renderCodeBlock(node, styles, onLongPress, true),
    code_block: (node, _children, _parent, styles) => renderCodeBlock(node, styles, onLongPress, false),

    // Render the whole table from the AST so we can size each column to its
    // content (clamped) and keep cells on a single line — matching the original
    // hand-rolled renderer, which laid out tables better than the library's
    // flex-based default.
    table: (node, _children, _parent, styles) => (
      <MarkdownTable key={node.key} node={node} onLinkPress={onLinkPress} styles={styles} />
    ),

    // Only render safe http(s) image sources; skip data:/file:/other schemes
    // (matches the inline-link policy and the original hand-rolled renderer).
    image: (node, _children, _parent, styles) => {
      const url = node.attributes?.src ?? '';
      if (!isSafeUrl(url)) return null;
      return <MarkdownImage alt={node.attributes?.alt ?? ''} key={node.key} style={styles.image} url={url} />;
    },
  };
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function renderCodeBlock(node: ASTNode, styles: Record<string, any>, onLongPress?: () => void, scrollable = true) {
  const code = node.content.replace(/\n$/, '');
  const block = (
    <View style={styles.fence}>
      <Text onLongPress={onLongPress} style={styles.fenceText}>{code}</Text>
    </View>
  );
  if (!scrollable) return <View key={node.key} style={styles.fenceScroll}>{block}</View>;
  return (
    <ScrollView horizontal key={node.key} nestedScrollEnabled showsHorizontalScrollIndicator style={styles.fenceScroll}>
      {block}
    </ScrollView>
  );
}

// Extra horizontal space added to a column's measured text width: cell padding
// (10 + 10) plus the 1px left border. Columns are capped so a very long cell
// truncates instead of producing an enormous column.
const CELL_HORIZONTAL_EXTRA = 21;
const MAX_COLUMN_WIDTH = 340;

// Ported from the original renderer (hand-laid table so it can scroll
// horizontally), but columns are now sized to the *measured* width of their
// widest cell instead of a char-count estimate: each cell renders at its
// natural width first, onLayout reports it, and we apply the per-column max so
// columns are exactly content-wide and aligned across rows.
function MarkdownTable({
  node,
  onLinkPress,
  styles,
}: {
  node: ASTNode;
  onLinkPress: (href: string) => void;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  styles: Record<string, any>;
}) {
  const { header, rows } = extractTableData(node);
  const columnCount = Math.max(header.length, ...rows.map((row) => row.length), 0);

  const [columnWidths, setColumnWidths] = useState<number[]>([]);
  const maxWidthsRef = useRef<number[]>([]);
  const recordCellWidth = useCallback((column: number, width: number) => {
    const next = Math.ceil(width);
    if ((maxWidthsRef.current[column] ?? 0) >= next) return;
    maxWidthsRef.current[column] = next;
    setColumnWidths([...maxWidthsRef.current]);
  }, []);

  // undefined while unmeasured -> the cell sizes to its content on first pass.
  const columnStyle = (column: number) => {
    const measured = columnWidths[column];
    if (measured == null) return undefined;
    return { width: Math.min(measured + CELL_HORIZONTAL_EXTRA, MAX_COLUMN_WIDTH) };
  };

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const renderCell = (cell: ASTNode | undefined, column: number, cellStyle: any, textStyle: any) => (
    <View key={column} style={[cellStyle, columnStyle(column)]}>
      <Text
        numberOfLines={1}
        onLayout={(event) => recordCellWidth(column, event.nativeEvent.layout.width)}
        style={textStyle}
      >
        {renderCellContent(cell, styles, onLinkPress)}
      </Text>
    </View>
  );

  return (
    <ScrollView
      directionalLockEnabled
      horizontal
      nestedScrollEnabled
      showsHorizontalScrollIndicator
      style={styles.tableScroll}
    >
      <View style={styles.table}>
        <View style={[styles.tr, styles.tableHeaderRow]}>
          {Array.from({ length: columnCount }, (_unused, column) =>
            renderCell(header[column], column, styles.th, styles.thText))}
        </View>
        {rows.map((row, rowIndex) => (
          <View key={rowIndex} style={styles.tr}>
            {Array.from({ length: columnCount }, (_unused, column) =>
              renderCell(row[column], column, styles.td, styles.tdText))}
          </View>
        ))}
      </View>
    </ScrollView>
  );
}

// Render a cell's inline content (bold / italic / strikethrough / code / links)
// into spans that nest inside the single-line cell <Text>.
function renderCellContent(
  node: ASTNode | undefined,
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  styles: Record<string, any>,
  onLinkPress: (href: string) => void,
): React.ReactNode {
  if (!node) return '';
  const children = () => (node.children ?? []).map((child, index) => (
    <React.Fragment key={index}>{renderCellContent(child, styles, onLinkPress)}</React.Fragment>
  ));
  switch (node.type) {
    case 'text':
      return node.content;
    case 'softbreak':
    case 'hardbreak':
      return ' ';
    case 'code_inline':
      return <Text style={styles.code_inline}>{node.content}</Text>;
    case 'strong':
      return <Text style={styles.strong}>{children()}</Text>;
    case 'em':
      return <Text style={styles.em}>{children()}</Text>;
    case 's':
      return <Text style={styles.s}>{children()}</Text>;
    case 'link':
      return <Text onPress={() => onLinkPress(node.attributes?.href ?? '')} style={styles.link}>{children()}</Text>;
    default:
      return children();
  }
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function MarkdownImage({ alt, style, url }: { alt: string; style: any; url: string }) {
  // Generated images have unknown dimensions; render full-width and lock the
  // aspect ratio once the natural size loads (falls back to 3:2 meanwhile).
  const [aspectRatio, setAspectRatio] = useState<number | null>(null);
  return (
    <Image
      accessibilityLabel={alt || undefined}
      accessibilityRole="image"
      accessible={Boolean(alt)}
      onLoad={(event) => {
        const { height, width } = event.nativeEvent.source;
        if (width > 0 && height > 0) setAspectRatio(width / height);
      }}
      resizeMode="contain"
      source={{ uri: url }}
      style={[style, { aspectRatio: aspectRatio ?? 1.5 }]}
    />
  );
}
