import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/chat/active_session_notifier.dart';
import '../../../core/chat/chat_notifier.dart';
import '../../../core/chat/session_closure_notifier.dart';
import '../../../core/constants.dart';
import '../../../core/theme/halo_tokens.dart';
import '../../../core/theme/widgets/halo_snackbar.dart';
import '../widgets/bestie_unavailable_dialog.dart';
import '../widgets/chat_expired_banner.dart';
import '../widgets/pricing_bottom_sheet.dart';

/// S10 Chat Room — strict Figma implementation (Phase 4, 2026-05-12).
///
/// Source-of-truth: `requirement/Figma/screens/session.jsx::S10Chat` (lines
/// 150–284) + `v3.jsx::HBChatExpiredBanner` (line 423). Phase 4 deltas the
/// older design had (entry banners, AppBar `akhiri` button, doodle bg) are
/// dropped — see [requirement/flow_customer.mermaid.md] §5.
class ChatScreen extends ConsumerStatefulWidget {
  final String sessionId;
  final String mitraName;

  const ChatScreen({super.key, required this.sessionId, required this.mitraName});

  @override
  ConsumerState<ChatScreen> createState() => _ChatScreenState();
}

class _ChatScreenState extends ConsumerState<ChatScreen> {
  final _messageController = TextEditingController();
  final _goodbyeController = TextEditingController();
  final _scrollController = ScrollController();
  Timer? _typingThrottle;
  StreamSubscription<String>? _warningSub;
  bool _rejectPopupShown = false;
  bool _threeMinShown = false;

  @override
  void initState() {
    super.initState();
    Future.microtask(() {
      ref.read(sessionClosureProvider.notifier).reset();
      ref.read(chatProvider.notifier).connectIfNotConnected(widget.sessionId);
    });
    _warningSub = ref.read(chatProvider.notifier).warningStream.listen((kind) {
      if (kind == 'three_minutes_left' && !_threeMinShown && mounted) {
        _threeMinShown = true;
        HaloSnackbar.show(
          context,
          'sisa 3 menit lagi ya 🤍',
          icon: '⏳',
        );
      }
    });
  }

  @override
  void dispose() {
    _messageController.dispose();
    _goodbyeController.dispose();
    _scrollController.dispose();
    _typingThrottle?.cancel();
    _warningSub?.cancel();
    super.dispose();
  }

  void _scrollToBottom() {
    void doScroll() {
      if (!mounted || !_scrollController.hasClients) return;
      _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
    }
    // Two passes: first captures the new bubble after the rebuild's layout;
    // second catches up once the keyboard animation finishes growing
    // maxScrollExtent. ~320ms covers the Android soft-keyboard rise.
    WidgetsBinding.instance.addPostFrameCallback((_) {
      doScroll();
      Future.delayed(const Duration(milliseconds: 320), doScroll);
    });
  }

  void _onTextChanged(String _) {
    if (_typingThrottle?.isActive ?? false) return;
    ref.read(chatProvider.notifier).sendTyping();
    _typingThrottle = Timer(const Duration(seconds: 2), () {});
  }

  void _sendMessage() {
    final text = _messageController.text.trim();
    if (text.isEmpty) return;
    ref.read(chatProvider.notifier).sendMessage(text);
    _messageController.clear();
    _scrollToBottom();
  }

  void _exitChat() {
    if (context.canPop()) {
      context.pop();
    } else {
      context.go('/home');
    }
  }

  void _goToThankYou() {
    if (!mounted) return;
    context.go('/chat/thank-you');
  }

  Future<void> _showBestieReturningPopup() async {
    if (_rejectPopupShown) return;
    _rejectPopupShown = true;
    if (!mounted) return;
    await BestieOfflinePopup.show(
      context,
      variant: BestieOfflineVariant.returning,
      mitraName: widget.mitraName,
    );
    _rejectPopupShown = false;
    ref.read(sessionClosureProvider.notifier).reset();
  }

  @override
  Widget build(BuildContext context) {
    // All `ref.listen` calls — pure side effects, never trigger rebuilds. The
    // parent ChatScreen used to `ref.watch(chatProvider)` + `ref.watch(timer)`
    // which forced a full-tree rebuild every second (timer ticks) and on every
    // WS frame; now those watches live in the leaf widgets that actually need
    // them (_ChatHeader for the timer, _ChatBodySection for the message list).

    ref.listen(sessionClosureProvider, (prev, next) {
      if (next is ClosureCompleteData) {
        ref.invalidate(activeSessionProvider);
        _goToThankYou();
      } else if (next is ClosureRejectedByMitraData) {
        _showBestieReturningPopup();
      }
    });

    ref.listen(chatProvider, (prev, next) {
      if (next is ChatConnectedData) {
        if (next.sessionClosing && !next.sessionExpired) {
          final closure = ref.read(sessionClosureProvider);
          if (closure is ClosureInitialData) {
            ref.read(sessionClosureProvider.notifier).declineExtension();
          }
        }
        if (!next.sessionPaused && !next.sessionExpired && !next.sessionClosing) {
          final closure = ref.read(sessionClosureProvider);
          if (closure is! ClosureInitialData) {
            ref.read(sessionClosureProvider.notifier).reset();
          }
        }
        _scrollToBottom();
        final unread = next.messages
            .where((m) => m.senderType == UserType.mitra && m.status != MessageStatus.read)
            .map((m) => m.id)
            .toList();
        if (unread.isNotEmpty) {
          ref.read(chatProvider.notifier).markRead(unread);
          ref.read(activeSessionProvider.notifier).markRead();
        }
      }
    });

    // 3-min snackbar side effect on the timer stream. Listening (not watching)
    // means parent doesn't rebuild every second — only this callback fires.
    // Backend also emits `session_warning kind=three_minutes_left` (handled in
    // initState via `warningStream`); `_threeMinShown` dedupes either path.
    ref.listen(chatRemainingSecondsProvider, (prev, next) {
      final tick = next.valueOrNull;
      if (tick == null) return;
      if (tick > 0 && tick <= 180 && !_threeMinShown && mounted) {
        _threeMinShown = true;
        HaloSnackbar.show(context, 'sisa 3 menit lagi ya 🤍', icon: '⏳');
      }
      // Re-arm when the session is extended back above 180s.
      if (tick > 180 && _threeMinShown) {
        _threeMinShown = false;
      }
    });

    return PopScope(
      canPop: false,
      onPopInvokedWithResult: (didPop, _) {
        if (!didPop) _exitChat();
      },
      child: Scaffold(
        backgroundColor: HaloTokens.brandSofter,
        body: SafeArea(
          bottom: false,
          child: Column(
            children: [
              _ChatHeader(mitraName: widget.mitraName, onBack: _exitChat),
              Expanded(
                child: _ChatBodySection(
                  sessionId: widget.sessionId,
                  mitraName: widget.mitraName,
                  messageController: _messageController,
                  goodbyeController: _goodbyeController,
                  scrollController: _scrollController,
                  onSend: _sendMessage,
                  onTextChanged: _onTextChanged,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

}

// ─── Body section ──────────────────────────────────────────────────────────
//
// Watches `chatProvider` + `sessionClosureProvider` and rebuilds only on those.
// The timer stream is NOT watched here — the lowTime/expired banners that need
// it live inside a tiny dedicated `Consumer` so timer ticks rebuild ONLY that
// banner, not the message list or the input bar.

class _ChatBodySection extends ConsumerWidget {
  final String sessionId;
  final String mitraName;
  final TextEditingController messageController;
  final TextEditingController goodbyeController;
  final ScrollController scrollController;
  final VoidCallback onSend;
  final ValueChanged<String> onTextChanged;

  const _ChatBodySection({
    required this.sessionId,
    required this.mitraName,
    required this.messageController,
    required this.goodbyeController,
    required this.scrollController,
    required this.onSend,
    required this.onTextChanged,
  });

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final chatState = ref.watch(chatProvider);
    final closureState = ref.watch(sessionClosureProvider);

    if (chatState is ChatConnectingData) {
      return const Center(child: CircularProgressIndicator());
    }
    if (chatState is ChatErrorData) {
      return Center(child: Text(chatState.message));
    }
    if (chatState is ChatConnectedData) {
      return _buildChatBody(context, ref, chatState, closureState);
    }
    return const SizedBox.shrink();
  }

  Widget _buildChatBody(
    BuildContext context,
    WidgetRef ref,
    ChatConnectedData state,
    SessionClosureData closureState,
  ) {
    final shouldShowGoodbye = !state.goodbyeSubmitted &&
        (closureState is ClosureShowGoodbyeData ||
            closureState is ClosureSubmittingData ||
            (state.sessionClosing &&
                !state.sessionExpired &&
                closureState is! ClosureCompleteData));
    if (shouldShowGoodbye) {
      return _buildGoodbyeView(ref, closureState);
    }
    if (state.sessionClosing && state.goodbyeSubmitted) {
      return _buildAwaitingMitraGoodbyeView(state);
    }
    if (state.sessionPaused) {
      return _buildPausedView();
    }

    return Column(
      children: [
        Expanded(
          child: ListView.builder(
            controller: scrollController,
            padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
            itemCount: state.messages.length + (state.isOtherTyping ? 1 : 0),
            itemBuilder: (listCtx, index) {
              if (state.isOtherTyping && index == state.messages.length) {
                return const Padding(
                  padding: EdgeInsets.symmetric(vertical: 4),
                  child: Align(
                    alignment: Alignment.centerLeft,
                    child: _TypingPill(),
                  ),
                );
              }
              final msg = state.messages[index];
              final isMe = msg.senderType == UserType.customer;
              return _MessageBubble(msg: msg, isMe: isMe);
            },
          ),
        ),
        // Banner gating runs on the timer stream — scoped to its own Consumer
        // so only the banner widget rebuilds every second, not the list or
        // input bar above/below.
        _TimerBanner(sessionId: sessionId, mitraName: mitraName),
        if (!state.sessionExpired) ...[
          _InputBar(
            controller: messageController,
            onChanged: onTextChanged,
            onSend: onSend,
          ),
          const _EncryptedFooter(),
        ],
      ],
    );
  }

  // Inline goodbye composer for the mitra-initiated early-end case
  // (sessionClosing true, customer hasn't been routed through the
  // ClosingMessageSheet). Primary path is the dedicated [ClosingMessageSheet].
  Widget _buildGoodbyeView(WidgetRef ref, SessionClosureData closureState) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(32),
      child: Column(
        children: [
          Container(
            width: double.infinity,
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
            decoration: BoxDecoration(
              color: HaloTokens.accentSoft,
              borderRadius: BorderRadius.circular(12),
            ),
            child: const Row(
              children: [
                Icon(Icons.info_outline, color: HaloTokens.brandDark, size: 20),
                SizedBox(width: 8),
                Expanded(
                  child: Text(
                    'sesi telah ditutup oleh bestie',
                    style: TextStyle(
                      fontFamily: HaloTokens.fontBody,
                      color: HaloTokens.brandDark,
                      fontWeight: FontWeight.w600,
                    ),
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(height: 32),
          const Text('🤍', style: TextStyle(fontSize: 48)),
          const SizedBox(height: 16),
          const Text(
            'pesan penutup',
            style: TextStyle(
              fontFamily: HaloTokens.fontDisplay,
              fontSize: 22,
              fontWeight: FontWeight.w700,
              color: HaloTokens.ink,
            ),
          ),
          const SizedBox(height: 8),
          const Text(
            'tuliskan pesan terakhirmu untuk bestie',
            textAlign: TextAlign.center,
            style: TextStyle(
              fontFamily: HaloTokens.fontBody,
              color: HaloTokens.inkSoft,
            ),
          ),
          const SizedBox(height: 24),
          TextField(
            controller: goodbyeController,
            maxLines: 3,
            decoration: InputDecoration(
              hintText: 'terima kasih, bestie...',
              filled: true,
              fillColor: HaloTokens.surface,
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(12),
                borderSide: const BorderSide(color: HaloTokens.border),
              ),
            ),
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            style: ElevatedButton.styleFrom(
              backgroundColor: HaloTokens.brand,
              foregroundColor: Colors.white,
              padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(999)),
            ),
            onPressed: closureState is ClosureSubmittingData
                ? null
                : () {
                    final text = goodbyeController.text.trim();
                    if (text.isNotEmpty) {
                      ref.read(sessionClosureProvider.notifier).submitGoodbye(
                            sessionId,
                            text,
                          );
                    }
                  },
            child: closureState is ClosureSubmittingData
                ? const SizedBox(
                    width: 20,
                    height: 20,
                    child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
                  )
                : const Text('kirim & selesai'),
          ),
        ],
      ),
    );
  }

  Widget _buildPausedView() {
    return const Center(
      child: Padding(
        padding: EdgeInsets.all(32),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CircularProgressIndicator(),
            SizedBox(height: 24),
            Text(
              'menunggu konfirmasi bestie...',
              style: TextStyle(
                fontFamily: HaloTokens.fontBody,
                fontSize: 16,
                color: HaloTokens.ink,
              ),
            ),
            SizedBox(height: 8),
            Text(
              'chat dijeda sementara',
              style: TextStyle(
                fontFamily: HaloTokens.fontBody,
                color: HaloTokens.inkSoft,
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildAwaitingMitraGoodbyeView(ChatConnectedData state) {
    return Column(
      children: [
        Container(
          width: double.infinity,
          color: HaloTokens.accentSoft,
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
          child: const Row(
            children: [
              Icon(Icons.hourglass_top, color: HaloTokens.brandDark, size: 20),
              SizedBox(width: 8),
              Expanded(
                child: Text(
                  'pesan penutupmu sudah terkirim. menunggu bestie...',
                  style: TextStyle(
                    fontFamily: HaloTokens.fontBody,
                    color: HaloTokens.brandDark,
                    fontWeight: FontWeight.w600,
                  ),
                ),
              ),
            ],
          ),
        ),
        Expanded(
          child: ListView.builder(
            controller: scrollController,
            padding: const EdgeInsets.all(16),
            itemCount: state.messages.length,
            itemBuilder: (context, index) {
              final msg = state.messages[index];
              final isMe = msg.senderType == UserType.customer;
              return _MessageBubble(msg: msg, isMe: isMe);
            },
          ),
        ),
      ],
    );
  }
}

// Tiny dedicated consumer for the in-chat low-time / expired banner. Scoped
// here so timer ticks rebuild only this widget — the message list above and
// input bar below stay still. Uses `.select` to collapse the timer stream to
// a 3-state enum so the rebuild only fires on banner-state transitions, not
// every second.
enum _BannerKind { none, lowTime, expired }

class _TimerBanner extends ConsumerWidget {
  final String sessionId;
  final String mitraName;
  const _TimerBanner({required this.sessionId, required this.mitraName});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final kind = ref.watch(chatRemainingSecondsProvider.select((async) {
      final r = async.valueOrNull;
      if (r == null) return _BannerKind.none;
      if (r <= 0) return _BannerKind.expired;
      if (r < 120) return _BannerKind.lowTime;
      return _BannerKind.none;
    }));
    void onExtend() {
      PricingBottomSheet.showForExtension(context, sessionId: sessionId);
    }
    switch (kind) {
      case _BannerKind.lowTime:
        return _SoftWarningBanner(mitraName: mitraName, onExtend: onExtend);
      case _BannerKind.expired:
        return ChatExpiredBanner(mitraName: mitraName, onExtend: onExtend);
      case _BannerKind.none:
        return const SizedBox.shrink();
    }
  }
}

// ─── Header (back · orb · name+status · timer pill) + progress bar ──────────

class _ChatHeader extends ConsumerStatefulWidget {
  final String mitraName;
  final VoidCallback onBack;

  const _ChatHeader({required this.mitraName, required this.onBack});

  @override
  ConsumerState<_ChatHeader> createState() => _ChatHeaderState();
}

class _ChatHeaderState extends ConsumerState<_ChatHeader> {
  // Progress-bar denominator. ChatConnectedData doesn't carry the session's
  // total duration, so we infer it as the max remaining we've seen since
  // mount. First tick after a fresh connect is effectively `total`; later
  // extensions raise it back up.
  int? _observedTotalSeconds;

  @override
  Widget build(BuildContext context) {
    final remainingSeconds = ref.watch(chatRemainingSecondsProvider).valueOrNull;
    // Only the `isOtherTyping` field of the chat state matters here. `.select`
    // means this widget rebuilds only when that boolean flips, not on every
    // message / status update.
    final isOtherTyping = ref.watch(chatProvider.select(
      (s) => s is ChatConnectedData && s.isOtherTyping,
    ));

    if (remainingSeconds != null &&
        remainingSeconds > 0 &&
        (_observedTotalSeconds == null || remainingSeconds > _observedTotalSeconds!)) {
      _observedTotalSeconds = remainingSeconds;
    }
    final totalSeconds = _observedTotalSeconds;
    final lowTime = remainingSeconds != null &&
        remainingSeconds > 0 &&
        remainingSeconds < 120;
    final progress = (remainingSeconds != null && totalSeconds != null && totalSeconds > 0)
        ? (remainingSeconds / totalSeconds).clamp(0.0, 1.0)
        : null;

    return Container(
      color: HaloTokens.surface,
      child: Column(
        children: [
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            decoration: const BoxDecoration(
              border: Border(bottom: BorderSide(color: HaloTokens.border)),
            ),
            child: Row(
              children: [
                _CircleIconButton(icon: Icons.chevron_left, onTap: widget.onBack),
                const SizedBox(width: 12),
                _MitraOrb(seed: widget.mitraName.hashCode),
                const SizedBox(width: 12),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        widget.mitraName,
                        overflow: TextOverflow.ellipsis,
                        style: const TextStyle(
                          fontFamily: HaloTokens.fontBody,
                          fontSize: 14,
                          fontWeight: FontWeight.w600,
                          color: HaloTokens.ink,
                        ),
                      ),
                      const SizedBox(height: 2),
                      Row(
                        children: [
                          Container(
                            width: 6,
                            height: 6,
                            decoration: const BoxDecoration(
                              color: HaloTokens.success,
                              shape: BoxShape.circle,
                            ),
                          ),
                          const SizedBox(width: 4),
                          Text(
                            isOtherTyping ? 'online · ngetik...' : 'online',
                            style: const TextStyle(
                              fontFamily: HaloTokens.fontBody,
                              fontSize: 11,
                              color: HaloTokens.success,
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
                if (remainingSeconds != null && remainingSeconds > 0)
                  _TimerPill(seconds: remainingSeconds, lowTime: lowTime),
              ],
            ),
          ),
          // Progress bar (3px) below the header
          if (progress != null)
            Container(
              height: 3,
              color: HaloTokens.border,
              child: FractionallySizedBox(
                alignment: Alignment.centerLeft,
                widthFactor: progress,
                child: AnimatedContainer(
                  duration: const Duration(seconds: 1),
                  color: lowTime ? const Color(0xFFFF8848) : HaloTokens.brand,
                ),
              ),
            ),
        ],
      ),
    );
  }
}

class _CircleIconButton extends StatelessWidget {
  final IconData icon;
  final VoidCallback onTap;
  const _CircleIconButton({required this.icon, required this.onTap});

  @override
  Widget build(BuildContext context) {
    return InkResponse(
      onTap: onTap,
      radius: 22,
      child: Container(
        width: 36,
        height: 36,
        decoration: const BoxDecoration(
          color: HaloTokens.brandSofter,
          shape: BoxShape.circle,
        ),
        alignment: Alignment.center,
        child: Icon(icon, color: HaloTokens.brandDark, size: 22),
      ),
    );
  }
}

/// Stand-in for the Figma `HBOrb` gradient avatar. Memory tracks this as a
/// Phase-4 follow-up — for now a deterministic two-stop gradient circle keeps
/// the same visual weight without depending on the unported component.
class _MitraOrb extends StatelessWidget {
  final int seed;
  const _MitraOrb({required this.seed});

  @override
  Widget build(BuildContext context) {
    final palette = _palettes[seed.abs() % _palettes.length];
    return Container(
      width: 36,
      height: 36,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: palette,
        ),
      ),
    );
  }

  static const _palettes = <List<Color>>[
    [Color(0xFFE17A9D), Color(0xFFF7B26A)],
    [Color(0xFFB8DBC8), Color(0xFFE17A9D)],
    [Color(0xFFD4C5E8), Color(0xFFF7B26A)],
    [Color(0xFFF7B26A), Color(0xFFE17A9D)],
  ];
}

class _TimerPill extends StatelessWidget {
  final int seconds;
  final bool lowTime;
  const _TimerPill({required this.seconds, required this.lowTime});

  @override
  Widget build(BuildContext context) {
    final mm = (seconds ~/ 60).toString().padLeft(2, '0');
    final ss = (seconds % 60).toString().padLeft(2, '0');
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
      decoration: BoxDecoration(
        color: lowTime ? const Color(0xFFFFF0E5) : HaloTokens.brandSofter,
        borderRadius: BorderRadius.circular(12),
        border: Border.all(
          color: lowTime ? const Color(0xFFFFB088) : HaloTokens.brandSoft,
        ),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          Text(
            'SISA WAKTU',
            style: TextStyle(
              fontFamily: HaloTokens.fontBody,
              fontSize: 9.5,
              fontWeight: FontWeight.w600,
              letterSpacing: 0.5,
              color: lowTime ? const Color(0xFFA8410E) : HaloTokens.brandDark,
            ),
          ),
          const SizedBox(height: 2),
          Text(
            '$mm:$ss',
            style: TextStyle(
              fontFamily: HaloTokens.fontMono,
              fontSize: 16,
              fontWeight: FontWeight.w700,
              letterSpacing: -0.3,
              color: lowTime ? const Color(0xFFA8410E) : HaloTokens.brandDark,
            ),
          ),
        ],
      ),
    );
  }
}

// ─── Messages ──────────────────────────────────────────────────────────────

class _MessageBubble extends StatelessWidget {
  final ChatMessage msg;
  final bool isMe;
  const _MessageBubble({required this.msg, required this.isMe});

  @override
  Widget build(BuildContext context) {
    final hh = msg.createdAt.hour.toString().padLeft(2, '0');
    final mm = msg.createdAt.minute.toString().padLeft(2, '0');
    final bubbleColor = isMe ? HaloTokens.brand : HaloTokens.surface;
    final textColor = isMe ? Colors.white : HaloTokens.ink;
    final timeColor = isMe ? Colors.white70 : HaloTokens.inkMuted;
    return Align(
      alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 4),
        padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
        constraints: BoxConstraints(
          maxWidth: MediaQuery.of(context).size.width * 0.78,
        ),
        decoration: BoxDecoration(
          color: bubbleColor,
          borderRadius: BorderRadius.only(
            topLeft: const Radius.circular(18),
            topRight: const Radius.circular(18),
            bottomLeft: Radius.circular(isMe ? 18 : 4),
            bottomRight: Radius.circular(isMe ? 4 : 18),
          ),
          boxShadow: isMe
              ? null
              : [
                  BoxShadow(
                    color: HaloTokens.brandDark.withValues(alpha: 0.06),
                    blurRadius: 2,
                    offset: const Offset(0, 1),
                  ),
                ],
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.end,
          children: [
            Text(
              msg.content,
              style: TextStyle(
                fontFamily: HaloTokens.fontBody,
                fontSize: 14,
                height: 1.45,
                color: textColor,
              ),
            ),
            const SizedBox(height: 4),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(
                  '$hh:$mm',
                  style: TextStyle(
                    fontFamily: HaloTokens.fontBody,
                    fontSize: 10,
                    color: timeColor,
                  ),
                ),
                if (isMe) ...[
                  const SizedBox(width: 4),
                  _StatusIcon(status: msg.status),
                ],
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class _StatusIcon extends StatelessWidget {
  final String status;
  const _StatusIcon({required this.status});

  @override
  Widget build(BuildContext context) {
    switch (status) {
      case 'sending':
        return const Icon(Icons.access_time, size: 14, color: Colors.white70);
      case MessageStatus.sent:
        return const Icon(Icons.check, size: 14, color: Colors.white70);
      case MessageStatus.delivered:
        return const Icon(Icons.done_all, size: 14, color: Colors.white70);
      case MessageStatus.read:
        return const Icon(Icons.done_all, size: 14, color: Colors.white);
      default:
        return const SizedBox.shrink();
    }
  }
}

/// Three-dot animated typing pill, rendered as a bestie-side message bubble.
class _TypingPill extends StatefulWidget {
  const _TypingPill();

  @override
  State<_TypingPill> createState() => _TypingPillState();
}

class _TypingPillState extends State<_TypingPill> with SingleTickerProviderStateMixin {
  late final AnimationController _ctrl;

  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 1400),
    )..repeat();
  }

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

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
      decoration: BoxDecoration(
        color: HaloTokens.surface,
        borderRadius: const BorderRadius.only(
          topLeft: Radius.circular(18),
          topRight: Radius.circular(18),
          bottomLeft: Radius.circular(4),
          bottomRight: Radius.circular(18),
        ),
        boxShadow: [
          BoxShadow(
            color: HaloTokens.brandDark.withValues(alpha: 0.06),
            blurRadius: 2,
            offset: const Offset(0, 1),
          ),
        ],
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: List.generate(3, (i) {
          return Padding(
            padding: EdgeInsets.only(right: i == 2 ? 0 : 4),
            child: AnimatedBuilder(
              animation: _ctrl,
              builder: (_, __) {
                final phase = (_ctrl.value + i * 0.2) % 1.0;
                final t = phase < 0.4 ? phase / 0.4 : 1 - (phase - 0.4) / 0.6;
                final opacity = 0.3 + (0.7 * t.clamp(0.0, 1.0));
                return Container(
                  width: 6,
                  height: 6,
                  decoration: BoxDecoration(
                    color: HaloTokens.brand.withValues(alpha: opacity),
                    shape: BoxShape.circle,
                  ),
                );
              },
            ),
          );
        }),
      ),
    );
  }
}

// ─── 2-minute soft-warning banner ──────────────────────────────────────────

class _SoftWarningBanner extends StatelessWidget {
  final String mitraName;
  final VoidCallback onExtend;
  const _SoftWarningBanner({required this.mitraName, required this.onExtend});

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.fromLTRB(12, 0, 12, 8),
      padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
      decoration: BoxDecoration(
        color: const Color(0xFFFFF0E5),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: const Color(0xFFFFD8B8)),
      ),
      child: Row(
        children: [
          const Text('⏳', style: TextStyle(fontSize: 16)),
          const SizedBox(width: 10),
          Expanded(
            child: RichText(
              text: TextSpan(
                style: const TextStyle(
                  fontFamily: HaloTokens.fontBody,
                  fontSize: 12,
                  height: 1.4,
                  color: Color(0xFF7A3E08),
                ),
                children: [
                  const TextSpan(
                    text: 'habis... ',
                    style: TextStyle(fontWeight: FontWeight.w700),
                  ),
                  TextSpan(text: 'mau lanjutin curhat sama $mitraName?'),
                ],
              ),
            ),
          ),
          const SizedBox(width: 8),
          ElevatedButton(
            onPressed: onExtend,
            style: ElevatedButton.styleFrom(
              backgroundColor: const Color(0xFFFF8848),
              foregroundColor: Colors.white,
              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(999)),
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
              minimumSize: const Size(0, 32),
              elevation: 0,
              textStyle: const TextStyle(
                fontFamily: HaloTokens.fontBody,
                fontSize: 12,
                fontWeight: FontWeight.w600,
              ),
            ),
            child: const Text('+30 menit'),
          ),
        ],
      ),
    );
  }
}

// ─── Input bar (+ button · rounded field · send arrow) ─────────────────────

class _InputBar extends StatelessWidget {
  final TextEditingController controller;
  final ValueChanged<String> onChanged;
  final VoidCallback onSend;
  const _InputBar({
    required this.controller,
    required this.onChanged,
    required this.onSend,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      color: HaloTokens.surface,
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
      child: Row(
        children: [
          // `+` attachment — placeholder (no attachment flow yet in this phase).
          _CircleIconButton(icon: Icons.add, onTap: () {}),
          const SizedBox(width: 8),
          Expanded(
            child: SizedBox(
              height: 40,
              child: Material(
                color: HaloTokens.bg,
                shape: const StadiumBorder(),
                clipBehavior: Clip.antiAlias,
                // Center wraps the (intentionally collapsed) TextField so it
                // sits vertically centered in the 40px pill — without it the
                // field anchors to the top because `isCollapsed: true` zeroes
                // out the decoration's vertical padding, and
                // `textAlignVertical` is a no-op on a collapsed field.
                child: Center(
                  child: TextField(
                  controller: controller,
                  onChanged: onChanged,
                  textInputAction: TextInputAction.send,
                  onSubmitted: (_) => onSend(),
                  maxLines: 1,
                  textAlignVertical: TextAlignVertical.center,
                  style: const TextStyle(
                    fontFamily: HaloTokens.fontBody,
                    fontSize: 14,
                    color: HaloTokens.ink,
                  ),
                  // The app-wide InputDecorationTheme (halo_theme.dart) ships
                  // form-style defaults — filled white, 64px min-height, brand
                  // focused border. None of those are wanted on the chat input
                  // pill, so override every relevant property explicitly here
                  // rather than rely on `border: none` (which only nukes the
                  // default border, not focused/enabled variants or the fill).
                  decoration: const InputDecoration(
                    filled: false,
                    fillColor: Colors.transparent,
                    border: InputBorder.none,
                    enabledBorder: InputBorder.none,
                    focusedBorder: InputBorder.none,
                    errorBorder: InputBorder.none,
                    focusedErrorBorder: InputBorder.none,
                    disabledBorder: InputBorder.none,
                    isCollapsed: true,
                    contentPadding: EdgeInsets.symmetric(horizontal: 16),
                    constraints: BoxConstraints(),
                    hintText: 'tulis sesuatu...',
                    hintStyle: TextStyle(
                      fontFamily: HaloTokens.fontBody,
                      fontSize: 14,
                      color: HaloTokens.inkMuted,
                    ),
                  ),
                ),
                ),
              ),
            ),
          ),
          const SizedBox(width: 8),
          InkResponse(
            onTap: onSend,
            radius: 22,
            child: Container(
              width: 40,
              height: 40,
              decoration: const BoxDecoration(
                color: HaloTokens.brand,
                shape: BoxShape.circle,
              ),
              alignment: Alignment.center,
              child: const Icon(Icons.arrow_upward, color: Colors.white, size: 18),
            ),
          ),
        ],
      ),
    );
  }
}

class _EncryptedFooter extends StatelessWidget {
  const _EncryptedFooter();

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      color: HaloTokens.surface,
      padding: const EdgeInsets.fromLTRB(12, 0, 12, 16),
      alignment: Alignment.centerRight,
      child: const Text(
        'terenkripsi · gak disimpan 🔒',
        style: TextStyle(
          fontFamily: HaloTokens.fontBody,
          fontSize: 11,
          color: HaloTokens.inkMuted,
        ),
      ),
    );
  }
}
