import 'dart:async';

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

import '../data/variety_repository.dart';
import '../services/offer_mapper.dart';
import '../services/offer_outbox.dart';
import '../services/social_service.dart';
import '../services/social_settings.dart';

/// State of the offer discovery/publish screen. Transport-agnostic — it holds
/// only what the UI shows, never a relay handle.
class OffersState extends Equatable {
  const OffersState({
    this.offers = const [],
    this.areaGeohash = '',
    this.searching = false,
    this.publishing = false,
    this.hasSearched = false,
    this.error,
  });

  /// Offers discovered so far for [areaGeohash], newest appended as they arrive.
  final List<Offer> offers;

  /// The coarse area currently being browsed.
  final String areaGeohash;

  final bool searching;
  final bool publishing;

  /// True once a discovery has been started, so the UI can tell "no search yet"
  /// from "searched, found nothing".
  final bool hasSearched;

  /// Last error, in human terms for the UI (null when fine).
  final String? error;

  OffersState copyWith({
    List<Offer>? offers,
    String? areaGeohash,
    bool? searching,
    bool? publishing,
    bool? hasSearched,
    String? Function()? error,
  }) {
    return OffersState(
      offers: offers ?? this.offers,
      areaGeohash: areaGeohash ?? this.areaGeohash,
      searching: searching ?? this.searching,
      publishing: publishing ?? this.publishing,
      hasSearched: hasSearched ?? this.hasSearched,
      error: error != null ? error() : this.error,
    );
  }

  @override
  List<Object?> get props =>
      [offers, areaGeohash, searching, publishing, hasSearched, error];
}

/// Drives offer discovery and publishing over an [OfferTransport]. Depends on
/// the interface, not the Nostr backend, so it unit-tests with a fake and the
/// UI stays offline-tolerant (a null transport = the social layer is unavailable
/// and the screen degrades gracefully).
class OffersCubit extends Cubit<OffersState> {
  OffersCubit(this._transport, {Future<void> Function()? onDispose})
      : _onDispose = onDispose,
        super(const OffersState());

  final OfferTransport? _transport;

  /// Closes the owning [SocialSession]/connection when the cubit is disposed
  /// (null in tests, where the transport is a fake with nothing to close).
  final Future<void> Function()? _onDispose;
  StreamSubscription<Offer>? _sub;

  /// Whether a live transport is available (relay configured and reachable).
  bool get isOnline => _transport != null;

  /// Starts (or restarts) discovery for [geohashPrefix]. Results stream in.
  Future<void> discover(String geohashPrefix) async {
    final transport = _transport;
    if (transport == null) {
      emit(state.copyWith(error: () => 'offline', hasSearched: true));
      return;
    }
    await _sub?.cancel();
    emit(OffersState(
      areaGeohash: geohashPrefix,
      searching: true,
      hasSearched: true,
    ));
    _sub = transport.discover(DiscoveryQuery(geohashPrefix: geohashPrefix)).listen(
      (offer) =>
          emit(state.copyWith(offers: [...state.offers, offer], searching: false)),
      onError: (Object e) =>
          emit(state.copyWith(searching: false, error: () => '$e')),
    );
  }

  /// Publishes [offer]; returns the transport's verdict. No-op result when
  /// offline.
  Future<PublishResult> publish(Offer offer) async {
    final transport = _transport;
    if (transport == null) {
      return const PublishResult(accepted: false, transportRef: '', message: 'offline');
    }
    emit(state.copyWith(publishing: true, error: () => null));
    try {
      final result = await transport.publish(offer);
      emit(state.copyWith(
        publishing: false,
        error: () => result.accepted ? null : result.message,
      ));
      return result;
    } catch (e) {
      emit(state.copyWith(publishing: false, error: () => '$e'));
      rethrow;
    }
  }

  /// Publishes the user's [lots] as offers, each tagged with the coarse
  /// [areaGeohash] and signed by [authorPubkeyHex]. Returns how many the relay
  /// accepted. No-op (returns 0) when offline or with no area set.
  Future<int> publishLots(
    List<ShareableLot> lots, {
    required String authorPubkeyHex,
    required String areaGeohash,
  }) async {
    final transport = _transport;
    if (transport == null || areaGeohash.isEmpty || lots.isEmpty) return 0;
    emit(state.copyWith(publishing: true, error: () => null));
    var accepted = 0;
    try {
      for (final lot in lots) {
        final offer = OfferMapper.fromSharedLot(
          lotId: lot.lotId,
          authorPubkeyHex: authorPubkeyHex,
          summary: lot.summary,
          sharing: lot.offerStatus,
          areaGeohash: areaGeohash,
          category: lot.category,
        );
        final result = await transport.publish(offer);
        if (result.accepted) accepted++;
      }
      emit(state.copyWith(publishing: false));
    } catch (e) {
      emit(state.copyWith(publishing: false, error: () => '$e'));
      rethrow;
    }
    return accepted;
  }

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

/// Opens an [OffersCubit] wired to the social layer, or an offline one that
/// degrades gracefully. Local-first: when no relay is configured (or connecting
/// fails), the cubit is created with a null transport and the screen still opens.
Future<OffersCubit> createOffersCubit(
  SocialService social,
  SocialSettings settings,
) async {
  final relays = await settings.relayUrls();
  if (relays.isEmpty) return OffersCubit(null);
  try {
    final session = await social.openSession(relays);
    return OffersCubit(session.offers, onDispose: session.close);
  } catch (_) {
    return OffersCubit(null); // offline / no relay reachable
  }
}

/// Publishes any queued (offline-parked) lots now that we're online, then clears
/// them from the [outbox]. Rebuilds each offer from the lot's CURRENT state, so
/// since-deleted or now-private lots simply drop out. Returns how many published.
Future<int> flushOutbox({
  required OfferOutbox outbox,
  required OffersCubit cubit,
  required List<ShareableLot> shareableLots,
  required String authorPubkeyHex,
  required String areaGeohash,
}) async {
  if (!cubit.isOnline || areaGeohash.isEmpty) return 0;
  final queued = await outbox.pending();
  if (queued.isEmpty) return 0;
  final toPublish =
      shareableLots.where((l) => queued.contains(l.lotId)).toList();
  var published = 0;
  if (toPublish.isNotEmpty) {
    published = await cubit.publishLots(
      toPublish,
      authorPubkeyHex: authorPubkeyHex,
      areaGeohash: areaGeohash,
    );
  }
  // Clear everything attempted (or gone) so we don't loop on it.
  await outbox.remove(queued);
  return published;
}
