part of '../message_group.dart';

class _LinkPreviewData {
  final String? title;
  final String? description;
  final String? imageMxc;
  const _LinkPreviewData({this.title, this.description, this.imageMxc});
  bool get isEmpty =>
      (title == null || title!.isEmpty) &&
      (description == null || description!.isEmpty) &&
      imageMxc == null;
}

class _LinkPreview extends ConsumerStatefulWidget {
  final String url;
  final PyramidTheme pt;
  const _LinkPreview({required this.url, required this.pt});

  // url → preview (null = fetched but nothing usable). Avoids refetching on
  // every rebuild/scroll.
  static final Map<String, _LinkPreviewData?> _cache = {};

  @override
  ConsumerState<_LinkPreview> createState() => _LinkPreviewState();
}

class _LinkPreviewState extends ConsumerState<_LinkPreview> {
  _LinkPreviewData? _data;
  bool _done = false;

  @override
  void initState() {
    super.initState();
    _load();
  }

  Future<void> _load() async {
    if (_LinkPreview._cache.containsKey(widget.url)) {
      setState(() { _data = _LinkPreview._cache[widget.url]; _done = true; });
      return;
    }
    try {
      final client = await ref.read(matrixClientProvider.future);
      final hs = client.homeserver.toString();
      final enc = Uri.encodeQueryComponent(widget.url);
      final headers = {'authorization': 'Bearer ${client.accessToken}'};

      var res = await client.httpClient.get(
        Uri.parse('$hs/_matrix/client/v1/media/preview_url?url=$enc'),
        headers: headers,
      );
      if (res.statusCode == 404) {
        // Legacy fallback for older servers.
        res = await client.httpClient.get(
          Uri.parse('$hs/_matrix/media/v3/preview_url?url=$enc'),
          headers: headers,
        );
      }
      _LinkPreviewData? data;
      if (res.statusCode == 200) {
        final json = jsonDecode(utf8.decode(res.bodyBytes)) as Map<String, dynamic>;
        data = _LinkPreviewData(
          title: (json['og:title'] as String?)?.trim(),
          description: (json['og:description'] as String?)?.trim(),
          imageMxc: json['og:image'] as String?,
        );
        if (data.isEmpty) data = null;
      }
      _LinkPreview._cache[widget.url] = data;
      if (mounted) setState(() { _data = data; _done = true; });
    } catch (_) {
      _LinkPreview._cache[widget.url] = null;
      if (mounted) setState(() { _data = null; _done = true; });
    }
  }

  @override
  Widget build(BuildContext context) {
    final data = _data;
    if (!_done || data == null) return const SizedBox.shrink();
    final pt = widget.pt;
    final client = ref.watch(matrixClientProvider).valueOrNull;

    return Padding(
      padding: const EdgeInsets.only(top: 6, bottom: 2),
      child: GestureDetector(
        onTap: () => launchUrl(Uri.parse(widget.url), mode: LaunchMode.externalApplication),
        child: ConstrainedBox(
          constraints: const BoxConstraints(maxWidth: 380),
          child: ClipRRect(
            borderRadius: BorderRadius.circular(pt.rBase),
            child: Container(
              decoration: BoxDecoration(
                color: pt.bg2,
                border: Border(left: BorderSide(color: pt.accent, width: 3)),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  if (data.imageMxc != null && client != null)
                    ConstrainedBox(
                      constraints: const BoxConstraints(maxHeight: 180),
                      child: SizedBox(
                        width: double.infinity,
                        child: MxcImage(
                          mxcUri: data.imageMxc!,
                          client: client,
                          fit: BoxFit.cover,
                          placeholder: const SizedBox.shrink(),
                        ),
                      ),
                    ),
                  Padding(
                    padding: const EdgeInsets.fromLTRB(10, 8, 10, 8),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        if (data.title != null && data.title!.isNotEmpty)
                          Text(
                            data.title!,
                            style: TextStyle(color: pt.fg, fontSize: 13, fontWeight: FontWeight.w600),
                            maxLines: 2,
                            overflow: TextOverflow.ellipsis,
                          ),
                        if (data.description != null && data.description!.isNotEmpty) ...[
                          const SizedBox(height: 3),
                          Text(
                            data.description!,
                            style: TextStyle(color: pt.fgMuted, fontSize: 12, height: 1.35),
                            maxLines: 3,
                            overflow: TextOverflow.ellipsis,
                          ),
                        ],
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _ReplyPreview extends StatelessWidget {
  final Event replyEvent;
  final PyramidTheme pt;
  const _ReplyPreview({required this.replyEvent, required this.pt});

  @override
  Widget build(BuildContext context) {
    final senderName = replyEvent.senderFromMemoryOrFallback.calcDisplayname();

    // Media-aware preview: show an icon + label instead of a raw filename,
    // matching WhatsApp/Discord. Falls back to stripped text for m.text.
    final (IconData? icon, String body) = _previewFor(replyEvent);

    return Padding(
      padding: const EdgeInsets.only(bottom: 4),
      child: IntrinsicHeight(
        child: ClipRRect(
          borderRadius: BorderRadius.circular(pt.rSm),
          child: Container(
            color: pt.accentSoft,
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                // Accent bar flush with the left edge of the tinted card.
                Container(width: 3, color: pt.accent),
                Expanded(
                  child: Padding(
                    padding: const EdgeInsets.fromLTRB(8, 5, 10, 5),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      mainAxisSize: MainAxisSize.min,
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Text(senderName, style: TextStyle(color: pt.accent, fontSize: 12, fontWeight: FontWeight.w600)),
                        Row(
                          children: [
                            if (icon != null) ...[
                              Icon(icon, size: 13, color: pt.fgMuted),
                              const SizedBox(width: 4),
                            ],
                            Expanded(
                              child: Text(
                                body,
                                style: TextStyle(color: pt.fgMuted, fontSize: 12),
                                maxLines: 1,
                                overflow: TextOverflow.ellipsis,
                              ),
                            ),
                          ],
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  (IconData?, String) _previewFor(Event e) {
    switch (e.messageType) {
      case 'm.image':
        return (Icons.image_outlined, 'Bild');
      case 'm.video':
        return (Icons.videocam_outlined, 'Video');
      case 'm.audio':
        return (Icons.mic_none_rounded, 'Sprachnachricht');
      case 'm.file':
        return (Icons.insert_drive_file_outlined, e.body.isNotEmpty ? e.body : 'Datei');
    }
    if (e.type == EventTypes.Sticker) return (Icons.emoji_emotions_outlined, 'Sticker');

    // Plain text: strip nested reply fallback, collapse to first line.
    String body = e.body.replaceAll('\r\n', '\n');
    if (body.startsWith('> ')) {
      final lines = body.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) body = lines.skip(i).join(' ').trim();
    }
    return (null, body.split('\n').first);
  }
}
