import 'package:commons_core/commons_core.dart' show QuantityKind;

import '../../db/database.dart';
import '../../db/enums.dart';
import '../../domain/crop_calendar.dart';
import 'inventory_snapshot.dart';

/// Flattens an [InventorySnapshot] into a spreadsheet-friendly CSV — one row
/// per lot, one row for a variety without lots. Export-only: the JSON format
/// is the canonical one for import (data-model §7). Never includes photo
/// bytes. Pure serialization — no I/O, no database.
class InventoryCsvCodec {
  const InventoryCsvCodec();

  static const List<String> _header = [
    'variety_id',
    'variety_label',
    'category',
    'species_scientific_name',
    'cultivar_name',
    'variety_notes',
    'vernacular_names',
    'external_links',
    'needs_reproduction',
    'sow_months',
    'transplant_months',
    'flowering_months',
    'fruiting_months',
    'seed_harvest_months',
    'lot_id',
    'lot_type',
    'harvest_year',
    'harvest_month',
    'quantity_kind',
    'quantity_precise',
    'quantity_label',
    'presentation',
    'storage_location',
    'offer_status',
    'origin_name',
    'origin_place',
    'abundance',
    'preservation_format',
    'latest_germination_rate',
    'container_count',
    'desiccant_state',
    'created_at',
    'updated_at',
  ];

  String encode(InventorySnapshot snapshot) {
    final lotsByVariety = <String, List<Lot>>{};
    for (final lot in snapshot.lots) {
      lotsByVariety.putIfAbsent(lot.varietyId, () => []).add(lot);
    }
    final namesByVariety = <String, List<String>>{};
    for (final n in snapshot.vernacularNames) {
      namesByVariety
          .putIfAbsent(n.varietyId, () => [])
          .add(n.language == null ? n.name : '${n.name} (${n.language})');
    }
    final linksByVariety = <String, List<String>>{};
    for (final e in snapshot.externalLinks) {
      if (e.parentType != ParentType.variety) continue;
      linksByVariety
          .putIfAbsent(e.parentId, () => [])
          .add(e.title == null ? e.url : '${e.title}|${e.url}');
    }
    final latestRateByLot = _latestGerminationRates(snapshot.germinationTests);
    final latestCheckByLot = _latestConditionChecks(snapshot.conditionChecks);

    final lines = <String>[_header.map(_escape).join(',')];
    for (final v in snapshot.varieties) {
      final varietyCells = [
        v.id,
        v.label,
        v.category ?? '',
        snapshot.speciesNamesById[v.speciesId] ?? '',
        v.cultivarName ?? '',
        v.notes ?? '',
        (namesByVariety[v.id] ?? const []).join('; '),
        (linksByVariety[v.id] ?? const []).join('; '),
        v.needsReproduction ? 'true' : '',
        _monthsCell(v.sowMonths),
        _monthsCell(v.transplantMonths),
        _monthsCell(v.floweringMonths),
        _monthsCell(v.fruitingMonths),
        _monthsCell(v.seedHarvestMonths),
      ];
      final lots = lotsByVariety[v.id] ?? const <Lot>[];
      if (lots.isEmpty) {
        lines.add(
          [
            ...varietyCells,
            ...List.filled(17, ''),
            _isoDate(v.createdAt),
            _hlcMillisIso(v.updatedAt),
          ].map(_escape).join(','),
        );
        continue;
      }
      for (final l in lots) {
        final check = latestCheckByLot[l.id];
        lines.add(
          [
            ...varietyCells,
            l.id,
            l.type.name,
            l.harvestYear?.toString() ?? '',
            l.harvestMonth?.toString() ?? '',
            l.quantityKind ?? '',
            l.quantityPrecise?.toString() ?? '',
            l.quantityLabel ?? '',
            l.presentation?.name ?? '',
            l.storageLocation ?? '',
            l.offerStatus.name,
            l.originName ?? '',
            l.originPlace ?? '',
            l.abundance?.name ?? '',
            l.preservationFormat?.name ?? '',
            latestRateByLot[l.id]?.toStringAsFixed(2) ?? '',
            check?.containerCount?.toString() ?? '',
            check?.desiccantState?.name ?? '',
            _isoDate(l.createdAt),
            _hlcMillisIso(l.updatedAt),
          ].map(_escape).join(','),
        );
      }
    }
    return '${lines.join('\r\n')}\r\n';
  }

  /// Renders a crop-calendar month mask as a space-separated list of month
  /// numbers (e.g. `3 4 9`), or empty when unset — readable in a spreadsheet.
  String _monthsCell(int? mask) => maskToMonths(mask).join(' ');

  /// lotId → most-recent condition check (by checkedOn), so the CSV can show a
  /// lot's latest jar count and drying-agent state as flat columns.
  Map<String, ConditionCheck> _latestConditionChecks(
    List<ConditionCheck> checks,
  ) {
    final latest = <String, ConditionCheck>{};
    for (final c in checks) {
      final current = latest[c.lotId];
      if (current == null || (c.checkedOn ?? 0) > (current.checkedOn ?? 0)) {
        latest[c.lotId] = c;
      }
    }
    return latest;
  }

  /// lotId → latest germination rate (0..1), from the most recent test that
  /// has a computable rate.
  Map<String, double> _latestGerminationRates(List<GerminationTest> tests) {
    final latest = <String, GerminationTest>{};
    for (final t in tests) {
      final sample = t.sampleSize;
      if (sample == null || sample <= 0 || t.germinatedCount == null) continue;
      final current = latest[t.lotId];
      if (current == null || (t.testedOn ?? 0) > (current.testedOn ?? 0)) {
        latest[t.lotId] = t;
      }
    }
    return latest.map(
      (lotId, t) => MapEntry(lotId, t.germinatedCount! / t.sampleSize!),
    );
  }

  String _isoDate(int millisSinceEpoch) => DateTime.fromMillisecondsSinceEpoch(
    millisSinceEpoch,
    isUtc: true,
  ).toIso8601String();

  /// Renders the physical-time part of a packed HLC as an ISO date; the raw
  /// stamp stays in the JSON export, the CSV favours readability.
  String _hlcMillisIso(String packedHlc) {
    final millis = int.tryParse(packedHlc.split(':').first);
    return millis == null ? '' : _isoDate(millis);
  }

  /// RFC 4180: quote fields containing separators/quotes/newlines, doubling
  /// embedded quotes.
  String _escape(String cell) {
    if (!cell.contains(',') &&
        !cell.contains('"') &&
        !cell.contains('\n') &&
        !cell.contains('\r')) {
      return cell;
    }
    return '"${cell.replaceAll('"', '""')}"';
  }

  /// Parses a CSV back into an **additive** import (the canonical import format
  /// is JSON; this is a convenience for lists a collective already keeps in a
  /// spreadsheet). Reader tolerance per data-model §5.2:
  ///
  /// - columns are matched **by header name** (any order); unknown columns are
  ///   ignored, missing columns are treated as empty;
  /// - only `variety_label` is required — a row without it is skipped;
  /// - unknown enum values fall back to a safe default (`lot_type` → seed,
  ///   `offer_status` → private) or are dropped (`quantity_kind`, `presentation`);
  /// - rows sharing a non-empty `variety_id` collapse into one variety holding
  ///   several lots; rows without a `variety_id` each become their own variety.
  ///
  /// Sync metadata (`variety_id`/`lot_id`/timestamps) and the derived
  /// `latest_germination_rate` are **not** carried over: the repository stamps
  /// fresh ids and HLCs so an import never overwrites existing rows.
  CsvImport decode(String source) {
    final table = _parse(source);
    if (table.length < 2) return const CsvImport(varieties: [], skippedRows: 0);

    final header = table.first;
    final index = <String, int>{};
    for (var i = 0; i < header.length; i++) {
      index[header[i].trim()] = i;
    }
    String? cell(List<String> row, String name) {
      final i = index[name];
      if (i == null || i >= row.length) return null;
      final value = row[i].trim();
      return value.isEmpty ? null : value;
    }

    final byGroup = <String, _CsvVarietyBuilder>{};
    final builders = <_CsvVarietyBuilder>[];
    var skipped = 0;

    for (final row in table.skip(1)) {
      if (row.every((c) => c.trim().isEmpty)) continue; // blank line
      final label = cell(row, 'variety_label');
      if (label == null) {
        skipped++;
        continue;
      }

      final groupId = cell(row, 'variety_id');
      _CsvVarietyBuilder builder;
      if (groupId != null && byGroup.containsKey(groupId)) {
        builder = byGroup[groupId]!;
      } else {
        builder = _CsvVarietyBuilder(
          label: label,
          category: cell(row, 'category'),
          scientificName: cell(row, 'species_scientific_name'),
          cultivarName: cell(row, 'cultivar_name'),
          notes: cell(row, 'variety_notes'),
          vernacularNames: _parseVernacular(cell(row, 'vernacular_names')),
          links: _parseLinks(cell(row, 'external_links')),
          needsReproduction: _parseBool(cell(row, 'needs_reproduction')),
          sowMonths: _parseMonths(cell(row, 'sow_months')),
          transplantMonths: _parseMonths(cell(row, 'transplant_months')),
          floweringMonths: _parseMonths(cell(row, 'flowering_months')),
          fruitingMonths: _parseMonths(cell(row, 'fruiting_months')),
          seedHarvestMonths: _parseMonths(cell(row, 'seed_harvest_months')),
        );
        builders.add(builder);
        if (groupId != null) byGroup[groupId] = builder;
      }

      final lot = _parseLot(row, cell);
      if (lot != null) builder.lots.add(lot);
    }

    return CsvImport(
      varieties: [for (final b in builders) b.build()],
      skippedRows: skipped,
    );
  }

  CsvImportLot? _parseLot(
    List<String> row,
    String? Function(List<String>, String) cell,
  ) {
    final typeCell = cell(row, 'lot_type');
    final year = cell(row, 'harvest_year');
    final month = cell(row, 'harvest_month');
    final kind = cell(row, 'quantity_kind');
    final precise = cell(row, 'quantity_precise');
    final label = cell(row, 'quantity_label');
    final presentation = cell(row, 'presentation');
    final storage = cell(row, 'storage_location');
    final offer = cell(row, 'offer_status');
    final originName = cell(row, 'origin_name');
    final originPlace = cell(row, 'origin_place');
    final abundance = cell(row, 'abundance');
    final preservationFormat = cell(row, 'preservation_format');

    final hasLot = [
      typeCell,
      year,
      month,
      kind,
      precise,
      label,
      presentation,
      storage,
      offer,
      originName,
      originPlace,
      abundance,
      preservationFormat,
    ].any((c) => c != null);
    if (!hasLot) return null;

    return CsvImportLot(
      type: _enumOr(LotType.values, typeCell, LotType.seed),
      harvestYear: year == null ? null : int.tryParse(year),
      harvestMonth: month == null ? null : int.tryParse(month),
      quantityKind: _enumName(QuantityKind.values, kind),
      quantityPrecise: precise == null ? null : double.tryParse(precise),
      quantityLabel: label,
      presentation: _enumOrNull(Presentation.values, presentation),
      storageLocation: storage,
      offerStatus: _enumOr(OfferStatus.values, offer, OfferStatus.private),
      originName: originName,
      originPlace: originPlace,
      abundance: _enumOrNull(Abundance.values, abundance),
      preservationFormat: _enumOrNull(
        PreservationFormat.values,
        preservationFormat,
      ),
    );
  }

  /// Parses a boolean cell: `true`/`yes`/`1` (case-insensitive) → true,
  /// anything else (including null) → false.
  bool _parseBool(String? cell) {
    final v = cell?.trim().toLowerCase();
    return v == 'true' || v == 'yes' || v == '1';
  }

  /// Parses a crop-calendar months cell (month numbers separated by spaces,
  /// commas or semicolons, e.g. `3 4 9`) into a 12-bit mask, or null when empty.
  int? _parseMonths(String? cell) {
    if (cell == null) return null;
    final months = <int>[];
    for (final part in cell.split(RegExp(r'[\s,;]+'))) {
      final n = int.tryParse(part.trim());
      if (n != null) months.add(n);
    }
    return monthsToMask(months);
  }

  /// `name (lang); name2` → vernacular names; the trailing `(xx)` is the ISO
  /// language, mirroring the export join in [encode].
  List<CsvImportName> _parseVernacular(String? cell) {
    if (cell == null) return const [];
    final out = <CsvImportName>[];
    for (final part in cell.split(';')) {
      final text = part.trim();
      if (text.isEmpty) continue;
      final match = RegExp(r'^(.*)\(([^()]+)\)$').firstMatch(text);
      if (match != null) {
        out.add(
          CsvImportName(
            name: match.group(1)!.trim(),
            language: match.group(2)!.trim(),
          ),
        );
      } else {
        out.add(CsvImportName(name: text));
      }
    }
    return out;
  }

  /// `title|url; url2` → external links, mirroring the export join in [encode].
  List<CsvImportLink> _parseLinks(String? cell) {
    if (cell == null) return const [];
    final out = <CsvImportLink>[];
    for (final part in cell.split(';')) {
      final text = part.trim();
      if (text.isEmpty) continue;
      final pipe = text.indexOf('|');
      if (pipe >= 0) {
        out.add(
          CsvImportLink(
            url: text.substring(pipe + 1).trim(),
            title: text.substring(0, pipe).trim(),
          ),
        );
      } else {
        out.add(CsvImportLink(url: text));
      }
    }
    return out;
  }

  /// Returns [name] only if it is a valid enum value name, else null — used to
  /// keep `quantity_kind` as the stored enum key while dropping unknown ones.
  String? _enumName<T extends Enum>(List<T> values, String? name) {
    if (name == null) return null;
    return values.asNameMap().containsKey(name) ? name : null;
  }

  T _enumOr<T extends Enum>(List<T> values, String? name, T fallback) =>
      _enumOrNull(values, name) ?? fallback;

  T? _enumOrNull<T extends Enum>(List<T> values, String? name) {
    if (name == null) return null;
    return values.asNameMap()[name];
  }

  /// A minimal RFC 4180 parser: fields may be quoted, quotes inside a quoted
  /// field are doubled, and quoted fields may contain commas and newlines.
  /// Handles both `\r\n` and `\n` line endings; a trailing newline does not
  /// produce a spurious empty row.
  List<List<String>> _parse(String input) {
    final rows = <List<String>>[];
    var row = <String>[];
    final field = StringBuffer();
    var inQuotes = false;
    var i = 0;
    while (i < input.length) {
      final c = input[i];
      if (inQuotes) {
        if (c == '"') {
          if (i + 1 < input.length && input[i + 1] == '"') {
            field.write('"');
            i += 2;
            continue;
          }
          inQuotes = false;
          i++;
          continue;
        }
        field.write(c);
        i++;
        continue;
      }
      if (c == '"') {
        inQuotes = true;
        i++;
        continue;
      }
      if (c == ',') {
        row.add(field.toString());
        field.clear();
        i++;
        continue;
      }
      if (c == '\r' || c == '\n') {
        row.add(field.toString());
        field.clear();
        rows.add(row);
        row = <String>[];
        if (c == '\r' && i + 1 < input.length && input[i + 1] == '\n') {
          i += 2;
        } else {
          i++;
        }
        continue;
      }
      field.write(c);
      i++;
    }
    row.add(field.toString());
    final trailingEmpty =
        rows.isNotEmpty && row.length == 1 && row.first.isEmpty;
    if (!trailingEmpty) rows.add(row);
    return rows;
  }
}

/// The result of decoding a CSV for an additive import: the varieties to add
/// (each with its lots, names and links) plus how many rows were skipped for
/// lacking a `variety_label`.
class CsvImport {
  const CsvImport({required this.varieties, required this.skippedRows});

  final List<CsvImportVariety> varieties;
  final int skippedRows;
}

/// One variety parsed from CSV, to be inserted fresh (new id + HLC).
class CsvImportVariety {
  const CsvImportVariety({
    required this.label,
    this.category,
    this.scientificName,
    this.cultivarName,
    this.notes,
    this.vernacularNames = const [],
    this.links = const [],
    this.lots = const [],
    this.needsReproduction = false,
    this.sowMonths,
    this.transplantMonths,
    this.floweringMonths,
    this.fruitingMonths,
    this.seedHarvestMonths,
  });

  final String label;
  final String? category;
  final String? scientificName;
  final String? cultivarName;
  final String? notes;
  final List<CsvImportName> vernacularNames;
  final List<CsvImportLink> links;
  final List<CsvImportLot> lots;
  final bool needsReproduction;
  final int? sowMonths;
  final int? transplantMonths;
  final int? floweringMonths;
  final int? fruitingMonths;
  final int? seedHarvestMonths;
}

/// One lot parsed from a CSV row.
class CsvImportLot {
  const CsvImportLot({
    this.type = LotType.seed,
    this.harvestYear,
    this.harvestMonth,
    this.quantityKind,
    this.quantityPrecise,
    this.quantityLabel,
    this.presentation,
    this.storageLocation,
    this.offerStatus = OfferStatus.private,
    this.originName,
    this.originPlace,
    this.abundance,
    this.preservationFormat,
  });

  final LotType type;
  final int? harvestYear;
  final int? harvestMonth;
  final String? quantityKind;
  final double? quantityPrecise;
  final String? quantityLabel;
  final Presentation? presentation;
  final String? storageLocation;
  final OfferStatus offerStatus;
  final String? originName;
  final String? originPlace;
  final Abundance? abundance;
  final PreservationFormat? preservationFormat;
}

/// One vernacular name parsed from CSV.
class CsvImportName {
  const CsvImportName({required this.name, this.language});

  final String name;
  final String? language;
}

/// One external link parsed from CSV.
class CsvImportLink {
  const CsvImportLink({required this.url, this.title});

  final String url;
  final String? title;
}

/// Mutable accumulator: several CSV rows can contribute lots to one variety.
class _CsvVarietyBuilder {
  _CsvVarietyBuilder({
    required this.label,
    this.category,
    this.scientificName,
    this.cultivarName,
    this.notes,
    this.vernacularNames = const [],
    this.links = const [],
    this.needsReproduction = false,
    this.sowMonths,
    this.transplantMonths,
    this.floweringMonths,
    this.fruitingMonths,
    this.seedHarvestMonths,
  });

  final String label;
  final String? category;
  final String? scientificName;
  final String? cultivarName;
  final String? notes;
  final List<CsvImportName> vernacularNames;
  final List<CsvImportLink> links;
  final bool needsReproduction;
  final int? sowMonths;
  final int? transplantMonths;
  final int? floweringMonths;
  final int? fruitingMonths;
  final int? seedHarvestMonths;
  final List<CsvImportLot> lots = [];

  CsvImportVariety build() => CsvImportVariety(
    label: label,
    category: category,
    scientificName: scientificName,
    cultivarName: cultivarName,
    notes: notes,
    vernacularNames: vernacularNames,
    links: links,
    needsReproduction: needsReproduction,
    sowMonths: sowMonths,
    transplantMonths: transplantMonths,
    floweringMonths: floweringMonths,
    fruitingMonths: fruitingMonths,
    seedHarvestMonths: seedHarvestMonths,
    lots: List.unmodifiable(lots),
  );
}
