/// Seed-saving know-how per crop: how a plant reproduces and what it takes to
/// keep a variety true when saving its seed. Bundled reference data (curated
/// from public seed-saving literature), keyed by botanical family with
/// per-species overrides. Pure Dart (only `dart:convert`) — no Flutter binding
/// — so it is trivially testable.
library;

import 'dart:convert';

/// How long the plant takes to make seed.
enum LifeCycle { annual, biennial, perennial }

/// Whether a plant fertilises itself or needs another plant.
enum Pollination { self, cross, mixed }

/// What carries the pollen when it does cross.
enum PollinationVector { self, insect, wind }

/// How the seed is cleaned once ripe.
enum SeedProcessing { dry, wet }

/// Roughly how hard it is to save true seed at home.
enum SavingDifficulty { easy, medium, hard }

/// The saving guidance for one crop. Every field is optional — a starter entry
/// may only know some of them. Distances are in metres; plant counts are how
/// many plants to save seed from to keep enough genetic diversity.
class SeedSavingGuide {
  const SeedSavingGuide({
    this.lifeCycle,
    this.pollination,
    this.vector,
    this.isolationMinM,
    this.isolationMaxM,
    this.minPlants,
    this.recommendedPlants,
    this.processing,
    this.difficulty,
    this.note = const {},
  });

  final LifeCycle? lifeCycle;
  final Pollination? pollination;
  final PollinationVector? vector;
  final int? isolationMinM;
  final int? isolationMaxM;
  final int? minPlants;
  final int? recommendedPlants;
  final SeedProcessing? processing;
  final SavingDifficulty? difficulty;

  /// Locale-keyed short tip (`{"es": "…", "en": "…"}`), extensible to any
  /// language — never assume es/en only.
  final Map<String, String> note;

  /// True when there's at least one fact worth showing.
  bool get hasAny =>
      lifeCycle != null ||
      pollination != null ||
      vector != null ||
      isolationMinM != null ||
      minPlants != null ||
      processing != null ||
      difficulty != null ||
      note.isNotEmpty;

  /// This guide with [other]'s non-null fields laid over it (species over
  /// family). Notes merge, with [other]'s languages winning.
  SeedSavingGuide mergedWith(SeedSavingGuide other) => SeedSavingGuide(
        lifeCycle: other.lifeCycle ?? lifeCycle,
        pollination: other.pollination ?? pollination,
        vector: other.vector ?? vector,
        isolationMinM: other.isolationMinM ?? isolationMinM,
        isolationMaxM: other.isolationMaxM ?? isolationMaxM,
        minPlants: other.minPlants ?? minPlants,
        recommendedPlants: other.recommendedPlants ?? recommendedPlants,
        processing: other.processing ?? processing,
        difficulty: other.difficulty ?? difficulty,
        note: {...note, ...other.note},
      );

  /// The tip in [lang], falling back to English then any language.
  String? noteFor(String lang) {
    if (note.isEmpty) return null;
    return note[lang] ?? note['en'] ?? note.values.first;
  }
}

/// The whole bundled dataset: family defaults plus per-taxon overrides (keyed by
/// scientific name OR bare genus). Lookups are case-insensitive.
class SeedSavingData {
  SeedSavingData({
    required Map<String, SeedSavingGuide> families,
    required Map<String, SeedSavingGuide> taxa,
  })  : _families = {
          for (final e in families.entries) e.key.toLowerCase().trim(): e.value,
        },
        _taxa = {
          for (final e in taxa.entries) e.key.toLowerCase().trim(): e.value,
        };

  final Map<String, SeedSavingGuide> _families;
  final Map<String, SeedSavingGuide> _taxa;

  /// The best guide for a crop: the family default, overlaid by a per-species
  /// (or per-genus) entry when one exists. Returns null when nothing matches.
  SeedSavingGuide? guideFor({String? scientificName, String? family}) {
    SeedSavingGuide? base =
        family == null ? null : _families[family.toLowerCase().trim()];

    if (scientificName != null) {
      final sci = scientificName.toLowerCase().trim();
      final genus = sci.split(RegExp(r'\s+')).first;
      final species = _taxa[sci] ?? (genus.isEmpty ? null : _taxa[genus]);
      if (species != null) base = base == null ? species : base.mergedWith(species);
    }
    return base;
  }
}

T? _enumByName<T extends Enum>(List<T> values, Object? name) {
  if (name is! String) return null;
  for (final v in values) {
    if (v.name == name) return v;
  }
  return null;
}

/// Parses one guide from a JSON map (already decoded).
SeedSavingGuide parseGuide(Map<String, dynamic> m) => SeedSavingGuide(
      lifeCycle: _enumByName(LifeCycle.values, m['lifeCycle']),
      pollination: _enumByName(Pollination.values, m['pollination']),
      vector: _enumByName(PollinationVector.values, m['vector']),
      isolationMinM: (m['isolationMinM'] as num?)?.toInt(),
      isolationMaxM: (m['isolationMaxM'] as num?)?.toInt(),
      minPlants: (m['minPlants'] as num?)?.toInt(),
      recommendedPlants: (m['recommendedPlants'] as num?)?.toInt(),
      processing: _enumByName(SeedProcessing.values, m['processing']),
      difficulty: _enumByName(SavingDifficulty.values, m['difficulty']),
      note: (m['note'] as Map<String, dynamic>? ?? const {})
          .map((k, v) => MapEntry(k, v as String)),
    );

/// Parses the bundled seed-saving catalog JSON into [SeedSavingData].
SeedSavingData parseSeedSaving(String jsonString) {
  final root = jsonDecode(jsonString) as Map<String, dynamic>;
  Map<String, SeedSavingGuide> section(String key) {
    final raw = root[key] as Map<String, dynamic>? ?? const {};
    return raw.map(
      (k, v) => MapEntry(k, parseGuide((v as Map).cast<String, dynamic>())),
    );
  }

  return SeedSavingData(families: section('families'), taxa: section('species'));
}
