import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';

import 'package:cryptography/cryptography.dart';

/// Default scheduler data directory name under the user's home directory.
const defaultSchedulerDirectoryName = 'ADHD_Scheduler';

/// Default SQLite database file name used by local scheduler persistence.
const defaultSchedulerDatabaseFileName = 'scheduler.sqlite';

/// Default encrypted backup directory name under the scheduler data directory.
const defaultSchedulerBackupDirectoryName = 'backups';

const _formatMagic = 'ADHD_SCHEDULER_BACKUP_V1';
const _fileExtension = '.db.enc';
const _kdfIterations = 200000;
const _saltLength = 16;
const _nonceLength = 12;
const _keyLength = 32;

final _utf8 = utf8.encoder;

/// Thrown when an encrypted backup cannot be decrypted with the passphrase.
final class BackupDecryptionException implements Exception {
  /// Creates a decryption failure with optional diagnostic [message].
  const BackupDecryptionException([
    this.message = 'Backup could not be decrypted.',
  ]);

  /// Diagnostic text for logs and tests.
  final String message;

  @override
  String toString() => 'BackupDecryptionException: $message';
}

/// Thrown when backup input paths or file contents are invalid.
final class BackupException implements Exception {
  /// Creates a backup exception with diagnostic [message].
  const BackupException(this.message);

  /// Diagnostic text for logs and tests.
  final String message;

  @override
  String toString() => 'BackupException: $message';
}

/// Create an encrypted backup of the scheduler SQLite file.
///
/// Defaults:
/// - Source DB: `~/ADHD_Scheduler/scheduler.sqlite`
/// - Backup dir: `~/ADHD_Scheduler/backups`
/// - File name: `yyyymmdd_hhmm.db.enc`
Future<File> createEncryptedBackup({
  required String passphrase,
  File? sqliteFile,
  Directory? backupDirectory,
  DateTime? now,
}) async {
  final source = sqliteFile ?? defaultSchedulerSqliteFile();
  if (!await source.exists()) {
    throw BackupException('SQLite file does not exist: ${source.path}');
  }

  final destinationDirectory =
      backupDirectory ?? defaultSchedulerBackupDirectory();
  await destinationDirectory.create(recursive: true);

  final timestamp = _backupTimestamp(now ?? DateTime.now());
  final destination = await _nextAvailableBackupFile(
    destinationDirectory,
    '$timestamp$_fileExtension',
  );

  final plaintext = await source.readAsBytes();
  final encoded = await _encryptBytes(
    passphrase: passphrase,
    plaintext: plaintext,
  );
  return destination.writeAsBytes(encoded, flush: true);
}

/// Restore an encrypted backup over the scheduler SQLite file.
///
/// Defaults to restoring into `~/ADHD_Scheduler/scheduler.sqlite`. Tests and
/// tools can pass [targetFile] to restore into a temp or alternate database.
Future<void> restoreEncryptedBackup(
  File backup,
  String passphrase, {
  File? targetFile,
}) async {
  if (!await backup.exists()) {
    throw BackupException('Backup file does not exist: ${backup.path}');
  }

  final plaintext = await _decryptBytes(
    passphrase: passphrase,
    encoded: await backup.readAsBytes(),
  );
  final target = targetFile ?? defaultSchedulerSqliteFile();
  await target.parent.create(recursive: true);

  final temporary = File('${target.path}.restore.tmp');
  await temporary.writeAsBytes(plaintext, flush: true);
  if (await target.exists()) {
    await target.delete();
  }
  await temporary.rename(target.path);
}

/// Default scheduler SQLite database path.
File defaultSchedulerSqliteFile() {
  return File(_joinPath(
    _homeDirectory().path,
    defaultSchedulerDirectoryName,
    defaultSchedulerDatabaseFileName,
  ));
}

/// Default encrypted backup output directory.
Directory defaultSchedulerBackupDirectory() {
  return Directory(_joinPath(
    _homeDirectory().path,
    defaultSchedulerDirectoryName,
    defaultSchedulerBackupDirectoryName,
  ));
}

Future<List<int>> _encryptBytes({
  required String passphrase,
  required List<int> plaintext,
}) async {
  final salt = _randomBytes(_saltLength);
  final nonce = _randomBytes(_nonceLength);
  final key = await _deriveKey(passphrase: passphrase, salt: salt);
  final algorithm = AesGcm.with256bits();
  final secretBox = await algorithm.encrypt(
    plaintext,
    secretKey: key,
    nonce: nonce,
  );
  final envelope = <String, Object?>{
    'magic': _formatMagic,
    'kdf': 'pbkdf2-hmac-sha256',
    'iterations': _kdfIterations,
    'cipher': 'aes-256-gcm',
    'salt': base64Encode(salt),
    'nonce': base64Encode(secretBox.nonce),
    'ciphertext': base64Encode(secretBox.cipherText),
    'mac': base64Encode(secretBox.mac.bytes),
  };
  return _utf8.convert(jsonEncode(envelope));
}

Future<List<int>> _decryptBytes({
  required String passphrase,
  required List<int> encoded,
}) async {
  try {
    final envelope = jsonDecode(utf8.decode(encoded)) as Map<String, Object?>;
    if (envelope['magic'] != _formatMagic ||
        envelope['kdf'] != 'pbkdf2-hmac-sha256' ||
        envelope['iterations'] != _kdfIterations ||
        envelope['cipher'] != 'aes-256-gcm') {
      throw const BackupDecryptionException('Unsupported backup format.');
    }

    final salt = base64Decode(envelope['salt'] as String);
    final nonce = base64Decode(envelope['nonce'] as String);
    final cipherText = base64Decode(envelope['ciphertext'] as String);
    final mac = Mac(base64Decode(envelope['mac'] as String));
    final key = await _deriveKey(passphrase: passphrase, salt: salt);
    final algorithm = AesGcm.with256bits();
    return await algorithm.decrypt(
      SecretBox(cipherText, nonce: nonce, mac: mac),
      secretKey: key,
    );
  } on BackupDecryptionException {
    rethrow;
  } on Object {
    throw const BackupDecryptionException();
  }
}

Future<SecretKey> _deriveKey({
  required String passphrase,
  required List<int> salt,
}) {
  final algorithm = Pbkdf2(
    macAlgorithm: Hmac.sha256(),
    iterations: _kdfIterations,
    bits: _keyLength * 8,
  );
  return algorithm.deriveKey(
    secretKey: SecretKey(utf8.encode(passphrase)),
    nonce: salt,
  );
}

List<int> _randomBytes(int length) {
  final random = Random.secure();
  return Uint8List.fromList(
    List<int>.generate(length, (_) => random.nextInt(256)),
  );
}

Future<File> _nextAvailableBackupFile(Directory directory, String name) async {
  final preferred = File(_joinPath(directory.path, name));
  if (!await preferred.exists()) {
    return preferred;
  }
  final dot = name.lastIndexOf('.');
  final stem = dot == -1 ? name : name.substring(0, dot);
  final extension = dot == -1 ? '' : name.substring(dot);
  var counter = 2;
  while (true) {
    final candidate =
        File(_joinPath(directory.path, '$stem-$counter$extension'));
    if (!await candidate.exists()) {
      return candidate;
    }
    counter += 1;
  }
}

String _backupTimestamp(DateTime value) {
  final local = value.toLocal();
  return '${_four(local.year)}${_two(local.month)}${_two(local.day)}_'
      '${_two(local.hour)}${_two(local.minute)}';
}

String _four(int value) => value.toString().padLeft(4, '0');

String _two(int value) => value.toString().padLeft(2, '0');

Directory _homeDirectory() {
  final home = Platform.environment['HOME'] ??
      Platform.environment['USERPROFILE'] ??
      Platform.environment['HOMEDRIVE'];
  if (home == null || home.trim().isEmpty) {
    throw const BackupException('Home directory could not be resolved.');
  }
  return Directory(home);
}

String _joinPath(String first, String second, [String? third]) {
  final separator = Platform.pathSeparator;
  final left = first.endsWith(separator)
      ? first.substring(0, first.length - separator.length)
      : first;
  final middle = second.startsWith(separator) ? second.substring(1) : second;
  final joined = '$left$separator$middle';
  if (third == null) {
    return joined;
  }
  final right = third.startsWith(separator) ? third.substring(1) : third;
  return '$joined$separator$right';
}
