import 'dart:convert';
import 'dart:typed_data';

import 'package:commons_core/commons_core.dart';

import '../data/export_import/inventory_csv_codec.dart';
import '../data/export_import/inventory_json_codec.dart';
import '../data/export_import/inventory_snapshot.dart';
import '../data/variety_repository.dart';
import '../security/secure_key_store.dart';
import 'file_service.dart';

/// A picked backup file awaiting restore — kept in memory so asking the user
/// for a recovery code never forces a second file dialog.
class PendingBackup {
  const PendingBackup(this.bytes);

  final Uint8List bytes;

  /// Sealed (encrypted) copy vs. a legacy plain interchange export.
  bool get isSealed => BackupBox.looksSealed(bytes);
}

/// End-to-end export/import flows: repository snapshot ↔ codec ↔ the file the
/// user picked. UI-free — screens call these and render the outcome.
///
/// Backups are **sealed** (encrypted under a key derived from the root seed;
/// backup-and-recovery.md): a copy that lands on someone's cloud folder
/// reveals nothing. The printed recovery code opens them on any device.
class ExportImportService {
  ExportImportService({
    required VarietyRepository repository,
    required FileService files,
    required SecureKeyStore keys,
    DateTime Function()? now,
  }) : _repository = repository,
       _files = files,
       _keys = keys,
       _now = now ?? DateTime.now;

  final VarietyRepository _repository;
  final FileService _files;
  final SecureKeyStore _keys;
  final DateTime Function() _now;

  static const _jsonCodec = InventoryJsonCodec();
  static const _csvCodec = InventoryCsvCodec();

  /// The backup file extension. The content is the sealed interchange JSON.
  static const backupExtension = 'tanemaki';

  /// Builds the full inventory as a sealed (encrypted) backup — the same bytes
  /// [exportBackup] writes to a file. Shared with the automatic backup so both
  /// paths seal identically.
  Future<Uint8List> buildSealedBackup() async {
    final snapshot = await _repository.exportInventory();
    final plain = utf8.encode(_jsonCodec.encode(snapshot));
    return BackupBox().seal(
      plain,
      rootSeed: _hexToBytes(await _keys.rootSeedHex()),
    );
  }

  /// Exports the full inventory as a sealed backup file. Returns true when
  /// saved, false when the user cancelled the save dialog.
  Future<bool> exportBackup() async {
    final sealed = await buildSealedBackup();
    final path = await _files.saveFile(
      suggestedName: _fileName(backupExtension),
      bytes: sealed,
    );
    return path != null;
  }

  /// Asks the user to pick a backup file. Returns null when they cancelled.
  Future<PendingBackup?> pickBackup() async {
    final bytes = await _files.pickFileBytes(
      allowedExtensions: [backupExtension, 'json'],
    );
    return bytes == null ? null : PendingBackup(bytes);
  }

  /// Restores [pending], merging by id (LWW). A sealed copy opens with this
  /// device's seed, or with the [recoveryCode] from the printed sheet — which
  /// is then adopted as the identity (recovering your bank recovers *you*).
  /// Legacy plain exports import directly.
  ///
  /// Throws [BackupAuthException] when the copy was sealed under a different
  /// seed (ask for the code and retry), and [FormatException] when the file or
  /// code is unreadable.
  Future<ImportSummary> restoreBackup(
    PendingBackup pending, {
    String? recoveryCode,
  }) async {
    Uint8List plain;
    if (pending.isSealed) {
      final seedHex = recoveryCode == null
          ? await _keys.rootSeedHex()
          : RecoveryCode.decode(recoveryCode);
      plain = await BackupBox().open(
        pending.bytes,
        rootSeed: _hexToBytes(seedHex),
      );
      if (recoveryCode != null) await _keys.adoptRootSeed(seedHex);
    } else {
      plain = pending.bytes;
    }
    final snapshot = _jsonCodec.decode(utf8.decode(plain));
    return _repository.importInventory(snapshot);
  }

  /// The printable recovery code for this device's identity seed.
  Future<String> recoveryCode() async =>
      RecoveryCode.encode(await _keys.rootSeedHex());

  /// Exports the inventory as flat CSV (export-only). Returns true when
  /// saved, false when the user cancelled the save dialog.
  Future<bool> exportCsv() async {
    final snapshot = await _repository.exportInventory();
    final path = await _files.saveFile(
      suggestedName: _fileName('csv'),
      bytes: utf8.encode(_csvCodec.encode(snapshot)),
    );
    return path != null;
  }

  /// Imports a spreadsheet CSV additively (every row becomes a new variety/lot;
  /// see [VarietyRepository.importCsv]). Returns the summary (`inserted` =
  /// varieties added), or null when the user cancelled the file dialog.
  Future<ImportSummary?> importCsv() async {
    final bytes = await _files.pickFileBytes(allowedExtensions: ['csv']);
    if (bytes == null) return null;
    final csv = _csvCodec.decode(utf8.decode(bytes));
    return _repository.importCsv(csv);
  }

  String _fileName(String extension) {
    final date = _now().toIso8601String().substring(0, 10);
    return 'tanemaki-backup-$date.$extension';
  }

  static List<int> _hexToBytes(String hex) => [
    for (var i = 0; i < hex.length; i += 2)
      int.parse(hex.substring(i, i + 2), radix: 16),
  ];
}
