import 'dart:async';

import 'package:commons_core/commons_core.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import '../services/message_store.dart';
import '../services/social_service.dart';
import '../services/social_settings.dart';

/// A 1:1 conversation with one peer. Holds the running message list plus send
/// status. Transport-agnostic — depends on [MessageTransport], not the relay.
class ChatState extends Equatable {
  const ChatState({
    this.messages = const [],
    this.sending = false,
    this.error,
  });

  /// Messages in this conversation, oldest first. Incoming ones arrive from the
  /// inbox; our own are appended optimistically on send (NIP-17 wraps don't come
  /// back to the sender).
  final List<PrivateMessage> messages;
  final bool sending;
  final String? error;

  ChatState copyWith({
    List<PrivateMessage>? messages,
    bool? sending,
    String? Function()? error,
  }) =>
      ChatState(
        messages: messages ?? this.messages,
        sending: sending ?? this.sending,
        error: error != null ? error() : this.error,
      );

  @override
  List<Object?> get props => [messages, sending, error];
}

/// Drives a 1:1 chat over a [MessageTransport]. Filters the shared inbox to
/// [peerPubkey], and tags our own sent messages with [selfPubkey] so the UI can
/// tell the two sides apart. Degrades gracefully when offline (null transport).
class MessagesCubit extends Cubit<ChatState> {
  MessagesCubit(
    this._transport, {
    required this.peerPubkey,
    required this.selfPubkey,
    MessageStore? store,
    Future<void> Function()? onDispose,
  })  : _store = store,
        _onDispose = onDispose,
        super(const ChatState());

  final MessageTransport? _transport;
  final MessageStore? _store;
  final String peerPubkey;
  final String selfPubkey;
  final Future<void> Function()? _onDispose;
  StreamSubscription<PrivateMessage>? _sub;

  bool get isOnline => _transport != null;

  /// Subscribes to incoming messages, then loads any saved history. Subscribing
  /// first (before the async history load) avoids dropping an event that arrives
  /// during the load.
  Future<void> start() async {
    final transport = _transport;
    if (transport != null) {
      _sub = transport.inbox().listen(
        (message) async {
          if (message.fromPubkey != peerPubkey) return; // another conversation
          await _store?.append(peerPubkey, message);
          emit(state.copyWith(messages: [...state.messages, message]));
        },
        onError: (Object e) => emit(state.copyWith(error: () => '$e')),
      );
    }
    // History is older than anything that arrives now, so prepend it.
    final history = await _store?.history(peerPubkey);
    if (history != null && history.isNotEmpty) {
      emit(state.copyWith(messages: [...history, ...state.messages]));
    }
  }

  /// Sends [text] to the peer and appends it optimistically.
  Future<void> send(String text) async {
    final transport = _transport;
    final trimmed = text.trim();
    if (transport == null || trimmed.isEmpty) return;
    emit(state.copyWith(sending: true, error: () => null));
    try {
      await transport.send(toPubkey: peerPubkey, text: trimmed);
      final mine = PrivateMessage(
        fromPubkey: selfPubkey,
        text: trimmed,
        at: DateTime.now(),
      );
      await _store?.append(peerPubkey, mine);
      emit(state.copyWith(
        sending: false,
        messages: [...state.messages, mine],
      ));
    } catch (e) {
      emit(state.copyWith(sending: false, error: () => '$e'));
    }
  }

  /// Whether [message] was sent by us (for right-aligned bubbles).
  bool isMine(PrivateMessage message) => message.fromPubkey == selfPubkey;

  @override
  Future<void> close() async {
    await _sub?.cancel();
    await _onDispose?.call();
    return super.close();
  }
}

/// Opens a [MessagesCubit] wired to the social layer for a chat with
/// [peerPubkey], or an offline one that degrades gracefully.
Future<MessagesCubit> createMessagesCubit(
  SocialService social,
  SocialSettings settings, {
  required String peerPubkey,
}) async {
  final relays = await settings.relayUrls();
  if (relays.isEmpty) {
    return MessagesCubit(null,
        peerPubkey: peerPubkey, selfPubkey: social.publicKeyHex);
  }
  try {
    final session = await social.openSession(relays);
    return MessagesCubit(
      session.messages,
      peerPubkey: peerPubkey,
      selfPubkey: social.publicKeyHex,
      onDispose: session.close,
    );
  } catch (_) {
    return MessagesCubit(null,
        peerPubkey: peerPubkey, selfPubkey: social.publicKeyHex);
  }
}
