#include "config.h"
#include <ccan/cast/cast.h>
#include <ccan/json_escape/json_escape.h> /* Ubuntu focal's gcc needs json_escape defined */
#include <common/bolt12_id.h>
#include <common/bolt12_merkle.h>
#include <common/json_command.h>
#include <hsmd/hsmd_wiregen.h>
#include <inttypes.h>
#include <lightningd/hsm_control.h>
#include <lightningd/jsonrpc.h>
#include <lightningd/lightningd.h>

static void json_populate_offer(struct json_stream *response,
				const struct sha256 *offer_id,
				const char *b12,
				const struct json_escape *label,
				enum offer_status status)
{
	json_add_sha256(response, "offer_id", offer_id);
	json_add_bool(response, "active", offer_status_active(status));
	json_add_bool(response, "single_use", offer_status_single(status));
	json_add_string(response, "bolt12", b12);
	json_add_bool(response, "used", offer_status_used(status));
	if (label)
		json_add_escaped_string(response, "label", label);
}

static const char *offer_description_from_b12(const tal_t *ctx,
					struct lightningd *ld,
					const char *b12)
{
    struct tlv_offer *offer;
    char *fail;

	offer = offer_decode(ctx, b12, strlen(b12),
                         NULL, NULL, &fail);
    if (!offer) {
        log_debug(ld->log, "Failed to decode BOLT12: %s", fail);
        return NULL;
    }

    if (!offer->offer_description)
        return NULL;

    return offer->offer_description;
}

static struct command_result *param_b12_offer(struct command *cmd,
					      const char *name,
					      const char *buffer,
					      const jsmntok_t *tok,
					      struct tlv_offer **offer)
{
	char *fail;
	*offer = offer_decode(cmd, buffer + tok->start,
			      tok->end - tok->start,
			      cmd->ld->our_features, chainparams, &fail);
	if (!*offer)
		return command_fail_badparam(cmd, name, buffer, tok, fail);
	return NULL;
}

static void hsm_sign_b12(struct lightningd *ld,
			 const char *messagename,
			 const char *fieldname,
			 const struct sha256 *merkle,
			 const u8 *publictweak,
			 const struct pubkey *key,
			 struct bip340sig *sig)
{
	const u8 *msg;
	struct sha256 sighash;
	/* Needs to be a (non-nul-terminated) tal_arr */
	const u8 *info = tal_dup_arr(tmpctx, u8,
				     (const u8 *)NODE_ALIAS_BASE_STRING,
				     strlen(NODE_ALIAS_BASE_STRING), 0);

	msg = towire_hsmd_sign_bolt12_2(NULL, messagename, fieldname, merkle,
					info, publictweak);
	msg = hsm_sync_req(tmpctx, ld, take(msg));
        if (!fromwire_hsmd_sign_bolt12_2_reply(msg, sig))
		fatal("HSM gave bad sign_bolt12_2 %s",
		      tal_hex(msg, msg));

	/* Now we sanity-check! */
	sighash_from_merkle(messagename, fieldname, merkle, &sighash);
 	if (!check_schnorr_sig(&sighash, &key->pubkey, sig))
		fatal("HSM gave bad signature %s for pubkey %s",
		      fmt_bip340sig(tmpctx, sig),
		      fmt_pubkey(tmpctx, key));
}

static struct command_result *json_createoffer(struct command *cmd,
					       const char *buffer,
					       const jsmntok_t *obj UNNEEDED,
					       const jsmntok_t *params)
{
	struct json_stream *response;
	struct json_escape *label;
	struct tlv_offer *offer;
	struct sha256 offer_id;
	const char *b12str;
	bool *single_use;
	enum offer_status status;
	bool created;

	if (!param(cmd, buffer, params,
		   p_req("bolt12", param_b12_offer, &offer),
		   p_opt("label", param_label, &label),
		   p_opt_def("single_use", param_bool, &single_use, false),
		   NULL))
		return command_param_failed();

	if (*single_use)
		status = OFFER_SINGLE_USE_UNUSED;
	else
		status = OFFER_MULTIPLE_USE_UNUSED;
	b12str = offer_encode(cmd, offer);
	offer_offer_id(offer, &offer_id);

	/* If it already exists, we use that one instead (and then
	 * the offer plugin will complain if it's inactive or expired) */
	if (!wallet_offer_create(cmd->ld->wallet, &offer_id,
				 b12str, label, status)) {
		if (!wallet_offer_find(cmd, cmd->ld->wallet, &offer_id,
				       cast_const2(const struct json_escape **,
						   &label),
				       &status)) {
			return command_fail(cmd, LIGHTNINGD,
					    "Could not create, nor find offer");
		}
		created = false;
	} else
		created = true;

	response = json_stream_success(cmd);
	json_populate_offer(response, &offer_id, b12str, label, status);
	json_add_bool(response, "created", created);
	return command_success(cmd, response);
}

static const struct json_command createoffer_command = {
	"createoffer",
	json_createoffer,
};
AUTODATA(json_command, &createoffer_command);

static struct command_result *json_listoffers(struct command *cmd,
					       const char *buffer,
					       const jsmntok_t *obj UNNEEDED,
					       const jsmntok_t *params)
{
	struct sha256 *offer_id;
	struct json_stream *response;
	struct wallet *wallet = cmd->ld->wallet;
	const char *b12;
	const char *description;
	const struct json_escape *label;
	bool *active_only;
	enum offer_status status;

	if (!param(cmd, buffer, params,
		   p_opt("offer_id", param_sha256, &offer_id),
		   p_opt_def("active_only", param_bool, &active_only, false),
		   NULL))
		return command_param_failed();

	response = json_stream_success(cmd);
	json_array_start(response, "offers");
	if (offer_id) {
		b12 = wallet_offer_find(tmpctx, wallet, offer_id, &label,
					&status);
		if (b12 && offer_status_active(status) >= *active_only) {
			json_object_start(response, NULL);
			json_populate_offer(response,
					    offer_id, b12,
					    label, status);
			description = offer_description_from_b12(tmpctx, cmd->ld, b12);
			if (description)
				json_add_stringn(response, "description", description, tal_bytelen(description));
			json_object_end(response);
		}
	} else {
		struct db_stmt *stmt;
		struct sha256 id;

		for (stmt = wallet_offer_id_first(cmd->ld->wallet, &id);
		     stmt;
		     stmt = wallet_offer_id_next(cmd->ld->wallet, stmt, &id)) {
			b12 = wallet_offer_find(tmpctx, wallet, &id,
						&label, &status);
			if (offer_status_active(status) >= *active_only) {
				json_object_start(response, NULL);
				json_populate_offer(response,
						    &id, b12,
						    label, status);
				description = offer_description_from_b12(tmpctx, cmd->ld, b12);
				if (description)
					json_add_stringn(response, "description", description, tal_bytelen(description));
				json_object_end(response);
			}
		}
	}
	json_array_end(response);
	return command_success(cmd, response);
}

static const struct json_command listoffers_command = {
	"listoffers",
	json_listoffers,
};
AUTODATA(json_command, &listoffers_command);

static struct command_result *json_disableoffer(struct command *cmd,
						const char *buffer,
						const jsmntok_t *obj UNNEEDED,
						const jsmntok_t *params)
{
	struct json_stream *response;
	struct sha256 *offer_id;
	struct wallet *wallet = cmd->ld->wallet;
	const char *b12;
	const char *description;
	const struct json_escape *label;
	enum offer_status status;

	if (!param_check(cmd, buffer, params,
			 p_req("offer_id", param_sha256, &offer_id),
			 NULL))
		return command_param_failed();

	b12 = wallet_offer_find(tmpctx, wallet, offer_id, &label, &status);
	if (!b12)
		return command_fail(cmd, LIGHTNINGD, "Unknown offer");

	if (!offer_status_active(status))
		return command_fail(cmd, OFFER_ALREADY_DISABLED,
				    "offer is not active");

	if (command_check_only(cmd))
		return command_check_done(cmd);

	status = wallet_offer_disable(wallet, offer_id, status);

	response = json_stream_success(cmd);
	json_populate_offer(response, offer_id, b12, label, status);
	description = offer_description_from_b12(tmpctx, cmd->ld, b12);
	if (description)
		json_add_stringn(response, "description", description, tal_bytelen(description));
	return command_success(cmd, response);
}

static const struct json_command disableoffer_command = {
	"disableoffer",
	json_disableoffer,
};
AUTODATA(json_command, &disableoffer_command);

static struct command_result *json_enableoffer(struct command *cmd,
						const char *buffer,
						const jsmntok_t *obj UNNEEDED,
						const jsmntok_t *params)
{
	struct json_stream *response;
	struct sha256 *offer_id;
	struct wallet *wallet = cmd->ld->wallet;
	const char *b12;
	const char *description;
	const struct json_escape *label;
	enum offer_status status;

	if (!param_check(cmd, buffer, params,
			 p_req("offer_id", param_sha256, &offer_id),
			 NULL))
		return command_param_failed();

	b12 = wallet_offer_find(tmpctx, wallet, offer_id, &label, &status);
	if (!b12)
		return command_fail(cmd, LIGHTNINGD, "Unknown offer");

	if (offer_status_active(status))
		return command_fail(cmd, OFFER_ALREADY_ENABLED,
				    "offer already active");

	if (offer_status_single(status) && offer_status_used(status))
		return command_fail(cmd, OFFER_USED_SINGLE_USE,
				    "cannot activate an used single use offer");

	if (command_check_only(cmd))
		return command_check_done(cmd);

	status = wallet_offer_enable(wallet, offer_id, status);

	response = json_stream_success(cmd);
	json_populate_offer(response, offer_id, b12, label, status);
	description = offer_description_from_b12(tmpctx, cmd->ld, b12);
	if (description)
		json_add_stringn(response, "description", description, tal_bytelen(description));
	return command_success(cmd, response);
}

static const struct json_command enableoffer_command = {
	"enableoffer",
	json_enableoffer,
};
AUTODATA(json_command, &enableoffer_command);


/* We do some sanity checks now, since we're looking up prev payment anyway,
 * but our main purpose is to fill in prev_basetime tweak. */
static struct command_result *prev_payment(struct command *cmd,
					   const struct json_escape *label,
					   const struct tlv_invoice_request *invreq,
					   u64 **prev_basetime)
{
	struct sha256 invreq_oid;
	u64 last_recurrence = UINT64_MAX;
	bool prev_unpaid = false;

	invreq_offer_id(invreq, &invreq_oid);

	for (struct db_stmt *stmt = payments_by_label(cmd->ld->wallet, label);
	     stmt;
	     stmt = payments_next(cmd->ld->wallet, stmt)) {
		const struct wallet_payment *payment;
		const struct tlv_invoice *inv;
		char *fail;
		struct sha256 inv_oid;

		payment = payment_get_details(tmpctx, stmt);
		if (!payment->invstring)
			continue;

		inv = invoice_decode(tmpctx, payment->invstring,
				     strlen(payment->invstring),
				     NULL, chainparams, &fail);
		if (!inv)
			continue;

		/* They can reuse labels across different offers. */
		invoice_offer_id(inv, &inv_oid);
		if (!sha256_eq(&inv_oid, &invreq_oid))
			continue;

		/* Be paranoid, in case someone inserts their own
		 * clashing label! */
		if (!inv->invreq_recurrence_counter)
			continue;

		/* BOLT-recurrence #12:
		 * - if `offer_recurrence_base` is present:
		 *   - MUST include `invreq_recurrence_start`
		 *   - MUST set `period_offset` to the period the sender wants for the
		 *     initial request
		 *   - MUST set `period_offset` to the same value on all following requests.
		 */
		if (inv->invreq_recurrence_start
		    && invreq->invreq_recurrence_start
		    && *inv->invreq_recurrence_start != *invreq->invreq_recurrence_start) {
			tal_free(stmt);
			return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
					    "recurrence_start was"
					    " previously %u",
					    *inv->invreq_recurrence_start);
		}

		/* They should all have the same basetime */
		if (!*prev_basetime)
			*prev_basetime = tal_dup(cmd, u64, inv->invoice_recurrence_basetime);

		/* Track highest one for better diagnostics */
		if (last_recurrence == UINT64_MAX
		    || last_recurrence < *inv->invreq_recurrence_counter) {
			last_recurrence = *inv->invreq_recurrence_counter;
		}

		if (*inv->invreq_recurrence_counter == *invreq->invreq_recurrence_counter-1) {
			/* Got it! */
			if (payment->status == PAYMENT_COMPLETE) {
				tal_free(stmt);
				return NULL;
			} else
				prev_unpaid = true;
		}
	}

	/* We found one, but it didn't succeed */
	if (prev_unpaid)
		return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
				    "previous invoice payment did not succeed");

	/* We found one, but it was not the previus one */
	if (last_recurrence != UINT64_MAX)
		return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
				    "previous invoice has not been paid (last was %"PRIu64")",
				    last_recurrence);

	return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
			    "No previous payment attempted for this"
			    " label and offer");
}

/* FIXME(vincenzopalazzo): move this to comm/bolt12.h */
static struct command_result *param_b12_invreq(struct command *cmd,
					       const char *name,
					       const char *buffer,
					       const jsmntok_t *tok,
					       struct tlv_invoice_request **invreq)
{
	char *fail;

	*invreq = invrequest_decode(cmd, buffer + tok->start,
				    tok->end - tok->start,
				    cmd->ld->our_features, chainparams, &fail);
	if (!*invreq)
		return command_fail_badparam(cmd, name, buffer, tok, fail);

	if (!(*invreq)->invreq_metadata)
		return command_fail_badparam(cmd, name, buffer, tok,
					     "must have invreq_metadata");

	if (!(*invreq)->invreq_payer_id)
		return command_fail_badparam(cmd, name, buffer, tok,
					     "must have invreq_payer_id");
	return NULL;
}

static bool payer_key(struct lightningd *ld,
		      const u8 *public_tweak, size_t public_tweak_len,
		      struct pubkey *key)
{
	struct sha256 tweakhash;

	*key = ld->our_pubkey;
	bolt12_alias_tweak(&ld->nodealias_base,
			   public_tweak, public_tweak_len,
			   &tweakhash);

	return secp256k1_ec_pubkey_tweak_add(secp256k1_ctx,
					     &key->pubkey,
					     tweakhash.u.u8) == 1;
}

static void json_populate_invreq(struct json_stream *response,
				 const struct sha256 *invreq_id,
				 const char *b12,
				 const struct json_escape *label,
				 enum offer_status status)
{
	json_add_sha256(response, "invreq_id", invreq_id);
	json_add_bool(response, "active", offer_status_active(status));
	json_add_bool(response, "single_use", offer_status_single(status));
	json_add_string(response, "bolt12", b12);
	json_add_bool(response, "used", offer_status_used(status));
	if (label)
		json_add_escaped_string(response, "label", label);
}

static struct command_result *json_createinvoicerequest(struct command *cmd,
							const char *buffer,
							const jsmntok_t *obj,
							const jsmntok_t *params)
{
	struct tlv_invoice_request *invreq;
	struct json_escape *label;
	struct json_stream *response;
	u64 *prev_basetime = NULL;
	struct sha256 merkle;
	bool *save, *single_use;
	enum offer_status status;
	struct sha256 invreq_id;
	const char *b12str;
	const u8 *tweak;

	if (!param_check(cmd, buffer, params,
			 p_req("bolt12", param_b12_invreq, &invreq),
			 p_req("savetodb", param_bool, &save),
			 p_opt("recurrence_label", param_label, &label),
			 p_opt_def("single_use", param_bool, &single_use, true),
			 NULL))
		return command_param_failed();

	if (*single_use)
		status = OFFER_SINGLE_USE_UNUSED;
	else
		status = OFFER_MULTIPLE_USE_UNUSED;

	/* If it's a recurring payment, we look for previous to copy basetime */
	if (invreq->invreq_recurrence_counter) {
		if (!label)
			return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
					    "Need payment label for recurring payments");

		if (*invreq->invreq_recurrence_counter != 0) {
			struct command_result *err
				= prev_payment(cmd, label, invreq,
					       &prev_basetime);
			if (err)
				return err;
		}
	}

	/* If the payer_id is not our node id, we sanity check that it
	 * correctly maps from invreq_metadata */
	if (!pubkey_eq(invreq->invreq_payer_id, &cmd->ld->our_pubkey)) {
		struct pubkey expected;
		if (!payer_key(cmd->ld,
			       invreq->invreq_metadata,
			       tal_bytelen(invreq->invreq_metadata),
			       &expected)) {
			return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
					    "Invalid tweak");
		}
		if (!pubkey_eq(invreq->invreq_payer_id, &expected)) {
			return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
					    "payer_id did not match invreq_metadata derivation %s",
					    fmt_pubkey(tmpctx, &expected));
		}
		tweak = invreq->invreq_metadata;
	} else {
		tweak = NULL;
	}

	if (command_check_only(cmd))
		return command_check_done(cmd);

	/* BOLT #12:
	 *  - MUST set `signature`.`sig` as detailed in
	 *  [Signature Calculation](#signature-calculation) using the `invreq_payer_id`.
	 */
	/* This populates the ->fields from our entries */
	tlv_update_fields(invreq, tlv_invoice_request, &invreq->fields);
	merkle_tlv(invreq->fields, &merkle);
	invreq->signature = tal(invreq, struct bip340sig);
	hsm_sign_b12(cmd->ld, "invoice_request", "signature",
		     &merkle, tweak,
		     invreq->invreq_payer_id, invreq->signature);

	b12str = invrequest_encode(cmd, invreq);

	invreq_invreq_id(invreq, &invreq_id);
	if (*save && !wallet_invoice_request_create(cmd->ld->wallet, &invreq_id,
						    b12str, label, status)) {
		return command_fail(cmd, LIGHTNINGD,
				    "Could not create invoice_request!");
	}

	response = json_stream_success(cmd);
	json_populate_invreq(response, &invreq_id,
			     b12str,
			     label,
			     status);
	if (prev_basetime)
		json_add_u64(response, "previous_basetime", *prev_basetime);
	return command_success(cmd, response);
}

static const struct json_command createinvreq_command = {
	"createinvoicerequest",
	json_createinvoicerequest,
};
AUTODATA(json_command, &createinvreq_command);

static struct command_result *json_payersign(struct command *cmd,
					     const char *buffer,
					     const jsmntok_t *obj UNNEEDED,
					     const jsmntok_t *params)
{
	struct json_stream *response;
	struct sha256 *merkle;
	u8 *tweak;
	struct bip340sig sig;
	const char *messagename, *fieldname;
	struct pubkey key;

	if (!param(cmd, buffer, params,
		   p_req("messagename", param_string, &messagename),
		   p_req("fieldname", param_string, &fieldname),
		   p_req("merkle", param_sha256, &merkle),
		   p_req("tweak", param_bin_from_hex, &tweak),
		   NULL))
		return command_param_failed();

	payer_key(cmd->ld, tweak, tal_bytelen(tweak), &key);
	hsm_sign_b12(cmd->ld, messagename, fieldname, merkle,
		     tweak, &key, &sig);

	response = json_stream_success(cmd);
	json_add_string(response, "signature", fmt_bip340sig(tmpctx, &sig));
	return command_success(cmd, response);
}

static const struct json_command payersign_command = {
	"payersign",
	json_payersign,
};
AUTODATA(json_command, &payersign_command);

static struct command_result *json_listinvoicerequests(struct command *cmd,
						       const char *buffer,
						       const jsmntok_t *obj UNNEEDED,
						       const jsmntok_t *params)
{
	struct sha256 *invreq_id;
	struct json_stream *response;
	struct wallet *wallet = cmd->ld->wallet;
	const char *b12;
	const struct json_escape *label;
	bool *active_only;
	enum offer_status status;

	if (!param(cmd, buffer, params,
		   p_opt("invreq_id", param_sha256, &invreq_id),
		   p_opt_def("active_only", param_bool, &active_only, false),
		   NULL))
		return command_param_failed();

	response = json_stream_success(cmd);
	json_array_start(response, "invoicerequests");
	if (invreq_id) {
		b12 = wallet_invoice_request_find(tmpctx, wallet,
						  invreq_id, &label,
						  &status);
		if (b12 && offer_status_active(status) >= *active_only) {
			json_object_start(response, NULL);
			json_populate_invreq(response,
					    invreq_id, b12,
					    label, status);
			json_object_end(response);
		}
	} else {
		struct db_stmt *stmt;
		struct sha256 id;

		for (stmt = wallet_invreq_id_first(cmd->ld->wallet, &id);
		     stmt;
		     stmt = wallet_invreq_id_next(cmd->ld->wallet, stmt, &id)) {
			b12 = wallet_invoice_request_find(tmpctx, wallet, &id,
						&label, &status);
			if (offer_status_active(status) >= *active_only) {
				json_object_start(response, NULL);
				json_populate_invreq(response,
						    &id, b12,
						    label, status);
				json_object_end(response);
			}
		}
	}
	json_array_end(response);
	return command_success(cmd, response);
}

static const struct json_command listinvoicerequests_command = {
	"listinvoicerequests",
	json_listinvoicerequests,
};
AUTODATA(json_command, &listinvoicerequests_command);

static struct command_result *json_disableinvoicerequest(struct command *cmd,
							 const char *buffer,
							 const jsmntok_t *obj UNNEEDED,
							 const jsmntok_t *params)
{
	struct json_stream *response;
	struct sha256 *invreq_id;
	struct wallet *wallet = cmd->ld->wallet;
	const char *b12;
	const struct json_escape *label;
	enum offer_status status;

	if (!param_check(cmd, buffer, params,
			 p_req("invreq_id", param_sha256, &invreq_id),
			 NULL))
		return command_param_failed();

	b12 = wallet_invoice_request_find(tmpctx, wallet, invreq_id,
					  &label, &status);
	if (!b12)
		return command_fail(cmd, LIGHTNINGD, "Unknown invoice_request");

	if (!offer_status_active(status))
		return command_fail(cmd, OFFER_ALREADY_DISABLED,
				    "invoice_request is not active");

	if (command_check_only(cmd))
		return command_check_done(cmd);

	status = wallet_invoice_request_disable(wallet, invreq_id, status);

	response = json_stream_success(cmd);
	json_populate_invreq(response, invreq_id, b12, label, status);
	return command_success(cmd, response);
}

static const struct json_command disableinvoicerequest_command = {
	"disableinvoicerequest",
	json_disableinvoicerequest,
};
AUTODATA(json_command, &disableinvoicerequest_command);

