import 'dart:async';

import 'package:commons_core/commons_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:url_launcher/url_launcher.dart';

import '../di/injector.dart';
import '../domain/chat_timeline.dart';
import '../domain/message_rules.dart';
import '../i18n/strings.g.dart';
import '../services/message_store.dart';
import '../services/profile_cache.dart';
import '../services/profile_store.dart';
import '../services/social_connection.dart';
import '../services/social_service.dart';
import '../services/trust_referents.dart';
import '../services/unread_service.dart';
import '../services/wot_settings.dart';
import '../state/messages_cubit.dart';
import '../state/trust_cubit.dart';
import 'peer_avatar.dart';
import 'theme.dart';

/// A 1:1 encrypted chat with one peer (redesign screen 10). Private and
/// metadata-hiding (NIP-17) under the hood; local-first, so it degrades to a
/// "set up sharing" note when there's no relay.
class ChatScreen extends StatefulWidget {
  const ChatScreen({
    required this.social,
    required this.connection,
    required this.peerPubkey,
    this.messageStore,
    this.profileCache,
    this.trustReferents,
    this.wotSettings,
    super.key,
  });

  final SocialService social;

  /// The shared relay connection (one per identity), carrying messaging + trust.
  final SocialConnection connection;
  final String peerPubkey;

  /// Optional persistence for chat history (encrypted [ChatDatabase]-backed);
  /// null in tests.
  final MessageStore? messageStore;

  /// Optional cache of peer display names; null in tests.
  final ProfileCache? profileCache;

  /// Web-of-trust bootstrap referents + parameters, for the membership verdict.
  final TrustReferents? trustReferents;
  final WotSettings? wotSettings;

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

class _ChatScreenState extends State<ChatScreen> {
  MessagesCubit? _messages;
  TrustCubit? _trust;
  bool _loading = true;
  String? _peerName;
  String? _selfName;
  String? _peerG1;
  final _input = TextEditingController();

  /// App-wide unread tracker (absent in tests / inventory-only builds). While
  /// this chat is open it is the "active peer", so incoming messages for it are
  /// read on arrival and never badge or notify.
  UnreadService? _unread;

  @override
  void initState() {
    super.initState();
    _unread = getIt.isRegistered<UnreadService>()
        ? getIt<UnreadService>()
        : null;
    // Set synchronously (before any await) so a message landing during _init is
    // already suppressed.
    _unread?.activePeer = widget.peerPubkey;
    unawaited(_unread?.markRead(widget.peerPubkey));
    _init();
  }

  Future<void> _init() async {
    // The shared connection carries both messaging and trust. Offline (not
    // connected / unreachable) → null transports and the screen degrades.
    final session = await widget.connection.session();
    final cachedName = await widget.profileCache?.name(widget.peerPubkey);
    // My own display name, so my avatar shows my initial (not a bare glyph).
    final selfName = getIt.isRegistered<ProfileStore>()
        ? await getIt<ProfileStore>().name()
        : '';
    final referents = await widget.trustReferents?.all() ?? const <String>{};
    final wotParams = await widget.wotSettings?.params() ?? WotParams.duniter;
    if (!mounted) return; // shared session is owned by the connection, not us
    final self = widget.social.publicKeyHex;
    final messages = MessagesCubit(
      session?.messages,
      peerPubkey: widget.peerPubkey,
      selfPubkey: self,
      store: widget.messageStore,
    )..start();
    final trust = TrustCubit(
      session?.trust,
      peerPubkey: widget.peerPubkey,
      selfPubkey: self,
      referents: referents,
      params: wotParams,
    );
    unawaited(trust.load());
    setState(() {
      _messages = messages;
      _trust = trust;
      _peerName = cachedName;
      _selfName = selfName.isEmpty ? null : selfName;
      _loading = false;
    });

    // Freshen the peer's name from their published profile.
    final live = session;
    if (live != null && widget.profileCache != null) {
      unawaited(() async {
        try {
          final profile = await live.profile.fetch(widget.peerPubkey);
          if (profile != null && mounted) {
            if (profile.name.isNotEmpty) {
              await widget.profileCache!.setName(
                widget.peerPubkey,
                profile.name,
              );
            }
            setState(() {
              if (profile.name.isNotEmpty) _peerName = profile.name;
              if (profile.g1.isNotEmpty) _peerG1 = profile.g1;
            });
          }
        } catch (_) {
          // best effort
        }
      }());
    }
  }

  /// Opens the peer's Ğ1 wallet so you can pay them (Ğ1 is separate from the
  /// Nostr key — this uses the address they published in their profile). Falls
  /// back to copying the address if no wallet handles the link.
  ///
  /// NOTE: confirm the deep-link scheme for the target wallet (Ğ1nkgo / Ğecko /
  /// Cesium) — this uses the Cesium web wallet as a universal fallback.
  Future<void> _payG1() async {
    final g1 = _peerG1;
    if (g1 == null) return;
    final messenger = ScaffoldMessenger.of(context);
    final t = context.t;
    final uri = Uri.parse('https://demo.cesium.app/#/app/wallet/$g1/');
    var launched = false;
    try {
      launched = await launchUrl(uri, mode: LaunchMode.externalApplication);
    } catch (_) {
      launched = false;
    }
    if (!launched && mounted) {
      await Clipboard.setData(ClipboardData(text: g1));
      messenger.showSnackBar(SnackBar(content: Text(t.chat.g1Copied)));
    }
  }

  Future<void> _send() async {
    final cubit = _messages;
    if (cubit == null || _input.text.trim().isEmpty) return;
    final text = _input.text;
    // Links aren't allowed — warn and keep the text so it can be edited.
    if (containsUrl(text)) {
      ScaffoldMessenger.of(context)
        ..hideCurrentSnackBar()
        ..showSnackBar(SnackBar(content: Text(context.t.chat.noLinks)));
      return;
    }
    _input.clear();
    await cubit.send(text);
  }

  @override
  void dispose() {
    if (_unread?.activePeer == widget.peerPubkey) _unread!.activePeer = null;
    // Mark read once more on the way out, so anything that arrived while the
    // chat was open (and was auto-read) stays cleared.
    unawaited(_unread?.markRead(widget.peerPubkey));
    _messages?.close();
    _trust?.close();
    // The session belongs to the shared connection — don't close it here.
    _input.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final messages = _messages;
    final trust = _trust;
    final t = context.t;
    return Scaffold(
      appBar: AppBar(
        title: Text(_peerName ?? shortPubkey(widget.peerPubkey)),
        actions: [
          if (_peerG1 != null)
            IconButton(
              key: const Key('chat.payG1'),
              icon: const Icon(Icons.payments_outlined),
              tooltip: t.chat.payG1,
              onPressed: _payG1,
            ),
        ],
      ),
      body: _loading || messages == null || trust == null
          ? const Center(child: CircularProgressIndicator())
          : MultiBlocProvider(
              providers: [
                BlocProvider.value(value: messages),
                BlocProvider.value(value: trust),
              ],
              child: _ChatBody(
                controller: _input,
                onSend: _send,
                peerName: _peerName,
                selfName: _selfName,
              ),
            ),
    );
  }
}

class _ChatBody extends StatelessWidget {
  const _ChatBody({
    required this.controller,
    required this.onSend,
    this.peerName,
    this.selfName,
  });

  final TextEditingController controller;
  final Future<void> Function() onSend;
  final String? peerName;
  final String? selfName;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    final cubit = context.read<MessagesCubit>();
    if (!cubit.isOnline) {
      return Center(
        child: Padding(
          padding: const EdgeInsets.all(32),
          child: Text(
            t.chat.offline,
            textAlign: TextAlign.center,
            style: const TextStyle(color: seedMuted, fontSize: 15),
          ),
        ),
      );
    }
    // Surface a failed send instead of losing it silently (relay down, etc.).
    return BlocListener<MessagesCubit, ChatState>(
      listenWhen: (prev, cur) => cur.error != null && prev.error != cur.error,
      listener: (context, state) {
        ScaffoldMessenger.of(context)
          ..hideCurrentSnackBar()
          ..showSnackBar(SnackBar(content: Text(t.chat.sendError)));
      },
      child: Column(
        children: [
          const _TrustBanner(),
          Expanded(
            child: ChatMessageList(peerName: peerName, selfName: selfName),
          ),
          _Composer(controller: controller, onSend: onSend),
        ],
      ),
    );
  }
}

/// The scrolling list of chat bubbles. Anchored at the bottom (chat
/// convention): the newest message is always in view, and new arrivals show up
/// in place instead of landing below the fold. Reads the [MessagesCubit] from
/// context.
class ChatMessageList extends StatelessWidget {
  const ChatMessageList({this.peerName, this.selfName, super.key});

  /// The peer's display name, for their avatar's initial (null → a glyph).
  final String? peerName;

  /// My own display name, for my avatar's initial (null → a glyph).
  final String? selfName;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    final cubit = context.read<MessagesCubit>();
    return BlocBuilder<MessagesCubit, ChatState>(
      builder: (context, state) {
        if (state.messages.isEmpty) {
          return Center(
            child: Text(t.chat.empty, style: const TextStyle(color: seedMuted)),
          );
        }
        // Group into bubbles + day separators (oldest-first), then render
        // bottom-anchored: with `reverse: true` index 0 is the bottom-most
        // (newest), so map it to the tail. Scrolling up to read history stays
        // put when new messages come in.
        final items = chatTimeline(state.messages);
        return ListView.builder(
          reverse: true,
          padding: const EdgeInsets.all(12),
          itemCount: items.length,
          itemBuilder: (context, i) {
            final item = items[items.length - 1 - i];
            return switch (item) {
              ChatDaySeparator(:final day) => _DaySeparator(day: day),
              ChatMessageRow(:final message) => _MessageRow(
                message: message,
                mine: cubit.isMine(message),
                peerName: peerName,
                selfName: selfName,
                selfPubkey: cubit.selfPubkey,
              ),
            };
          },
        );
      },
    );
  }
}

/// A slim "web of trust" strip: how many vouch for this peer, and a toggle to
/// add/remove your own vouch ("I know this person").
class _TrustBanner extends StatelessWidget {
  const _TrustBanner();

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    return BlocBuilder<TrustCubit, TrustState>(
      builder: (context, state) {
        final cubit = context.read<TrustCubit>();
        if (!cubit.isOnline || state.loading) return const SizedBox.shrink();
        // Strongest applicable signal decides the badge (member > circle >
        // vouched > unknown).
        final (IconData icon, String label, bool strong) = switch (state.tier) {
          TrustTier.networkMember => (Icons.verified, t.trust.member, true),
          TrustTier.inYourCircle => (Icons.verified_user, t.trust.circle, true),
          TrustTier.vouched => (
            Icons.people_outline,
            t.trust.count(n: state.certifierCount),
            false,
          ),
          TrustTier.unknown => (Icons.person_outline, t.trust.none, false),
        };
        return Container(
          width: double.infinity,
          color: seedPrimaryContainer.withValues(alpha: 0.5),
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: Row(
            children: [
              Icon(icon, size: 18, color: strong ? seedGreen : seedMuted),
              const SizedBox(width: 8),
              Expanded(
                child: Text(
                  label,
                  style: TextStyle(
                    color: strong ? seedGreen : seedOnSurface,
                    fontSize: 13,
                    fontWeight: strong ? FontWeight.w600 : FontWeight.w400,
                  ),
                ),
              ),
              TextButton(
                key: const Key('chat.vouch'),
                onPressed: state.busy ? null : cubit.toggleVouch,
                child: Text(state.iVouch ? t.trust.vouched : t.trust.vouch),
              ),
            ],
          ),
        );
      },
    );
  }
}

/// A centered pill introducing a new day ("Today", "Yesterday", or a
/// locale-formatted date). Localized via [chatDayLabel].
class _DaySeparator extends StatelessWidget {
  const _DaySeparator({required this.day});

  final DateTime day;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    // Localizations locale, not the raw app locale: `ast` has no intl date
    // symbols and is mapped to Spanish for the framework (see app.dart).
    final localeCode = Localizations.localeOf(context).languageCode;
    final label = chatDayLabel(
      day,
      now: DateTime.now(),
      today: t.chat.today,
      yesterday: t.chat.yesterday,
      localeCode: localeCode,
    );
    return Center(
      key: const Key('chat.daySeparator'),
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 8),
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
        decoration: BoxDecoration(
          color: seedMuted.withValues(alpha: 0.15),
          borderRadius: BorderRadius.circular(12),
        ),
        child: Text(
          label,
          style: const TextStyle(
            color: seedMuted,
            fontSize: 12,
            fontWeight: FontWeight.w500,
          ),
        ),
      ),
    );
  }
}

/// One message line. Each sender gets their own coloured avatar on their side —
/// mine on the trailing edge, the peer's on the leading edge — so at a glance
/// the two sides are two people, not one column of look-alike bubbles.
class _MessageRow extends StatelessWidget {
  const _MessageRow({
    required this.message,
    required this.mine,
    this.peerName,
    this.selfName,
    this.selfPubkey,
  });

  final PrivateMessage message;
  final bool mine;
  final String? peerName;
  final String? selfName;

  /// My own pubkey, so my messages carry my (distinctly coloured) avatar.
  final String? selfPubkey;

  @override
  Widget build(BuildContext context) {
    // Each avatar shows its owner's initial (falling back to a glyph when no
    // name is known) on a colour derived from their pubkey.
    final avatar = mine
        ? PeerAvatar(pubkey: selfPubkey ?? message.fromPubkey, name: selfName)
        : PeerAvatar(pubkey: message.fromPubkey, name: peerName);
    final bubble = Flexible(
      child: _Bubble(message: message, mine: mine),
    );
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisAlignment: mine
            ? MainAxisAlignment.end
            : MainAxisAlignment.start,
        children: mine
            ? [bubble, const SizedBox(width: 8), avatar]
            : [avatar, const SizedBox(width: 8), bubble],
      ),
    );
  }
}

class _Bubble extends StatelessWidget {
  const _Bubble({required this.message, required this.mine});

  final PrivateMessage message;
  final bool mine;

  @override
  Widget build(BuildContext context) {
    final localeCode = Localizations.localeOf(context).languageCode;
    final time = chatBubbleTime(message.at, localeCode);
    return Container(
      margin: const EdgeInsets.symmetric(vertical: 4),
      padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
      constraints: BoxConstraints(
        maxWidth: MediaQuery.of(context).size.width * 0.75,
      ),
      // Soft, light bubbles both ways (dark text): mine a tonal green, the
      // peer's white with a hairline. Sides + avatar colours carry the
      // "who", so the bubbles themselves stay light and easy on the eye.
      decoration: BoxDecoration(
        color: mine ? seedPrimaryContainer : Colors.white,
        borderRadius: BorderRadius.circular(16),
        border: mine ? null : Border.all(color: seedOutline),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // Long text can be selected/copied — useful for addresses, links.
          SelectableText(
            message.text,
            style: const TextStyle(color: seedOnSurface, fontSize: 15),
          ),
          const SizedBox(height: 2),
          // A subtle timestamp, trailing edge (RTL-aware).
          Align(
            alignment: AlignmentDirectional.centerEnd,
            child: Text(
              time,
              style: const TextStyle(color: seedMuted, fontSize: 11),
            ),
          ),
        ],
      ),
    );
  }
}

class _Composer extends StatelessWidget {
  const _Composer({required this.controller, required this.onSend});

  final TextEditingController controller;
  final Future<void> Function() onSend;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    return SafeArea(
      top: false,
      child: Padding(
        padding: const EdgeInsets.fromLTRB(12, 6, 12, 12),
        child: Row(
          children: [
            Expanded(
              child: TextField(
                key: const Key('chat.input'),
                controller: controller,
                textInputAction: TextInputAction.send,
                onSubmitted: (_) => onSend(),
                decoration: InputDecoration(
                  hintText: t.chat.hint,
                  filled: true,
                  fillColor: seedField,
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(24),
                    borderSide: BorderSide.none,
                  ),
                  contentPadding: const EdgeInsets.symmetric(
                    horizontal: 18,
                    vertical: 10,
                  ),
                ),
              ),
            ),
            const SizedBox(width: 8),
            IconButton.filled(
              key: const Key('chat.send'),
              icon: const Icon(Icons.send),
              tooltip: t.chat.send,
              onPressed: onSend,
            ),
          ],
        ),
      ),
    );
  }
}
