#include <furi_hal.h>
#include <lib/mlib/m-dict.h>
#include <toolbox/sha256.h>

#ifndef FURI_HAL_CRYPTO_ADVANCED_AVAIL
#include "crypto/gcm.h"
#endif /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */

#include "crypto_wrapper.h"

DICT_DEF2(ESubGhzChatReplayDict, uint64_t, uint32_t)

struct ESugGhzChatCryptoCtx {
	uint8_t key[KEY_BITS / 8];
#ifndef FURI_HAL_CRYPTO_ADVANCED_AVAIL
	gcm_context gcm_ctx;
#endif /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
	ESubGhzChatReplayDict_t replay_dict;
	uint64_t run_id;
	uint32_t counter;
};

struct ESubGhzChatCryptoMsg {
	uint64_t run_id;
	uint32_t counter;
	uint8_t iv[IV_BYTES];
	uint8_t tag[TAG_BYTES];
	uint8_t data[0];
} __attribute__ ((packed));

void crypto_init(void)
{
#ifndef FURI_HAL_CRYPTO_ADVANCED_AVAIL
	/* init the GCM and AES tables */
	gcm_initialize();
#endif /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
}

void crypto_explicit_bzero(void *s, size_t len)
{
	memset(s, 0, len);
	asm volatile("" ::: "memory");
}

ESubGhzChatCryptoCtx *crypto_ctx_alloc(void)
{
	ESubGhzChatCryptoCtx *ret = malloc(sizeof(ESubGhzChatCryptoCtx));

	if (ret != NULL) {
		memset(ret, 0, sizeof(ESubGhzChatCryptoCtx));
		ESubGhzChatReplayDict_init(ret->replay_dict);
		ret->run_id = 0;
		ret->counter = 1;
	}

	return ret;
}

void crypto_ctx_free(ESubGhzChatCryptoCtx *ctx)
{
	crypto_ctx_clear(ctx);
	ESubGhzChatReplayDict_clear(ctx->replay_dict);
	free(ctx);
}

void crypto_ctx_clear(ESubGhzChatCryptoCtx *ctx)
{
	crypto_explicit_bzero(ctx->key, sizeof(ctx->key));
#ifndef FURI_HAL_CRYPTO_ADVANCED_AVAIL
	crypto_explicit_bzero(&(ctx->gcm_ctx), sizeof(ctx->gcm_ctx));
#endif /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
	ESubGhzChatReplayDict_reset(ctx->replay_dict);
	ctx->run_id = 0;
	ctx->counter = 1;
}

static uint64_t crypto_calc_run_id(FuriString *flipper_name, uint32_t tick)
{
	const char *fn = furi_string_get_cstr(flipper_name);
	size_t fn_len = strlen(fn);

	uint8_t h_in[fn_len + sizeof(uint32_t)];
	memcpy(h_in, fn, fn_len);
	memcpy(h_in + fn_len, &tick, sizeof(uint32_t));

	uint8_t h_out[256];
	sha256(h_in, fn_len + sizeof(uint32_t), h_out);

	uint64_t run_id;
	memcpy(&run_id, h_out, sizeof(uint64_t));

	return run_id;
}

bool crypto_ctx_set_key(ESubGhzChatCryptoCtx *ctx, const uint8_t *key,
		FuriString *flipper_name, uint32_t tick)
{
	ctx->run_id = crypto_calc_run_id(flipper_name, tick);
	ctx->counter = 1;

	memcpy(ctx->key, key, KEY_BITS / 8);
#ifdef FURI_HAL_CRYPTO_ADVANCED_AVAIL
	return true;
#else /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
	return (gcm_setkey(&(ctx->gcm_ctx), key, KEY_BITS / 8) == 0);
#endif /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
}

void crypto_ctx_get_key(ESubGhzChatCryptoCtx *ctx, uint8_t *key)
{
	memcpy(key, ctx->key, KEY_BITS / 8);
}

bool crypto_ctx_decrypt(ESubGhzChatCryptoCtx *ctx, uint8_t *in, size_t in_len,
		uint8_t *out)
{
	if (in_len < MSG_OVERHEAD + 1) {
		return false;
	}

	struct ESubGhzChatCryptoMsg *msg = (struct ESubGhzChatCryptoMsg *) in;

	// check if message is stale, if yes, discard
	uint32_t *counter = ESubGhzChatReplayDict_get(ctx->replay_dict,
			msg->run_id);
	if (counter != NULL) {
		if (*counter >= __ntohl(msg->counter)) {
			return false;
		}
	}

	// decrypt and auth message
#ifdef FURI_HAL_CRYPTO_ADVANCED_AVAIL
	bool ret = (furi_hal_crypto_gcm_decrypt_and_verify(ctx->key,
			msg->iv,
			(uint8_t *) msg, RUN_ID_BYTES + COUNTER_BYTES,
			msg->data, out,
			in_len - MSG_OVERHEAD,
			msg->tag) == FuriHalCryptoGCMStateOk);
#else /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
	bool ret = (gcm_auth_decrypt(&(ctx->gcm_ctx),
			msg->iv, IV_BYTES,
			(uint8_t *) msg, RUN_ID_BYTES + COUNTER_BYTES,
			msg->data, out,
			in_len - MSG_OVERHEAD,
			msg->tag, TAG_BYTES) == 0);
#endif /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */

	// if auth was successful update replay dict
	if (ret) {
		ESubGhzChatReplayDict_set_at(ctx->replay_dict, msg->run_id,
				__ntohl(msg->counter));
	}

	return ret;
}

bool crypto_ctx_encrypt(ESubGhzChatCryptoCtx *ctx, uint8_t *in, size_t in_len,
		uint8_t *out)
{
	struct ESubGhzChatCryptoMsg *msg = (struct ESubGhzChatCryptoMsg *) out;

	// fill message header
	msg->run_id = ctx->run_id;
	msg->counter = __htonl(ctx->counter);
	furi_hal_random_fill_buf(msg->iv, IV_BYTES);

	// encrypt message and store tag in header
#ifdef FURI_HAL_CRYPTO_ADVANCED_AVAIL
	bool ret = (furi_hal_crypto_gcm_encrypt_and_tag(ctx->key,
			msg->iv,
			(uint8_t *) msg, RUN_ID_BYTES + COUNTER_BYTES,
			in, msg->data,
			in_len,
			msg->tag) == FuriHalCryptoGCMStateOk);
#else /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
	bool ret = (gcm_crypt_and_tag(&(ctx->gcm_ctx), ENCRYPT,
			msg->iv, IV_BYTES,
			(uint8_t *) msg, RUN_ID_BYTES + COUNTER_BYTES,
			in, msg->data,
			in_len,
			msg->tag, TAG_BYTES) == 0);
#endif /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */

	// update replay dict and increase internal counter
	if (ret) {
		ESubGhzChatReplayDict_set_at(ctx->replay_dict, ctx->run_id,
				ctx->counter);
		ctx->counter++;
	}

	return ret;
}

size_t crypto_ctx_dump_replay_dict(ESubGhzChatCryptoCtx *ctx,
		CryptoCtxReplayDictWriter writer, void *writer_ctx)
{
	size_t ret = 0;
	ESubGhzChatReplayDict_it_t i;

	for (ESubGhzChatReplayDict_it(i, ctx->replay_dict);
			!ESubGhzChatReplayDict_end_p(i);
			ESubGhzChatReplayDict_next(i), ret++) {
		ESubGhzChatReplayDict_itref_t *ref =
			ESubGhzChatReplayDict_ref(i);
		if (!writer(ref->key, ref->value, writer_ctx)) {
			break;
		}
	}

	return ret;
}

size_t crypto_ctx_read_replay_dict(ESubGhzChatCryptoCtx *ctx,
		CryptoCtxReplayDictReader reader, void *reader_ctx)
{
	size_t ret = 0;

	uint64_t run_id;
	uint32_t counter;

	while (reader(&run_id, &counter, reader_ctx)) {
		ESubGhzChatReplayDict_set_at(ctx->replay_dict, run_id,
				counter);
		ret++;
	}

	return ret;
}
