import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import '../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
import '../../../../extensions/date_time.dart';
import '../../../../extensions/text.dart';
import '../../../../share_intent/remote_file_ref.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../utils/downloads/download_job.dart';
import '../../../../utils/haptics.dart';
import '../../../../widget/a11y/a11y_labels.dart';
import '../../../../widget/demo_restricted.dart';
import '../../../../widget/downloads/download_trigger.dart';
import '../data/chat_bubble_styles.dart';
import '../data/chat_message.dart';
import 'answer_reference.dart';
import 'bubble.dart';
import 'chat_bubble_poll.dart';
import 'chat_bubble_reactions.dart';
import 'chat_message_options_dialog.dart';
import 'highlighted_linkify.dart';

enum SearchHighlight { none, secondary, active }

class ChatBubble extends StatefulWidget {
  final bool isSender;
  final GetChatResponseObject bubbleData;
  final GetRoomResponseObject chatData;
  final bool isRead;
  final String? selfId;

  final double spacing = 3;
  final double timeIconSize = 11;
  final Color timeIconColor = Colors.grey;

  final void Function({bool renew}) refetch;

  final String? highlightQuery;
  final SearchHighlight matchHighlight;

  const ChatBubble({
    required this.isSender,
    required this.bubbleData,
    required this.chatData,
    required this.refetch,
    this.isRead = false,
    this.selfId,
    this.highlightQuery,
    this.matchHighlight = SearchHighlight.none,
    super.key,
  });

  @override
  State<ChatBubble> createState() => _ChatBubbleState();
}

class _ChatBubbleState extends State<ChatBubble>
    with SingleTickerProviderStateMixin, DownloadTrigger<ChatBubble> {
  late ChatMessage message;

  Offset _position = Offset.zero;
  Offset _dragStartPosition = Offset.zero;
  bool _swipeActionArmed = false;

  @override
  String? get downloadRemotePath =>
      widget.bubbleData.messageParameters?['file']?.path;

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

  @override
  void didUpdateWidget(covariant ChatBubble oldWidget) {
    super.didUpdateWidget(oldWidget);
    final oldPath = oldWidget.bubbleData.messageParameters?['file']?.path;
    if (oldPath != downloadRemotePath) refreshDownloadTrigger();
  }

  @override
  void dispose() {
    disposeDownloadTrigger();
    super.dispose();
  }

  void _startFileDownload() {
    final file = message.file;
    if (file == null) return;
    startDownload(name: file.name, remoteFile: RemoteFileRef.fromTalk(file));
  }

  bool get _rendersAsCommentBubble =>
      widget.bubbleData.messageType ==
          GetRoomResponseObjectMessageType.comment ||
      widget.bubbleData.messageType ==
          GetRoomResponseObjectMessageType.deletedComment;

  TextStyle? _messageTextStyle(BuildContext context) {
    final theme = Theme.of(context);
    switch (widget.bubbleData.messageType) {
      case GetRoomResponseObjectMessageType.system:
        return theme.textTheme.bodySmall;
      case GetRoomResponseObjectMessageType.deletedComment:
        return theme.textTheme.bodyMedium?.copyWith(
          color: theme.hintColor,
          fontStyle: FontStyle.italic,
        );
      case GetRoomResponseObjectMessageType.comment:
      case GetRoomResponseObjectMessageType.voiceMessage:
      case GetRoomResponseObjectMessageType.command:
        return null;
    }
  }

  BubbleStyle _getStyle() {
    final styles = ChatBubbleStyles(context);
    final BubbleStyle base;
    if (!_rendersAsCommentBubble) {
      base = styles.getSystemStyle();
    } else {
      base = widget.isSender ? styles.getSelfStyle() : styles.getRemoteStyle();
    }
    switch (widget.matchHighlight) {
      case SearchHighlight.none:
        return base;
      case SearchHighlight.secondary:
        return base.copyWith(
          borderWidth: 1.5,
          borderColor: Theme.of(
            context,
          ).colorScheme.primary.withValues(alpha: 0.45),
        );
      case SearchHighlight.active:
        return base.copyWith(
          borderWidth: 3,
          borderColor: Theme.of(context).colorScheme.primary,
        );
    }
  }

  void _showOptionsDialog() {
    Haptics.longPress();
    showChatMessageOptionsDialog(
      context,
      chatData: widget.chatData,
      bubbleData: widget.bubbleData,
      isSender: widget.isSender,
      onRefetch: widget.refetch,
    );
  }

  /// True only for messages whose body has a meaningful tap action (poll
  /// dialog or file download/cancel). For plain text messages we leave
  /// `onTap: null` on the bubble's `GestureDetector` so its
  /// `TapGestureRecognizer` does not enter the gesture arena — otherwise
  /// it competes with (and blocks) the per-link `TapGestureRecognizer`s
  /// that `HighlightedLinkify` / the Markdown renderer attach to link spans.
  bool get _hasTapAction {
    final obj = message.originalData?['object'];
    if (obj?.type == RichObjectStringObjectType.talkPoll) return true;
    return message.file != null;
  }

  void _onTap() {
    final obj = message.originalData?['object'];
    if (obj?.type == RichObjectStringObjectType.talkPoll) {
      showChatBubblePollSheet(
        context,
        room: widget.chatData,
        pollId: int.parse(obj!.id),
        pollName: obj.name,
        refetch: widget.refetch,
      );
      return;
    }
    if (message.file == null) return;
    if (guardDemoAction(context)) return;
    if (isDownloading) {
      confirmCancelDownload();
    } else {
      _startFileDownload();
    }
  }

  @override
  Widget build(BuildContext context) {
    message = ChatMessage(
      originalMessage: widget.bubbleData.message,
      originalData: widget.bubbleData.messageParameters,
    );
    final showActorDisplayName =
        _rendersAsCommentBubble &&
        widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne;
    final showBubbleTime =
        widget.bubbleData.messageType !=
        GetRoomResponseObjectMessageType.system;

    final parent = widget.bubbleData.parent;
    final actorBaseStyle = TextStyle(
      color: Theme.of(context).primaryColor,
      fontWeight: FontWeight.bold,
    );
    final actorText = Text(
      widget.bubbleData.actorDisplayName,
      textAlign: TextAlign.start,
      overflow: TextOverflow.ellipsis,
      style: actorBaseStyle,
    );
    final actorWidget = (widget.highlightQuery?.trim().isNotEmpty ?? false)
        ? Text.rich(
            TextSpan(
              children: buildHighlightedSpans(
                text: widget.bubbleData.actorDisplayName,
                query: widget.highlightQuery,
                baseStyle: actorBaseStyle,
              ),
            ),
            textAlign: TextAlign.start,
            overflow: TextOverflow.ellipsis,
          )
        : actorText;

    final timeText = Text(
      DateTime.fromMillisecondsSinceEpoch(
        widget.bubbleData.timestamp * 1000,
      ).formatHm(),
      textAlign: TextAlign.end,
      style: TextStyle(
        color: widget.timeIconColor,
        fontSize: widget.timeIconSize,
      ),
    );

    return Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.end,
      textDirection: TextDirection.ltr,
      crossAxisAlignment: CrossAxisAlignment.end,
      children: [
        GestureDetector(
          onHorizontalDragStart: (_) => _dragStartPosition = _position,
          onHorizontalDragUpdate: (details) {
            if (!widget.bubbleData.isReplyable) return;
            final dx = details.delta.dx - _dragStartPosition.dx;
            setState(() {
              _position = (_position.dx + dx).abs() > 60
                  ? Offset(_position.dx, 0)
                  : Offset(_position.dx + dx, 0);
            });
            // Beim Überqueren der Action-Schwelle einmalig Haptik feuern,
            // damit der User physisch spürt: "jetzt löst's beim Loslassen aus".
            final isArmed = _position.dx.abs() > 50;
            if (isArmed && !_swipeActionArmed) Haptics.longPress();
            _swipeActionArmed = isArmed;
          },
          onHorizontalDragEnd: (_) {
            final isAction = _position.dx.abs() > 50;
            setState(() => _position = Offset.zero);
            _swipeActionArmed = false;
            if (widget.bubbleData.isReplyable && isAction) {
              context.read<ChatBloc>().setReferenceMessageId(
                widget.bubbleData.id,
              );
            }
          },
          onLongPress: _showOptionsDialog,
          onDoubleTap: _showOptionsDialog,
          onTap: _hasTapAction ? _onTap : null,
          child: Transform.translate(
            offset: _position,
            child: Bubble(
              style: _getStyle(),
              child: _BubbleContent(
                actorText: actorText,
                actorWidget: actorWidget,
                timeText: timeText,
                messageWidget: message.getWidget(
                  highlightQuery: widget.highlightQuery,
                  style: _messageTextStyle(context),
                  renderMarkdown:
                      widget.bubbleData.markdown &&
                      widget.bubbleData.messageType ==
                          GetRoomResponseObjectMessageType.comment,
                ),
                parent: parent,
                bubbleData: widget.bubbleData,
                isSender: widget.isSender,
                isRead: widget.isRead,
                selfId: widget.selfId,
                spacing: widget.spacing,
                timeIconSize: widget.timeIconSize,
                timeIconColor: widget.timeIconColor,
                showActorDisplayName: showActorDisplayName,
                showBubbleTime: showBubbleTime,
                downloadJob: downloadJob,
              ),
            ),
          ),
        ),
        ChatBubbleReactions(
          bubbleData: widget.bubbleData,
          chatData: widget.chatData,
          isSender: widget.isSender,
          onChanged: widget.refetch,
        ),
      ],
    );
  }
}

class _BubbleContent extends StatelessWidget {
  final Text actorText;
  final Widget actorWidget;
  final Text timeText;
  final Widget messageWidget;
  final GetChatResponseObject? parent;
  final GetChatResponseObject bubbleData;
  final bool isSender;
  final bool isRead;
  final String? selfId;
  final double spacing;
  final double timeIconSize;
  final Color timeIconColor;
  final bool showActorDisplayName;
  final bool showBubbleTime;
  final DownloadJob? downloadJob;

  const _BubbleContent({
    required this.actorText,
    required this.actorWidget,
    required this.timeText,
    required this.messageWidget,
    required this.parent,
    required this.bubbleData,
    required this.isSender,
    required this.isRead,
    required this.selfId,
    required this.spacing,
    required this.timeIconSize,
    required this.timeIconColor,
    required this.showActorDisplayName,
    required this.showBubbleTime,
    required this.downloadJob,
  });

  @override
  Widget build(BuildContext context) => MergeSemantics(
    child: Container(
      constraints: BoxConstraints(
        maxWidth: MediaQuery.of(context).size.width * 0.9,
        minWidth: showActorDisplayName
            ? actorText.size.width
            : timeText.size.width + (isSender ? spacing + timeIconSize : 0) + 3,
      ),
      child: Stack(
        children: [
          if (showActorDisplayName)
            Positioned(top: 0, left: 0, child: actorWidget),
          Padding(
            padding: EdgeInsets.only(
              bottom: showBubbleTime ? 18 : 0,
              top: showActorDisplayName ? 18 : 0,
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (parent != null &&
                    bubbleData.messageType ==
                        GetRoomResponseObjectMessageType.comment) ...[
                  AnswerReference(referenceMessage: parent!, selfId: selfId),
                  const SizedBox(height: 5),
                ],
                messageWidget,
              ],
            ),
          ),
          if (showBubbleTime)
            Positioned(
              bottom: 0,
              right: 0,
              child: Row(
                children: [
                  timeText,
                  if (isSender) ...[
                    SizedBox(width: spacing),
                    Icon(
                      isRead ? Icons.done_all_outlined : Icons.done_outlined,
                      size: timeIconSize,
                      color: timeIconColor,
                      semanticLabel: isRead
                          ? A11yLabels.messageRead
                          : A11yLabels.messageSent,
                    ),
                  ],
                ],
              ),
            ),
          if (downloadJob?.status.value is DownloadInProgress)
            Positioned(
              bottom: 0,
              right: 0,
              left: 0,
              child: LinearProgressIndicator(
                semanticsLabel: A11yLabels.downloadingFile,
                value: () {
                  final s = downloadJob!.status.value as DownloadInProgress;
                  return s.percent <= 0 ? null : s.percent / 100;
                }(),
              ),
            ),
        ],
      ),
    ),
  );
}
