#include "config.h"
#include <common/json_stream.h>
#include <common/jsonrpc_errors.h>
#include <plugins/renepay/payplugin.h>
#include <plugins/renepay/routefail.h>
#include <plugins/renepay/routetracker.h>
#include <wire/peer_wiregen.h>

enum node_type {
	FINAL_NODE,
	INTERMEDIATE_NODE,
	ORIGIN_NODE,
	UNKNOWN_NODE
};

struct routefail {
	struct command *cmd;
	struct payment *payment;
	struct route *route;
};

static struct command_result *update_gossip(struct routefail *r);
static struct command_result *handle_failure(struct routefail *r);

struct command_result *routefail_start(const tal_t *ctx, struct route *route,
				       struct command *cmd)
{
	assert(route);
	struct routefail *r = tal(ctx, struct routefail);
	struct payment *payment =
		    payment_map_get(pay_plugin->payment_map, route->key.payment_hash);

	if (payment == NULL)
		plugin_err(pay_plugin->plugin,
			   "%s: payment with hash %s not found.",
			   __func__,
			   fmt_sha256(tmpctx, &route->key.payment_hash));

	r->payment = payment;
	r->route = route;
	r->cmd = cmd;
	assert(route->result);
	return update_gossip(r);
}

static struct command_result *routefail_end(struct routefail *r TAKES)
{
	/* Notify the tracker that route has failed and routefail have completed
	 * handling all possible errors cases. */
	struct command *cmd = r->cmd;
	route_failure_register(r->payment->routetracker, r->route);
	if (taken(r))
		r = tal_steal(tmpctx, r);
	return notification_handled(cmd);
}

/*****************************************************************************
 * update_gossip
 *
 * Update gossip from waitsendpay error message.
 */

/* Fix up the channel_update to include the type if it doesn't currently have
 * one. See ElementsProject/lightning#1730 and lightningnetwork/lnd#1599 for the
 * in-depth discussion on why we break message parsing here... */
static u8 *patch_channel_update(const tal_t *ctx, u8 *channel_update TAKES)
{
	u8 *fixed;
	if (channel_update != NULL &&
	    fromwire_peektype(channel_update) != WIRE_CHANNEL_UPDATE) {
		/* This should be a channel_update, prefix with the
		 * WIRE_CHANNEL_UPDATE type, but isn't. Let's prefix it. */
		fixed = tal_arr(ctx, u8, 0);
		towire_u16(&fixed, WIRE_CHANNEL_UPDATE);
		towire(&fixed, channel_update, tal_bytelen(channel_update));
		tal_free_if_taken(channel_update);
		return fixed;
	} else {
		return tal_dup_talarr(ctx, u8, channel_update);
	}
}

/* Return NULL if the wrapped onion error message has no channel_update field,
 * or return the embedded channel_update message otherwise. */
static u8 *channel_update_from_onion_error(const tal_t *ctx,
					   const u8 *onion_message)
{
	u8 *channel_update = NULL;
	struct amount_msat unused_msat;
	u32 unused32;

	/* Identify failcodes that have some channel_update.
	 *
	 * TODO > BOLT 1.0: Add new failcodes when updating to a
	 * new BOLT version. */
	if (!fromwire_temporary_channel_failure(ctx, onion_message,
						&channel_update) &&
	    !fromwire_amount_below_minimum(ctx, onion_message, &unused_msat,
					   &channel_update) &&
	    !fromwire_fee_insufficient(ctx, onion_message, &unused_msat,
				       &channel_update) &&
	    !fromwire_incorrect_cltv_expiry(ctx, onion_message, &unused32,
					    &channel_update) &&
	    !fromwire_expiry_too_soon(ctx, onion_message, &channel_update))
		/* No channel update. */
		return NULL;

	return patch_channel_update(ctx, take(channel_update));
}

static struct command_result *update_gossip_done(struct command *cmd UNUSED,
						 const char *method UNUSED,
						 const char *buf UNUSED,
						 const jsmntok_t *result UNUSED,
						 struct routefail *r)
{
	return handle_failure(r);
}

static struct command_result *update_gossip_failure(struct command *cmd UNUSED,
						    const char *method UNUSED,
						    const char *buf,
						    const jsmntok_t *result,
						    struct routefail *r)
{
	assert(r);
	assert(r->payment);
	assert(r->route->result->erring_index);

	const int index = *r->route->result->erring_index;
	struct short_channel_id_dir scidd;

	if (r->route->result->erring_channel) {
		scidd.scid = *r->route->result->erring_channel;
		scidd.dir = *r->route->result->erring_direction;
	} else if (r->route->hops) {
		assert(index < tal_count(r->route->hops));
		const struct route_hop *hop = &r->route->hops[index];
		scidd.scid = hop->scid;
		scidd.dir = hop->direction;

	} else /* don't have information to disable the erring channel */
		goto finish;

	payment_disable_chan(
	    r->payment, scidd, LOG_INFORM, "addgossip failed (%.*s)",
	    json_tok_full_len(result), json_tok_full(buf, result));

finish:
	return update_gossip_done(cmd, method, buf, result, r);
}

static struct command_result *update_gossip(struct routefail *r)
{
	/* if there is no raw_message we continue */
	if (!r->route->result->raw_message)
		goto skip_update_gossip;

	const u8 *update = channel_update_from_onion_error(
	    tmpctx, r->route->result->raw_message);

	if (!update)
		goto skip_update_gossip;

	struct out_req *req =
	    jsonrpc_request_start(r->cmd, "addgossip",
				  update_gossip_done, update_gossip_failure, r);
	json_add_hex_talarr(req->js, "message", update);
	return send_outreq(req);

skip_update_gossip:
	return handle_failure(r);
}

/*****************************************************************************
 * handle_cases
 *
 * Process the kind of error, we might decide that this payment cannot continue
 * or is it worth continue trying.
 */

/* Mark this as a final error. When read this route result will inmediately end
 * the payment. */
static void route_final_error(struct route *route, enum jsonrpc_errcode error,
			      const char *fmt, ...)
{
	assert(route);

	va_list ap;
	const char *what;

	va_start(ap, fmt);
	what = tal_vfmt(tmpctx, fmt, ap);
	va_end(ap);

	route->final_error = error;
	route->final_msg = tal_strdup(route, what);
}

/* FIXME: do proper error handling for BOLT12 */
static struct command_result *handle_failure(struct routefail *r)
{
	/* BOLT #4:
	 *
	 * A _forwarding node_ MAY, but a _final node_ MUST NOT:
	 *...
	 *     - return an `invalid_onion_version` error.
	 *...
	 *     - return an `invalid_onion_hmac` error.
	 *...
	 *     - return an `invalid_onion_key` error.
	 *...
	 *     - return a `temporary_channel_failure` error.
	 *...
	 *     - return a `permanent_channel_failure` error.
	 *...
	 *     - return a `required_channel_feature_missing` error.
	 *...
	 *     - return an `unknown_next_peer` error.
	 *...
	 *     - return an `amount_below_minimum` error.
	 *...
	 *     - return a `fee_insufficient` error.
	 *...
	 *     - return an `incorrect_cltv_expiry` error.
	 *...
	 *     - return an `expiry_too_soon` error.
	 *...
	 *     - return an `expiry_too_far` error.
	 *...
	 *     - return a `channel_disabled` error.
	 */
	/* BOLT #4:
	 *
	 * An _intermediate hop_ MUST NOT, but the _final node_:
	 *...
	 *     - MUST return an `incorrect_or_unknown_payment_details` error.
	 *...
	 *     - MUST return `final_incorrect_cltv_expiry` error.
	 *...
	 *     - MUST return a `final_incorrect_htlc_amount` error.
	 */
	assert(r);
	struct route *route = r->route;
	assert(route);
	struct payment_result *result = route->result;
	assert(result);
	struct payment *payment = r->payment;
	assert(payment);

	int path_len = 0;
	if (route->hops)
		path_len = tal_count(route->hops);

	enum onion_wire failcode;
	if(result->failcode)
		failcode = *result->failcode;
	else{
		payment_note(
		    payment, LOG_UNUSUAL,
		    "The failcode is unknown we skip error handling");
		goto finish;
	}

	if (!result->erring_index) {
		payment_note(
		    payment, LOG_UNUSUAL,
		    "The erring_index is unknown we skip error handling");
		goto finish;
	}

	enum node_type node_type = UNKNOWN_NODE;
	if (route->hops) {
		if (*result->erring_index == path_len)
			node_type = FINAL_NODE;
		else if (*result->erring_index == 0)
			node_type = ORIGIN_NODE;
		else
			node_type = INTERMEDIATE_NODE;
	}

	switch (failcode) {
	// intermediate only
	case WIRE_INVALID_ONION_VERSION:
	case WIRE_INVALID_ONION_HMAC:
	case WIRE_INVALID_ONION_KEY:
		switch (node_type) {
		case FINAL_NODE:
			payment_note(payment, LOG_UNUSUAL,
				     "Final node reported strange "
				     "error code %04x (%s)",
				     failcode,
				     onion_wire_name(failcode));
			break;
		case ORIGIN_NODE:
		case INTERMEDIATE_NODE:
		case UNKNOWN_NODE:
			break;
		}

	case WIRE_INVALID_ONION_BLINDING:
		switch (node_type) {
		case FINAL_NODE:
			/* these errors from a final node mean a permanent
			 * failure */
			route_final_error(
			    route, PAY_DESTINATION_PERM_FAIL,
			    "Received error code %04x (%s) at final node.",
			    failcode,
			    onion_wire_name(failcode));

			break;
		case INTERMEDIATE_NODE:
		case ORIGIN_NODE:
			if (!route->hops)
				break;

			/* we disable the next node in the hop */
			assert(*result->erring_index < path_len);
			payment_disable_node(
			    payment, route->hops[*result->erring_index].node_id,
			    LOG_DBG, "received %s from previous hop",
			    onion_wire_name(failcode));
			break;
		case UNKNOWN_NODE:
			break;
		}
		break;

	// final only
	case WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS:
		switch (node_type) {
		case FINAL_NODE:
			route_final_error(route, PAY_DESTINATION_PERM_FAIL,
					  "Unknown invoice or wrong payment "
					  "details at destination.");
			break;
		case ORIGIN_NODE:
			route_final_error(
			    route, PAY_UNSPECIFIED_ERROR,
			    "Error code %04x (%s) reported at the origin.",
			    failcode,
			    onion_wire_name(failcode));
			break;
		case INTERMEDIATE_NODE:
			if (!route->hops)
				break;
			payment_disable_node(
			    payment,
			    route->hops[*result->erring_index - 1].node_id,
			    LOG_INFORM, "received error %s",
			    onion_wire_name(failcode));
			break;
		case UNKNOWN_NODE:
			break;
		}
		break;
	case WIRE_FINAL_INCORRECT_HTLC_AMOUNT:
	case WIRE_FINAL_INCORRECT_CLTV_EXPIRY:
		switch (node_type) {
		case INTERMEDIATE_NODE:
			payment_note(payment, LOG_UNUSUAL,
				     "Intermediate node reported strange "
				     "error code %04x (%s)",
				     failcode,
				     onion_wire_name(failcode));
			break;
		case ORIGIN_NODE:
		case FINAL_NODE:
		case UNKNOWN_NODE:
			break;
		}

	case WIRE_PERMANENT_NODE_FAILURE:
	case WIRE_REQUIRED_NODE_FEATURE_MISSING:
	case WIRE_TEMPORARY_NODE_FAILURE:
	case WIRE_INVALID_ONION_PAYLOAD:
		switch (node_type) {
		case FINAL_NODE:
			route_final_error(
			    route, PAY_DESTINATION_PERM_FAIL,
			    "Received error code %04x (%s) at final node.",
			    failcode,
			    onion_wire_name(failcode));
			break;
		case ORIGIN_NODE:
			route_final_error(
			    route, PAY_UNSPECIFIED_ERROR,
			    "Error code %04x (%s) reported at the origin.",
			    failcode,
			    onion_wire_name(failcode));
			break;
		case INTERMEDIATE_NODE:
			if (!route->hops)
				break;
			payment_disable_node(
			    payment,
			    route->hops[*result->erring_index - 1].node_id,
			    LOG_INFORM, "received error %s",
			    onion_wire_name(failcode));
			break;
		case UNKNOWN_NODE:
			break;
		}
		break;

	// intermediate only
	case WIRE_PERMANENT_CHANNEL_FAILURE:
	case WIRE_REQUIRED_CHANNEL_FEATURE_MISSING:
	case WIRE_UNKNOWN_NEXT_PEER:
	case WIRE_EXPIRY_TOO_FAR:
	case WIRE_CHANNEL_DISABLED:
		switch (node_type) {
		case FINAL_NODE:
			payment_note(payment, LOG_UNUSUAL,
				     "Final node reported strange "
				     "error code %04x (%s)",
				     failcode,
				     onion_wire_name(failcode));

			route_final_error(
			    route, PAY_DESTINATION_PERM_FAIL,
			    "Received error code %04x (%s) at final node.",
			    failcode,
			    onion_wire_name(failcode));

			break;
		case ORIGIN_NODE:
			payment_note(payment, LOG_UNUSUAL,
				     "First node reported strange "
				     "error code %04x (%s)",
				     failcode,
				     onion_wire_name(failcode));

			break;
		case INTERMEDIATE_NODE:
			if (!route->hops)
				break;
			struct short_channel_id_dir scidd = {
			    .scid = route->hops[*result->erring_index].scid,
			    .dir = route->hops[*result->erring_index].direction};
			payment_disable_chan(payment, scidd, LOG_INFORM, "%s",
					     onion_wire_name(failcode));

			break;
		case UNKNOWN_NODE:
			break;
		}
		break;
	// final only
	case WIRE_MPP_TIMEOUT:
		switch (node_type) {
		case INTERMEDIATE_NODE:
			/* Normally WIRE_MPP_TIMEOUT is raised by the final
			 * node. If this is not the final node, then something
			 * wrong is going on. We report it and disable that
			 * node. */
			payment_note(payment, LOG_UNUSUAL,
				     "Intermediate node reported strange "
				     "error code %04x (%s)",
				     failcode,
				     onion_wire_name(failcode));

			if (!route->hops)
				break;
			payment_disable_node(
			    payment,
			    route->hops[*result->erring_index - 1].node_id,
			    LOG_INFORM, "received error %s",
			    onion_wire_name(failcode));

			break;
		case ORIGIN_NODE:
		case FINAL_NODE:
		case UNKNOWN_NODE:
			break;
		}
		break;

	// intermediate only
	case WIRE_EXPIRY_TOO_SOON:
	case WIRE_INCORRECT_CLTV_EXPIRY:
	case WIRE_FEE_INSUFFICIENT:
	case WIRE_AMOUNT_BELOW_MINIMUM:
		switch (node_type) {
		case FINAL_NODE:
			payment_note(payment, LOG_UNUSUAL,
				     "Final node reported strange "
				     "error code %04x (%s)",
				     failcode,
				     onion_wire_name(failcode));

			route_final_error(
			    route, PAY_DESTINATION_PERM_FAIL,
			    "Received error code %04x (%s) at final node.",
			    failcode,
			    onion_wire_name(failcode));

			break;
		case ORIGIN_NODE:
			payment_note(payment, LOG_UNUSUAL,
				     "First node reported strange "
				     "error code %04x (%s)",
				     failcode,
				     onion_wire_name(failcode));

			break;
		case INTERMEDIATE_NODE:
			if (!route->hops)
				break;
			/* Usually this means we need to update the channel
			 * information and try again. To avoid hitting this
			 * error again with the same channel we flag it. */
			struct short_channel_id_dir scidd = {
			    .scid = route->hops[*result->erring_index].scid,
			    .dir = route->hops[*result->erring_index].direction};
			payment_warn_chan(payment, scidd, LOG_INFORM,
					  "received error %s",
					  onion_wire_name(failcode));

			break;
		case UNKNOWN_NODE:
			break;
		}
		break;
	// intermediate only
	case WIRE_TEMPORARY_CHANNEL_FAILURE:
		switch (node_type) {
		case FINAL_NODE:
			/* WIRE_TEMPORARY_CHANNEL_FAILURE could mean that the
			 * next channel has not enough outbound liquidity or
			 * cannot add another HTLC. A final node cannot raise
			 * this error. */
			payment_note(payment, LOG_UNUSUAL,
				     "Final node reported strange "
				     "error code %04x (%s)",
				     failcode,
				     onion_wire_name(failcode));

			route_final_error(
			    route, PAY_DESTINATION_PERM_FAIL,
			    "Received error code %04x (%s) at final node.",
			    failcode,
			    onion_wire_name(failcode));

			break;
		case INTERMEDIATE_NODE:
		case ORIGIN_NODE:
		case UNKNOWN_NODE:
			break;
		}
		break;
	}

finish:
	return routefail_end(take(r));
}
