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

import 'package:pointycastle/export.dart';
import 'package:pointycastle/src/utils.dart' as pc_utils;
import 'package:openhaystack_mobile/findMy/models.dart';

class DecryptReports {
  /// Decrypts a given [FindMyReport] with the given private key.
  static Future<FindMyLocationReport> decryptReport(
      FindMyReport report, Uint8List key) async {
    final curveDomainParam = ECCurve_secp224r1();

    final payloadData = report.payload;
    final ephemeralKeyBytes = payloadData.sublist(5, 62);
    final encData = payloadData.sublist(62, 72);
    final tag = payloadData.sublist(72, payloadData.length);

    _decodeTimeAndConfidence(payloadData, report);

    final privateKey = ECPrivateKey(
        pc_utils.decodeBigIntWithSign(1, key),
        curveDomainParam);

    final decodePoint = curveDomainParam.curve.decodePoint(ephemeralKeyBytes);
    final ephemeralPublicKey = ECPublicKey(decodePoint, curveDomainParam);

    final Uint8List sharedKeyBytes = _ecdh(ephemeralPublicKey, privateKey);
    final Uint8List derivedKey = _kdf(sharedKeyBytes, ephemeralKeyBytes);

    final decryptedPayload = _decryptPayload(encData, derivedKey, tag);
    final locationReport = _decodePayload(decryptedPayload, report);

    return locationReport;
  }

  /// Decodes the unencrypted timestamp and confidence
  static void _decodeTimeAndConfidence(Uint8List payloadData, FindMyReport report) {
    final seenTimeStamp = payloadData.sublist(0, 4).buffer.asByteData()
        .getInt32(0, Endian.big);
    final timestamp = DateTime(2001).add(Duration(seconds: seenTimeStamp));
    final confidence = payloadData.elementAt(4);
    report.timestamp = timestamp;
    report.confidence = confidence;
  }

  /// Performs an Elliptic Curve Diffie-Hellman with the given keys.
  /// Returns the derived raw key data.
  static Uint8List _ecdh(ECPublicKey ephemeralPublicKey, ECPrivateKey privateKey) {
    final sharedKey = ephemeralPublicKey.Q! * privateKey.d;
    final sharedKeyBytes = pc_utils.encodeBigIntAsUnsigned(
        sharedKey!.x!.toBigInteger()!);
    print("Isolate:${Isolate.current.hashCode}: Shared Key (shared secret): ${base64Encode(sharedKeyBytes)}");

    return sharedKeyBytes;
  }

  /// Decodes the raw decrypted payload and constructs and returns
  /// the resulting [FindMyLocationReport].
  static FindMyLocationReport _decodePayload(
      Uint8List payload, FindMyReport report) {

    final latitude = payload.buffer.asByteData(0, 4).getUint32(0, Endian.big);
    final longitude = payload.buffer.asByteData(4, 4).getUint32(0, Endian.big);
    final accuracy = payload.buffer.asByteData(8, 1).getUint8(0);

    final latitudeDec = latitude / 10000000.0;
    final longitudeDec = longitude / 10000000.0;

    return FindMyLocationReport(latitudeDec, longitudeDec, accuracy,
        report.datePublished, report.timestamp, report.confidence);
  }

  /// Decrypts the given cipher text with the key data using an AES-GCM block cipher.
  /// Returns the decrypted raw data.
  static Uint8List _decryptPayload(
      Uint8List cipherText, Uint8List symmetricKey, Uint8List tag) {
    final decryptionKey = symmetricKey.sublist(0, 16);
    final iv = symmetricKey.sublist(16, symmetricKey.length);

    final aesGcm = GCMBlockCipher(AESEngine())
      ..init(false, AEADParameters(KeyParameter(decryptionKey),
          tag.lengthInBytes * 8, iv, tag));

    final plainText = Uint8List(cipherText.length);
    var offset = 0;
    while (offset < cipherText.length) {
      offset += aesGcm.processBlock(cipherText, offset, plainText, offset);
    }

    assert(offset == cipherText.length);
    return plainText;
  }

  /// ANSI X.963 key derivation to calculate the actual (symmetric) advertisement
  /// key and returns the raw key data.
  static Uint8List _kdf(Uint8List secret, Uint8List ephemeralKey) {
    var shaDigest = SHA256Digest();
    shaDigest.update(secret, 0, secret.length);

    var counter = 1;
    var counterData = ByteData(4)..setUint32(0, counter);
    var counterDataBytes = counterData.buffer.asUint8List();
    shaDigest.update(counterDataBytes, 0, counterDataBytes.lengthInBytes);

    shaDigest.update(ephemeralKey, 0, ephemeralKey.lengthInBytes);

    Uint8List out = Uint8List(shaDigest.digestSize);
    shaDigest.doFinal(out, 0);

    print("Isolate:${Isolate.current.hashCode}: Derived key: ${base64Encode(out)}");
    return out;
  }
}
