/* global
  Signal,
  libsignal,
  StringView,
  dcodeIO,
  libloki,
  log,
  crypto,
  textsecure
*/

/* eslint-disable more/no-then */

const toHex = buffer => StringView.arrayBufferToHex(buffer);

const fromHex = hex => dcodeIO.ByteBuffer.wrap(hex, 'hex').toArrayBuffer();

async function saveSenderKeysInner(
  groupId,
  senderIdentity,
  chainKey,
  keyIdx,
  messageKeys
) {
  const ratchet = {
    chainKey,
    messageKeys,
    idx: keyIdx,
  };

  await Signal.Data.createOrUpdateSenderKeys({
    groupId,
    senderIdentity,
    ratchet,
  });

  log.debug(
    `Saving sender keys for groupId ${groupId}, sender ${senderIdentity}`
  );
}

// Save somebody else's key
async function saveSenderKeys(groupId, senderIdentity, chainKey, keyIdx) {
  const messageKeys = {};
  await saveSenderKeysInner(
    groupId,
    senderIdentity,
    chainKey,
    keyIdx,
    messageKeys
  );
}

async function createSenderKeyForGroup(groupId, senderIdentity) {
  // Generate Chain Key (32 random bytes)
  const rootChainKey = await libsignal.crypto.getRandomBytes(32);
  const rootChainKeyHex = toHex(rootChainKey);

  const keyIdx = 0;
  const messageKeys = {};

  await saveSenderKeysInner(
    groupId,
    senderIdentity,
    rootChainKeyHex,
    keyIdx,
    messageKeys
  );

  return rootChainKeyHex;
}

async function hmacSHA256(keybuf, data) {
  const key = await crypto.subtle.importKey(
    'raw',
    keybuf,
    { name: 'HMAC', hash: { name: 'SHA-256' } },
    false,
    ['sign']
  );

  return crypto.subtle.sign({ name: 'HMAC', hash: 'SHA-256' }, key, data);
}

async function stepRatchet(ratchet) {
  const { chainKey, keyIdx, messageKeys } = ratchet;

  const byteArray = new Uint8Array(1);
  byteArray[0] = 1;
  const messageKey = await hmacSHA256(chainKey, byteArray.buffer);

  byteArray[0] = 2;
  const nextChainKey = await hmacSHA256(chainKey, byteArray.buffer);

  const nextKeyIdx = keyIdx + 1;

  return { nextChainKey, messageKey, nextKeyIdx, messageKeys };
}

async function stepRatchetOnce(groupId, senderIdentity) {
  const ratchet = await loadChainKey(groupId, senderIdentity);

  if (!ratchet) {
    log.error(
      `Could not find ratchet for groupId ${groupId} sender: ${senderIdentity}`
    );
    return null;
  }

  const { nextChainKey, messageKey, nextKeyIdx } = await stepRatchet(ratchet);

  // Don't need to remember message keys for a sending ratchet
  const messageKeys = {};
  const nextChainKeyHex = toHex(nextChainKey);

  await saveSenderKeysInner(
    groupId,
    senderIdentity,
    nextChainKeyHex,
    nextKeyIdx,
    messageKeys
  );

  return { messageKey, keyIdx: nextKeyIdx };
}

// Advance the ratchet until idx
async function advanceRatchet(groupId, senderIdentity, idx) {
  const ratchet = await loadChainKey(groupId, senderIdentity);

  if (!ratchet) {
    log.error(
      `Could not find ratchet for groupId ${groupId} sender: ${senderIdentity}`
    );
    throw new textsecure.SenderKeyMissing(senderIdentity);
  }

  // Normally keyIdx will be 1 behind, in which case we stepRatchet one time only

  if (idx < ratchet.keyIdx) {
    // If the request is for some old index, retrieve the key generated earlier and
    // remove it from the database (there is no need to advance the ratchet)
    const messageKey = ratchet.messageKeys[idx];
    if (messageKey) {
      delete ratchet.messageKeys[idx];
      // TODO: just pass in the ratchet?
      const chainKeyHex = toHex(ratchet.chainKey);
      await saveSenderKeysInner(
        groupId,
        senderIdentity,
        chainKeyHex,
        ratchet.keyIdx,
        ratchet.messageKeys
      );

      return fromHex(messageKey);
    }

    log.error('[idx] not found key for idx: ', idx);
    // I probably want a better error handling than this
    return null;
  }

  const { messageKeys } = ratchet;

  let curMessageKey;

  // eslint-disable-next-line no-constant-condition
  while (true) {
    // eslint-disable-next-line no-await-in-loop
    const { nextKeyIdx, nextChainKey, messageKey } = await stepRatchet(ratchet);

    ratchet.chainKey = nextChainKey;
    ratchet.keyIdx = nextKeyIdx;

    if (nextKeyIdx === idx) {
      curMessageKey = messageKey;
      break;
    } else if (nextKeyIdx > idx) {
      log.error(
        `Could not decrypt for an older ratchet step: (${nextKeyIdx})nextKeyIdx > (${idx})idx`
      );
      throw new Error(`Cannot revert ratchet for group ${groupId}!`);
    } else {
      // Store keys for skipped nextKeyIdx, we might need them to decrypt
      // messages that arrive out-of-order
      messageKeys[nextKeyIdx] = toHex(messageKey);
    }
  }

  const chainKeyHex = toHex(ratchet.chainKey);

  await saveSenderKeysInner(
    groupId,
    senderIdentity,
    chainKeyHex,
    idx,
    messageKeys
  );

  return curMessageKey;
}

async function loadChainKey(groupId, senderIdentity) {
  const senderKeyEntry = await Signal.Data.getSenderKeys(
    groupId,
    senderIdentity
  );

  if (!senderKeyEntry) {
    // TODO: we should try to request the key from the sender in this case
    log.error(
      `Sender key not found for group ${groupId} sender ${senderIdentity}`
    );
    // TODO: throw instead?
    return null;
  }

  const {
    chainKey: chainKeyHex,
    idx: keyIdx,
    messageKeys,
  } = senderKeyEntry.ratchet;

  if (!chainKeyHex) {
    log.error('Chain key not found');
    return null;
  }

  // TODO: This could fail if the data is not hex, handle
  // this case
  const chainKey = fromHex(chainKeyHex);

  return { chainKey, keyIdx, messageKeys };
}

const jobQueue = {};

function queueJobForNumber(number, runJob) {
  const runPrevious = jobQueue[number] || Promise.resolve();
  const runCurrent = runPrevious.then(runJob, runJob);
  jobQueue[number] = runCurrent;
  runCurrent.then(() => {
    if (jobQueue[number] === runCurrent) {
      delete jobQueue[number];
    }
  });
  return runCurrent;
}

async function decryptWithSenderKey(
  ciphertext,
  curKeyIdx,
  groupId,
  senderIdentity
) {
  // We only want to serialize jobs with the same pair (groupId, senderIdentity)
  const id = groupId + senderIdentity;
  return queueJobForNumber(id, () =>
    decryptWithSenderKeyInner(ciphertext, curKeyIdx, groupId, senderIdentity)
  );
}

async function decryptWithSenderKeyInner(
  ciphertext,
  curKeyIdx,
  groupId,
  senderIdentity
) {
  const messageKey = await advanceRatchet(groupId, senderIdentity, curKeyIdx);

  // TODO: this might fail, handle this
  const plaintext = await libloki.crypto.DecryptGCM(messageKey, ciphertext);

  return plaintext;
}

async function encryptWithSenderKey(plaintext, groupId, ourIdentity) {
  // We only want to serialize jobs with the same pair (groupId, ourIdentity)
  const id = groupId + ourIdentity;
  return queueJobForNumber(id, () =>
    encryptWithSenderKeyInner(plaintext, groupId, ourIdentity)
  );
}

async function encryptWithSenderKeyInner(plaintext, groupId, ourIdentity) {
  const { messageKey, keyIdx } = await stepRatchetOnce(groupId, ourIdentity);

  const ciphertext = await libloki.crypto.EncryptGCM(messageKey, plaintext);

  return { ciphertext, keyIdx };
}

async function getSenderKeys(groupId, senderIdentity) {
  const { chainKey, keyIdx } = await loadChainKey(groupId, senderIdentity);

  return { chainKey, keyIdx };
}

module.exports = {
  createSenderKeyForGroup,
  encryptWithSenderKey,
  decryptWithSenderKey,
  saveSenderKeys,
  getSenderKeys,
};