part of '../message_group.dart';

class _ImageContent extends ConsumerStatefulWidget {
  final Event event;
  final PyramidTheme pt;
  const _ImageContent({required this.event, required this.pt});

  @override
  ConsumerState<_ImageContent> createState() => _ImageContentState();
}

class _ImageContentState extends ConsumerState<_ImageContent> {
  late Future<_MediaResult> _future;

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

  Future<_MediaResult> _load() async {
    final event = widget.event;
    final rawUrl = event.content.tryGet<String>('url') ?? '';
    if (rawUrl.startsWith('http://') || rawUrl.startsWith('https://')) {
      return _MediaResult.network(rawUrl);
    }
    // Serve from the shared LRU cache if this image was viewed recently.
    final cached = MediaCache.instance.get(event.eventId);
    if (cached != null) return _MediaResult.bytes(cached);

    final client = await ref.read(matrixClientProvider.future);
    final mxcUri = event.attachmentMxcUrl;
    if (mxcUri == null) return _MediaResult.none();
    if (event.isAttachmentEncrypted) {
      try {
        final file = await event.downloadAndDecryptAttachment(getThumbnail: false, downloadCallback: _checkedDownload(client));
        MediaCache.instance.put(event.eventId, file.bytes);
        return _MediaResult.bytes(file.bytes);
      } catch (e) {
        return _MediaResult.error(_classifyMediaError(e));
      }
    } else {
      try {
        final httpUri = await mxcUri.getThumbnailUri(client, width: 800, height: 600, method: ThumbnailMethod.scale);
        final res = await client.httpClient.get(httpUri, headers: {'authorization': 'Bearer ${client.accessToken}'});
        if (res.statusCode != 200) return _MediaResult.error('HTTP ${res.statusCode}');
        MediaCache.instance.put(event.eventId, res.bodyBytes);
        return _MediaResult.bytes(res.bodyBytes);
      } catch (_) {
        try {
          final httpUri = await mxcUri.getDownloadUri(client);
          final res = await client.httpClient.get(httpUri, headers: {'authorization': 'Bearer ${client.accessToken}'});
          if (res.statusCode != 200) return _MediaResult.error('HTTP ${res.statusCode}');
          MediaCache.instance.put(event.eventId, res.bodyBytes);
          return _MediaResult.bytes(res.bodyBytes);
        } catch (e) {
          return _MediaResult.error(e.toString().split('\n').first);
        }
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    final pt = widget.pt;

    return LayoutBuilder(builder: (context, constraints) {
      // Never exceed the available chat width (important on narrow phones),
      // capped at the comfortable desktop maximum.
      final maxW = constraints.maxWidth.isFinite
          ? constraints.maxWidth.clamp(80.0, 420.0)
          : 420.0;
      const maxH = 320.0;
      // Reserve the final display box up-front from the event's info dimensions,
      // so the layout doesn't shift (and yank the scroll position) when the image
      // finishes loading. Null when dimensions are absent.
      final box = _reservedImageSize(widget.event, maxW: maxW, maxH: maxH);

      return FutureBuilder<_MediaResult>(
        future: _future,
        builder: (context, snap) {
          Widget inner;
          if (snap.connectionState == ConnectionState.waiting) {
            inner = Center(child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: pt.fgDim)));
          } else if (snap.hasData) {
            final result = snap.data!;
            if (result.bytes != null) {
              inner = Image.memory(
                result.bytes!,
                fit: BoxFit.contain,
                gaplessPlayback: true,
                errorBuilder: (context, e, stack) => _mediaFallback(pt, e.toString().split('\n').first, widget.event.body),
              );
            } else if (result.networkUrl != null) {
              inner = Image.network(result.networkUrl!, fit: BoxFit.contain);
            } else {
              inner = _mediaFallback(pt, result.error, widget.event.body);
            }
          } else {
            inner = _mediaFallback(pt, null, widget.event.body);
          }

          final result = snap.data;
          return GestureDetector(
            onTap: (snap.hasData && result != null)
                ? () => MediaViewer.show(
                      context,
                      senderName: widget.event.senderFromMemoryOrFallback.calcDisplayname(),
                      filename: widget.event.body,
                      bytes: result.bytes,
                      networkUrl: result.networkUrl,
                      event: widget.event,
                    )
                : null,
            child: Align(
              alignment: Alignment.centerLeft,
              child: Container(
                margin: const EdgeInsets.only(top: 4, bottom: 4),
                width: box?.width,
                height: box?.height,
                constraints: box == null
                    ? BoxConstraints(
                        minWidth: 120, minHeight: 90,
                        maxWidth: maxW, maxHeight: maxH,
                      )
                    : null,
                decoration: BoxDecoration(
                  color: pt.bg1,
                  borderRadius: BorderRadius.circular(pt.rBase),
                ),
                clipBehavior: Clip.hardEdge,
                child: inner,
              ),
            ),
          );
        },
      );
    });
  }
}

// Computes the on-screen box for an image/video event, fitting within
// [maxW]×[maxH] while preserving the aspect ratio from the event's `info`
// (w/h). Used to reserve layout space before the media loads so nothing shifts.
// Returns null when the event carries no dimensions — callers should then use
// loose constraints instead of a fixed box.
Size? _reservedImageSize(Event event, {required double maxW, required double maxH}) {
  final info = event.content.tryGetMap<String, Object?>('info');
  final w = (info?['w'] as num?)?.toDouble();
  final h = (info?['h'] as num?)?.toDouble();
  if (w == null || h == null || w <= 0 || h <= 0) return null;
  // Fit inside maxW×maxH without distorting; never upscale small images.
  var scale = (maxW / w).clamp(0.0, maxH / h);
  if (scale > 1.0) scale = 1.0;
  return Size(w * scale, h * scale);
}

class _AudioContent extends ConsumerStatefulWidget {
  final Event event;
  final PyramidTheme pt;
  const _AudioContent({required this.event, required this.pt});

  @override
  ConsumerState<_AudioContent> createState() => _AudioContentState();
}

class _AudioContentState extends ConsumerState<_AudioContent> {
  late Future<_MediaResult> _future;

  @override
  void initState() {
    super.initState();
    _future = _loadBytes();
  }

  Future<_MediaResult> _loadBytes() async {
    final client = await ref.read(matrixClientProvider.future);
    final event = widget.event;
    if (event.isAttachmentEncrypted) {
      try {
        final file = await event.downloadAndDecryptAttachment(getThumbnail: false, downloadCallback: _checkedDownload(client));
        return _MediaResult.bytes(file.bytes);
      } catch (e) {
        return _MediaResult.error(_classifyMediaError(e));
      }
    }
    final mxcUri = event.attachmentMxcUrl;
    if (mxcUri == null) return _MediaResult.none();
    try {
      final httpUri = await mxcUri.getDownloadUri(client);
      final res = await client.httpClient.get(httpUri, headers: {'authorization': 'Bearer ${client.accessToken}'});
      if (res.statusCode != 200) return _MediaResult.error('HTTP ${res.statusCode}');
      return _MediaResult.bytes(res.bodyBytes);
    } catch (e) {
      return _MediaResult.error(e.toString().split('\n').first);
    }
  }

  @override
  Widget build(BuildContext context) {
    final pt = widget.pt;
    return FutureBuilder<_MediaResult>(
      future: _future,
      builder: (ctx, snap) {
        if (snap.connectionState == ConnectionState.waiting) {
          return Padding(
            padding: const EdgeInsets.symmetric(vertical: 8),
            child: Row(mainAxisSize: MainAxisSize.min, children: [
              SizedBox(width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2, color: pt.fgDim)),
              const SizedBox(width: 10),
              Text('Lade Audio…', style: TextStyle(color: pt.fgMuted, fontSize: 12)),
            ]),
          );
        }
        final result = snap.data;
        if (result?.bytes == null) {
          return _mediaFallback(pt, result?.error, widget.event.body);
        }
        return PyramidAudioPlayer(
          bytes: result!.bytes!,
          filename: widget.event.body,
          pt: pt,
          isVoice: widget.event.content['msgtype'] == 'm.audio',
        );
      },
    );
  }
}

class _VideoContent extends ConsumerStatefulWidget {
  final Event event;
  final PyramidTheme pt;
  const _VideoContent({required this.event, required this.pt});

  @override
  ConsumerState<_VideoContent> createState() => _VideoContentState();
}

class _VideoContentState extends ConsumerState<_VideoContent> {
  late Future<_MediaResult> _future;

  @override
  void initState() {
    super.initState();
    _future = _loadBytes();
  }

  Future<_MediaResult> _loadBytes() async {
    final client = await ref.read(matrixClientProvider.future);
    final event = widget.event;
    if (event.isAttachmentEncrypted) {
      try {
        final file = await event.downloadAndDecryptAttachment(getThumbnail: false, downloadCallback: _checkedDownload(client));
        return _MediaResult.bytes(file.bytes);
      } catch (e) {
        return _MediaResult.error(_classifyMediaError(e));
      }
    }
    final mxcUri = event.attachmentMxcUrl;
    if (mxcUri == null) return _MediaResult.none();
    try {
      final httpUri = await mxcUri.getDownloadUri(client);
      final res = await client.httpClient.get(httpUri, headers: {'authorization': 'Bearer ${client.accessToken}'});
      if (res.statusCode != 200) return _MediaResult.error('HTTP ${res.statusCode}');
      return _MediaResult.bytes(res.bodyBytes);
    } catch (e) {
      return _MediaResult.error(e.toString().split('\n').first);
    }
  }

  @override
  Widget build(BuildContext context) {
    final pt = widget.pt;
    return FutureBuilder<_MediaResult>(
      future: _future,
      builder: (ctx, snap) {
        if (snap.connectionState == ConnectionState.waiting) {
          return Container(
            margin: const EdgeInsets.only(top: 4, bottom: 4),
            constraints: const BoxConstraints(maxWidth: 360),
            height: 200,
            decoration: BoxDecoration(
              color: Colors.black,
              borderRadius: BorderRadius.circular(pt.rBase),
              border: Border.all(color: pt.border),
            ),
            child: Center(child: CircularProgressIndicator(strokeWidth: 2, color: pt.accent)),
          );
        }
        final result = snap.data;
        if (result?.bytes == null) {
          return _FileContent(event: widget.event, pt: pt, icon: Icons.videocam_rounded);
        }
        return PyramidVideoPlayer(
          bytes: result!.bytes!,
          filename: widget.event.body,
          pt: pt,
          senderName: widget.event.senderFromMemoryOrFallback.calcDisplayname(),
        );
      },
    );
  }
}

class _MediaContent extends ConsumerStatefulWidget {
  final Event event;
  final PyramidTheme pt;

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

  @override
  ConsumerState<_MediaContent> createState() => _MediaContentState();
}

class _MediaContentState extends ConsumerState<_MediaContent> {
  late Future<_MediaResult> _future;

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

  Future<_MediaResult> _load() async {
    final client = await ref.read(matrixClientProvider.future);
    final event = widget.event;

    // External URL (e.g. Giphy GIFs) — not an MXC URI
    final rawUrl = event.content.tryGet<String>('url') ?? '';
    if (rawUrl.startsWith('http://') || rawUrl.startsWith('https://')) {
      return _MediaResult.network(rawUrl);
    }

    final mxcUri = event.attachmentMxcUrl;
    if (mxcUri == null) return _MediaResult.none();

    if (event.isAttachmentEncrypted) {
      try {
        final file = await event.downloadAndDecryptAttachment(getThumbnail: false, downloadCallback: _checkedDownload(client));
        return _MediaResult.bytes(file.bytes);
      } catch (e) {
        return _MediaResult.error(_classifyMediaError(e));
      }
    } else {
      try {
        final httpUri = await mxcUri.getThumbnailUri(
          client,
          width: 800,
          height: 600,
          method: ThumbnailMethod.scale,
        );
        final res = await client.httpClient.get(
          httpUri,
          headers: {'authorization': 'Bearer ${client.accessToken}'},
        );
        if (res.statusCode != 200) return _MediaResult.error('HTTP ${res.statusCode}');
        return _MediaResult.bytes(res.bodyBytes);
      } catch (_) {
        // Fall back to full-size download
        try {
          final httpUri = await mxcUri.getDownloadUri(client);
          final res = await client.httpClient.get(
            httpUri,
            headers: {'authorization': 'Bearer ${client.accessToken}'},
          );
          if (res.statusCode != 200) return _MediaResult.error('HTTP ${res.statusCode}');
          return _MediaResult.bytes(res.bodyBytes);
        } catch (e) {
          return _MediaResult.error(e.toString().split('\n').first);
        }
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    final pt = widget.pt;
    final isSticker = widget.event.type == EventTypes.Sticker;
    final maxW = isSticker ? 160.0 : 420.0;
    final maxH = isSticker ? 160.0 : 320.0;

    return FutureBuilder<_MediaResult>(
      future: _future,
      builder: (context, snap) {
        Widget inner;
        if (snap.connectionState == ConnectionState.waiting) {
          inner = Center(
            child: SizedBox(
              width: 24, height: 24,
              child: CircularProgressIndicator(strokeWidth: 2, color: pt.fgDim),
            ),
          );
        } else if (snap.hasData) {
          final result = snap.data!;
          if (result.networkUrl != null) {
            inner = Image.network(
              result.networkUrl!,
              fit: BoxFit.contain,
              errorBuilder: (context, e, stack) => _fallback(pt),
            );
          } else if (result.bytes != null) {
            inner = Image.memory(
              result.bytes!,
              fit: BoxFit.contain,
              errorBuilder: (context, e, stack) => _fallback(pt),
            );
          } else {
            inner = _fallback(pt, result.error);
          }
        } else {
          inner = _fallback(pt);
        }

        return Align(
          alignment: Alignment.centerLeft,
          child: Container(
            margin: const EdgeInsets.only(top: 4, bottom: 4),
            constraints: BoxConstraints(maxWidth: maxW, maxHeight: maxH),
            decoration: isSticker ? null : BoxDecoration(
              color: pt.bg3,
              borderRadius: BorderRadius.circular(pt.rBase),
              border: Border.all(color: pt.border),
            ),
            clipBehavior: isSticker ? Clip.none : Clip.hardEdge,
            child: inner,
          ),
        );
      },
    );
  }

  Widget _fallback(PyramidTheme pt, [String? error]) =>
      _mediaFallback(pt, error, widget.event.body);
}

class _DocPreviewContent extends ConsumerStatefulWidget {
  final Event event;
  final PyramidTheme pt;
  const _DocPreviewContent({required this.event, required this.pt});

  @override
  ConsumerState<_DocPreviewContent> createState() => _DocPreviewContentState();
}

class _DocPreviewContentState extends ConsumerState<_DocPreviewContent> {
  late Future<_MediaResult> _future;

  @override
  void initState() {
    super.initState();
    _future = _loadBytes();
  }

  Future<_MediaResult> _loadBytes() async {
    final event = widget.event;
    // Serve from cache if we already downloaded this attachment this session.
    final cached = _DocBytesCache.get(event.eventId);
    if (cached != null) return _MediaResult.bytes(cached);

    final client = await ref.read(matrixClientProvider.future);
    if (event.isAttachmentEncrypted) {
      try {
        final file = await event.downloadAndDecryptAttachment(getThumbnail: false, downloadCallback: _checkedDownload(client));
        _DocBytesCache.put(event.eventId, file.bytes);
        return _MediaResult.bytes(file.bytes);
      } catch (e) {
        return _MediaResult.error(_classifyMediaError(e));
      }
    }
    final mxcUri = event.attachmentMxcUrl;
    if (mxcUri == null) return _MediaResult.none();
    try {
      final httpUri = await mxcUri.getDownloadUri(client);
      final res = await client.httpClient.get(httpUri, headers: {'authorization': 'Bearer ${client.accessToken}'});
      if (res.statusCode != 200) return _MediaResult.error('HTTP ${res.statusCode}');
      _DocBytesCache.put(event.eventId, res.bodyBytes);
      return _MediaResult.bytes(res.bodyBytes);
    } catch (e) {
      return _MediaResult.error(e.toString().split('\n').first);
    }
  }

  Future<void> _saveFile(Uint8List bytes) async {
    try {
      Directory? dir;
      if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
        dir = await getDownloadsDirectory();
      }
      dir ??= await getTemporaryDirectory();
      final filename = widget.event.body.isNotEmpty ? widget.event.body : 'file';
      final ext = filename.contains('.') ? filename.split('.').last : 'bin';
      final name = 'pyramid_${DateTime.now().millisecondsSinceEpoch}.$ext';
      final file = File('${dir.path}/$name');
      await file.writeAsBytes(bytes);
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content: Text('Gespeichert: ${file.path}'),
          duration: const Duration(seconds: 4),
        ));
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Fehler: $e')));
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    final pt = widget.pt;
    final ext = widget.event.body.contains('.')
        ? widget.event.body.split('.').last.toLowerCase()
        : '';

    return FutureBuilder<_MediaResult>(
      future: _future,
      builder: (ctx, snap) {
        if (snap.connectionState == ConnectionState.waiting) {
          return Container(
            width: 280,
            height: 80,
            margin: const EdgeInsets.only(top: 4, bottom: 4),
            decoration: BoxDecoration(
              color: pt.bg2,
              borderRadius: BorderRadius.circular(pt.rBase),
              border: Border.all(color: pt.border),
            ),
            child: Center(child: CircularProgressIndicator(strokeWidth: 2, color: pt.accent)),
          );
        }

        final result = snap.data;
        if (result?.bytes == null) {
          return _FileContent(event: widget.event, pt: pt, icon: Icons.picture_as_pdf_outlined);
        }

        final bytes = result!.bytes!;
        final sender = widget.event.senderFromMemoryOrFallback.calcDisplayname();

        if (ext == 'pdf' || ext == 'ai' || ext == 'eps') {
          return PdfInlinePreview(
            bytes: bytes,
            filename: widget.event.body,
            pt: pt,
            senderName: sender,
            onDownload: () => _saveFile(bytes),
          );
        }

        // SVG — show inline like an image
        return GestureDetector(
          onTap: () => DocumentViewer.show(
            context,
            filename: widget.event.body,
            bytes: bytes,
            senderName: sender,
          ),
          child: Container(
            margin: const EdgeInsets.only(top: 4, bottom: 4),
            constraints: const BoxConstraints(maxWidth: 300, maxHeight: 300),
            decoration: BoxDecoration(
              color: pt.bg1,
              borderRadius: BorderRadius.circular(pt.rBase),
              border: Border.all(color: pt.border),
            ),
            clipBehavior: Clip.hardEdge,
            child: SvgPicture.memory(bytes, fit: BoxFit.contain),
          ),
        );
      },
    );
  }
}

class _ProprietaryInlineCard extends ConsumerStatefulWidget {
  final Event event;
  final PyramidTheme pt;
  const _ProprietaryInlineCard({required this.event, required this.pt});

  @override
  ConsumerState<_ProprietaryInlineCard> createState() => _ProprietaryInlineCardState();
}

class _ProprietaryInlineCardState extends ConsumerState<_ProprietaryInlineCard> {
  bool _loading = false;
  Uint8List? _cachedBytes;

  String get _filename => widget.event.body.isNotEmpty ? widget.event.body : 'file';
  String get _ext => _filename.contains('.') ? _filename.split('.').last.toLowerCase() : '';
  int? get _fileSize => widget.event.content.tryGetMap<String, dynamic>('info')?['size'] as int?;

  String _fmtSize(int b) {
    if (b < 1024) return '$b B';
    if (b < 1024 * 1024) return '${(b / 1024).toStringAsFixed(1)} KB';
    return '${(b / (1024 * 1024)).toStringAsFixed(1)} MB';
  }

  IconData get _icon => switch (_ext) {
    'psd' || 'psb'                         => Icons.photo_camera_outlined,
    'ai' || 'eps'                          => Icons.gesture_outlined,
    'indd' || 'inx'                        => Icons.menu_book_outlined,
    'afphoto' || 'afdesign' || 'afpub'     => Icons.palette_outlined,
    'sketch' || 'fig' || 'xd'             => Icons.design_services_outlined,
    'doc'                                  => Icons.description_outlined,
    'ppt'                                  => Icons.slideshow_outlined,
    'xls'                                  => Icons.table_chart_outlined,
    _                                      => Icons.insert_drive_file_outlined,
  };

  Future<Uint8List> _download() async {
    if (_cachedBytes != null) return _cachedBytes!;
    final event = widget.event;
    final shared = _DocBytesCache.get(event.eventId);
    if (shared != null) return _cachedBytes = shared;

    final client = await ref.read(matrixClientProvider.future);
    if (event.isAttachmentEncrypted) {
      final file = await event.downloadAndDecryptAttachment(getThumbnail: false, downloadCallback: _checkedDownload(client));
      _DocBytesCache.put(event.eventId, file.bytes);
      return _cachedBytes = file.bytes;
    }
    final mxcUri = event.attachmentMxcUrl;
    if (mxcUri == null) throw Exception('Kein URI');
    final httpUri = await mxcUri.getDownloadUri(client);
    final res = await client.httpClient.get(httpUri, headers: {'authorization': 'Bearer ${client.accessToken}'});
    if (res.statusCode != 200) throw Exception('HTTP ${res.statusCode}');
    _DocBytesCache.put(event.eventId, res.bodyBytes);
    return _cachedBytes = res.bodyBytes;
  }

  Future<void> _openViewer() async {
    if (_loading) return;
    setState(() => _loading = true);
    try {
      final bytes = await _download();
      if (!mounted) return;
      await DocumentViewer.show(
        context,
        filename: _filename,
        bytes: bytes,
        senderName: widget.event.senderFromMemoryOrFallback.calcDisplayname(),
      );
    } catch (e) {
      if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Fehler: $e'), duration: const Duration(seconds: 3)));
    } finally {
      if (mounted) setState(() => _loading = false);
    }
  }

  Future<void> _save() async {
    if (_loading) return;
    setState(() => _loading = true);
    try {
      final bytes = await _download();
      Directory? dir;
      if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
        dir = await getDownloadsDirectory();
      }
      dir ??= await getTemporaryDirectory();
      final outFile = File('${dir.path}/pyramid_${DateTime.now().millisecondsSinceEpoch}.$_ext');
      await outFile.writeAsBytes(bytes);
      if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Gespeichert: ${outFile.path}'), duration: const Duration(seconds: 4)));
    } catch (e) {
      if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Fehler: $e'), duration: const Duration(seconds: 3)));
    } finally {
      if (mounted) setState(() => _loading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    final pt = widget.pt;
    final extLabel = _ext.toUpperCase();
    final size = _fileSize;

    return GestureDetector(
      onTap: _openViewer,
      child: Container(
        width: 280,
        margin: const EdgeInsets.only(top: 4, bottom: 4),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(pt.rBase),
          border: Border.all(color: pt.border),
        ),
        clipBehavior: Clip.hardEdge,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            // Visual area
            Container(
              width: 280,
              height: 140,
              color: pt.accentSoft,
              child: Stack(
                alignment: Alignment.center,
                children: [
                  Icon(_icon, size: 56, color: pt.accent.withAlpha(180)),
                  Positioned(
                    bottom: 12,
                    right: 12,
                    child: Container(
                      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
                      decoration: BoxDecoration(
                        color: pt.accent,
                        borderRadius: BorderRadius.circular(4),
                      ),
                      child: Text(
                        extLabel.length > 4 ? extLabel.substring(0, 4) : extLabel,
                        style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700),
                      ),
                    ),
                  ),
                ],
              ),
            ),
            // Footer
            Container(
              color: pt.bg2,
              padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
              child: Row(
                children: [
                  Icon(_icon, size: 14, color: pt.fgDim),
                  const SizedBox(width: 6),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Text(_filename, style: TextStyle(color: pt.fg, fontSize: 12, fontWeight: FontWeight.w500), overflow: TextOverflow.ellipsis),
                        if (size != null)
                          Text(_fmtSize(size), style: TextStyle(color: pt.fgMuted, fontSize: 10)),
                      ],
                    ),
                  ),
                  if (_loading)
                    SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: pt.accent))
                  else
                    GestureDetector(
                      onTap: _save,
                      behavior: HitTestBehavior.opaque,
                      child: Padding(
                        padding: const EdgeInsets.all(2),
                        child: Icon(Icons.download_rounded, size: 16, color: pt.fgMuted),
                      ),
                    ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _FileContent extends ConsumerStatefulWidget {
  final Event event;
  final PyramidTheme pt;
  final IconData icon;
  const _FileContent({required this.event, required this.pt, required this.icon});

  @override
  ConsumerState<_FileContent> createState() => _FileContentState();
}

class _FileContentState extends ConsumerState<_FileContent> {
  bool _saving = false;
  bool _previewing = false;

  String get _filename => widget.event.body.isNotEmpty ? widget.event.body : 'file';

  String _ext() {
    final n = _filename;
    return n.contains('.') ? n.split('.').last.toLowerCase() : 'bin';
  }

  Future<void> _preview() async {
    if (_previewing || _saving) return;
    setState(() => _previewing = true);
    try {
      final client = await ref.read(matrixClientProvider.future);
      final event = widget.event;
      Uint8List bytes;
      if (event.isAttachmentEncrypted) {
        final file = await event.downloadAndDecryptAttachment(getThumbnail: false, downloadCallback: _checkedDownload(client));
        bytes = file.bytes;
      } else {
        final mxcUri = event.attachmentMxcUrl;
        if (mxcUri == null) throw Exception('Kein URI');
        final httpUri = await mxcUri.getDownloadUri(client);
        final res = await client.httpClient.get(httpUri, headers: {'authorization': 'Bearer ${client.accessToken}'});
        if (res.statusCode != 200) throw Exception('HTTP ${res.statusCode}');
        bytes = res.bodyBytes;
      }
      if (!mounted) return;
      await DocumentViewer.show(
        context,
        filename: _filename,
        bytes: bytes,
        senderName: event.senderFromMemoryOrFallback.calcDisplayname(),
      );
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Fehler: $e'), duration: const Duration(seconds: 3)),
        );
      }
    } finally {
      if (mounted) setState(() => _previewing = false);
    }
  }

  Future<void> _save({bool openAfter = false}) async {
    if (_saving) return;
    setState(() => _saving = true);
    try {
      final client = await ref.read(matrixClientProvider.future);
      final event = widget.event;
      Uint8List bytes;
      if (event.isAttachmentEncrypted) {
        final file = await event.downloadAndDecryptAttachment(getThumbnail: false, downloadCallback: _checkedDownload(client));
        bytes = file.bytes;
      } else {
        final mxcUri = event.attachmentMxcUrl;
        if (mxcUri == null) throw Exception('Kein URI');
        final httpUri = await mxcUri.getDownloadUri(client);
        final res = await client.httpClient.get(httpUri, headers: {'authorization': 'Bearer ${client.accessToken}'});
        if (res.statusCode != 200) throw Exception('HTTP ${res.statusCode}');
        bytes = res.bodyBytes;
      }

      Directory? dir;
      if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
        dir = await getDownloadsDirectory();
      }
      dir ??= await getTemporaryDirectory();
      final name = 'pyramid_${DateTime.now().millisecondsSinceEpoch}.${_ext()}';
      final outFile = File('${dir.path}/$name');
      await outFile.writeAsBytes(bytes);

      if (openAfter) {
        if (Platform.isWindows) {
          await Process.run('cmd', ['/c', 'start', '', outFile.path]);
        } else if (Platform.isMacOS) {
          await Process.run('open', [outFile.path]);
        } else {
          await Process.run('xdg-open', [outFile.path]);
        }
      }

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content: Text(openAfter ? 'Geöffnet: ${outFile.path}' : 'Gespeichert: ${outFile.path}'),
          duration: const Duration(seconds: 4),
        ));
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Fehler: $e'), duration: const Duration(seconds: 3)),
        );
      }
    } finally {
      if (mounted) setState(() => _saving = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    final pt = widget.pt;
    final isVideo = widget.event.messageType == 'm.video';

    return Container(
      margin: const EdgeInsets.only(top: 4, bottom: 4),
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
      constraints: const BoxConstraints(maxWidth: 340),
      decoration: BoxDecoration(
        color: pt.bg3,
        borderRadius: BorderRadius.circular(pt.rBase),
        border: Border.all(color: pt.border),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Container(
            width: 40,
            height: 40,
            decoration: BoxDecoration(color: pt.accentSoft, borderRadius: BorderRadius.circular(pt.rSm)),
            child: Icon(widget.icon, size: 22, color: pt.accent),
          ),
          const SizedBox(width: 10),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(_filename, style: TextStyle(color: pt.fg, fontSize: 13, fontWeight: FontWeight.w500), maxLines: 2, overflow: TextOverflow.ellipsis),
                const SizedBox(height: 2),
                Text(_ext().toUpperCase(), style: TextStyle(color: pt.fgDim, fontSize: 11)),
              ],
            ),
          ),
          const SizedBox(width: 8),
          if (_saving || _previewing)
            SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: pt.accent))
          else
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                if (DocumentViewer.supportsPreview(_filename))
                  IconButton(
                    onPressed: _preview,
                    tooltip: 'Vorschau',
                    icon: Icon(Icons.visibility_outlined, size: 22, color: pt.fgMuted),
                    constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
                    padding: EdgeInsets.zero,
                  ),
                if (isVideo)
                  IconButton(
                    onPressed: () => _save(openAfter: true),
                    tooltip: 'Öffnen',
                    icon: Icon(Icons.play_circle_outline_rounded, size: 22, color: pt.fgMuted),
                    constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
                    padding: EdgeInsets.zero,
                  ),
                IconButton(
                  onPressed: () => _save(),
                  tooltip: 'Herunterladen',
                  icon: Icon(Icons.download_rounded, size: 22, color: pt.fgMuted),
                  constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
                  padding: EdgeInsets.zero,
                ),
              ],
            ),
        ],
      ),
    );
  }
}

String _classifyMediaError(Object e) {
  final s = e.toString();
  if (s.contains('HandshakeException') || s.contains('CERTIFICATE') || s.contains('SocketException')) {
    return 'Verbindungsfehler (TLS/Netzwerk)';
  }
  if (s.contains('HTTP 4') || s.contains('HTTP 5') || s.contains('Unknown error when fetching')) {
    return 'Datei nicht verfügbar (Serverfehler)';
  }
  if (s.contains('decrypt') || s.contains('Decrypt') || s.contains('OlmException')) {
    return 'Entschlüsselung fehlgeschlagen';
  }
  return 'Download fehlgeschlagen';
}

class _DocBytesCache {
  static Uint8List? get(String eventId) => MediaCache.instance.get(eventId);
  static void put(String eventId, Uint8List bytes) =>
      MediaCache.instance.put(eventId, bytes);
}

// Download callback that validates HTTP status before returning bytes.
// Prevents false "decryption failed" errors when the server returns an
// error page instead of the actual encrypted file bytes.
Future<Uint8List> Function(Uri) _checkedDownload(Client client) {
  return (Uri url) async {
    final res = await client.httpClient.get(
      url,
      headers: {'authorization': 'Bearer ${client.accessToken}'},
    );
    if (res.statusCode != 200) {
      throw Exception('HTTP ${res.statusCode}: ${res.body}');
    }
    return res.bodyBytes;
  };
}

Widget _mediaFallback(PyramidTheme pt, String? error, String body) => Padding(
  padding: const EdgeInsets.all(12),
  child: Row(
    mainAxisSize: MainAxisSize.min,
    children: [
      Icon(Icons.broken_image_outlined, size: 16, color: pt.fgDim),
      const SizedBox(width: 6),
      Flexible(child: Text(error ?? body, style: TextStyle(color: pt.fgDim, fontSize: 12), overflow: TextOverflow.ellipsis)),
    ],
  ),
);

class _MediaResult {
  final String? networkUrl;
  final Uint8List? bytes;
  final String? error;

  _MediaResult._({this.networkUrl, this.bytes, this.error});
  factory _MediaResult.network(String url) => _MediaResult._(networkUrl: url);
  factory _MediaResult.bytes(Uint8List b) => _MediaResult._(bytes: b);
  factory _MediaResult.error(String e) => _MediaResult._(error: e);
  factory _MediaResult.none() => _MediaResult._();
}
