part of '../message_group.dart';

class _MessageContent extends StatelessWidget {
  final Event event;
  final PyramidTheme pt;
  final Timeline? timeline;

  const _MessageContent({required this.event, required this.pt, this.timeline});

  // Resolves edits: returns the display event (last edit) for this event.
  Event get _displayEvent =>
      timeline != null ? event.getDisplayEvent(timeline!) : event;

  String? _getReplyId() {
    final relatesTo = event.content.tryGetMap<String, dynamic>('m.relates_to');
    final replyMap = relatesTo?.tryGetMap<String, dynamic>('m.in_reply_to');
    return replyMap?['event_id'] as String?;
  }

  String _stripReplyFallback(String body) {
    final norm = body.replaceAll('\r\n', '\n');
    if (!norm.startsWith('> ') && !norm.startsWith('>\n')) return norm;
    final lines = norm.split('\n');
    var i = 0;
    while (i < lines.length && (lines[i].startsWith('> ') || lines[i].trimRight() == '>')) {
      i++;
    }
    if (i < lines.length && lines[i].trim().isEmpty) i++;
    if (i >= lines.length) return norm;
    return lines.skip(i).join('\n').trim();
  }

  Event? _findReplyEvent(String replyId) {
    if (timeline == null) return null;
    try {
      return timeline!.events.firstWhere((e) => e.eventId == replyId);
    } catch (_) {
      return null;
    }
  }

  @override
  Widget build(BuildContext context) {
    final displayEvent = _displayEvent;
    final isEdited = displayEvent.eventId != event.eventId;

    // Still encrypted (decryption pending or failed)
    if (displayEvent.type == EventTypes.Encrypted) {
      return Padding(
        padding: const EdgeInsets.only(bottom: 2),
        child: Wrap(
          crossAxisAlignment: WrapCrossAlignment.center,
          spacing: 4,
          children: [
            Icon(Icons.lock_outline_rounded, size: 13, color: pt.fgDim),
            Text(
              'Nachricht wird entschlüsselt…',
              style: TextStyle(color: pt.fgDim, fontSize: 13, fontStyle: FontStyle.italic),
            ),
            if (timeline != null)
              Builder(
                builder: (ctx) => GestureDetector(
                  onTap: () {
                    timeline!.requestKeys();
                    ScaffoldMessenger.of(ctx).showSnackBar(
                      const SnackBar(
                        content: Text('Schlüssel werden von anderen Geräten angefordert…'),
                        duration: Duration(seconds: 3),
                      ),
                    );
                  },
                  child: Text(
                    'Schlüssel anfordern',
                    style: TextStyle(
                      color: pt.accent,
                      fontSize: 12,
                      decoration: TextDecoration.underline,
                      decorationColor: pt.accent,
                    ),
                  ),
                ),
              ),
          ],
        ),
      );
    }

    if (displayEvent.type == EventTypes.Sticker) {
      return _MediaContent(event: displayEvent, pt: pt);
    }

    final msgType = displayEvent.messageType;

    if (msgType == 'm.image') {
      final ext = displayEvent.body.contains('.')
          ? displayEvent.body.split('.').last.toLowerCase()
          : '';
      const kImageExts = {
        'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp',
        'tiff', 'tif', 'heic', 'heif', 'avif', 'ico', 'jfif',
      };
      // SVG kann Image.memory nicht dekodieren — über den SVG-Renderer anzeigen.
      if (ext == 'svg' ||
          displayEvent.content
                  .tryGetMap<String, Object?>('info')?['mimetype'] ==
              'image/svg+xml') {
        return _DocPreviewContent(event: displayEvent, pt: pt);
      }
      if (ext.isNotEmpty && !kImageExts.contains(ext)) {
        return _FileContent(event: displayEvent, pt: pt, icon: Icons.insert_drive_file_rounded);
      }
      return _ImageContent(event: displayEvent, pt: pt);
    }
    if (msgType == 'm.audio') {
      return _AudioContent(event: displayEvent, pt: pt);
    }
    if (msgType == 'm.video') {
      return _VideoContent(event: displayEvent, pt: pt);
    }
    if (msgType == 'm.file') {
      if (DocumentViewer.isInlinePreviewable(displayEvent.body)) {
        return _DocPreviewContent(event: displayEvent, pt: pt);
      }
      if (DocumentViewer.showsInlineCard(displayEvent.body)) {
        return _ProprietaryInlineCard(event: displayEvent, pt: pt);
      }
      return _FileContent(event: displayEvent, pt: pt, icon: Icons.insert_drive_file_rounded);
    }

    final replyId = _getReplyId();
    final replyEvent = replyId != null ? _findReplyEvent(replyId) : null;
    final plainBody = replyId != null
        ? _stripReplyFallback(displayEvent.body)
        : displayEvent.body;

    // Use formatted_body (HTML) if the event has one, otherwise plain text.
    final format = displayEvent.content.tryGet<String>('format');
    // Strip the Matrix reply fallback <mx-reply>…</mx-reply> block — we render
    // our own quote card (_ReplyPreview) above, so the inline "In reply to @user"
    // would be redundant and ugly.
    final formattedBody = displayEvent.formattedText.replaceAll(
      RegExp(r'<mx-reply>.*?</mx-reply>', caseSensitive: false, dotAll: true),
      '',
    ).trim();
    final hasHtml = format == 'org.matrix.custom.html' && formattedBody.isNotEmpty;

    final firstUrl = _firstUrl(plainBody);

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        if (replyEvent != null) _ReplyPreview(replyEvent: replyEvent, pt: pt),
        Padding(
          padding: const EdgeInsets.only(bottom: 2),
          child: hasHtml
              ? _MatrixHtmlText(
                  html: formattedBody,
                  baseStyle: TextStyle(color: pt.fg, fontSize: 14, height: 1.55),
                  linkColor: pt.accent,
                )
              : _PlainLinkText(
                  text: plainBody,
                  baseStyle: TextStyle(color: pt.fg, fontSize: 14, height: 1.55),
                  linkColor: pt.accent,
                ),
        ),
        if (isEdited)
          Text(
            '(bearbeitet)',
            style: TextStyle(color: pt.fgDim, fontSize: 11, fontStyle: FontStyle.italic),
          ),
        if (firstUrl != null) _LinkPreview(url: firstUrl, pt: pt),
      ],
    );
  }

  // Extracts the first http(s) URL in the text, for link-preview fetching.
  static final RegExp _urlRegex = RegExp(r'https?://[^\s<>()\[\]]+', caseSensitive: false);
  String? _firstUrl(String text) {
    final m = _urlRegex.firstMatch(text);
    if (m == null) return null;
    var url = m.group(0)!;
    // Trim common trailing punctuation that isn't part of the URL.
    url = url.replaceAll(RegExp(r'[.,;:!?]+$'), '');
    return url;
  }
}

class _MatrixHtmlText extends StatelessWidget {
  final String html;
  final TextStyle baseStyle;
  final Color linkColor;

  const _MatrixHtmlText({
    required this.html,
    required this.baseStyle,
    required this.linkColor,
  });

  @override
  Widget build(BuildContext context) {
    final spans = _parseHtml(html, baseStyle, linkColor);
    return SelectionArea(
      child: RichText(text: TextSpan(children: spans, style: baseStyle)),
    );
  }

  List<InlineSpan> _parseHtml(String html, TextStyle base, Color linkColor) {
    // Normalise line endings and collapse whitespace around block elements.
    var src = html
        .replaceAll('\r\n', '\n')
        .replaceAll(RegExp(r'<br\s*/?>', caseSensitive: false), '\n')
        .replaceAll(RegExp(r'</(p|div|blockquote|pre|li)>', caseSensitive: false), '\n')
        .replaceAll(RegExp(r'<(p|div|li)[^>]*>', caseSensitive: false), '')
        .replaceAll(RegExp(r'<blockquote[^>]*>', caseSensitive: false), '  ')
        .replaceAll(RegExp(r'<pre[^>]*>', caseSensitive: false), '');

    final spans = <InlineSpan>[];

    // Simple recursive stack-based parser.
    // We tokenise into (tag | text) chunks, then apply style based on open tags.
    final tagRe = RegExp(r'<(/?)(\w+)([^>]*)>', caseSensitive: false);

    var bold = 0;
    var italic = 0;
    var strike = 0;
    var underline = 0;
    var code = 0;
    String? pendingHref;

    int pos = 0;
    for (final m in tagRe.allMatches(src)) {
      // Text before this tag.
      if (m.start > pos) {
        final text = _decodeEntities(src.substring(pos, m.start));
        if (text.isNotEmpty) {
          spans.add(_buildSpan(
            text, base, bold > 0, italic > 0, strike > 0, underline > 0,
            code > 0, pendingHref, linkColor,
          ));
          if (pendingHref != null) pendingHref = null;
        }
      }
      pos = m.end;

      final closing = m.group(1) == '/';
      final tag = m.group(2)!.toLowerCase();
      final attrs = m.group(3) ?? '';

      if (closing) {
        switch (tag) {
          case 'b': case 'strong': bold = (bold - 1).clamp(0, 99); break;
          case 'i': case 'em':    italic = (italic - 1).clamp(0, 99); break;
          case 'del': case 's': case 'strike': strike = (strike - 1).clamp(0, 99); break;
          case 'u':   underline = (underline - 1).clamp(0, 99); break;
          case 'code': code = (code - 1).clamp(0, 99); break;
          case 'a':   pendingHref = null; break;
        }
      } else {
        switch (tag) {
          case 'b': case 'strong': bold++; break;
          case 'i': case 'em':    italic++; break;
          case 'del': case 's': case 'strike': strike++; break;
          case 'u':   underline++; break;
          case 'code': code++; break;
          case 'a':
            final hrefMatch = RegExp("""href=["']([^"']+)["']""", caseSensitive: false)
                .firstMatch(attrs);
            pendingHref = hrefMatch?.group(1);
            break;
        }
      }
    }

    // Remaining text after last tag.
    if (pos < src.length) {
      final text = _decodeEntities(src.substring(pos));
      if (text.isNotEmpty) {
        spans.add(_buildSpan(
          text, base, bold > 0, italic > 0, strike > 0, underline > 0,
          code > 0, pendingHref, linkColor,
        ));
      }
    }

    return spans;
  }

  InlineSpan _buildSpan(
    String text,
    TextStyle base,
    bool bold, bool italic, bool strike, bool underline, bool code,
    String? href, Color linkColor,
  ) {
    var style = base.copyWith(
      fontWeight: bold ? FontWeight.bold : null,
      fontStyle: italic ? FontStyle.italic : null,
      decoration: TextDecoration.combine([
        if (strike) TextDecoration.lineThrough,
        if (underline) TextDecoration.underline,
      ]),
      fontFamily: code ? 'monospace' : null,
      fontSize: code ? (base.fontSize ?? 14) * 0.9 : null,
    );

    if (href != null) {
      style = style.copyWith(
        color: linkColor,
        decoration: TextDecoration.underline,
        decorationColor: linkColor,
      );
      return TextSpan(
        text: text,
        style: style,
        recognizer: TapGestureRecognizer()
          ..onTap = () => launchUrl(Uri.parse(href), mode: LaunchMode.externalApplication),
      );
    }
    return TextSpan(text: text, style: style);
  }

  static String _decodeEntities(String s) => s
      .replaceAll('&amp;', '&')
      .replaceAll('&lt;', '<')
      .replaceAll('&gt;', '>')
      .replaceAll('&quot;', '"')
      .replaceAll('&#39;', "'")
      .replaceAll('&apos;', "'")
      .replaceAll('&nbsp;', ' ');
}

class _PlainLinkText extends StatelessWidget {
  final String text;
  final TextStyle baseStyle;
  final Color linkColor;

  const _PlainLinkText({
    required this.text,
    required this.baseStyle,
    required this.linkColor,
  });

  static final _urlRe = RegExp(
    r'https?://[^\s\]\)>]+',
    caseSensitive: false,
  );

  @override
  Widget build(BuildContext context) {
    final matches = _urlRe.allMatches(text).toList();
    if (matches.isEmpty) {
      return SelectableText(text, style: baseStyle);
    }

    final spans = <InlineSpan>[];
    int pos = 0;
    for (final m in matches) {
      if (m.start > pos) {
        spans.add(TextSpan(text: text.substring(pos, m.start)));
      }
      final url = m.group(0)!;
      spans.add(TextSpan(
        text: url,
        style: baseStyle.copyWith(
          color: linkColor,
          decoration: TextDecoration.underline,
          decorationColor: linkColor,
        ),
        recognizer: TapGestureRecognizer()
          ..onTap = () => launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication),
      ));
      pos = m.end;
    }
    if (pos < text.length) {
      spans.add(TextSpan(text: text.substring(pos)));
    }

    return SelectionArea(
      child: RichText(text: TextSpan(children: spans, style: baseStyle)),
    );
  }
}
