/*
 * Copyright (c) 2020 Nordic Semiconductor ASA
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include "mesh_cfg.h"
#include "api/mesh.h"
#include "dfu.h"
#include "blob.h"
#include "access.h"

#define LOG_LEVEL CONFIG_BT_MESH_DFU_LOG_LEVEL
#include "api/mesh_log.h"

#if (CONFIG_BT_MESH_DFU_SRV)

#define UPDATE_IDX_NONE 0xff

BUILD_ASSERT((DFU_UPDATE_START_MSG_MAXLEN + BT_MESH_MODEL_OP_LEN(BT_MESH_DFU_OP_UPDATE_START) +
	      BT_MESH_MIC_SHORT) <= BT_MESH_RX_SDU_MAX,
	     "The Firmware Update Start message does not fit into the maximum incoming SDU size.");

BUILD_ASSERT((DFU_UPDATE_INFO_STATUS_MSG_MINLEN +
	      BT_MESH_MODEL_OP_LEN(BT_MESH_DFU_OP_UPDATE_INFO_STATUS) + BT_MESH_MIC_SHORT)
	     <= BT_MESH_TX_SDU_MAX,
	     "The Firmware Update Info Status message does not fit into the maximum outgoing SDU "
	     "size.");

static void store_state(struct bt_mesh_dfu_srv *srv)
{
	bt_mesh_model_data_store(srv->mod, false, NULL, &srv->update,
				 sizeof(srv->update));
}

static void erase_state(struct bt_mesh_dfu_srv *srv)
{
	bt_mesh_model_data_store(srv->mod, false, NULL, NULL, 0);
}

static void xfer_failed(struct bt_mesh_dfu_srv *srv)
{
	if (!bt_mesh_dfu_srv_is_busy(srv) ||
	    srv->update.idx >= srv->img_count) {
		return;
	}

	erase_state(srv);

	if (srv->cb->end) {
		srv->cb->end(srv, &srv->imgs[srv->update.idx], false);
	}
}

static enum bt_mesh_dfu_status metadata_check(struct bt_mesh_dfu_srv *srv,
					      uint8_t idx,
					      struct net_buf_simple *buf,
					      enum bt_mesh_dfu_effect *effect)
{
	*effect = BT_MESH_DFU_EFFECT_NONE;

	if (idx >= srv->img_count) {
		return BT_MESH_DFU_ERR_FW_IDX;
	}

	if (!srv->cb->check) {
		return BT_MESH_DFU_SUCCESS;
	}

	if (srv->cb->check(srv, &srv->imgs[idx], buf, effect)) {
		*effect = BT_MESH_DFU_EFFECT_NONE;
		return BT_MESH_DFU_ERR_METADATA;
	}

	return BT_MESH_DFU_SUCCESS;
}

static void apply_rsp_sent(int err, void *cb_params)
{
	struct bt_mesh_dfu_srv *srv = cb_params;

	if (err) {
		/* return phase back to give client one more chance. */
		srv->update.phase = BT_MESH_DFU_PHASE_VERIFY_OK;
		LOG_WRN("Apply response failed, wait for retry (err %d)", err);
		return;
	}

	LOG_DBG("");

	if (!srv->cb->apply || srv->update.idx == UPDATE_IDX_NONE) {
		srv->update.phase = BT_MESH_DFU_PHASE_IDLE;
		store_state(srv);
		LOG_DBG("Prerequisites for apply callback are wrong");
		return;
	}

	store_state(srv);

	err = srv->cb->apply(srv, &srv->imgs[srv->update.idx]);
	if (err) {
		srv->update.phase = BT_MESH_DFU_PHASE_IDLE;
		store_state(srv);
		LOG_DBG("Application apply callback failed (err %d)", err);
	}
}

static void apply_rsp_sending(uint16_t duration, int err, void *cb_params)
{
	if (err) {
		apply_rsp_sent(err, cb_params);
	}
}

static void verify(struct bt_mesh_dfu_srv *srv)
{
	srv->update.phase = BT_MESH_DFU_PHASE_VERIFY;

	if (srv->update.idx >= srv->img_count) {
		bt_mesh_dfu_srv_rejected(srv);
		return;
	}

	if (!srv->cb->end) {
		bt_mesh_dfu_srv_verified(srv);
		return;
	}

	srv->cb->end(srv, &srv->imgs[srv->update.idx], true);
	if (srv->update.phase == BT_MESH_DFU_PHASE_VERIFY) {
		store_state(srv);
	}
}

static int handle_info_get(const struct bt_mesh_model *mod, struct bt_mesh_msg_ctx *ctx,
			   struct net_buf_simple *buf)
{
	struct bt_mesh_dfu_srv *srv = mod->rt->user_data;
	uint8_t idx, limit;

	if (srv->update.phase == BT_MESH_DFU_PHASE_APPLYING) {
		LOG_INF("Still applying, not responding");
		return -EBUSY;
	}

	idx = net_buf_simple_pull_u8(buf);
	limit = net_buf_simple_pull_u8(buf);

	LOG_DBG("from %u (limit: %u)", idx, limit);

	NET_BUF_SIMPLE_DEFINE(rsp, BT_MESH_TX_SDU_MAX);
	bt_mesh_model_msg_init(&rsp, BT_MESH_DFU_OP_UPDATE_INFO_STATUS);
	net_buf_simple_add_u8(&rsp, srv->img_count);
	net_buf_simple_add_u8(&rsp, idx);

	for (; idx < srv->img_count && limit > 0; ++idx) {
		uint32_t entry_len;

		if (!srv->imgs[idx].fwid) {
			continue;
		}

		entry_len = 2 + srv->imgs[idx].fwid_len;
		if (srv->imgs[idx].uri) {
			entry_len += strlen(srv->imgs[idx].uri);
		}

		if (net_buf_simple_tailroom(&rsp) + BT_MESH_MIC_SHORT < entry_len) {
			break;
		}

		net_buf_simple_add_u8(&rsp, srv->imgs[idx].fwid_len);
		net_buf_simple_add_mem(&rsp, srv->imgs[idx].fwid,
				       srv->imgs[idx].fwid_len);

		if (srv->imgs[idx].uri) {
			size_t len = strlen(srv->imgs[idx].uri);

			net_buf_simple_add_u8(&rsp, len);
			net_buf_simple_add_mem(&rsp, srv->imgs[idx].uri, len);
		} else {
			net_buf_simple_add_u8(&rsp, 0);
		}

		limit--;
	}

	if (srv->update.phase != BT_MESH_DFU_PHASE_IDLE) {
		ctx->send_ttl = srv->update.ttl;
	}

	bt_mesh_model_send(mod, ctx, &rsp, NULL, NULL);

	return 0;
}

static int handle_metadata_check(const struct bt_mesh_model *mod, struct bt_mesh_msg_ctx *ctx,
				 struct net_buf_simple *buf)
{
	struct bt_mesh_dfu_srv *srv = mod->rt->user_data;
	enum bt_mesh_dfu_status status;
	enum bt_mesh_dfu_effect effect;
	uint8_t idx;

	BT_MESH_MODEL_BUF_DEFINE(rsp, BT_MESH_DFU_OP_UPDATE_METADATA_STATUS, 2);
	bt_mesh_model_msg_init(&rsp, BT_MESH_DFU_OP_UPDATE_METADATA_STATUS);

	idx = net_buf_simple_pull_u8(buf);
	status = metadata_check(srv, idx, buf, &effect);

	LOG_DBG("%u", idx);

	net_buf_simple_add_u8(&rsp, (status & BIT_MASK(3)) | (effect << 3));
	net_buf_simple_add_u8(&rsp, idx);

	if (srv->update.phase != BT_MESH_DFU_PHASE_IDLE) {
		ctx->send_ttl = srv->update.ttl;
	}

	bt_mesh_model_send(mod, ctx, &rsp, NULL, NULL);

	return 0;
}

static void update_status_rsp(struct bt_mesh_dfu_srv *srv,
			      struct bt_mesh_msg_ctx *ctx,
			      enum bt_mesh_dfu_status status,
			      const struct bt_mesh_send_cb *send_cb)
{
	BT_MESH_MODEL_BUF_DEFINE(buf, BT_MESH_DFU_OP_UPDATE_STATUS, 14);
	bt_mesh_model_msg_init(&buf, BT_MESH_DFU_OP_UPDATE_STATUS);

	net_buf_simple_add_u8(&buf, ((status & BIT_MASK(3)) |
				     (srv->update.phase << 5)));

	if (srv->update.phase != BT_MESH_DFU_PHASE_IDLE) {
		net_buf_simple_add_u8(&buf, srv->update.ttl);
		net_buf_simple_add_u8(&buf, srv->update.effect);
		net_buf_simple_add_le16(&buf, srv->update.timeout_base);
		net_buf_simple_add_le64(&buf, srv->blob.state.xfer.id);
		net_buf_simple_add_u8(&buf, srv->update.idx);

		ctx->send_ttl = srv->update.ttl;
	}

	bt_mesh_model_send(srv->mod, ctx, &buf, send_cb, srv);
}

static int handle_get(const struct bt_mesh_model *mod, struct bt_mesh_msg_ctx *ctx,
		      struct net_buf_simple *buf)
{
	struct bt_mesh_dfu_srv *srv = mod->rt->user_data;

	LOG_DBG("");

	update_status_rsp(srv, ctx, BT_MESH_DFU_SUCCESS, NULL);

	return 0;
}

static inline bool is_active_update(struct bt_mesh_dfu_srv *srv, uint8_t idx,
				    uint16_t timeout_base,
				    const uint64_t *blob_id, uint8_t ttl,
				    uint16_t meta_checksum)
{
	return (srv->update.idx != idx || srv->blob.state.xfer.id != *blob_id ||
		srv->update.ttl != ttl ||
		srv->update.timeout_base != timeout_base ||
		srv->update.meta != meta_checksum);
}

static int handle_start(const struct bt_mesh_model *mod, struct bt_mesh_msg_ctx *ctx,
			struct net_buf_simple *buf)
{
	struct bt_mesh_dfu_srv *srv = mod->rt->user_data;
	const struct bt_mesh_blob_io *io;
	uint16_t timeout_base, meta_checksum;
	enum bt_mesh_dfu_status status;
	uint8_t ttl, idx;
	uint64_t blob_id;
	int err;
	struct net_buf_simple_state buf_state;

	ttl = net_buf_simple_pull_u8(buf);
	timeout_base = net_buf_simple_pull_le16(buf);
	blob_id = net_buf_simple_pull_le64(buf);
	idx = net_buf_simple_pull_u8(buf);
	meta_checksum = dfu_metadata_checksum(buf);

	LOG_DBG("%u ttl: %u extra time: %u", idx, ttl, timeout_base);

	if ((!buf->len || meta_checksum == srv->update.meta) &&
	    srv->update.phase == BT_MESH_DFU_PHASE_TRANSFER_ERR &&
	    srv->update.ttl == ttl &&
	    srv->update.timeout_base == timeout_base &&
	    srv->update.idx == idx &&
	    srv->blob.state.xfer.id == blob_id) {
		srv->update.phase = BT_MESH_DFU_PHASE_TRANSFER_ACTIVE;
		status = BT_MESH_DFU_SUCCESS;
		store_state(srv);
		/* blob srv will resume the transfer. */
		LOG_DBG("Resuming transfer");
		goto rsp;
	}

	if (bt_mesh_dfu_srv_is_busy(srv)) {
		if (is_active_update(srv, idx, timeout_base, &blob_id, ttl,
				     meta_checksum)) {
			status = BT_MESH_DFU_ERR_WRONG_PHASE;
		} else {
			status = BT_MESH_DFU_SUCCESS;
			srv->update.ttl = ttl;
			srv->blob.state.xfer.id = blob_id;
		}

		LOG_WRN("Busy. Phase: %u", srv->update.phase);
		goto rsp;
	}

	net_buf_simple_save(buf, &buf_state);
	status = metadata_check(srv, idx, buf,
				(enum bt_mesh_dfu_effect *)&srv->update.effect);
	net_buf_simple_restore(buf, &buf_state);
	if (status != BT_MESH_DFU_SUCCESS) {
		goto rsp;
	}

	srv->update.ttl = ttl;
	srv->update.timeout_base = timeout_base;
	srv->update.meta = meta_checksum;

	io = NULL;
	err = srv->cb->start(srv, &srv->imgs[idx], buf, &io);
	if (err == -EALREADY || (!err && bt_mesh_has_addr(ctx->addr))) {
		/* This image has already been received or this is a
		 * self-update. Skip the transfer phase and proceed to
		 * verifying update.
		 */
		status = BT_MESH_DFU_SUCCESS;
		srv->update.idx = idx;
		srv->blob.state.xfer.id = blob_id;
		srv->update.phase = BT_MESH_DFU_PHASE_VERIFY;
		update_status_rsp(srv, ctx, status, NULL);
		verify(srv);
		return 0;
	}

	if (err == -ENOMEM) {
		status = BT_MESH_DFU_ERR_RESOURCES;
		goto rsp;
	}

	if (err == -EBUSY) {
		status = BT_MESH_DFU_ERR_TEMPORARILY_UNAVAILABLE;
		goto rsp;
	}

	if (err || !io || !io->wr) {
		status = BT_MESH_DFU_ERR_INTERNAL;
		goto rsp;
	}

	err = bt_mesh_blob_srv_recv(&srv->blob, blob_id, io,
				    ttl, timeout_base);
	if (err) {
		status = BT_MESH_DFU_ERR_BLOB_XFER_BUSY;
		goto rsp;
	}

	srv->update.idx = idx;
	srv->update.phase = BT_MESH_DFU_PHASE_TRANSFER_ACTIVE;
	status = BT_MESH_DFU_SUCCESS;
	store_state(srv);

rsp:
	update_status_rsp(srv, ctx, status, NULL);

	return 0;
}

static int handle_cancel(const struct bt_mesh_model *mod, struct bt_mesh_msg_ctx *ctx,
			 struct net_buf_simple *buf)
{
	struct bt_mesh_dfu_srv *srv = mod->rt->user_data;

	if (srv->update.idx == UPDATE_IDX_NONE) {
		goto rsp;
	}

	LOG_DBG("");

	bt_mesh_blob_srv_cancel(&srv->blob);
	srv->update.phase = BT_MESH_DFU_PHASE_IDLE;
	xfer_failed(srv);

rsp:
	update_status_rsp(srv, ctx, BT_MESH_DFU_SUCCESS, NULL);

	return 0;
}

static int handle_apply(const struct bt_mesh_model *mod, struct bt_mesh_msg_ctx *ctx,
			struct net_buf_simple *buf)
{
	struct bt_mesh_dfu_srv *srv = mod->rt->user_data;
	static const struct bt_mesh_send_cb send_cb = {
		.start = apply_rsp_sending,
		.end = apply_rsp_sent,
	};

	LOG_DBG("");

	if (srv->update.phase == BT_MESH_DFU_PHASE_APPLYING) {
		update_status_rsp(srv, ctx, BT_MESH_DFU_SUCCESS, NULL);
		return 0;
	}

	if (srv->update.phase != BT_MESH_DFU_PHASE_VERIFY_OK) {
		LOG_WRN("Apply: Invalid phase %u", srv->update.phase);
		update_status_rsp(srv, ctx, BT_MESH_DFU_ERR_WRONG_PHASE, NULL);
		return 0;
	}

	/* Postponing the apply callback until the response has been sent, in
	 * case it triggers a reboot:
	 */
	srv->update.phase = BT_MESH_DFU_PHASE_APPLYING;
	update_status_rsp(srv, ctx, BT_MESH_DFU_SUCCESS, &send_cb);

	return 0;
}

const struct bt_mesh_model_op _bt_mesh_dfu_srv_op[] = {
	{ BT_MESH_DFU_OP_UPDATE_INFO_GET, BT_MESH_LEN_EXACT(2), handle_info_get },
	{ BT_MESH_DFU_OP_UPDATE_METADATA_CHECK, BT_MESH_LEN_MIN(1), handle_metadata_check },
	{ BT_MESH_DFU_OP_UPDATE_GET, BT_MESH_LEN_EXACT(0), handle_get },
	{ BT_MESH_DFU_OP_UPDATE_START, BT_MESH_LEN_MIN(12), handle_start },
	{ BT_MESH_DFU_OP_UPDATE_CANCEL, BT_MESH_LEN_EXACT(0), handle_cancel },
	{ BT_MESH_DFU_OP_UPDATE_APPLY, BT_MESH_LEN_EXACT(0), handle_apply },
	BT_MESH_MODEL_OP_END,
};

static int dfu_srv_init(const struct bt_mesh_model *mod)
{
	int err;
	struct bt_mesh_dfu_srv *srv = mod->rt->user_data;

	srv->mod = mod;
	srv->update.idx = UPDATE_IDX_NONE;

	if (!srv->cb || !srv->cb->start || !srv->imgs || srv->img_count == 0 ||
	    srv->img_count == UPDATE_IDX_NONE) {
		LOG_ERR("Invalid DFU Server initialization");
		return -EINVAL;
	}

	const struct bt_mesh_model *blob_srv =
		bt_mesh_model_find(bt_mesh_model_elem(mod), BT_MESH_MODEL_ID_BLOB_SRV);

	if (blob_srv == NULL) {
		LOG_ERR("Missing BLOB Srv.");
		return -EINVAL;
	}

	err = bt_mesh_model_extend(mod, srv->blob.mod);

	if (err) {
		return err;
	}

	return 0;
}

static int dfu_srv_settings_set(const struct bt_mesh_model *mod, const char *name,
				size_t len_rd, settings_read_cb read_cb,
				void *cb_arg)
{
	struct bt_mesh_dfu_srv *srv = mod->rt->user_data;
	ssize_t len;

	if (len_rd < sizeof(srv->update)) {
		return -EINVAL;
	}

	len = read_cb(cb_arg, &srv->update, sizeof(srv->update));
	if (len < 0) {
		return len;
	}

	LOG_DBG("Recovered transfer (phase: %u, idx: %u)", srv->update.phase,
		srv->update.idx);
	if (srv->update.phase == BT_MESH_DFU_PHASE_TRANSFER_ACTIVE) {
		LOG_DBG("Settings recovered mid-transfer, setting transfer error");
		srv->update.phase = BT_MESH_DFU_PHASE_TRANSFER_ERR;
	} else if (srv->update.phase == BT_MESH_DFU_PHASE_VERIFY_OK) {
		LOG_DBG("Settings recovered before application, setting verification fail");
		srv->update.phase = BT_MESH_DFU_PHASE_VERIFY_FAIL;
	}

	return 0;
}

static void dfu_srv_reset(const struct bt_mesh_model *mod)
{
	struct bt_mesh_dfu_srv *srv = mod->rt->user_data;

	srv->update.phase = BT_MESH_DFU_PHASE_IDLE;
	erase_state(srv);
}

const struct bt_mesh_model_cb _bt_mesh_dfu_srv_cb = {
	.init = dfu_srv_init,
	.settings_set = dfu_srv_settings_set,
	.reset = dfu_srv_reset,
};

static void blob_suspended(struct bt_mesh_blob_srv *b)
{
	struct bt_mesh_dfu_srv *srv = CONTAINER_OF(b, struct bt_mesh_dfu_srv, blob);

	srv->update.phase = BT_MESH_DFU_PHASE_TRANSFER_ERR;
	store_state(srv);
}

static void blob_end(struct bt_mesh_blob_srv *b, uint64_t id, bool success)
{
	struct bt_mesh_dfu_srv *srv =
		CONTAINER_OF(b, struct bt_mesh_dfu_srv, blob);

	LOG_DBG("success: %u", success);

	if (!success) {
		srv->update.phase = BT_MESH_DFU_PHASE_TRANSFER_ERR;
		xfer_failed(srv);
		return;
	}

	verify(srv);
}

static int blob_recover(struct bt_mesh_blob_srv *b,
			struct bt_mesh_blob_xfer *xfer,
			const struct bt_mesh_blob_io **io)
{
	struct bt_mesh_dfu_srv *srv =
		CONTAINER_OF(b, struct bt_mesh_dfu_srv, blob);

	if (!srv->cb->recover ||
	    srv->update.phase != BT_MESH_DFU_PHASE_TRANSFER_ERR ||
	    srv->update.idx >= srv->img_count) {
		return -ENOTSUP;
	}

	return srv->cb->recover(srv, &srv->imgs[srv->update.idx], io);
}

const struct bt_mesh_blob_srv_cb _bt_mesh_dfu_srv_blob_cb = {
	.suspended = blob_suspended,
	.end = blob_end,
	.recover = blob_recover,
};

void bt_mesh_dfu_srv_verified(struct bt_mesh_dfu_srv *srv)
{
	if (srv->update.phase != BT_MESH_DFU_PHASE_VERIFY) {
		LOG_WRN("Wrong state");
		return;
	}

	LOG_DBG("");

	srv->update.phase = BT_MESH_DFU_PHASE_VERIFY_OK;
	store_state(srv);
}

void bt_mesh_dfu_srv_rejected(struct bt_mesh_dfu_srv *srv)
{
	if (srv->update.phase != BT_MESH_DFU_PHASE_VERIFY) {
		LOG_WRN("Wrong state");
		return;
	}

	LOG_DBG("");

	srv->update.phase = BT_MESH_DFU_PHASE_VERIFY_FAIL;
	store_state(srv);
}

void bt_mesh_dfu_srv_cancel(struct bt_mesh_dfu_srv *srv)
{
	if (srv->update.phase == BT_MESH_DFU_PHASE_IDLE) {
		LOG_WRN("Wrong state");
		return;
	}

	(void)bt_mesh_blob_srv_cancel(&srv->blob);
}

void bt_mesh_dfu_srv_applied(struct bt_mesh_dfu_srv *srv)
{
	if (srv->update.phase != BT_MESH_DFU_PHASE_APPLYING) {
		LOG_WRN("Wrong state");
		return;
	}

	LOG_DBG("");

	srv->update.phase = BT_MESH_DFU_PHASE_IDLE;
	store_state(srv);
}

bool bt_mesh_dfu_srv_is_busy(const struct bt_mesh_dfu_srv *srv)
{
	return srv->update.phase != BT_MESH_DFU_PHASE_IDLE &&
	       srv->update.phase != BT_MESH_DFU_PHASE_TRANSFER_ERR &&
	       srv->update.phase != BT_MESH_DFU_PHASE_VERIFY_FAIL;
}

uint8_t bt_mesh_dfu_srv_progress(const struct bt_mesh_dfu_srv *srv)
{
	if (!bt_mesh_dfu_srv_is_busy(srv)) {
		return 0U;
	}

	if (srv->update.phase == BT_MESH_DFU_PHASE_TRANSFER_ACTIVE) {
		return bt_mesh_blob_srv_progress(&srv->blob);
	}

	return 100U;
}

#endif // CONFIG_BT_MESH_DFU_SRV