import 'dart:async';

import 'package:commons_core/commons_core.dart';
import 'package:flutter/foundation.dart';

import '../i18n/strings.g.dart';
import 'message_store.dart';
import 'notification_service.dart';
import 'profile_cache.dart';
import 'social_connection.dart';
import 'social_service.dart' show SocialSession;
import 'unread_service.dart';

/// App-wide inbox listener for private messages (NIP-17).
///
/// Without this, a message only arrived while its specific chat screen was open
/// — so a first message from a new peer was invisible. This keeps one inbox
/// subscription over the SHARED [SocialConnection] for the whole app while
/// online: it persists every incoming message to the [MessageStore], updates
/// unread counts, fires an OS notification, and fires [changes] so the inbox
/// list refreshes. It re-subscribes automatically each time the connection
/// reconnects (and stops when it drops), because it listens to the connection's
/// session stream rather than owning a socket itself.
///
/// Foreground only — background/push delivery is a later concern.
class InboxService {
  InboxService({
    required SocialConnection connection,
    required String selfPubkey,
    required MessageStore store,
    ProfileCache? profileCache,
    UnreadService? unread,
    NotificationService? notifications,
  })  : _connection = connection,
        _selfPubkey = selfPubkey,
        _store = store,
        _profileCache = profileCache,
        _unread = unread,
        _notifications = notifications;

  final SocialConnection _connection;
  final String _selfPubkey;
  final MessageStore _store;
  final ProfileCache? _profileCache;
  final UnreadService? _unread;
  final NotificationService? _notifications;

  final _changes = StreamController<void>.broadcast();
  StreamSubscription<SocialSession?>? _sessionsSub;
  StreamSubscription<PrivateMessage>? _inboxSub;
  SocialSession? _session;
  bool _started = false;

  /// Fires (no payload) after a NEW message is persisted — the inbox list
  /// listens to reload. Broadcast, so several screens can listen.
  Stream<void> get changes => _changes.stream;

  /// Begins listening. Subscribe to the connection's sessions BEFORE the
  /// connection starts connecting, so the first session is caught too.
  void start() {
    if (_started) return;
    _started = true;
    _sessionsSub = _connection.sessions.listen(_onSession);
    // In case the connection is already up (a screen connected first).
    final current = _connection.current;
    if (current != null) _onSession(current);
  }

  /// (Re)binds the inbox subscription to [session] — the connection handing us a
  /// fresh session (reconnect) or null (dropped).
  void _onSession(SocialSession? session) {
    if (identical(session, _session) && _inboxSub != null) return;
    unawaited(_inboxSub?.cancel());
    _inboxSub = null;
    _session = session;
    if (session != null) {
      _inboxSub = session.messages.inbox().listen(
            ingest,
            onError: (_) {}, // drop handled by the connection's reconnect
          );
    }
  }

  /// Persists one incoming [message] (deduped) and, when new, fires [changes],
  /// updates unread, notifies, and best-effort caches the sender's name. A
  /// testable seam (no relay).
  @visibleForTesting
  Future<void> ingest(PrivateMessage message) async {
    final stored = await _store.append(message.fromPubkey, message);
    if (!stored) return; // a re-delivered duplicate
    if (!_changes.isClosed) _changes.add(null);

    final peer = message.fromPubkey;
    // Defensive: never badge or notify for your own message. NIP-17 doesn't
    // loop a sent gift wrap back to its author; the guard keeps the seam honest.
    if (peer != _selfPubkey) {
      await _unread?.onMessageReceived(peer, message);
      await _maybeNotify(peer);
    }
    await _cacheName(peer);
  }

  /// Fires a text-free OS notification for a new message from [peer], unless its
  /// chat is the one already on screen (then the message is visible anyway).
  Future<void> _maybeNotify(String peer) async {
    final notifications = _notifications;
    if (notifications == null) return;
    if (_unread?.activePeer == peer) return;
    final name = await _profileCache?.name(peer) ?? shortPubkey(peer);
    await notifications.showMessage(
      peerPubkey: peer,
      title: t.notifications.newMessageFrom(name: name),
    );
  }

  Future<void> _cacheName(String peerPubkey) async {
    final cache = _profileCache;
    final session = _session;
    if (cache == null || session == null) return;
    if (await cache.name(peerPubkey) != null) return; // already known
    try {
      final profile = await session.profile.fetch(peerPubkey);
      if (profile != null && profile.name.isNotEmpty) {
        await cache.setName(peerPubkey, profile.name);
        if (!_changes.isClosed) _changes.add(null); // re-render with the name
      }
    } catch (_) {
      // best effort — the short key shows meanwhile
    }
  }

  Future<void> stop() async {
    _started = false;
    await _sessionsSub?.cancel();
    _sessionsSub = null;
    await _inboxSub?.cancel();
    _inboxSub = null;
    _session = null;
    if (!_changes.isClosed) await _changes.close();
  }
}
