import 'dart:convert';

import 'package:drift/drift.dart' show Uint8List;

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

/// Encodes/decodes an [InventorySnapshot] to/from the versioned interchange
/// JSON (data-model §7). Pure serialization — no I/O, no database.
///
/// Reader tolerance follows data-model §5.2: unknown JSON fields are ignored;
/// unknown enum values fall back to a safe default, or drop the row when no
/// default makes sense (e.g. an unknown movement type). Photo/doc bytes are
/// embedded as base64 — the JSON export is the complete interchange backup.
///
/// Throws [FormatException] on malformed input or a `formatVersion` newer
/// than [inventoryFormatVersion].
class InventoryJsonCodec {
  const InventoryJsonCodec();

  String encode(InventorySnapshot snapshot) {
    return const JsonEncoder.withIndent('  ').convert({
      'formatVersion': inventoryFormatVersion,
      'schemaVersion': AppDatabase.currentSchemaVersion,
      'appId': inventoryAppId,
      'varieties': [
        for (final v in snapshot.varieties)
          {
            ..._syncMeta(
              id: v.id,
              createdAt: v.createdAt,
              updatedAt: v.updatedAt,
              lastAuthor: v.lastAuthor,
              schemaRowVersion: v.schemaRowVersion,
              isDeleted: v.isDeleted,
            ),
            'label': v.label,
            'speciesId': v.speciesId,
            // Species ids are per-install; the scientific name is what an
            // importer can re-resolve against its own bundled catalog.
            'speciesScientificName': snapshot.speciesNamesById[v.speciesId],
            'cultivarName': v.cultivarName,
            'category': v.category,
            'notes': v.notes,
            'isDraft': v.isDraft,
            'isOrganic': v.isOrganic,
            'needsReproduction': v.needsReproduction,
            'sowMonths': v.sowMonths,
            'transplantMonths': v.transplantMonths,
            'floweringMonths': v.floweringMonths,
            'fruitingMonths': v.fruitingMonths,
            'seedHarvestMonths': v.seedHarvestMonths,
          },
      ],
      'lots': [
        for (final l in snapshot.lots)
          {
            ..._syncMeta(
              id: l.id,
              createdAt: l.createdAt,
              updatedAt: l.updatedAt,
              lastAuthor: l.lastAuthor,
              schemaRowVersion: l.schemaRowVersion,
              isDeleted: l.isDeleted,
            ),
            'varietyId': l.varietyId,
            'type': l.type.name,
            'harvestYear': l.harvestYear,
            'harvestMonth': l.harvestMonth,
            'quantityKind': l.quantityKind,
            'quantityPrecise': l.quantityPrecise,
            'quantityLabel': l.quantityLabel,
            'presentation': l.presentation?.name,
            'storageLocation': l.storageLocation,
            'offerStatus': l.offerStatus.name,
            'seedbankId': l.seedbankId,
            'originName': l.originName,
            'originPlace': l.originPlace,
            'abundance': l.abundance?.name,
            'preservationFormat': l.preservationFormat?.name,
          },
      ],
      'vernacularNames': [
        for (final n in snapshot.vernacularNames)
          {
            ..._syncMeta(
              id: n.id,
              createdAt: n.createdAt,
              updatedAt: n.updatedAt,
              lastAuthor: n.lastAuthor,
              schemaRowVersion: n.schemaRowVersion,
              isDeleted: n.isDeleted,
            ),
            'varietyId': n.varietyId,
            'name': n.name,
            'language': n.language,
            'region': n.region,
          },
      ],
      'externalLinks': [
        for (final e in snapshot.externalLinks)
          {
            ..._syncMeta(
              id: e.id,
              createdAt: e.createdAt,
              updatedAt: e.updatedAt,
              lastAuthor: e.lastAuthor,
              schemaRowVersion: e.schemaRowVersion,
              isDeleted: e.isDeleted,
            ),
            'parentType': e.parentType.name,
            'parentId': e.parentId,
            'url': e.url,
            'title': e.title,
          },
      ],
      'germinationTests': [
        for (final g in snapshot.germinationTests)
          {
            ..._syncMeta(
              id: g.id,
              createdAt: g.createdAt,
              updatedAt: g.updatedAt,
              lastAuthor: g.lastAuthor,
              schemaRowVersion: g.schemaRowVersion,
              isDeleted: g.isDeleted,
            ),
            'lotId': g.lotId,
            'testedOn': g.testedOn,
            'sampleSize': g.sampleSize,
            'germinatedCount': g.germinatedCount,
            'notes': g.notes,
          },
      ],
      'conditionChecks': [
        for (final c in snapshot.conditionChecks)
          {
            ..._syncMeta(
              id: c.id,
              createdAt: c.createdAt,
              updatedAt: c.updatedAt,
              lastAuthor: c.lastAuthor,
              schemaRowVersion: c.schemaRowVersion,
              isDeleted: c.isDeleted,
            ),
            'lotId': c.lotId,
            'checkedOn': c.checkedOn,
            'containerCount': c.containerCount,
            'desiccantState': c.desiccantState?.name,
            'notes': c.notes,
          },
      ],
      'movements': [
        for (final m in snapshot.movements)
          {
            // Append-only rows: no updatedAt/isDeleted.
            'id': m.id,
            'createdAt': m.createdAt,
            'lastAuthor': m.lastAuthor,
            'schemaRowVersion': m.schemaRowVersion,
            'lotId': m.lotId,
            'type': m.type.name,
            'occurredOn': m.occurredOn,
            'counterpartyId': m.counterpartyId,
            'quantityKind': m.quantityKind,
            'quantityPrecise': m.quantityPrecise,
            'quantityLabel': m.quantityLabel,
            'parentMovementId': m.parentMovementId,
            'plantareId': m.plantareId,
            'notes': m.notes,
          },
      ],
      'parties': [
        for (final p in snapshot.parties)
          {
            ..._syncMeta(
              id: p.id,
              createdAt: p.createdAt,
              updatedAt: p.updatedAt,
              lastAuthor: p.lastAuthor,
              schemaRowVersion: p.schemaRowVersion,
              isDeleted: p.isDeleted,
            ),
            'displayName': p.displayName,
            'publicKey': p.publicKey,
            'kind': p.kind.name,
            'note': p.note,
          },
      ],
      'attachments': [
        for (final a in snapshot.attachments)
          {
            ..._syncMeta(
              id: a.id,
              createdAt: a.createdAt,
              updatedAt: a.updatedAt,
              lastAuthor: a.lastAuthor,
              schemaRowVersion: a.schemaRowVersion,
              isDeleted: a.isDeleted,
            ),
            'parentType': a.parentType.name,
            'parentId': a.parentId,
            'kind': a.kind.name,
            'uri': a.uri,
            'bytesBase64': a.bytes == null ? null : base64Encode(a.bytes!),
            'mimeType': a.mimeType,
            'sortOrder': a.sortOrder,
          },
      ],
      'plantares': [
        for (final p in snapshot.plantares)
          {
            ..._syncMeta(
              id: p.id,
              createdAt: p.createdAt,
              updatedAt: p.updatedAt,
              lastAuthor: p.lastAuthor,
              schemaRowVersion: p.schemaRowVersion,
              isDeleted: p.isDeleted,
            ),
            'varietyId': p.varietyId,
            'direction': p.direction.name,
            'counterparty': p.counterparty,
            'owedDescription': p.owedDescription,
            'madeOn': p.madeOn,
            'dueBy': p.dueBy,
            'status': p.status.name,
            'settledOn': p.settledOn,
            'note': p.note,
          },
      ],
      'sales': [
        for (final s in snapshot.sales)
          {
            ..._syncMeta(
              id: s.id,
              createdAt: s.createdAt,
              updatedAt: s.updatedAt,
              lastAuthor: s.lastAuthor,
              schemaRowVersion: s.schemaRowVersion,
              isDeleted: s.isDeleted,
            ),
            'varietyId': s.varietyId,
            'direction': s.direction.name,
            'counterparty': s.counterparty,
            'amount': s.amount,
            'currency': s.currency,
            'soldOn': s.soldOn,
            'note': s.note,
          },
      ],
    });
  }

  InventorySnapshot decode(String source) {
    final Object? root;
    try {
      root = jsonDecode(source);
    } on FormatException {
      throw const FormatException('Not a valid JSON file.');
    }
    if (root is! Map<String, dynamic>) {
      throw const FormatException('Not a Tanemaki inventory file.');
    }
    final version = root['formatVersion'];
    if (version is! int) {
      throw const FormatException('Missing formatVersion.');
    }
    if (version > inventoryFormatVersion) {
      throw FormatException(
        'This file uses format v$version; this app reads up to '
        'v$inventoryFormatVersion. Update the app to import it.',
      );
    }

    final speciesNamesById = <String, String>{};
    final varieties = _rows(root, 'varieties', (m) {
      final speciesId = m['speciesId'] as String?;
      final scientificName = m['speciesScientificName'] as String?;
      if (speciesId != null && scientificName != null) {
        speciesNamesById[speciesId] = scientificName;
      }
      return Variety(
        id: _string(m, 'id'),
        createdAt: _int(m, 'createdAt'),
        updatedAt: _string(m, 'updatedAt'),
        lastAuthor: _string(m, 'lastAuthor'),
        isDeleted: _bool(m, 'isDeleted'),
        schemaRowVersion: _int(m, 'schemaRowVersion', fallback: 1),
        label: _string(m, 'label'),
        speciesId: speciesId,
        cultivarName: m['cultivarName'] as String?,
        category: m['category'] as String?,
        notes: m['notes'] as String?,
        isDraft: m['isDraft'] == true,
        isOrganic: m['isOrganic'] == true,
        needsReproduction: m['needsReproduction'] == true,
        sowMonths: m['sowMonths'] as int?,
        transplantMonths: m['transplantMonths'] as int?,
        floweringMonths: m['floweringMonths'] as int?,
        fruitingMonths: m['fruitingMonths'] as int?,
        seedHarvestMonths: m['seedHarvestMonths'] as int?,
      );
    });

    return InventorySnapshot(
      varieties: varieties,
      speciesNamesById: speciesNamesById,
      lots: _rows(root, 'lots', (m) {
        return Lot(
          id: _string(m, 'id'),
          createdAt: _int(m, 'createdAt'),
          updatedAt: _string(m, 'updatedAt'),
          lastAuthor: _string(m, 'lastAuthor'),
          isDeleted: _bool(m, 'isDeleted'),
          schemaRowVersion: _int(m, 'schemaRowVersion', fallback: 1),
          varietyId: _string(m, 'varietyId'),
          // Unknown enum values → safe defaults (data-model §5.2).
          type: _enumOr(LotType.values, m['type'], LotType.seed),
          harvestYear: m['harvestYear'] as int?,
          harvestMonth: m['harvestMonth'] as int?,
          quantityKind: m['quantityKind'] as String?,
          quantityPrecise: (m['quantityPrecise'] as num?)?.toDouble(),
          quantityLabel: m['quantityLabel'] as String?,
          presentation: _enumOrNull(Presentation.values, m['presentation']),
          storageLocation: m['storageLocation'] as String?,
          offerStatus: _enumOr(
            OfferStatus.values,
            m['offerStatus'],
            OfferStatus.private,
          ),
          seedbankId: m['seedbankId'] as String?,
          originName: m['originName'] as String?,
          originPlace: m['originPlace'] as String?,
          abundance: _enumOrNull(Abundance.values, m['abundance']),
          preservationFormat: _enumOrNull(
            PreservationFormat.values,
            m['preservationFormat'],
          ),
        );
      }),
      vernacularNames: _rows(root, 'vernacularNames', (m) {
        return VarietyVernacularName(
          id: _string(m, 'id'),
          createdAt: _int(m, 'createdAt'),
          updatedAt: _string(m, 'updatedAt'),
          lastAuthor: _string(m, 'lastAuthor'),
          isDeleted: _bool(m, 'isDeleted'),
          schemaRowVersion: _int(m, 'schemaRowVersion', fallback: 1),
          varietyId: _string(m, 'varietyId'),
          name: _string(m, 'name'),
          language: m['language'] as String?,
          region: m['region'] as String?,
        );
      }),
      externalLinks: _rows(root, 'externalLinks', (m) {
        final parentType = _enumOrNull(ParentType.values, m['parentType']);
        if (parentType == null) return null; // no safe default → drop row
        return ExternalLink(
          id: _string(m, 'id'),
          createdAt: _int(m, 'createdAt'),
          updatedAt: _string(m, 'updatedAt'),
          lastAuthor: _string(m, 'lastAuthor'),
          isDeleted: _bool(m, 'isDeleted'),
          schemaRowVersion: _int(m, 'schemaRowVersion', fallback: 1),
          parentType: parentType,
          parentId: _string(m, 'parentId'),
          url: _string(m, 'url'),
          title: m['title'] as String?,
        );
      }),
      germinationTests: _rows(root, 'germinationTests', (m) {
        return GerminationTest(
          id: _string(m, 'id'),
          createdAt: _int(m, 'createdAt'),
          updatedAt: _string(m, 'updatedAt'),
          lastAuthor: _string(m, 'lastAuthor'),
          isDeleted: _bool(m, 'isDeleted'),
          schemaRowVersion: _int(m, 'schemaRowVersion', fallback: 1),
          lotId: _string(m, 'lotId'),
          testedOn: m['testedOn'] as int?,
          sampleSize: m['sampleSize'] as int?,
          germinatedCount: m['germinatedCount'] as int?,
          notes: m['notes'] as String?,
        );
      }),
      conditionChecks: _rows(root, 'conditionChecks', (m) {
        return ConditionCheck(
          id: _string(m, 'id'),
          createdAt: _int(m, 'createdAt'),
          updatedAt: _string(m, 'updatedAt'),
          lastAuthor: _string(m, 'lastAuthor'),
          isDeleted: _bool(m, 'isDeleted'),
          schemaRowVersion: _int(m, 'schemaRowVersion', fallback: 1),
          lotId: _string(m, 'lotId'),
          checkedOn: m['checkedOn'] as int?,
          containerCount: m['containerCount'] as int?,
          desiccantState: _enumOrNull(
            DesiccantState.values,
            m['desiccantState'],
          ),
          notes: m['notes'] as String?,
        );
      }),
      movements: _rows(root, 'movements', (m) {
        final type = _enumOrNull(MovementType.values, m['type']);
        if (type == null) return null; // no safe default → drop row
        return Movement(
          id: _string(m, 'id'),
          createdAt: _int(m, 'createdAt'),
          lastAuthor: _string(m, 'lastAuthor'),
          schemaRowVersion: _int(m, 'schemaRowVersion', fallback: 1),
          lotId: _string(m, 'lotId'),
          type: type,
          occurredOn: m['occurredOn'] as int?,
          counterpartyId: m['counterpartyId'] as String?,
          quantityKind: m['quantityKind'] as String?,
          quantityPrecise: (m['quantityPrecise'] as num?)?.toDouble(),
          quantityLabel: m['quantityLabel'] as String?,
          parentMovementId: m['parentMovementId'] as String?,
          plantareId: m['plantareId'] as String?,
          notes: m['notes'] as String?,
        );
      }),
      parties: _rows(root, 'parties', (m) {
        return Party(
          id: _string(m, 'id'),
          createdAt: _int(m, 'createdAt'),
          updatedAt: _string(m, 'updatedAt'),
          lastAuthor: _string(m, 'lastAuthor'),
          isDeleted: _bool(m, 'isDeleted'),
          schemaRowVersion: _int(m, 'schemaRowVersion', fallback: 1),
          displayName: _string(m, 'displayName'),
          publicKey: m['publicKey'] as String?,
          kind: _enumOr(PartyKind.values, m['kind'], PartyKind.person),
          note: m['note'] as String?,
        );
      }),
      attachments: _rows(root, 'attachments', (m) {
        final parentType = _enumOrNull(ParentType.values, m['parentType']);
        final kind = _enumOrNull(AttachmentKind.values, m['kind']);
        if (parentType == null || kind == null) return null; // drop row
        final bytesBase64 = m['bytesBase64'] as String?;
        return Attachment(
          id: _string(m, 'id'),
          createdAt: _int(m, 'createdAt'),
          updatedAt: _string(m, 'updatedAt'),
          lastAuthor: _string(m, 'lastAuthor'),
          isDeleted: _bool(m, 'isDeleted'),
          schemaRowVersion: _int(m, 'schemaRowVersion', fallback: 1),
          parentType: parentType,
          parentId: _string(m, 'parentId'),
          kind: kind,
          uri: m['uri'] as String?,
          bytes: bytesBase64 == null
              ? null
              : Uint8List.fromList(base64Decode(bytesBase64)),
          mimeType: m['mimeType'] as String?,
          sortOrder: _int(m, 'sortOrder', fallback: 0),
        );
      }),
      plantares: _rows(root, 'plantares', (m) {
        return Plantare(
          id: _string(m, 'id'),
          createdAt: _int(m, 'createdAt'),
          updatedAt: _string(m, 'updatedAt'),
          lastAuthor: _string(m, 'lastAuthor'),
          isDeleted: _bool(m, 'isDeleted'),
          schemaRowVersion: _int(m, 'schemaRowVersion', fallback: 1),
          varietyId: m['varietyId'] as String?,
          direction:
              _enumOr(PlantareDirection.values, m['direction'],
                  PlantareDirection.iReturn),
          counterparty: m['counterparty'] as String?,
          owedDescription: m['owedDescription'] as String?,
          madeOn: _int(m, 'madeOn'),
          dueBy: m['dueBy'] as int?,
          status:
              _enumOr(PlantareStatus.values, m['status'], PlantareStatus.open),
          settledOn: m['settledOn'] as int?,
          note: m['note'] as String?,
        );
      }),
      sales: _rows(root, 'sales', (m) {
        return Sale(
          id: _string(m, 'id'),
          createdAt: _int(m, 'createdAt'),
          updatedAt: _string(m, 'updatedAt'),
          lastAuthor: _string(m, 'lastAuthor'),
          isDeleted: _bool(m, 'isDeleted'),
          schemaRowVersion: _int(m, 'schemaRowVersion', fallback: 1),
          varietyId: m['varietyId'] as String?,
          direction:
              _enumOr(SaleDirection.values, m['direction'], SaleDirection.iSold),
          counterparty: m['counterparty'] as String?,
          amount: (m['amount'] as num?)?.toDouble(),
          currency: m['currency'] as String?,
          soldOn: _int(m, 'soldOn'),
          note: m['note'] as String?,
        );
      }),
    );
  }

  Map<String, Object?> _syncMeta({
    required String id,
    required int createdAt,
    required String updatedAt,
    required String lastAuthor,
    required int schemaRowVersion,
    bool isDeleted = false,
  }) => {
    'id': id,
    'createdAt': createdAt,
    'updatedAt': updatedAt,
    'lastAuthor': lastAuthor,
    // Only emitted for tombstones — user backups stay tombstone-free, so their
    // bytes are unchanged; the sync snapshot carries deletions so they replicate.
    if (isDeleted) 'isDeleted': true,
    'schemaRowVersion': schemaRowVersion,
  };

  /// Maps the JSON list under [key] through [decodeRow], skipping entries
  /// that are not objects, fail to decode, or return null (unknown enums).
  List<T> _rows<T>(
    Map<String, dynamic> root,
    String key,
    T? Function(Map<String, dynamic>) decodeRow,
  ) {
    final raw = root[key];
    if (raw is! List) return const [];
    final rows = <T>[];
    for (final entry in raw) {
      if (entry is! Map<String, dynamic>) continue;
      final T? row;
      try {
        row = decodeRow(entry);
      } on FormatException {
        continue; // a broken row must not abort the whole import
      }
      if (row != null) rows.add(row);
    }
    return rows;
  }

  String _string(Map<String, dynamic> m, String key) {
    final value = m[key];
    if (value is! String) throw FormatException('Missing "$key"');
    return value;
  }

  bool _bool(Map<String, dynamic> m, String key) => m[key] == true;

  int _int(Map<String, dynamic> m, String key, {int? fallback}) {
    final value = m[key];
    if (value is int) return value;
    if (fallback != null) return fallback;
    throw FormatException('Missing "$key"');
  }

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

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