import { Timestamp, SyncProtoBuf } from '@actual-app/crdt';

import * as encryption from '../encryption';
import { SyncError } from '../errors';
import * as prefs from '../prefs';

import { Message } from './index';

function coerceBuffer(value) {
  // The web encryption APIs give us back raw Uint8Array... but our
  // encryption code assumes we can work with it as a buffer. This is
  // a leaky abstraction and ideally the our abstraction over the web
  // encryption APIs should do this.
  if (!Buffer.isBuffer(value)) {
    return Buffer.from(value);
  }
  return value;
}

export async function encode(
  groupId: string,
  fileId: string,
  since: Timestamp | string,
  messages: Message[],
): Promise<Uint8Array> {
  let { encryptKeyId } = prefs.getPrefs();
  let requestPb = new SyncProtoBuf.SyncRequest();

  for (let i = 0; i < messages.length; i++) {
    let msg = messages[i];
    let envelopePb = new SyncProtoBuf.MessageEnvelope();
    envelopePb.setTimestamp(msg.timestamp.toString());

    let messagePb = new SyncProtoBuf.Message();
    messagePb.setDataset(msg.dataset);
    messagePb.setRow(msg.row);
    messagePb.setColumn(msg.column);
    messagePb.setValue(msg.value as string);
    let binaryMsg = messagePb.serializeBinary();

    if (encryptKeyId) {
      let encrypted = new SyncProtoBuf.EncryptedData();

      let result;
      try {
        result = await encryption.encrypt(binaryMsg, encryptKeyId);
      } catch (e) {
        throw new SyncError('encrypt-failure', {
          isMissingKey: e.message === 'missing-key',
        });
      }

      encrypted.setData(result.value);
      encrypted.setIv(Buffer.from(result.meta.iv, 'base64'));
      encrypted.setAuthtag(Buffer.from(result.meta.authTag, 'base64'));

      envelopePb.setContent(encrypted.serializeBinary());
      envelopePb.setIsencrypted(true);
    } else {
      envelopePb.setContent(binaryMsg);
    }

    requestPb.addMessages(envelopePb);
  }

  requestPb.setGroupid(groupId);
  requestPb.setFileid(fileId);
  requestPb.setKeyid(encryptKeyId);
  requestPb.setSince(since.toString());

  return requestPb.serializeBinary();
}

export async function decode(
  data: Uint8Array,
): Promise<{ messages: Message[]; merkle: { hash: number } }> {
  let { encryptKeyId } = prefs.getPrefs();

  let responsePb = SyncProtoBuf.SyncResponse.deserializeBinary(data);
  let merkle = JSON.parse(responsePb.getMerkle());
  let list = responsePb.getMessagesList();
  let messages = [];

  for (let i = 0; i < list.length; i++) {
    let envelopePb = list[i];
    let timestamp = Timestamp.parse(envelopePb.getTimestamp());
    let encrypted = envelopePb.getIsencrypted();
    let msg;

    if (encrypted) {
      let binary = SyncProtoBuf.EncryptedData.deserializeBinary(
        envelopePb.getContent() as Uint8Array,
      );

      let decrypted;
      try {
        decrypted = await encryption.decrypt(coerceBuffer(binary.getData()), {
          keyId: encryptKeyId,
          algorithm: 'aes-256-gcm',
          iv: coerceBuffer(binary.getIv()),
          authTag: coerceBuffer(binary.getAuthtag()),
        });
      } catch (e) {
        console.log(e);
        throw new SyncError('decrypt-failure', {
          isMissingKey: e.message === 'missing-key',
        });
      }

      msg = SyncProtoBuf.Message.deserializeBinary(decrypted);
    } else {
      msg = SyncProtoBuf.Message.deserializeBinary(
        envelopePb.getContent() as Uint8Array,
      );
    }

    messages.push({
      timestamp: timestamp,
      dataset: msg.getDataset(),
      row: msg.getRow(),
      column: msg.getColumn(),
      value: msg.getValue(),
    });
  }

  return { messages, merkle };
}