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

import '../data/variety_repository.dart';
import '../i18n/strings.g.dart';
import '../services/coarse_location.dart';
import '../services/discovery_area.dart';
import '../services/offer_outbox.dart';
import '../services/social_connection.dart';
import '../services/social_service.dart';
import '../services/social_settings.dart';
import '../state/offers_cubit.dart';
import 'edge_fade.dart';
import 'market_widgets.dart';
import 'theme.dart';

/// The market (redesign screens 04/05): discover seeds shared near you. Opens
/// an offer transport lazily; local-first, so it degrades to a "set up sharing"
/// prompt when no relay/area is configured or the network is unreachable.
class MarketScreen extends StatefulWidget {
  const MarketScreen({
    required this.social,
    required this.settings,
    required this.connection,
    this.location,
    this.outbox,
    super.key,
  });

  final SocialService social;
  final SocialSettings settings;

  /// The shared relay connection (one per identity), reused for discovery.
  final SocialConnection connection;

  /// Optional device-location source for the "use my location" shortcut; when
  /// null (tests, or a platform without it) the shortcut is hidden.
  final CoarseLocationProvider? location;

  /// Optional offline outbox: parks shares attempted with no network and flushes
  /// them on reconnect. Null in tests → sharing offline is simply a no-op.
  final OfferOutbox? outbox;

  @override
  State<MarketScreen> createState() => _MarketScreenState();
}

class _MarketScreenState extends State<MarketScreen> {
  OffersCubit? _cubit;
  bool _loading = true;
  bool _hasArea = false;

  @override
  void initState() {
    super.initState();
    _init();
  }

  Future<void> _init() async {
    // Only needed to flush the outbox; read here (while mounted) to avoid using
    // context across the awaits below. Null when there's no outbox (e.g. tests).
    final repo =
        widget.outbox != null ? context.read<VarietyRepository>() : null;
    setState(() => _loading = true);
    final cubit =
        await createOffersCubit(widget.connection, repository: repo);
    final area = await widget.settings.areaGeohash();
    if (!mounted) {
      await cubit.close();
      return;
    }
    final previous = _cubit;
    setState(() {
      _cubit = cubit;
      _hasArea = area.isNotEmpty;
      _loading = false;
    });
    await previous?.close();
    if (area.isNotEmpty && cubit.isOnline) {
      // Flush anything parked while offline, then show what's out there.
      final outbox = widget.outbox;
      if (outbox != null && repo != null) {
        await flushOutbox(
          outbox: outbox,
          cubit: cubit,
          shareableLots: await repo.shareableLots(),
          authorPubkeyHex: widget.social.publicKeyHex,
          areaGeohash: area,
        );
      }
      if (mounted) {
        final precision = await widget.settings.searchPrecision();
        if (mounted) await cubit.discover(searchPrefix(area, precision));
      }
    }
  }

  Future<void> _openConfig() async {
    final changed = await showModalBottomSheet<bool>(
      context: context,
      isScrollControlled: true,
      builder: (_) =>
          _ConfigSheet(settings: widget.settings, location: widget.location),
    );
    if (changed == true) await _init();
  }

  /// Publishes the user's shared lots as offers to the network, tagged with the
  /// coarse area. Prompts for setup when the area isn't set.
  Future<void> _shareMine() async {
    final cubit = _cubit;
    if (cubit == null) return;
    final repo = context.read<VarietyRepository>();
    final messenger = ScaffoldMessenger.of(context);
    final t = context.t;
    final area = await widget.settings.areaGeohash();
    if (!mounted) return;
    if (area.isEmpty) {
      await _openConfig();
      return;
    }
    final lots = await repo.shareableLots();
    if (!mounted) return;
    if (lots.isEmpty) {
      messenger.showSnackBar(SnackBar(content: Text(t.market.nothingToShare)));
      return;
    }
    final ids = lots.map((l) => l.lotId).toList();

    // Offline: park it and tell the person we'll share it later.
    if (!cubit.isOnline) {
      await widget.outbox?.enqueue(ids);
      if (!mounted) return;
      messenger.showSnackBar(SnackBar(content: Text(t.market.queued)));
      return;
    }

    final count = await cubit.publishLots(
      lots,
      authorPubkeyHex: widget.social.publicKeyHex,
      areaGeohash: area,
    );
    if (!mounted) return;
    await widget.outbox?.remove(ids); // published now, so unpark any duplicates
    // count == 0 with lots to share means every relay refused — say so plainly
    // rather than "shared 0", which reads like success.
    messenger.showSnackBar(SnackBar(
      content: Text(count == 0 ? t.market.shareFailed : t.market.sharedCount(n: count)),
    ));
    final precision = await widget.settings.searchPrecision();
    if (!mounted) return;
    await cubit.discover(searchPrefix(area, precision)); // refresh incl. just-shared
  }

  @override
  void dispose() {
    _cubit?.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    final cubit = _cubit;
    return Scaffold(
      appBar: AppBar(
        title: Text(t.market.title),
        actions: [
          IconButton(
            key: const Key('market.config'),
            icon: const Icon(Icons.tune),
            tooltip: t.market.configTitle,
            onPressed: _openConfig,
          ),
        ],
      ),
      floatingActionButton:
          cubit != null && (cubit.isOnline || widget.outbox != null)
              ? FloatingActionButton.extended(
                  key: const Key('market.shareMine'),
                  onPressed: _shareMine,
                  icon: const Icon(Icons.campaign_outlined),
                  label: Text(t.market.shareMine),
                )
              : null,
      body: _loading || cubit == null
          ? const Center(child: CircularProgressIndicator())
          : BlocProvider.value(
              value: cubit,
              child: MarketBody(
                onConfigure: _openConfig,
                onRetry: _init,
                hasArea: _hasArea,
                selfPubkey: widget.social.publicKeyHex,
              ),
            ),
    );
  }
}

class MarketBody extends StatelessWidget {
  const MarketBody({
    required this.onConfigure,
    this.onRetry,
    this.hasArea = true,
    this.selfPubkey,
    super.key,
  });

  final VoidCallback onConfigure;

  /// Re-opens the connection (for the "can't reach servers" retry).
  final VoidCallback? onRetry;

  /// Whether the user has set their coarse area yet.
  final bool hasArea;

  /// This user's own pubkey, so we don't offer to message our own listings.
  final String? selfPubkey;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    return BlocBuilder<OffersCubit, OffersState>(
      builder: (context, state) {
        if (!context.read<OffersCubit>().isOnline) {
          // Default community servers exist, so "offline" means unreachable —
          // offer a retry rather than "set up sharing".
          return _EmptyState(
            icon: Icons.wifi_tethering_off_outlined,
            title: t.market.cantReach,
            body: t.market.cantReachBody,
            actionLabel: onRetry != null ? t.market.retry : null,
            onAction: onRetry,
          );
        }
        if (!hasArea) {
          return _EmptyState(
            icon: Icons.place_outlined,
            title: t.market.setArea,
            body: t.market.setAreaBody,
            actionLabel: t.market.setUp,
            onAction: onConfigure,
          );
        }
        if (state.searching && state.offers.isEmpty) {
          return Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                const CircularProgressIndicator(),
                const SizedBox(height: 16),
                Text(t.market.searching,
                    style: const TextStyle(color: seedMuted)),
              ],
            ),
          );
        }
        final cubit = context.read<OffersCubit>();
        // Pull-to-refresh re-runs discovery for the current area (keeping the
        // text filter), so the list is never stuck on a stale scan.
        Future<void> refresh() => cubit.discover(state.areaGeohash);

        // Nothing discovered at all: keep the friendly empty state, but make it
        // pull-to-refreshable so a swipe re-scans.
        if (state.offers.isEmpty) {
          return RefreshIndicator(
            onRefresh: refresh,
            child: ListView(
              // AlwaysScrollable so the pull gesture works with a single child.
              physics: const AlwaysScrollableScrollPhysics(),
              children: [
                SizedBox(
                  height: MediaQuery.of(context).size.height * 0.6,
                  child: _EmptyState(
                    icon: Icons.grass_outlined,
                    title: t.market.empty,
                    body: '',
                  ),
                ),
              ],
            ),
          );
        }

        final visible = state.visibleOffers;
        return Column(
          children: [
            Padding(
              padding: const EdgeInsets.fromLTRB(12, 12, 12, 4),
              child: TextField(
                key: const Key('market.search'),
                decoration: InputDecoration(
                  hintText: t.market.searchHint,
                  prefixIcon: const Icon(Icons.search, color: seedMuted),
                  hintStyle: const TextStyle(color: seedMuted),
                  filled: true,
                  fillColor: Colors.white,
                  isDense: true,
                  contentPadding: const EdgeInsets.symmetric(vertical: 14),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(28),
                    borderSide: BorderSide.none,
                  ),
                ),
                onChanged: cubit.search,
              ),
            ),
            _MarketFilterBar(state: state),
            Expanded(
              child: RefreshIndicator(
                onRefresh: refresh,
                child: visible.isEmpty
                    // Discovered offers exist, but the search filtered them out.
                    ? ListView(
                        physics: const AlwaysScrollableScrollPhysics(),
                        children: [
                          SizedBox(
                            height: MediaQuery.of(context).size.height * 0.5,
                            child: _EmptyState(
                              icon: Icons.search_off,
                              title: t.market.noMatches,
                              body: '',
                            ),
                          ),
                        ],
                      )
                    : ListView.separated(
                        physics: const AlwaysScrollableScrollPhysics(),
                        padding: const EdgeInsets.all(16),
                        itemCount: visible.length,
                        separatorBuilder: (_, _) => const SizedBox(height: 12),
                        itemBuilder: (context, i) {
                          final o = visible[i];
                          final mine = o.authorPubkeyHex == selfPubkey;
                          return _OfferCard(
                            offer: o,
                            mine: mine,
                            onTap: () =>
                                context.push('/market/offer', extra: o),
                          );
                        },
                      ),
              ),
            ),
          ],
        );
      },
    );
  }
}

/// One discovered offer. Human words for the reciprocity mode; coarse "near you"
/// instead of any precise location; price only when it's a sale.
class _OfferCard extends StatelessWidget {
  const _OfferCard({required this.offer, this.mine = false, this.onTap});

  final Offer offer;

  /// Whether this is the current user's own listing (shown with a "You" badge).
  final bool mine;

  /// Opens the offer's detail ("product") page.
  final VoidCallback? onTap;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    final radius = BorderRadius.circular(14);
    return Material(
      color: Colors.white,
      borderRadius: radius,
      child: InkWell(
        key: Key('market.offer.${offer.authorPubkeyHex}.${offer.id}'),
        onTap: onTap,
        borderRadius: radius,
        child: Container(
          decoration: BoxDecoration(
            borderRadius: radius,
            border: Border.all(color: seedOutline),
          ),
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  if (offer.imageUrl != null) ...[
                    OfferThumbnail(
                      url: offer.imageUrl!,
                      semanticLabel: t.market.photo,
                    ),
                    const SizedBox(width: 12),
                  ],
                  Expanded(
                    child: Text(
                      offer.summary,
                      style: const TextStyle(
                        fontSize: 17,
                        fontWeight: FontWeight.w500,
                        color: seedOnSurface,
                      ),
                    ),
                  ),
                  if (mine) ...[
                    Container(
                      padding: const EdgeInsets.symmetric(
                          horizontal: 8, vertical: 3),
                      decoration: BoxDecoration(
                        color: seedGreen,
                        borderRadius: BorderRadius.circular(20),
                      ),
                      child: Text(
                        t.market.mine,
                        style: const TextStyle(
                          color: Colors.white,
                          fontSize: 11,
                          fontWeight: FontWeight.w700,
                        ),
                      ),
                    ),
                    const SizedBox(width: 6),
                  ],
                  OfferTypeChip(label: offerTypeLabel(t, offer.type)),
                ],
              ),
              const SizedBox(height: 6),
              Row(
                children: [
                  const Icon(Icons.place_outlined, size: 15, color: seedMuted),
                  const SizedBox(width: 4),
                  Text(t.market.near,
                      style: const TextStyle(color: seedMuted, fontSize: 13)),
                  if (offer.isOrganic) ...[
                    const SizedBox(width: 10),
                    const Icon(Icons.eco, size: 15, color: seedGreen),
                  ],
                  const Spacer(),
                  if (offer.type == OfferType.sale && offer.priceAmount != null)
                    Text(
                      '${offer.priceAmount} ${offer.priceCurrency ?? ''}'.trim(),
                      style: const TextStyle(
                        color: seedOnSurface,
                        fontWeight: FontWeight.w600,
                      ),
                    ),
                  const SizedBox(width: 4),
                  const Icon(Icons.chevron_right, size: 18, color: seedMuted),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

/// Optional filters over the discovered offers, mirroring the inventory filter
/// bar: reciprocity mode (give/swap/sell/wanted), category, and the eco flag.
/// Each chip group only appears when some discovered offer can match it.
class _MarketFilterBar extends StatelessWidget {
  const _MarketFilterBar({required this.state});

  final OffersState state;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    final cubit = context.read<OffersCubit>();
    final categories = state.categories;
    // Only offer type chips for modes some discovered offer actually holds.
    final types = <OfferType>[
      for (final type in const [
        OfferType.gift,
        OfferType.exchange,
        OfferType.sale,
        OfferType.wanted,
      ])
        if (state.offers.any((o) => o.type == type)) type,
    ];
    final hasOrganic = state.hasOrganic;
    if (categories.isEmpty && types.isEmpty && !hasOrganic) {
      return const SizedBox.shrink();
    }

    final chips = <Widget>[
      if (hasOrganic)
        FilterChip(
          key: const Key('market.filter.organic'),
          avatar: Icon(
            Icons.eco,
            size: 18,
            color: state.organicOnly ? Colors.white : seedGreen,
          ),
          label: Text(t.editVariety.organic),
          selected: state.organicOnly,
          onSelected: (_) => cubit.toggleOrganicOnly(),
        ),
      for (final type in types)
        FilterChip(
          key: Key('market.filter.type.${type.name}'),
          label: Text(offerTypeLabel(t, type)),
          selected: state.typeFilter.contains(type),
          onSelected: (_) => cubit.toggleType(type),
        ),
      for (final category in categories)
        FilterChip(
          key: Key('market.filter.category.$category'),
          label: Text(category),
          selected: state.categoryFilter.contains(category),
          onSelected: (_) => cubit.toggleCategory(category),
        ),
    ];

    return EdgeFade(
      child: SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
        child: Row(
          children: [
            for (final chip in chips)
              Padding(
                padding: const EdgeInsetsDirectional.only(end: 8),
                child: chip,
              ),
            if (state.hasActiveFilter)
              TextButton.icon(
                key: const Key('market.filter.clear'),
                onPressed: cubit.clearFilters,
                icon: const Icon(Icons.clear, size: 18),
                label: Text(t.inventory.clearFilters),
              ),
          ],
        ),
      ),
    );
  }
}

class _EmptyState extends StatelessWidget {
  const _EmptyState({
    required this.icon,
    required this.title,
    required this.body,
    this.actionLabel,
    this.onAction,
  });

  final IconData icon;
  final String title;
  final String body;
  final String? actionLabel;
  final VoidCallback? onAction;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(32),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(icon, size: 56, color: seedMuted),
            const SizedBox(height: 16),
            Text(
              title,
              textAlign: TextAlign.center,
              style: const TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.w500,
                color: seedOnSurface,
              ),
            ),
            if (body.isNotEmpty) ...[
              const SizedBox(height: 8),
              Text(
                body,
                textAlign: TextAlign.center,
                style: const TextStyle(color: seedMuted, fontSize: 14),
              ),
            ],
            if (actionLabel != null && onAction != null) ...[
              const SizedBox(height: 20),
              FilledButton(onPressed: onAction, child: Text(actionLabel!)),
            ],
          ],
        ),
      ),
    );
  }
}

/// Coarse-area + community-server setup. Kept behind progressive disclosure — a
/// power-user surface — with human-worded labels.
class _ConfigSheet extends StatefulWidget {
  const _ConfigSheet({required this.settings, this.location});

  final SocialSettings settings;
  final CoarseLocationProvider? location;

  @override
  State<_ConfigSheet> createState() => _ConfigSheetState();
}

class _ConfigSheetState extends State<_ConfigSheet> {
  final _area = TextEditingController();
  final _servers = TextEditingController();
  bool _loading = true;
  bool _locationBusy = false;
  String? _locationError;
  int _precision = SocialSettings.defaultSearchPrecision;

  @override
  void initState() {
    super.initState();
    _load();
  }

  Future<void> _load() async {
    _area.text = await widget.settings.areaGeohash();
    _servers.text = (await widget.settings.relayUrls()).join('\n');
    _precision = await widget.settings.searchPrecision();
    if (mounted) setState(() => _loading = false);
  }

  Future<void> _save() async {
    await widget.settings.setAreaGeohash(_area.text);
    await widget.settings.setRelayUrls(_servers.text.split('\n'));
    await widget.settings.setSearchPrecision(_precision);
    if (mounted) Navigator.of(context).pop(true);
  }

  /// Fills the area from the device's approximate location, reduced to a coarse
  /// geohash so no precise fix is stored.
  Future<void> _useLocation() async {
    final provider = widget.location;
    if (provider == null || _locationBusy) return;
    setState(() {
      _locationBusy = true;
      _locationError = null;
    });
    final loc = await provider.currentCoarseLatLon();
    if (!mounted) return;
    setState(() {
      _locationBusy = false;
      if (loc == null) {
        _locationError = context.t.market.locationFailed;
      } else {
        _area.text = Geohash.encode(loc.lat, loc.lon, precision: 5);
      }
    });
  }

  @override
  void dispose() {
    _area.dispose();
    _servers.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    final hasArea = _area.text.trim().isNotEmpty;
    return Padding(
      padding: EdgeInsets.only(
        left: 20,
        right: 20,
        top: 20,
        bottom: MediaQuery.of(context).viewInsets.bottom + 20,
      ),
      child: _loading
          ? const SizedBox(
              height: 120, child: Center(child: CircularProgressIndicator()))
          : SingleChildScrollView(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  Text(
                    t.market.configTitle,
                    style: const TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.w600,
                      color: seedTitle,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    t.market.setupIntro,
                    style: const TextStyle(
                        color: seedMuted, fontSize: 13, height: 1.4),
                  ),
                  const SizedBox(height: 18),
                  // Setting your area from device location is the human path;
                  // typing a code is the advanced fallback.
                  if (widget.location != null)
                    FilledButton.tonalIcon(
                      key: const Key('market.useLocation'),
                      onPressed: _locationBusy ? null : _useLocation,
                      icon: _locationBusy
                          ? const SizedBox(
                              width: 16,
                              height: 16,
                              child: CircularProgressIndicator(strokeWidth: 2),
                            )
                          : const Icon(Icons.my_location, size: 18),
                      label: Text(t.market.useLocation),
                    ),
                  if (_locationError != null)
                    Padding(
                      padding: const EdgeInsets.only(top: 6),
                      child: Text(
                        _locationError!,
                        style: const TextStyle(
                            color: Color(0xFFB3261E), fontSize: 12),
                      ),
                    ),
                  const SizedBox(height: 12),
                  Row(
                    children: [
                      Icon(
                        hasArea
                            ? Icons.check_circle
                            : Icons.pending_outlined,
                        size: 18,
                        color: hasArea ? seedGreen : seedMuted,
                      ),
                      const SizedBox(width: 8),
                      Expanded(
                        child: Text(
                          hasArea ? t.market.areaSet : t.market.areaNotSet,
                          style: TextStyle(
                            color: hasArea ? seedOnSurface : seedMuted,
                            fontSize: 13,
                          ),
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 16),
                  Align(
                    alignment: AlignmentDirectional.centerStart,
                    child: Text(
                      t.market.rangeLabel,
                      style: const TextStyle(fontSize: 13, color: seedMuted),
                    ),
                  ),
                  const SizedBox(height: 8),
                  SegmentedButton<int>(
                    key: const Key('market.range'),
                    segments: [
                      ButtonSegment(value: 5, label: Text(t.market.rangeNear)),
                      ButtonSegment(value: 4, label: Text(t.market.rangeArea)),
                      ButtonSegment(value: 3, label: Text(t.market.rangeRegion)),
                    ],
                    selected: {_precision},
                    showSelectedIcon: false,
                    onSelectionChanged: (s) =>
                        setState(() => _precision = s.first),
                  ),
                  const SizedBox(height: 8),
                  Theme(
                    data: Theme.of(context)
                        .copyWith(dividerColor: Colors.transparent),
                    child: ExpansionTile(
                      key: const Key('market.advanced'),
                      tilePadding: EdgeInsets.zero,
                      childrenPadding: const EdgeInsets.only(bottom: 8),
                      title: Text(
                        t.market.advanced,
                        style: const TextStyle(fontSize: 13, color: seedMuted),
                      ),
                      children: [
                        TextField(
                          key: const Key('market.area'),
                          controller: _area,
                          onChanged: (_) => setState(() {}),
                          decoration: InputDecoration(
                            labelText: t.market.areaCodeLabel,
                            helperText: t.market.areaCodeHint,
                            helperMaxLines: 2,
                          ),
                        ),
                        const SizedBox(height: 14),
                        TextField(
                          key: const Key('market.servers'),
                          controller: _servers,
                          minLines: 1,
                          maxLines: 3,
                          decoration: InputDecoration(
                            labelText: t.market.serversLabel,
                            helperText: t.market.serversHelp,
                            helperMaxLines: 2,
                          ),
                        ),
                      ],
                    ),
                  ),
                  const SizedBox(height: 20),
                  FilledButton(
                    key: const Key('market.save'),
                    onPressed: _save,
                    child: Text(t.market.save),
                  ),
                ],
              ),
            ),
    );
  }
}
