import { useEffect, useMemo, useRef, useState } from 'react';
import Taro from '@tarojs/taro';
import { Image, RichText, Text, View } from '@tarojs/components';
import { signAssetPreview, type AssetWatermarkContext, type SignedAssetLink } from '@/services/catalog';
import './rich-content.css';

type InlineToken =
  | { type: 'text'; text: string }
  | { type: 'bold'; text: string }
  | { type: 'code'; text: string }
  | { type: 'formula'; text: string; display?: boolean };

type RichBlock =
  | { type: 'paragraph'; tokens: InlineToken[] }
  | { type: 'heading'; level: number; tokens: InlineToken[] }
  | { type: 'formula'; text: string }
  | { type: 'code'; language?: string; text: string }
  | { type: 'image'; url?: string; assetId?: string; alt: string }
  | { type: 'blocked-image'; alt: string }
  | { type: 'table'; header: string[]; align: string[]; rows: string[][] };

export interface RichContentProps {
  content?: unknown;
  className?: string;
  compact?: boolean;
  muted?: boolean;
  mediaUrls?: Array<string | null | undefined>;
  emptyText?: string;
}

const IMAGE_RE = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
const ASSET_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

interface SignedMediaState {
  link?: SignedAssetLink;
  watermark?: AssetWatermarkContext;
  title?: string | null;
  error?: string;
  loading?: boolean;
}

type KatexModule = typeof import('katex');
let katexModulePromise: Promise<KatexModule> | null = null;

function classNames(...values: Array<string | false | null | undefined>) {
  return values.filter(Boolean).join(' ');
}

function decodeHtmlEntities(value: string) {
  return value
    .replace(/&nbsp;/gi, ' ')
    .replace(/&amp;/gi, '&')
    .replace(/&lt;/gi, '<')
    .replace(/&gt;/gi, '>')
    .replace(/&quot;/gi, '"')
    .replace(/&#39;/gi, "'")
    .replace(/&#(\d+);/g, (_, code) => {
      const value = Number(code);
      return Number.isFinite(value) ? String.fromCharCode(value) : '';
    })
    .replace(/&#x([0-9a-f]+);/gi, (_, code) => {
      const value = Number.parseInt(code, 16);
      return Number.isFinite(value) ? String.fromCharCode(value) : '';
    });
}

function attrValue(attrs: string, name: string) {
  const pattern = new RegExp(`${name}\\s*=\\s*["']([^"']+)["']`, 'i');
  return attrs.match(pattern)?.[1] || '';
}

function normalizeSource(value: unknown) {
  if (value === undefined || value === null) return '';
  return decodeHtmlEntities(String(value))
    .replace(/\r\n?/g, '\n')
    .replace(/\u0000/g, '')
    .replace(/<script\b[\s\S]*?<\/script>/gi, '')
    .replace(/<style\b[\s\S]*?<\/style>/gi, '')
    .replace(/<br\s*\/?>/gi, '\n')
    .replace(/<\/p>/gi, '\n\n')
    .replace(/<p\b[^>]*>/gi, '')
    .replace(/<img\b([^>]*)>/gi, (_, attrs) => {
      const src = attrValue(attrs, 'src');
      const alt = attrValue(attrs, 'alt');
      return src ? `\n![${alt || '图片'}](${src})\n` : '';
    })
    .replace(/<(strong|b)\b[^>]*>([\s\S]*?)<\/\1>/gi, '**$2**')
    .replace(/<code\b[^>]*>([\s\S]*?)<\/code>/gi, '`$1`')
    .replace(/<\/?[a-z][^>]*>/gi, '');
}

export function safeTextContent(value: unknown) {
  return normalizeSource(value).trim();
}

export function normalizeSafeMediaUrl(value: unknown) {
  const raw = typeof value === 'string' ? value.trim() : '';
  if (!raw) return null;
  if (/^(javascript|data|vbscript):/i.test(raw)) return null;
  if (raw.startsWith('//')) return null;
  if (raw.startsWith('/')) return raw;
  try {
    const url = new URL(raw);
    if (url.protocol === 'https:') return url.href;
    if (url.protocol === 'http:' && ['localhost', '127.0.0.1'].includes(url.hostname)) return url.href;
  } catch (_error) {
    return null;
  }
  return null;
}

function normalizeAssetId(value: unknown) {
  const raw = typeof value === 'string' ? value.trim() : '';
  if (!raw) return null;
  if (ASSET_ID_RE.test(raw)) return raw;

  const byScheme = raw.match(/^(?:asset|content_asset):([0-9a-f-]{36})$/i);
  if (byScheme && ASSET_ID_RE.test(byScheme[1])) return byScheme[1];

  const byPath = raw.match(/^\/(?:asset|content-assets|content_assets)\/([0-9a-f-]{36})$/i);
  if (byPath && ASSET_ID_RE.test(byPath[1])) return byPath[1];

  return null;
}

function normalizeImageSource(value: unknown): { url?: string; assetId?: string } | null {
  const assetId = normalizeAssetId(value);
  if (assetId) return { assetId };
  const url = normalizeSafeMediaUrl(value);
  return url ? { url } : null;
}

function splitTableCells(line: string) {
  const trimmed = line.trim().replace(/^\|/, '').replace(/\|$/, '');
  return trimmed.split('|').map(cell => cell.trim());
}

function isTableSeparator(line?: string) {
  if (!line) return false;
  const cells = splitTableCells(line);
  return cells.length > 0 && cells.every(cell => /^:?-{3,}:?$/.test(cell.trim()));
}

function tableAligns(separator: string) {
  return splitTableCells(separator).map(cell => {
    if (cell.startsWith(':') && cell.endsWith(':')) return 'center';
    if (cell.endsWith(':')) return 'right';
    return 'left';
  });
}

function parseInline(value: string): InlineToken[] {
  const tokens: InlineToken[] = [];
  const pattern = /(\*\*[^*]+\*\*|`[^`]+`|\\\([^)]+\\\)|\\\[[\s\S]+?\\\]|\$[^$\n]+\$)/g;
  let cursor = 0;
  for (const match of value.matchAll(pattern)) {
    const index = match.index || 0;
    if (index > cursor) tokens.push({ type: 'text', text: value.slice(cursor, index) });
    const raw = match[0];
    if (raw.startsWith('**')) {
      tokens.push({ type: 'bold', text: raw.slice(2, -2) });
    } else if (raw.startsWith('`')) {
      tokens.push({ type: 'code', text: raw.slice(1, -1) });
    } else if (raw.startsWith('\\[')) {
      tokens.push({ type: 'formula', text: raw.slice(2, -2), display: true });
    } else if (raw.startsWith('\\(')) {
      tokens.push({ type: 'formula', text: raw.slice(2, -2) });
    } else {
      tokens.push({ type: 'formula', text: raw.slice(1, -1) });
    }
    cursor = index + raw.length;
  }
  if (cursor < value.length) tokens.push({ type: 'text', text: value.slice(cursor) });
  return tokens.filter(token => token.text.length > 0);
}

function appendParagraphWithImages(blocks: RichBlock[], rawText: string) {
  const text = rawText.trim();
  if (!text) return;
  IMAGE_RE.lastIndex = 0;
  let cursor = 0;
  let matched = false;
  for (const match of text.matchAll(IMAGE_RE)) {
    matched = true;
    const index = match.index || 0;
    const before = text.slice(cursor, index).trim();
    if (before) blocks.push({ type: 'paragraph', tokens: parseInline(before) });
    const source = normalizeImageSource(match[2]);
    if (source) {
      blocks.push({ type: 'image', ...source, alt: match[1] || '图片' });
    } else {
      blocks.push({ type: 'blocked-image', alt: match[1] || '图片' });
    }
    cursor = index + match[0].length;
  }
  const rest = text.slice(cursor).trim();
  if (rest || !matched) blocks.push({ type: 'paragraph', tokens: parseInline(rest || text) });
}

function parseBlocks(input: unknown, mediaUrls: RichContentProps['mediaUrls'] = []) {
  const source = normalizeSource(input);
  const blocks: RichBlock[] = [];
  const paragraph: string[] = [];

  function flushParagraph() {
    if (!paragraph.length) return;
    appendParagraphWithImages(blocks, paragraph.join('\n'));
    paragraph.length = 0;
  }

  const lines = source.split('\n');
  let index = 0;
  while (index < lines.length) {
    const line = lines[index];
    const trimmed = line.trim();

    if (!trimmed) {
      flushParagraph();
      index += 1;
      continue;
    }

    const codeStart = trimmed.match(/^```([a-z0-9_-]+)?\s*$/i);
    if (codeStart) {
      flushParagraph();
      const codeLines: string[] = [];
      index += 1;
      while (index < lines.length && !lines[index].trim().startsWith('```')) {
        codeLines.push(lines[index]);
        index += 1;
      }
      if (index < lines.length) index += 1;
      blocks.push({ type: 'code', language: codeStart[1], text: codeLines.join('\n') });
      continue;
    }

    if (trimmed.startsWith('$$')) {
      flushParagraph();
      const formulaLines: string[] = [];
      const first = trimmed.replace(/^\$\$/, '');
      if (first.endsWith('$$') && first.length > 2) {
        formulaLines.push(first.replace(/\$\$$/, ''));
        index += 1;
      } else {
        if (first) formulaLines.push(first);
        index += 1;
        while (index < lines.length && !lines[index].trim().endsWith('$$')) {
          formulaLines.push(lines[index]);
          index += 1;
        }
        if (index < lines.length) {
          formulaLines.push(lines[index].trim().replace(/\$\$$/, ''));
          index += 1;
        }
      }
      blocks.push({ type: 'formula', text: formulaLines.join('\n').trim() });
      continue;
    }

    const heading = trimmed.match(/^(#{1,4})\s+(.+)$/);
    if (heading) {
      flushParagraph();
      blocks.push({ type: 'heading', level: heading[1].length, tokens: parseInline(heading[2]) });
      index += 1;
      continue;
    }

    if (trimmed.includes('|') && isTableSeparator(lines[index + 1])) {
      flushParagraph();
      const header = splitTableCells(trimmed);
      const align = tableAligns(lines[index + 1]);
      const rows: string[][] = [];
      index += 2;
      while (index < lines.length && lines[index].trim().includes('|')) {
        rows.push(splitTableCells(lines[index]));
        index += 1;
      }
      blocks.push({ type: 'table', header, align, rows });
      continue;
    }

    paragraph.push(line);
    index += 1;
  }
  flushParagraph();

  for (const mediaUrl of mediaUrls || []) {
    const source = normalizeImageSource(mediaUrl);
    if (source) blocks.push({ type: 'image', ...source, alt: '题目图片' });
  }

  return blocks;
}

function loadKatex() {
  if (!katexModulePromise) {
    katexModulePromise = import('katex');
  }
  return katexModulePromise;
}

function renderFormulaHtml(katex: KatexModule, text: string, displayMode = false) {
  return katex.default.renderToString(text, {
    displayMode,
    throwOnError: false,
    trust: false,
    strict: 'warn',
    output: 'html',
  });
}

function FormulaView({ text, display = false, block = false }: { text: string; display?: boolean; block?: boolean }) {
  const [html, setHtml] = useState('');

  useEffect(() => {
    let active = true;
    loadKatex()
      .then(katex => {
        if (!active) return;
        try {
          setHtml(renderFormulaHtml(katex, text, display || block));
        } catch (_error) {
          setHtml('');
        }
      })
      .catch(() => {
        if (active) setHtml('');
      });
    return () => {
      active = false;
    };
  }, [text, display, block]);

  if (html && block) return <View className='rich-formula-block rendered'><RichText nodes={html} /></View>;
  if (html) return <RichText className={display ? 'rich-formula-rendered display' : 'rich-formula-rendered'} nodes={html} />;
  if (block) return <View className='rich-formula-block'><Text>{text}</Text></View>;
  return <Text className={display ? 'rich-formula-inline display' : 'rich-formula-inline'}>{text}</Text>;
}

function renderInline(token: InlineToken, index: number) {
  if (token.type === 'bold') {
    return <Text key={index} className='rich-bold'>{token.text}</Text>;
  }
  if (token.type === 'code') {
    return <Text key={index} className='rich-code-inline'>{token.text}</Text>;
  }
  if (token.type === 'formula') {
    return <FormulaView key={index} text={token.text} display={token.display} />;
  }
  return <Text key={index}>{token.text}</Text>;
}

function preview(url: string) {
  Taro.previewImage({ current: url, urls: [url] }).catch(() => undefined);
}

function assetIdsFromBlocks(blocks: RichBlock[]) {
  return Array.from(new Set(blocks.flatMap(block => (block.type === 'image' && block.assetId ? [block.assetId] : []))));
}

export default function RichContent({ content, className, compact = false, muted = false, mediaUrls, emptyText }: RichContentProps) {
  const blocks = useMemo(() => parseBlocks(content, mediaUrls), [content, mediaUrls]);
  const assetIds = useMemo(() => assetIdsFromBlocks(blocks), [blocks]);
  const [signedMedia, setSignedMedia] = useState<Record<string, SignedMediaState>>({});
  const requestedAssetIds = useRef(new Set<string>());

  useEffect(() => {
    if (!assetIds.length) return;
    let active = true;
    for (const assetId of assetIds) {
      if (requestedAssetIds.current.has(assetId)) continue;
      requestedAssetIds.current.add(assetId);
      setSignedMedia(current => ({ ...current, [assetId]: { ...current[assetId], loading: true } }));
      signAssetPreview(assetId)
        .then(payload => {
          if (!active) return;
          setSignedMedia(current => ({
            ...current,
            [assetId]: {
              link: payload.preview,
              watermark: payload.watermark,
              title: payload.item?.title || payload.item?.fileName || null,
              loading: false,
            },
          }));
        })
        .catch(error => {
          if (!active) return;
          setSignedMedia(current => ({
            ...current,
            [assetId]: {
              error: error instanceof Error ? error.message : '图片签名失败',
              loading: false,
            },
          }));
        });
    }
    return () => {
      active = false;
    };
  }, [assetIds.join('|')]);

  if (!blocks.length) {
    return emptyText ? <Text className='rich-empty'>{emptyText}</Text> : null;
  }

  return (
    <View className={classNames('rich-content', compact && 'compact', muted && 'muted', className)}>
      {blocks.map((block, index) => {
        if (block.type === 'heading') {
          return (
            <View key={index} className={classNames('rich-heading', `level-${block.level}`)}>
              {block.tokens.map(renderInline)}
            </View>
          );
        }
        if (block.type === 'paragraph') {
          return (
            <View key={index} className='rich-paragraph'>
              {block.tokens.map(renderInline)}
            </View>
          );
        }
        if (block.type === 'formula') {
          return <FormulaView key={index} text={block.text} block />;
        }
        if (block.type === 'code') {
          return (
            <View key={index} className='rich-code-block'>
              {block.language ? <Text className='rich-code-language'>{block.language}</Text> : null}
              <Text className='rich-code-text'>{block.text}</Text>
            </View>
          );
        }
        if (block.type === 'image') {
          const signed = block.assetId ? signedMedia[block.assetId] : undefined;
          const imageUrl = block.url || signed?.link?.url;
          if (block.assetId && signed?.error) {
            return <View key={index} className='rich-blocked-image'>私有图片签名失败：{signed.error}</View>;
          }
          if (block.assetId && !imageUrl) {
            return <View key={index} className='rich-image-pending'>私有图片授权中...</View>;
          }
          if (!imageUrl) {
            return <View key={index} className='rich-blocked-image'>图片地址未通过安全校验：{block.alt}</View>;
          }
          return (
            <View key={index} className='rich-image-wrap' onClick={() => preview(imageUrl)}>
              <Image className='rich-image' src={imageUrl} mode='widthFix' />
              {block.alt || signed?.watermark?.traceId ? (
                <Text className='rich-image-caption'>
                  {block.alt || signed?.title || '题目图片'}{signed?.watermark?.traceId ? ` · ${signed.watermark.traceId}` : ''}
                </Text>
              ) : null}
            </View>
          );
        }
        if (block.type === 'blocked-image') {
          return <View key={index} className='rich-blocked-image'>图片地址未通过安全校验：{block.alt}</View>;
        }
        return (
          <View key={index} className='rich-table'>
            <View className='rich-table-row header'>
              {block.header.map((cell, cellIndex) => (
                <View key={cellIndex} className={classNames('rich-table-cell', block.align[cellIndex])}>
                  {parseInline(cell).map(renderInline)}
                </View>
              ))}
            </View>
            {block.rows.map((row, rowIndex) => (
              <View key={rowIndex} className='rich-table-row'>
                {block.header.map((_, cellIndex) => (
                  <View key={cellIndex} className={classNames('rich-table-cell', block.align[cellIndex])}>
                    {parseInline(row[cellIndex] || '').map(renderInline)}
                  </View>
                ))}
              </View>
            ))}
          </View>
        );
      })}
    </View>
  );
}
