import 'dart:async';
import 'dart:typed_data';

import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:url_launcher/url_launcher.dart';

import '../data/seed_saving_catalog.dart';
import '../data/species_repository.dart';
import '../data/variety_repository.dart';
import '../db/enums.dart';
import '../domain/crop_calendar.dart';
import '../domain/seed_saving.dart';
import '../domain/seed_viability.dart';
import '../domain/species_reference_links.dart';
import '../i18n/strings.g.dart';
import '../state/variety_detail_cubit.dart';
import 'harvest_date_picker.dart';
import 'photo_pick.dart';
import 'plantare_sheet.dart';
import 'quantity_kind_l10n.dart';
import 'sale_sheet.dart';
import 'quantity_picker.dart';
import 'seed_glyph.dart';
import 'theme.dart';

/// Read + edit view of a single variety: its photo, category, notes, other
/// names and lots. Edits go through [VarietyDetailCubit]; the view is reactive.
class VarietyDetailScreen extends StatelessWidget {
  const VarietyDetailScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocConsumer<VarietyDetailCubit, VarietyDetailState>(
      listenWhen: (prev, curr) => !prev.deleted && curr.deleted,
      listener: (context, state) => Navigator.of(context).maybePop(),
      builder: (context, state) {
        if (state.loading) {
          return const Scaffold(
            body: Center(child: CircularProgressIndicator()),
          );
        }
        final detail = state.detail;
        if (detail == null) {
          return Scaffold(
            appBar: AppBar(),
            body: Center(child: Text(context.t.detail.notFound)),
          );
        }
        return _DetailView(detail: detail);
      },
    );
  }
}

class _DetailView extends StatelessWidget {
  const _DetailView({required this.detail});

  final VarietyDetail detail;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    final cubit = context.read<VarietyDetailCubit>();
    return Scaffold(
      appBar: AppBar(
        title: Text(detail.label),
        actions: [
          IconButton(
            key: const Key('detail.edit'),
            icon: const Icon(Icons.edit_outlined),
            tooltip: t.common.edit,
            onPressed: () => _showEditSheet(
              context,
              cubit,
              detail,
              context.read<SpeciesRepository>(),
            ),
          ),
          IconButton(
            key: const Key('detail.delete'),
            icon: const Icon(Icons.delete_outline),
            tooltip: t.common.delete,
            onPressed: () => _confirmDelete(context, cubit),
          ),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _DetailHeader(detail: detail),
          const SizedBox(height: 16),
          _SectionTitle(t.detail.names),
          Wrap(
            spacing: 8,
            runSpacing: 4,
            crossAxisAlignment: WrapCrossAlignment.center,
            children: [
              for (final name in detail.vernacularNames)
                InputChip(
                  key: Key('name.${name.id}'),
                  label: Text(name.name),
                  deleteIcon: const Icon(Icons.close, size: 18),
                  onDeleted: () => cubit.removeVernacularName(name.id),
                ),
              ActionChip(
                key: const Key('name.add'),
                avatar: const Icon(Icons.add, size: 18),
                label: Text(t.detail.addName),
                onPressed: () => _showAddNameDialog(context, cubit),
              ),
            ],
          ),
          const SizedBox(height: 16),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: [
              OutlinedButton.icon(
                key: const Key('detail.addPlantare'),
                icon: const Icon(Icons.volunteer_activism_outlined, size: 18),
                onPressed: () => showPlantareSheet(
                  context,
                  repository: context.read<VarietyRepository>(),
                  varietyId: detail.id,
                ),
                label: Text(t.plantare.add),
              ),
              OutlinedButton.icon(
                key: const Key('detail.addSale'),
                icon: const Icon(Icons.sell_outlined, size: 18),
                onPressed: () => showSaleSheet(
                  context,
                  repository: context.read<VarietyRepository>(),
                  varietyId: detail.id,
                ),
                label: Text(t.sale.add),
              ),
            ],
          ),
          if (detail.notes != null && detail.notes!.trim().isNotEmpty) ...[
            const SizedBox(height: 16),
            _SectionTitle(t.detail.notes),
            Text(detail.notes!),
          ],
          if (detail.hasCropCalendar) ...[
            const SizedBox(height: 16),
            _SectionTitle(t.cropCalendar.title),
            _CropCalendarView(detail: detail),
          ],
          if (SeedSavingCatalog.guideFor(
                scientificName: detail.scientificName,
                family: detail.family ?? detail.category,
              ) case final guide? when guide.hasAny) ...[
            const SizedBox(height: 16),
            _SectionTitle(t.seedSaving.title),
            _SeedSavingView(guide: guide),
          ],
          if (_referenceLinks(context, detail) case final references
              when references.isNotEmpty) ...[
            const SizedBox(height: 16),
            _SectionTitle(t.detail.reference),
            Wrap(
              spacing: 8,
              runSpacing: 4,
              children: [
                for (final ref in references)
                  ActionChip(
                    key: Key('ref.${ref.source.name}'),
                    avatar: const Icon(Icons.open_in_new, size: 18),
                    label: Text(_referenceLabel(t, ref.source)),
                    onPressed: () => _openUrl(ref.url),
                  ),
              ],
            ),
          ],
          // The grower's own links: a rarer power move, so kept subtle — the
          // section only appears once there are links, and adding one lives
          // behind a low-emphasis button rather than a prominent chip.
          if (detail.links.isNotEmpty) ...[
            const SizedBox(height: 16),
            _SectionTitle(t.detail.links),
            Wrap(
              spacing: 8,
              runSpacing: 4,
              children: [
                for (final link in detail.links)
                  InputChip(
                    key: Key('link.${link.id}'),
                    avatar: const Icon(Icons.link, size: 18),
                    label: Text(link.display),
                    deleteIcon: const Icon(Icons.close, size: 18),
                    onDeleted: () => cubit.removeExternalLink(link.id),
                    onPressed: () => _openUrl(link.url),
                  ),
              ],
            ),
          ],
          Align(
            alignment: AlignmentDirectional.centerStart,
            child: TextButton.icon(
              key: const Key('link.add'),
              onPressed: () => _showAddLinkDialog(context, cubit),
              icon: const Icon(Icons.add_link, size: 18),
              label: Text(t.detail.addLink),
            ),
          ),
          const SizedBox(height: 16),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              _SectionTitle(t.detail.lots),
              TextButton.icon(
                key: const Key('detail.addLot'),
                onPressed: () => _showLotSheet(context, cubit),
                icon: const Icon(Icons.add),
                label: Text(t.detail.addLot),
              ),
            ],
          ),
          if (detail.lots.isEmpty)
            Text(t.detail.noLots)
          else
            for (final lot in detail.lots)
              _LotTile(
                cubit: cubit,
                lot: lot,
                viabilityYears: detail.viabilityYears,
              ),
        ],
      ),
    );
  }
}

String _lotSubtitle(Translations t, VarietyLot lot) {
  final parts = <String>[
    if (lot.harvestYear != null)
      harvestDateLabel(t, year: lot.harvestYear!, month: lot.harvestMonth)
    else
      t.detail.noYear,
    if (lot.quantity != null)
      quantityDisplay(t, lot.quantity!)
    else if (lot.abundance != null)
      abundanceLabel(t, lot.abundance!),
    if (lot.presentation != null) presentationLabel(t, lot.presentation!),
    if (lot.offerStatus != OfferStatus.private)
      shareStatusLabel(t, lot.offerStatus),
  ];
  return parts.join(' · ');
}

/// The provenance line for a lot ("from · place"), or null when neither is set.
String? _lotOrigin(VarietyLot lot) {
  final parts = <String>[?lot.originName, ?lot.originPlace];
  return parts.isEmpty ? null : parts.join(' · ');
}

String _germinationPercent(Translations t, double rate) =>
    t.germination.result(percent: (rate * 100).round());

/// One lot row: kind, harvest/quantity summary, storage location, an aging
/// (viability) warning for seed lots, and the germination test affordance.
class _LotTile extends StatelessWidget {
  const _LotTile({
    required this.cubit,
    required this.lot,
    required this.viabilityYears,
  });

  final VarietyDetailCubit cubit;
  final VarietyLot lot;

  /// Typical seed longevity of the variety's species (years), or null.
  final int? viabilityYears;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    // Aging only makes sense for seed lots (living material is not stored dry).
    final viability = lot.type == LotType.seed
        ? seedViability(
            harvestYear: lot.harvestYear,
            viabilityYears: viabilityYears,
            currentYear: DateTime.now().year,
          )
        : SeedViability.unknown;
    final warning = _ViabilityWarning.forStatus(viability, viabilityYears);

    return ListTile(
      key: Key('lot.${lot.id}'),
      contentPadding: EdgeInsets.zero,
      onTap: () => _showLotSheet(context, cubit, existing: lot),
      leading: lot.quantity == null
          ? const Icon(Icons.inventory_2_outlined)
          : QuantityKindIcon(lot.quantity!.kind, size: 28),
      title: Row(
        children: [
          _LotTypeChip(type: lot.type),
          const SizedBox(width: 8),
          Expanded(child: Text(_lotSubtitle(t, lot))),
        ],
      ),
      subtitle:
          (lot.storageLocation == null &&
              warning == null &&
              _lotOrigin(lot) == null)
          ? null
          : Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (_lotOrigin(lot) case final origin?)
                  Row(
                    children: [
                      const Icon(Icons.place_outlined, size: 14),
                      const SizedBox(width: 4),
                      Expanded(child: Text(origin)),
                    ],
                  ),
                if (lot.storageLocation case final loc?) Text(loc),
                ?warning,
              ],
            ),
      // Germination only applies to seed lots, not to plantel.
      trailing: lot.type != LotType.seed
          ? null
          : Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                if (lot.latestGerminationRate != null)
                  _GerminationBadge(rate: lot.latestGerminationRate!),
                IconButton(
                  key: Key('lot.germination.${lot.id}'),
                  icon: const Icon(Icons.grass_outlined),
                  tooltip: t.germination.title,
                  onPressed: () => _showGerminationSheet(context, cubit, lot),
                ),
              ],
            ),
    );
  }
}

/// An inline "sow/reproduce this seed soon" line for an aging seed lot. Renders
/// nothing (returns null) when the lot is fresh or its age can't be judged.
class _ViabilityWarning extends StatelessWidget {
  const _ViabilityWarning._({
    required this.status,
    required this.viabilityYears,
  });

  static _ViabilityWarning? forStatus(SeedViability status, int? years) {
    if (status != SeedViability.expiringSoon &&
        status != SeedViability.expired) {
      return null;
    }
    return _ViabilityWarning._(status: status, viabilityYears: years);
  }

  final SeedViability status;
  final int? viabilityYears;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    final scheme = Theme.of(context).colorScheme;
    final expired = status == SeedViability.expired;
    final color = expired ? scheme.error : const Color(0xFFB26A00);
    final years = viabilityYears;
    final text = expired
        ? (years == null
              ? t.viability.expired
              : t.viability.expiredYears(years: years))
        : (years == null
              ? t.viability.expiringSoon
              : t.viability.expiringSoonYears(years: years));
    return Padding(
      key: Key('lot.viability.${expired ? 'expired' : 'soon'}'),
      padding: const EdgeInsets.only(top: 2),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(
            expired ? Icons.error_outline : Icons.schedule,
            size: 14,
            color: color,
          ),
          const SizedBox(width: 4),
          Flexible(
            child: Text(
              text,
              style: TextStyle(
                color: color,
                fontSize: 12,
                fontWeight: FontWeight.w600,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

/// A small pill making a lot's kind (seeds vs plants/plantel) unmistakable.
class _LotTypeChip extends StatelessWidget {
  const _LotTypeChip({required this.type});

  final LotType type;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    final isSeed = type == LotType.seed;
    // Seed lots read green; living/vegetative forms read teal.
    final color = isSeed ? seedGreenDark : const Color(0xFF00796B);
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
      decoration: BoxDecoration(
        color: color.withValues(alpha: 0.12),
        borderRadius: BorderRadius.circular(8),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          if (isSeed)
            SeedGlyph(SeedGlyphs.jars, size: 14, color: color)
          else
            Icon(iconForLotType(type), size: 14, color: color),
          const SizedBox(width: 4),
          Text(
            lotTypeLabel(t, type),
            style: TextStyle(
              fontSize: 11,
              color: color,
              fontWeight: FontWeight.w600,
            ),
          ),
        ],
      ),
    );
  }
}

/// A compact pill showing the latest germination rate on a lot tile.
class _GerminationBadge extends StatelessWidget {
  const _GerminationBadge({required this.rate});

  final double rate;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      decoration: BoxDecoration(
        color: scheme.secondaryContainer,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Text(
        _germinationPercent(context.t, rate),
        style: TextStyle(
          color: scheme.onSecondaryContainer,
          fontWeight: FontWeight.w600,
        ),
      ),
    );
  }
}

Future<void> _showGerminationSheet(
  BuildContext context,
  VarietyDetailCubit cubit,
  VarietyLot lot,
) {
  final t = context.t;
  final sample = TextEditingController();
  final germinated = TextEditingController();
  return showModalBottomSheet<void>(
    context: context,
    isScrollControlled: true,
    builder: (sheetContext) => Padding(
      padding: EdgeInsets.only(
        left: 16,
        right: 16,
        top: 16,
        bottom: MediaQuery.of(sheetContext).viewInsets.bottom + 16,
      ),
      child: SingleChildScrollView(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              t.germination.title,
              style: Theme.of(sheetContext).textTheme.titleLarge,
            ),
          const SizedBox(height: 8),
          if (lot.germinationTests.isEmpty)
            Text(t.germination.none)
          else
            for (final test in lot.germinationTests)
              ListTile(
                dense: true,
                contentPadding: EdgeInsets.zero,
                leading: const Icon(Icons.grass_outlined),
                title: Text(
                  test.rate == null ? '—' : _germinationPercent(t, test.rate!),
                ),
                subtitle:
                    (test.sampleSize != null && test.germinatedCount != null)
                    ? Text('${test.germinatedCount}/${test.sampleSize}')
                    : null,
              ),
          const Divider(),
          Row(
            children: [
              Expanded(
                child: TextField(
                  key: const Key('germination.germinated'),
                  controller: germinated,
                  keyboardType: TextInputType.number,
                  decoration: InputDecoration(
                    labelText: t.germination.germinated,
                    border: const OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(8)),
                    ),
                  ),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: TextField(
                  key: const Key('germination.sampleSize'),
                  controller: sample,
                  keyboardType: TextInputType.number,
                  decoration: InputDecoration(
                    labelText: t.germination.sampleSize,
                    border: const OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(8)),
                    ),
                  ),
                ),
              ),
            ],
          ),
          const SizedBox(height: 16),
          Row(
            children: [
              TextButton(
                onPressed: () => Navigator.of(sheetContext).pop(),
                child: Text(t.common.cancel),
              ),
              const Spacer(),
              FilledButton(
                key: const Key('germination.save'),
                onPressed: () {
                  cubit.addGerminationTest(
                    lotId: lot.id,
                    testedOn: DateTime.now().millisecondsSinceEpoch,
                    sampleSize: int.tryParse(sample.text.trim()),
                    germinatedCount: int.tryParse(germinated.text.trim()),
                  );
                  Navigator.of(sheetContext).pop();
                },
                child: Text(t.germination.add),
              ),
            ],
          ),
        ],
      ),
      ),
    ),
  );
}

Future<void> _confirmDelete(
  BuildContext context,
  VarietyDetailCubit cubit,
) async {
  final t = context.t;
  final confirmed = await showDialog<bool>(
    context: context,
    builder: (dialogContext) => AlertDialog(
      content: Text(t.detail.deleteConfirm),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(dialogContext).pop(false),
          child: Text(t.common.cancel),
        ),
        FilledButton(
          key: const Key('detail.deleteConfirm'),
          onPressed: () => Navigator.of(dialogContext).pop(true),
          child: Text(t.common.delete),
        ),
      ],
    ),
  );
  if (confirmed ?? false) await cubit.deleteVariety();
}

Future<void> _showEditSheet(
  BuildContext context,
  VarietyDetailCubit cubit,
  VarietyDetail detail,
  SpeciesRepository species,
) {
  return showModalBottomSheet<void>(
    context: context,
    isScrollControlled: true,
    builder: (_) =>
        _EditVarietySheet(cubit: cubit, detail: detail, species: species),
  );
}

/// The edit sheet, stateful so it can search the species catalog live and link
/// the picked species on save.
class _EditVarietySheet extends StatefulWidget {
  const _EditVarietySheet({
    required this.cubit,
    required this.detail,
    required this.species,
  });

  final VarietyDetailCubit cubit;
  final VarietyDetail detail;
  final SpeciesRepository species;

  @override
  State<_EditVarietySheet> createState() => _EditVarietySheetState();
}

class _EditVarietySheetState extends State<_EditVarietySheet> {
  late final TextEditingController _name = TextEditingController(
    text: widget.detail.label,
  );
  late final TextEditingController _category = TextEditingController(
    text: widget.detail.category ?? '',
  );
  late final TextEditingController _notes = TextEditingController(
    text: widget.detail.notes ?? '',
  );
  late final TextEditingController _speciesField = TextEditingController(
    text: widget.detail.scientificName ?? '',
  );

  List<SpeciesMatch> _suggestions = const [];
  String? _pickedSpeciesId;

  /// A species inferred from the typed name, offered as a one-tap suggestion.
  /// Only surfaced while the variety has no species and the grower has not
  /// picked one, so it never nags over a deliberate choice.
  SpeciesMatch? _suggested;
  late bool _isOrganic = widget.detail.isOrganic;
  late bool _needsReproduction = widget.detail.needsReproduction;
  late int? _sowMonths = widget.detail.sowMonths;
  late int? _transplantMonths = widget.detail.transplantMonths;
  late int? _floweringMonths = widget.detail.floweringMonths;
  late int? _fruitingMonths = widget.detail.fruitingMonths;
  late int? _seedHarvestMonths = widget.detail.seedHarvestMonths;
  late bool _calendarOpen = widget.detail.hasCropCalendar;

  // Typing stays responsive by not touching the DB on every keystroke: each
  // field waits until the grower pauses before running its (heavy) lookup, and
  // a per-field query counter drops any result that a later keystroke has
  // already superseded, so stale suggestions never flicker back in.
  static const _debounce = Duration(milliseconds: 250);
  Timer? _speciesDebounce;
  Timer? _nameDebounce;
  int _speciesQueryId = 0;
  int _nameQueryId = 0;

  @override
  void dispose() {
    _speciesDebounce?.cancel();
    _nameDebounce?.cancel();
    _name.dispose();
    _category.dispose();
    _notes.dispose();
    _speciesField.dispose();
    super.dispose();
  }

  void _onSpeciesQuery(String query) {
    _speciesDebounce?.cancel();
    _speciesDebounce = Timer(_debounce, () => _runSpeciesQuery(query));
  }

  Future<void> _runSpeciesQuery(String query) async {
    final queryId = ++_speciesQueryId;
    final lang = Localizations.localeOf(context).languageCode;
    final results = await widget.species.search(query, languageCode: lang);
    // A manual species search takes over from the name-based suggestion.
    if (mounted && queryId == _speciesQueryId) {
      setState(() => _suggestions = results);
    }
  }

  void _onNameChanged(String name) {
    if (widget.detail.speciesId != null || _pickedSpeciesId != null) return;
    _nameDebounce?.cancel();
    _nameDebounce = Timer(_debounce, () => _runNameClassify(name));
  }

  /// As the name is typed, offer the species it names — but only when the
  /// variety has no species yet and the grower hasn't picked one, so an
  /// existing link is never second-guessed.
  Future<void> _runNameClassify(String name) async {
    if (widget.detail.speciesId != null || _pickedSpeciesId != null) return;
    final queryId = ++_nameQueryId;
    final lang = Localizations.localeOf(context).languageCode;
    final match = await widget.species.classifyLabel(name, languageCode: lang);
    if (mounted && queryId == _nameQueryId) {
      setState(() => _suggested = match);
    }
  }

  void _pick(SpeciesMatch match) {
    setState(() {
      _pickedSpeciesId = match.id;
      _speciesField.text = match.displayLabel;
      _suggestions = const [];
      _suggested = null;
    });
  }

  void _save() {
    final name = _name.text.trim();
    widget.cubit.updateFields(
      label: name.isEmpty ? null : name,
      category: _nullIfBlank(_category.text),
      notes: _nullIfBlank(_notes.text),
      isOrganic: _isOrganic,
      needsReproduction: _needsReproduction,
      sowMonths: Value(_sowMonths),
      transplantMonths: Value(_transplantMonths),
      floweringMonths: Value(_floweringMonths),
      fruitingMonths: Value(_fruitingMonths),
      seedHarvestMonths: Value(_seedHarvestMonths),
    );
    if (_pickedSpeciesId != null) {
      widget.cubit.linkSpecies(_pickedSpeciesId!);
    }
    Navigator.of(context).pop();
  }

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    return Padding(
      padding: EdgeInsets.only(
        left: 16,
        right: 16,
        top: 16,
        bottom: MediaQuery.of(context).viewInsets.bottom + 16,
      ),
      // Fields scroll; the Cancel/Save row is pinned as a fixed footer so Save
      // stays visible and tappable no matter how tall the sheet grows.
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Flexible(
            child: SingleChildScrollView(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  Text(
                    t.editVariety.title,
                    style: Theme.of(context).textTheme.titleLarge,
                  ),
                  const SizedBox(height: 12),
                  TextField(
                    key: const Key('editVariety.name'),
                    controller: _name,
                    textCapitalization: TextCapitalization.sentences,
                    decoration: InputDecoration(
                      labelText: t.editVariety.name,
                      border: const OutlineInputBorder(
                        borderRadius: BorderRadius.all(Radius.circular(8)),
                      ),
                    ),
                    onChanged: _onNameChanged,
                  ),
                  const SizedBox(height: 12),
                  TextField(
                    key: const Key('editVariety.species'),
                    controller: _speciesField,
                    decoration: InputDecoration(
                      labelText: t.editVariety.species,
                      hintText: t.editVariety.speciesHint,
                      prefixIcon: const Icon(Icons.eco_outlined),
                      border: const OutlineInputBorder(
                        borderRadius: BorderRadius.all(Radius.circular(8)),
                      ),
                    ),
                    onChanged: _onSpeciesQuery,
                  ),
                  for (final match in _suggestions)
                    ListTile(
                      dense: true,
                      key: Key('species.option.${match.id}'),
                      title: Text(match.displayLabel),
                      subtitle: match.family == null
                          ? null
                          : Text(match.family!),
                      onTap: () => _pick(match),
                    ),
                  // One-tap suggestion inferred from the name (only when the
                  // species field is idle, so it never competes with a manual
                  // search).
                  if (_suggested case final suggested?
                      when _suggestions.isEmpty && _pickedSpeciesId == null)
                    ListTile(
                      dense: true,
                      key: const Key('editVariety.speciesSuggestion'),
                      leading: const Icon(Icons.auto_awesome_outlined),
                      title: Text(suggested.displayLabel),
                      subtitle: Text(t.editVariety.speciesSuggested),
                      onTap: () => _pick(suggested),
                    ),
                  const SizedBox(height: 12),
                  TextField(
                    controller: _category,
                    textCapitalization: TextCapitalization.sentences,
                    decoration: InputDecoration(
                      labelText: t.editVariety.category,
                      border: const OutlineInputBorder(
                        borderRadius: BorderRadius.all(Radius.circular(8)),
                      ),
                    ),
                  ),
                  const SizedBox(height: 12),
                  TextField(
                    controller: _notes,
                    minLines: 2,
                    maxLines: 5,
                    decoration: InputDecoration(
                      labelText: t.editVariety.notes,
                      border: const OutlineInputBorder(
                        borderRadius: BorderRadius.all(Radius.circular(8)),
                      ),
                    ),
                  ),
                  const SizedBox(height: 4),
                  SwitchListTile(
                    key: const Key('editVariety.organic'),
                    contentPadding: EdgeInsets.zero,
                    secondary: const Icon(Icons.eco_outlined, color: seedGreen),
                    title: Text(t.editVariety.organic),
                    subtitle: Text(t.editVariety.organicHint),
                    value: _isOrganic,
                    onChanged: (v) => setState(() => _isOrganic = v),
                  ),
                  SwitchListTile(
                    key: const Key('editVariety.needsReproduction'),
                    contentPadding: EdgeInsets.zero,
                    secondary: const Icon(Icons.autorenew, color: seedGreen),
                    title: Text(t.needsReproduction.label),
                    subtitle: Text(t.needsReproduction.hint),
                    value: _needsReproduction,
                    onChanged: (v) => setState(() => _needsReproduction = v),
                  ),
                  // Crop calendar: one collapsed reveal, not five dropdowns up front.
                  ExpansionTile(
                    key: const Key('editVariety.cropCalendar'),
                    initiallyExpanded: _calendarOpen,
                    tilePadding: EdgeInsets.zero,
                    childrenPadding: const EdgeInsets.only(bottom: 8),
                    leading: const Icon(Icons.calendar_month_outlined),
                    title: Text(t.cropCalendar.title),
                    onExpansionChanged: (v) => _calendarOpen = v,
                    children: [
                      _MonthMultiSelect(
                        label: t.cropCalendar.sow,
                        mask: _sowMonths,
                        onChanged: (m) => setState(() => _sowMonths = m),
                      ),
                      _MonthMultiSelect(
                        label: t.cropCalendar.transplant,
                        mask: _transplantMonths,
                        onChanged: (m) => setState(() => _transplantMonths = m),
                      ),
                      _MonthMultiSelect(
                        label: t.cropCalendar.flowering,
                        mask: _floweringMonths,
                        onChanged: (m) => setState(() => _floweringMonths = m),
                      ),
                      _MonthMultiSelect(
                        label: t.cropCalendar.fruiting,
                        mask: _fruitingMonths,
                        onChanged: (m) => setState(() => _fruitingMonths = m),
                      ),
                      _MonthMultiSelect(
                        label: t.cropCalendar.seedHarvest,
                        mask: _seedHarvestMonths,
                        onChanged: (m) =>
                            setState(() => _seedHarvestMonths = m),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          Row(
            children: [
              TextButton(
                onPressed: () => Navigator.of(context).pop(),
                child: Text(t.common.cancel),
              ),
              const Spacer(),
              FilledButton(
                key: const Key('editVariety.save'),
                onPressed: _save,
                child: Text(t.common.save),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

/// A labelled multi-select of the 12 months for one crop-calendar phase — a
/// phase can span several months. [mask] is a 12-bit month mask; tapping a
/// month toggles its bit.
class _MonthMultiSelect extends StatelessWidget {
  const _MonthMultiSelect({
    required this.label,
    required this.mask,
    required this.onChanged,
  });

  final String label;
  final int? mask;
  final ValueChanged<int?> onChanged;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 6),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(label, style: Theme.of(context).textTheme.labelLarge),
          const SizedBox(height: 6),
          Wrap(
            spacing: 6,
            runSpacing: 4,
            children: [
              for (var m = 1; m <= 12; m++)
                FilterChip(
                  key: Key('calendar.$label.$m'),
                  visualDensity: VisualDensity.compact,
                  labelPadding: const EdgeInsets.symmetric(horizontal: 2),
                  label: Text(_monthAbbrev(t.harvest.monthNames[m - 1])),
                  selected: maskHasMonth(mask, m),
                  onSelected: (_) => onChanged(toggleMonth(mask, m)),
                ),
            ],
          ),
        ],
      ),
    );
  }
}

/// A short (≤3-char) label for a month name, for the compact calendar chips.
String _monthAbbrev(String name) =>
    name.length <= 3 ? name : name.substring(0, 3);

/// Read-only crop calendar: one line per recorded phase ("Sow · Mar, Apr, Sep").
/// Renders only the phases that have months set.
class _CropCalendarView extends StatelessWidget {
  const _CropCalendarView({required this.detail});

  final VarietyDetail detail;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    final phases = <(String, int?)>[
      (t.cropCalendar.sow, detail.sowMonths),
      (t.cropCalendar.transplant, detail.transplantMonths),
      (t.cropCalendar.flowering, detail.floweringMonths),
      (t.cropCalendar.fruiting, detail.fruitingMonths),
      (t.cropCalendar.seedHarvest, detail.seedHarvestMonths),
    ];
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        for (final (label, mask) in phases)
          if (maskToMonths(mask).isNotEmpty)
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 2),
              child: Text.rich(
                TextSpan(
                  children: [
                    TextSpan(
                      text: '$label · ',
                      style: const TextStyle(fontWeight: FontWeight.w600),
                    ),
                    TextSpan(
                      text: maskToMonths(mask)
                          .map((m) => _monthAbbrev(t.harvest.monthNames[m - 1]))
                          .join(', '),
                    ),
                  ],
                ),
              ),
            ),
      ],
    );
  }
}

/// "How to save this seed" — the bundled seed-saving guidance for the crop
/// (pollination, isolation distance, how many plants, wet/dry, difficulty, a
/// tip), in human words. Shown only when a guide resolves for the variety.
class _SeedSavingView extends StatelessWidget {
  const _SeedSavingView({required this.guide});

  final SeedSavingGuide guide;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    final rows = <Widget>[];

    final cycle = switch (guide.lifeCycle) {
      LifeCycle.annual => t.seedSaving.cycleAnnual,
      LifeCycle.biennial => t.seedSaving.cycleBiennial,
      LifeCycle.perennial => t.seedSaving.cyclePerennial,
      null => null,
    };
    if (cycle != null) {
      rows.add(_line(Icons.event_repeat, t.seedSaving.lifeCycle, cycle));
    }

    if (guide.pollination case final poll?) {
      final base = switch (poll) {
        Pollination.self => t.seedSaving.pollSelf,
        Pollination.cross => t.seedSaving.pollCross,
        Pollination.mixed => t.seedSaving.pollMixed,
      };
      final via = poll == Pollination.self
          ? ''
          : switch (guide.vector) {
              PollinationVector.insect => ' · ${t.seedSaving.byInsect}',
              PollinationVector.wind => ' · ${t.seedSaving.byWind}',
              _ => '',
            };
      final icon = switch (guide.vector) {
        PollinationVector.wind => Icons.air,
        PollinationVector.insect => Icons.emoji_nature,
        _ => Icons.lock_outline,
      };
      rows.add(_line(icon, t.seedSaving.pollination, '$base$via'));
    }

    if (guide.isolationMinM case final min?) {
      final max = guide.isolationMaxM;
      final value = (max != null && max != min)
          ? t.seedSaving.isolationRange(min: min, max: max)
          : t.seedSaving.isolationSingle(min: min);
      rows.add(_line(Icons.social_distance, t.seedSaving.isolation, value));
    }

    if ((guide.recommendedPlants ?? guide.minPlants) case final n?) {
      rows.add(_line(
          Icons.groups_outlined, t.seedSaving.plants, t.seedSaving.plantsValue(n: n)));
    }

    if (guide.processing case final proc?) {
      final wet = proc == SeedProcessing.wet;
      rows.add(_line(wet ? Icons.water_drop_outlined : Icons.grass,
          t.seedSaving.processing, wet ? t.seedSaving.procWet : t.seedSaving.procDry));
    }

    final note = guide.noteFor(LocaleSettings.currentLocale.languageCode);

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        ...rows,
        if (guide.difficulty case final d?) ...[
          const SizedBox(height: 4),
          _DifficultyChip(difficulty: d),
        ],
        if (note != null) ...[
          const SizedBox(height: 8),
          Text(note, style: const TextStyle(color: seedMuted, height: 1.35)),
        ],
      ],
    );
  }

  Widget _line(IconData icon, String label, String value) => Padding(
        padding: const EdgeInsets.symmetric(vertical: 3),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Icon(icon, size: 18, color: seedGreen),
            const SizedBox(width: 10),
            Expanded(
              child: Text.rich(TextSpan(children: [
                TextSpan(
                    text: '$label · ',
                    style: const TextStyle(fontWeight: FontWeight.w600)),
                TextSpan(text: value),
              ])),
            ),
          ],
        ),
      );
}

class _DifficultyChip extends StatelessWidget {
  const _DifficultyChip({required this.difficulty});

  final SavingDifficulty difficulty;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    final (label, color) = switch (difficulty) {
      SavingDifficulty.easy => (t.seedSaving.diffEasy, const Color(0xFF3B6D11)),
      SavingDifficulty.medium =>
        (t.seedSaving.diffMedium, const Color(0xFF8A6D1E)),
      SavingDifficulty.hard => (t.seedSaving.diffHard, const Color(0xFFA14234)),
    };
    return Row(
      children: [
        Icon(Icons.insights, size: 18, color: seedGreen),
        const SizedBox(width: 10),
        Text('${t.seedSaving.difficulty} · ',
            style: const TextStyle(fontWeight: FontWeight.w600)),
        Container(
          padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
          decoration: BoxDecoration(
            color: color.withValues(alpha: 0.14),
            borderRadius: BorderRadius.circular(10),
          ),
          child: Text(label,
              style: TextStyle(color: color, fontWeight: FontWeight.w600)),
        ),
      ],
    );
  }
}

/// Human label for a coarse-abundance level.
String abundanceLabel(Translations t, Abundance a) => switch (a) {
  Abundance.plentyToShare => t.abundance.plentyToShare,
  Abundance.enoughToShare => t.abundance.enoughToShare,
  Abundance.enoughForMe => t.abundance.enoughForMe,
  Abundance.runningLow => t.abundance.runningLow,
};

/// Human label for how (whether) a lot is offered to others.
String shareStatusLabel(Translations t, OfferStatus s) => switch (s) {
  OfferStatus.private => t.share.private,
  OfferStatus.shared => t.share.gift,
  OfferStatus.exchange => t.share.exchange,
  OfferStatus.sell => t.share.sell,
};

/// Human label for a seed preservation format.
String preservationLabel(Translations t, PreservationFormat p) => switch (p) {
  PreservationFormat.jarWithDesiccant => t.preservation.jarWithDesiccant,
  PreservationFormat.glassJar => t.preservation.glassJar,
  PreservationFormat.paperEnvelope => t.preservation.paperEnvelope,
  PreservationFormat.paperBag => t.preservation.paperBag,
  PreservationFormat.plasticBag => t.preservation.plasticBag,
};

/// Human label for a drying-agent (silica) state.
String desiccantLabel(Translations t, DesiccantState d) => switch (d) {
  DesiccantState.none => t.desiccant.none,
  DesiccantState.add => t.desiccant.add,
  DesiccantState.replace => t.desiccant.replace,
  DesiccantState.dry => t.desiccant.dry,
  DesiccantState.fresh => t.desiccant.fresh,
};

/// Single-select chips for the coarse-abundance level; tapping the current one
/// clears it (null).
class _AbundanceSelector extends StatelessWidget {
  const _AbundanceSelector({required this.value, required this.onChanged});

  final Abundance? value;
  final ValueChanged<Abundance?> onChanged;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    return Wrap(
      spacing: 8,
      runSpacing: 4,
      children: [
        for (final a in Abundance.values)
          ChoiceChip(
            key: Key('abundance.${a.name}'),
            label: Text(abundanceLabel(t, a)),
            selected: value == a,
            onSelected: (sel) => onChanged(sel ? a : null),
          ),
      ],
    );
  }
}

/// Single-select chips for the share terms. Gift and swap come first and sale
/// last, never preselected — the gift is first-class, the sale a choice
/// (sharing-model §4.1). There is always a selection ("just for me" default).
class _ShareSelector extends StatelessWidget {
  const _ShareSelector({required this.value, required this.onChanged});

  final OfferStatus value;
  final ValueChanged<OfferStatus> onChanged;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    return Wrap(
      spacing: 8,
      runSpacing: 4,
      children: [
        for (final s in const [
          OfferStatus.private,
          OfferStatus.shared,
          OfferStatus.exchange,
          OfferStatus.sell,
        ])
          ChoiceChip(
            key: Key('share.${s.name}'),
            label: Text(shareStatusLabel(t, s)),
            selected: value == s,
            onSelected: (sel) {
              if (sel) onChanged(s);
            },
          ),
      ],
    );
  }
}

/// Single-select chips for the preservation format; tapping the current one
/// clears it (null).
class _PreservationSelector extends StatelessWidget {
  const _PreservationSelector({required this.value, required this.onChanged});

  final PreservationFormat? value;
  final ValueChanged<PreservationFormat?> onChanged;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    return Wrap(
      spacing: 8,
      runSpacing: 4,
      children: [
        for (final p in PreservationFormat.values)
          ChoiceChip(
            key: Key('preservation.${p.name}'),
            label: Text(preservationLabel(t, p)),
            selected: value == p,
            onSelected: (sel) => onChanged(sel ? p : null),
          ),
      ],
    );
  }
}

/// Lists a lot's storage-condition checks (newest first) and offers to add one.
class _ConditionChecksBlock extends StatelessWidget {
  const _ConditionChecksBlock({required this.cubit, required this.lot});

  final VarietyDetailCubit cubit;
  final VarietyLot lot;

  @override
  Widget build(BuildContext context) {
    final t = context.t;
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Align(
          alignment: Alignment.centerLeft,
          child: Text(
            t.conditionCheck.title,
            style: Theme.of(context).textTheme.labelLarge,
          ),
        ),
        if (lot.conditionChecks.isEmpty)
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 4),
            child: Text(t.conditionCheck.none),
          )
        else
          for (final check in lot.conditionChecks)
            ListTile(
              dense: true,
              contentPadding: EdgeInsets.zero,
              leading: const Icon(Icons.inventory_2_outlined),
              title: Text(
                t.conditionCheck.summary(
                  count: check.containerCount?.toString() ?? '—',
                  state: check.desiccantState == null
                      ? '—'
                      : desiccantLabel(t, check.desiccantState!),
                ),
              ),
            ),
        Align(
          alignment: Alignment.centerLeft,
          child: TextButton.icon(
            key: const Key('conditionCheck.open'),
            icon: const Icon(Icons.add),
            label: Text(t.conditionCheck.add),
            onPressed: () => _showConditionSheet(context, cubit, lot),
          ),
        ),
      ],
    );
  }
}

Future<void> _showConditionSheet(
  BuildContext context,
  VarietyDetailCubit cubit,
  VarietyLot lot,
) {
  final t = context.t;
  final containers = TextEditingController();
  DesiccantState? state;
  return showModalBottomSheet<void>(
    context: context,
    isScrollControlled: true,
    builder: (sheetContext) => StatefulBuilder(
      builder: (sheetContext, setState) => Padding(
        padding: EdgeInsets.only(
          left: 16,
          right: 16,
          top: 16,
          bottom: MediaQuery.of(sheetContext).viewInsets.bottom + 16,
        ),
        child: SingleChildScrollView(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text(
                t.conditionCheck.title,
                style: Theme.of(sheetContext).textTheme.titleLarge,
              ),
            const SizedBox(height: 12),
            TextField(
              key: const Key('conditionCheck.containers'),
              controller: containers,
              keyboardType: TextInputType.number,
              decoration: InputDecoration(
                labelText: t.conditionCheck.containers,
                border: const OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(8)),
                ),
              ),
            ),
            const SizedBox(height: 16),
            Text(
              t.conditionCheck.desiccant,
              style: Theme.of(sheetContext).textTheme.labelLarge,
            ),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              runSpacing: 4,
              children: [
                for (final d in DesiccantState.values)
                  ChoiceChip(
                    key: Key('desiccant.${d.name}'),
                    label: Text(desiccantLabel(t, d)),
                    selected: state == d,
                    onSelected: (sel) => setState(() => state = sel ? d : null),
                  ),
              ],
            ),
            const SizedBox(height: 16),
            Row(
              children: [
                TextButton(
                  onPressed: () => Navigator.of(sheetContext).pop(),
                  child: Text(t.common.cancel),
                ),
                const Spacer(),
                FilledButton(
                  key: const Key('conditionCheck.save'),
                  onPressed: () {
                    cubit.addConditionCheck(
                      lotId: lot.id,
                      checkedOn: DateTime.now().millisecondsSinceEpoch,
                      containerCount: int.tryParse(containers.text.trim()),
                      desiccantState: state,
                    );
                    Navigator.of(sheetContext).pop();
                  },
                  child: Text(t.conditionCheck.add),
                ),
              ],
            ),
          ],
        ),
        ),
      ),
    ),
  );
}

Future<void> _showLotSheet(
  BuildContext context,
  VarietyDetailCubit cubit, {
  VarietyLot? existing,
}) {
  final t = context.t;
  var selectedYear = existing?.harvestYear ?? DateTime.now().year;
  var selectedMonth = existing?.harvestMonth;
  var selectedType = existing?.type ?? LotType.seed;
  var selectedQuantity = existing?.quantity;
  var selectedPresentation = existing?.presentation;
  final editing = existing != null;
  // Provenance + abundance + preservation: hidden behind reveal-on-tap chips so
  // the default form stays small (progressive disclosure).
  final originNameCtrl = TextEditingController(
    text: existing?.originName ?? '',
  );
  final originPlaceCtrl = TextEditingController(
    text: existing?.originPlace ?? '',
  );
  var selectedAbundance = existing?.abundance;
  var selectedPreservation = existing?.preservationFormat;
  var selectedShare = existing?.offerStatus ?? OfferStatus.private;
  var showOrigin =
      existing?.originName != null || existing?.originPlace != null;
  var showAbundance = existing?.abundance != null;
  var showShare = selectedShare != OfferStatus.private;
  return showModalBottomSheet<void>(
    context: context,
    isScrollControlled: true,
    builder: (sheetContext) => StatefulBuilder(
      builder: (sheetContext, setState) => Padding(
        padding: EdgeInsets.only(
          left: 16,
          right: 16,
          top: 16,
          bottom: MediaQuery.of(sheetContext).viewInsets.bottom + 16,
        ),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Flexible(
              child: SingleChildScrollView(
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    Row(
                      children: [
                        Expanded(
                          child: Text(
                            editing ? t.detail.editLot : t.addLot.title,
                            style: Theme.of(sheetContext).textTheme.titleLarge,
                          ),
                        ),
                        if (editing)
                          IconButton(
                            key: const Key('lot.delete'),
                            icon: const Icon(Icons.delete_outline),
                            tooltip: t.common.delete,
                            onPressed: () {
                              cubit.deleteLot(existing.id);
                              Navigator.of(sheetContext).pop();
                            },
                          ),
                      ],
                    ),
                    const SizedBox(height: 12),
                    LotTypeSelector(
                      value: selectedType,
                      onChanged: (type) => setState(() {
                        selectedType = type;
                        selectedQuantity = null;
                        if (!typeHasPresentation(type)) {
                          selectedPresentation = null;
                        }
                      }),
                    ),
                    if (typeHasPresentation(selectedType)) ...[
                      const SizedBox(height: 16),
                      Text(
                        t.presentation.title,
                        style: Theme.of(sheetContext).textTheme.labelLarge,
                      ),
                      const SizedBox(height: 8),
                      PresentationSelector(
                        value: selectedPresentation,
                        onChanged: (p) =>
                            setState(() => selectedPresentation = p),
                      ),
                    ],
                    const SizedBox(height: 12),
                    Text(
                      t.addLot.year,
                      style: Theme.of(sheetContext).textTheme.labelLarge,
                    ),
                    const SizedBox(height: 8),
                    HarvestDateField(
                      year: selectedYear,
                      month: selectedMonth,
                      onChanged: (year, month) => setState(() {
                        selectedYear = year;
                        selectedMonth = month;
                      }),
                    ),
                    const SizedBox(height: 16),
                    Text(
                      t.addLot.quantity,
                      style: Theme.of(sheetContext).textTheme.labelLarge,
                    ),
                    const SizedBox(height: 8),
                    QuantityPicker(
                      type: selectedType,
                      value: selectedQuantity,
                      onChanged: (q) => selectedQuantity = q,
                    ),
                    const SizedBox(height: 12),
                    // Layer 1: reveal-on-tap extras, one field per chip.
                    Wrap(
                      spacing: 8,
                      children: [
                        if (!showOrigin)
                          ActionChip(
                            key: const Key('lot.addOrigin'),
                            avatar: const Icon(Icons.place_outlined, size: 18),
                            label: Text(t.provenance.section),
                            onPressed: () => setState(() => showOrigin = true),
                          ),
                        if (!showAbundance)
                          ActionChip(
                            key: const Key('lot.addAbundance'),
                            avatar: const Icon(
                              Icons.inventory_2_outlined,
                              size: 18,
                            ),
                            label: Text(t.abundance.add),
                            onPressed: () =>
                                setState(() => showAbundance = true),
                          ),
                        if (!showShare)
                          ActionChip(
                            key: const Key('lot.addShare'),
                            avatar: const Icon(
                              Icons.volunteer_activism_outlined,
                              size: 18,
                            ),
                            label: Text(t.share.add),
                            onPressed: () => setState(() => showShare = true),
                          ),
                      ],
                    ),
                    if (showOrigin) ...[
                      const SizedBox(height: 12),
                      TextField(
                        key: const Key('lot.originName'),
                        controller: originNameCtrl,
                        textCapitalization: TextCapitalization.sentences,
                        decoration: InputDecoration(
                          labelText: t.provenance.seedsFrom,
                          helperText: t.provenance.seedsFromHint,
                          border: const OutlineInputBorder(
                            borderRadius: BorderRadius.all(Radius.circular(8)),
                          ),
                        ),
                      ),
                      const SizedBox(height: 12),
                      TextField(
                        key: const Key('lot.originPlace'),
                        controller: originPlaceCtrl,
                        textCapitalization: TextCapitalization.sentences,
                        decoration: InputDecoration(
                          labelText: t.provenance.place,
                          helperText: t.provenance.placeHint,
                          border: const OutlineInputBorder(
                            borderRadius: BorderRadius.all(Radius.circular(8)),
                          ),
                        ),
                      ),
                    ],
                    if (showAbundance) ...[
                      const SizedBox(height: 16),
                      Text(
                        t.abundance.title,
                        style: Theme.of(sheetContext).textTheme.labelLarge,
                      ),
                      const SizedBox(height: 8),
                      _AbundanceSelector(
                        value: selectedAbundance,
                        onChanged: (a) => setState(() {
                          selectedAbundance = a;
                          // The bridge from "how much" to "do you share it":
                          // declaring plenty reveals the sharing choice. A
                          // nudge, never a change of the choice itself.
                          if (a == Abundance.plentyToShare ||
                              a == Abundance.enoughToShare) {
                            showShare = true;
                          }
                        }),
                      ),
                    ],
                    if (showShare) ...[
                      const SizedBox(height: 16),
                      Text(
                        t.share.title,
                        style: Theme.of(sheetContext).textTheme.labelLarge,
                      ),
                      const SizedBox(height: 8),
                      _ShareSelector(
                        value: selectedShare,
                        onChanged: (s) => setState(() => selectedShare = s),
                      ),
                      if (selectedShare == OfferStatus.private &&
                          (selectedAbundance == Abundance.plentyToShare ||
                              selectedAbundance == Abundance.enoughToShare))
                        Padding(
                          padding: const EdgeInsets.only(top: 6),
                          child: Text(
                            t.share.nudge,
                            key: const Key('share.nudge'),
                            style: Theme.of(sheetContext).textTheme.bodySmall,
                          ),
                        ),
                    ],
                    // Layer 2: advanced seed-bank details, collapsed by default.
                    // Only for seed lots, and (condition checks) an existing lot.
                    if (selectedType == LotType.seed)
                      ExpansionTile(
                        key: const Key('lot.advanced'),
                        tilePadding: EdgeInsets.zero,
                        childrenPadding: const EdgeInsets.only(bottom: 8),
                        leading: const Icon(Icons.inventory_outlined),
                        title: Text(t.conditionCheck.advanced),
                        children: [
                          const SizedBox(height: 4),
                          Align(
                            alignment: Alignment.centerLeft,
                            child: Text(
                              t.preservation.title,
                              style: Theme.of(
                                sheetContext,
                              ).textTheme.labelLarge,
                            ),
                          ),
                          const SizedBox(height: 8),
                          _PreservationSelector(
                            value: selectedPreservation,
                            onChanged: (p) =>
                                setState(() => selectedPreservation = p),
                          ),
                          if (editing) ...[
                            const SizedBox(height: 8),
                            _ConditionChecksBlock(cubit: cubit, lot: existing),
                          ],
                        ],
                      ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),
            Row(
              children: [
                TextButton(
                  onPressed: () => Navigator.of(sheetContext).pop(),
                  child: Text(t.common.cancel),
                ),
                const Spacer(),
                FilledButton(
                  key: const Key('addLot.save'),
                  onPressed: () {
                    final originName = _nullIfBlank(originNameCtrl.text);
                    final originPlace = _nullIfBlank(originPlaceCtrl.text);
                    if (editing) {
                      cubit.updateLot(
                        lotId: existing.id,
                        type: selectedType,
                        year: selectedYear,
                        month: selectedMonth,
                        quantity: selectedQuantity,
                        presentation: selectedPresentation,
                        originName: originName,
                        originPlace: originPlace,
                        abundance: selectedAbundance,
                        preservationFormat: selectedPreservation,
                        offerStatus: selectedShare,
                      );
                    } else {
                      cubit.addLot(
                        type: selectedType,
                        year: selectedYear,
                        month: selectedMonth,
                        quantity: selectedQuantity,
                        presentation: selectedPresentation,
                        originName: originName,
                        originPlace: originPlace,
                        abundance: selectedAbundance,
                        preservationFormat: selectedPreservation,
                        offerStatus: selectedShare,
                      );
                    }
                    Navigator.of(sheetContext).pop();
                  },
                  child: Text(t.common.save),
                ),
              ],
            ),
          ],
        ),
      ),
    ),
  );
}

Future<void> _showAddNameDialog(
  BuildContext context,
  VarietyDetailCubit cubit,
) {
  final t = context.t;
  final controller = TextEditingController();
  void submit(BuildContext dialogContext) {
    final name = controller.text.trim();
    if (name.isNotEmpty) cubit.addVernacularName(name);
    Navigator.of(dialogContext).pop();
  }

  return showDialog<void>(
    context: context,
    builder: (dialogContext) => AlertDialog(
      title: Text(t.detail.addName),
      content: TextField(
        key: const Key('name.field'),
        controller: controller,
        autofocus: true,
        textCapitalization: TextCapitalization.sentences,
        decoration: InputDecoration(labelText: t.editVariety.name),
        onSubmitted: (_) => submit(dialogContext),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(dialogContext).pop(),
          child: Text(t.common.cancel),
        ),
        FilledButton(
          key: const Key('name.save'),
          onPressed: () => submit(dialogContext),
          child: Text(t.common.save),
        ),
      ],
    ),
  );
}

/// Verified reference links for the variety's linked species, localized to the
/// UI language (drives the locale-aware Wikipedia link). Empty when no species
/// is linked or the catalog entry carries no identifiers.
List<ReferenceLink> _referenceLinks(
  BuildContext context,
  VarietyDetail detail,
) {
  final name = detail.scientificName;
  if (name == null) return const [];
  return speciesReferenceLinks(
    scientificName: name,
    gbifKey: detail.gbifKey,
    wikidataQid: detail.wikidataQid,
    languageCode: Localizations.localeOf(context).languageCode,
  );
}

String _referenceLabel(Translations t, ReferenceSource source) =>
    switch (source) {
      ReferenceSource.gbif => t.detail.refGbif,
      ReferenceSource.wikipedia => t.detail.refWikipedia,
      ReferenceSource.wikispecies => t.detail.refWikispecies,
    };

Future<void> _openUrl(String url) async {
  final normalized = url.contains('://') ? url : 'https://$url';
  final uri = Uri.tryParse(normalized);
  if (uri != null) {
    await launchUrl(uri, mode: LaunchMode.externalApplication);
  }
}

Future<void> _showAddLinkDialog(
  BuildContext context,
  VarietyDetailCubit cubit,
) {
  final t = context.t;
  final urlController = TextEditingController();
  final titleController = TextEditingController();
  void submit(BuildContext dialogContext) {
    final url = urlController.text.trim();
    if (url.isNotEmpty) {
      cubit.addExternalLink(url, title: _nullIfBlank(titleController.text));
    }
    Navigator.of(dialogContext).pop();
  }

  return showDialog<void>(
    context: context,
    builder: (dialogContext) => AlertDialog(
      title: Text(t.detail.addLink),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          TextField(
            key: const Key('link.url'),
            controller: urlController,
            autofocus: true,
            keyboardType: TextInputType.url,
            decoration: InputDecoration(labelText: t.detail.linkUrl),
          ),
          const SizedBox(height: 8),
          TextField(
            key: const Key('link.title'),
            controller: titleController,
            decoration: InputDecoration(labelText: t.detail.linkTitle),
          ),
        ],
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(dialogContext).pop(),
          child: Text(t.common.cancel),
        ),
        FilledButton(
          key: const Key('link.save'),
          onPressed: () => submit(dialogContext),
          child: Text(t.common.save),
        ),
      ],
    ),
  );
}

String? _nullIfBlank(String value) {
  final trimmed = value.trim();
  return trimmed.isEmpty ? null : trimmed;
}

/// Mockup-07 header: category link + scientific name on the left, the photo on
/// the right.
class _DetailHeader extends StatelessWidget {
  const _DetailHeader({required this.detail});

  final VarietyDetail detail;

  @override
  Widget build(BuildContext context) {
    final hasText =
        detail.category != null ||
        detail.scientificName != null ||
        detail.isOrganic ||
        detail.needsReproduction;
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              if (detail.isOrganic) ...[
                _OrganicChip(),
                const SizedBox(height: 6),
              ],
              if (detail.needsReproduction) ...[
                _NeedsReproductionChip(),
                const SizedBox(height: 6),
              ],
              if (detail.category != null)
                Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    const Icon(
                      Icons.category_outlined,
                      size: 17,
                      color: seedGreen,
                    ),
                    const SizedBox(width: 6),
                    Flexible(
                      child: Text(
                        detail.category!,
                        style: const TextStyle(
                          color: seedGreen,
                          fontWeight: FontWeight.w600,
                        ),
                      ),
                    ),
                  ],
                ),
              if (detail.scientificName != null) ...[
                const SizedBox(height: 2),
                Text(
                  detail.scientificName!,
                  style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                    fontStyle: FontStyle.italic,
                    color: Theme.of(context).colorScheme.onSurfaceVariant,
                  ),
                ),
              ],
            ],
          ),
        ),
        if (hasText) const SizedBox(width: 12),
        _PhotoGallery(detail: detail),
      ],
    );
  }
}

/// A small green "Organic" pill shown on an organic variety's detail header.
class _OrganicChip extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      key: const Key('detail.organic'),
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
      decoration: BoxDecoration(
        color: seedGreen.withValues(alpha: 0.12),
        borderRadius: BorderRadius.circular(8),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Icon(Icons.eco, size: 15, color: seedGreen),
          const SizedBox(width: 4),
          Text(
            context.t.editVariety.organic,
            style: const TextStyle(
              color: seedGreen,
              fontWeight: FontWeight.w600,
              fontSize: 12,
            ),
          ),
        ],
      ),
    );
  }
}

/// A small "to regrow" pill shown on a variety flagged as needing reproduction.
class _NeedsReproductionChip extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      key: const Key('detail.needsReproduction'),
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
      decoration: BoxDecoration(
        color: seedGreen.withValues(alpha: 0.12),
        borderRadius: BorderRadius.circular(8),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Icon(Icons.autorenew, size: 15, color: seedGreen),
          const SizedBox(width: 4),
          Text(
            context.t.needsReproduction.badge,
            style: const TextStyle(
              color: seedGreen,
              fontWeight: FontWeight.w600,
              fontSize: 12,
            ),
          ),
        ],
      ),
    );
  }
}

/// A 140×140 photo carousel (mockup 07): swipe between photos, page dots, add a
/// photo, and delete the current one. Shows just an "add" button when empty.
class _PhotoGallery extends StatefulWidget {
  const _PhotoGallery({required this.detail});

  final VarietyDetail detail;

  @override
  State<_PhotoGallery> createState() => _PhotoGalleryState();
}

class _PhotoGalleryState extends State<_PhotoGallery> {
  final _controller = PageController();
  int _page = 0;

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  Future<void> _addPhoto(VarietyDetailCubit cubit) async {
    final bytes = await pickPhoto(context);
    if (bytes != null) await cubit.addPhoto(bytes);
  }

  @override
  Widget build(BuildContext context) {
    final cubit = context.read<VarietyDetailCubit>();
    final photos = widget.detail.photos;

    if (photos.isEmpty) {
      return SizedBox(
        width: 140,
        height: 140,
        child: OutlinedButton(
          key: const Key('photo.add'),
          onPressed: () => _addPhoto(cubit),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Icon(Icons.add_a_photo_outlined),
              const SizedBox(height: 8),
              Text(context.t.quickAdd.addPhoto, textAlign: TextAlign.center),
            ],
          ),
        ),
      );
    }

    final page = _page.clamp(0, photos.length - 1);
    return SizedBox(
      width: 140,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          // Tap a photo to open the full-screen viewer, where you can set the
          // cover or delete it. The first photo is the cover.
          SizedBox(
            height: 140,
            child: ClipRRect(
              borderRadius: BorderRadius.circular(12),
              child: PageView.builder(
                controller: _controller,
                itemCount: photos.length,
                onPageChanged: (i) => setState(() => _page = i),
                itemBuilder: (_, i) => GestureDetector(
                  key: Key('photo.thumb.$i'),
                  onTap: () => _openPhotoViewer(context, cubit, i),
                  child: Image.memory(
                    photos[i].bytes,
                    width: 140,
                    height: 140,
                    fit: BoxFit.cover,
                  ),
                ),
              ),
            ),
          ),
          const SizedBox(height: 6),
          if (photos.length > 1)
            _Dots(
              count: photos.length,
              active: page,
              onTap: (i) => _controller.animateToPage(
                i,
                duration: const Duration(milliseconds: 250),
                curve: Curves.easeInOut,
              ),
            ),
          TextButton.icon(
            key: const Key('photo.add'),
            onPressed: () => _addPhoto(cubit),
            icon: const Icon(Icons.add_a_photo_outlined, size: 18),
            label: Text(context.t.quickAdd.addPhoto),
          ),
        ],
      ),
    );
  }
}

class _Dots extends StatelessWidget {
  const _Dots({required this.count, required this.active, this.onTap});

  final int count;
  final int active;
  final ValueChanged<int>? onTap;

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        for (var i = 0; i < count; i++)
          GestureDetector(
            behavior: HitTestBehavior.opaque,
            onTap: onTap == null ? null : () => onTap!(i),
            child: Padding(
              // Bigger, tappable hit area around each dot.
              padding: const EdgeInsets.all(6),
              child: Container(
                width: 8,
                height: 8,
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  color: i == active ? seedGreen : Colors.black26,
                ),
              ),
            ),
          ),
      ],
    );
  }
}

void _openPhotoViewer(
  BuildContext context,
  VarietyDetailCubit cubit,
  int initialIndex,
) {
  Navigator.of(context).push(
    MaterialPageRoute<void>(
      fullscreenDialog: true,
      // Re-provide the cubit so the pushed route can set the cover / delete and
      // rebuild live as the photo list reorders or shrinks.
      builder: (_) => BlocProvider.value(
        value: cubit,
        child: _PhotoViewer(initialIndex: initialIndex),
      ),
    ),
  );
}

/// Full-screen, swipeable, pinch-to-zoom photo viewer. Its app bar hosts the
/// per-photo actions (set as cover, delete) — the thumbnail gallery only opens
/// it. Stays in sync with the cubit, so deleting the last photo closes it.
class _PhotoViewer extends StatefulWidget {
  const _PhotoViewer({required this.initialIndex});

  final int initialIndex;

  @override
  State<_PhotoViewer> createState() => _PhotoViewerState();
}

class _PhotoViewerState extends State<_PhotoViewer> {
  late final PageController _controller = PageController(
    initialPage: widget.initialIndex,
  );
  late int _page = widget.initialIndex;
  bool _popped = false;

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  Future<void> _confirmDelete(
    VarietyDetailCubit cubit,
    VarietyPhoto photo,
  ) async {
    final t = context.t;
    final confirmed = await showDialog<bool>(
      context: context,
      builder: (dialogContext) => AlertDialog(
        content: Text(t.photo.deleteConfirm),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(dialogContext).pop(false),
            child: Text(t.common.cancel),
          ),
          FilledButton(
            key: const Key('photo.deleteConfirm'),
            onPressed: () => Navigator.of(dialogContext).pop(true),
            child: Text(t.common.delete),
          ),
        ],
      ),
    );
    if (confirmed ?? false) await cubit.removePhoto(photo.id);
  }

  @override
  Widget build(BuildContext context) {
    final cubit = context.read<VarietyDetailCubit>();
    return BlocBuilder<VarietyDetailCubit, VarietyDetailState>(
      builder: (context, state) {
        final photos = state.detail?.photos ?? const <VarietyPhoto>[];
        // Nothing left to show (last photo deleted / variety gone): close once.
        // The guard stops the post-frame callback re-arming every rebuild.
        if (photos.isEmpty) {
          if (!_popped) {
            _popped = true;
            WidgetsBinding.instance.addPostFrameCallback((_) {
              if (mounted) Navigator.of(context).maybePop();
            });
          }
          return const Scaffold(backgroundColor: Colors.black);
        }
        final page = _page.clamp(0, photos.length - 1);
        // A delete can leave the pager past the end: snap it back. jumpToPage
        // fires onPageChanged, which updates _page — so no setState here (that
        // would re-arm the callback and spin the frame loop).
        if (page != _page && _controller.hasClients) {
          WidgetsBinding.instance.addPostFrameCallback((_) {
            if (mounted && _controller.hasClients) _controller.jumpToPage(page);
          });
        }
        final isCover = page == 0;
        return Scaffold(
          backgroundColor: Colors.black,
          appBar: AppBar(
            backgroundColor: Colors.black,
            foregroundColor: Colors.white,
            elevation: 0,
            actions: [
              if (photos.length > 1)
                IconButton(
                  key: const Key('photo.cover'),
                  icon: Icon(isCover ? Icons.star : Icons.star_border),
                  // The cover star is disabled; without an explicit disabled
                  // color it renders faded/dark on the black bar. Amber both
                  // keeps the contrast and reads as "selected".
                  color: Colors.white,
                  disabledColor: Colors.amber,
                  tooltip: isCover
                      ? context.t.photo.isCover
                      : context.t.photo.setAsCover,
                  onPressed: isCover
                      ? null
                      : () async {
                          await cubit.setPreferredPhoto(photos[page].id);
                          // The chosen photo is now the cover (index 0): follow
                          // it there so the filled star reflects the change.
                          if (mounted && _controller.hasClients) {
                            _controller.jumpToPage(0);
                            setState(() => _page = 0);
                          }
                        },
                ),
              IconButton(
                key: const Key('photo.delete'),
                icon: const Icon(Icons.delete_outline),
                tooltip: context.t.common.delete,
                onPressed: () => _confirmDelete(cubit, photos[page]),
              ),
            ],
          ),
          body: PageView.builder(
            controller: _controller,
            itemCount: photos.length,
            onPageChanged: (i) => setState(() => _page = i),
            itemBuilder: (_, i) => _ZoomablePhoto(bytes: photos[i].bytes),
          ),
        );
      },
    );
  }
}

/// Pinch-to-zoom photo that only pans while zoomed, so a plain horizontal
/// swipe still pages the surrounding [PageView].
class _ZoomablePhoto extends StatefulWidget {
  const _ZoomablePhoto({required this.bytes});

  final Uint8List bytes;

  @override
  State<_ZoomablePhoto> createState() => _ZoomablePhotoState();
}

class _ZoomablePhotoState extends State<_ZoomablePhoto> {
  final _controller = TransformationController();
  bool _zoomed = false;

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return InteractiveViewer(
      transformationController: _controller,
      minScale: 1,
      maxScale: 5,
      panEnabled: _zoomed,
      onInteractionEnd: (_) {
        final zoomed = _controller.value.getMaxScaleOnAxis() > 1.01;
        if (zoomed != _zoomed) setState(() => _zoomed = zoomed);
      },
      child: Center(child: Image.memory(widget.bytes)),
    );
  }
}

class _SectionTitle extends StatelessWidget {
  const _SectionTitle(this.text);

  final String text;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: Text(
        text,
        style: Theme.of(context).textTheme.titleMedium?.copyWith(
          color: Theme.of(context).colorScheme.primary,
        ),
      ),
    );
  }
}
