#include "config.h"
#include <common/dijkstra.h>
#include <common/features.h>
#include <common/gossmap.h>
#include <common/json_stream.h>
#include <common/route.h>
#include <plugins/establish_onion_path.h>

struct connect_info {
	struct pubkey local_id, dst;
	bool connect_disable;
	struct gossmap *gossmap;
	struct command_result *(*cb)(struct command *,
				     const struct pubkey *,
				     void *arg);
	struct command_result *(*fail)(struct command *, const char *,
				       void *arg);
	void *arg;
};

static struct command_result *connect_ok(struct command *cmd,
					 const char *method,
					 const char *buf,
					 const jsmntok_t *result,
					 struct connect_info *ci)
{
	struct pubkey *path = tal_arr(tmpctx, struct pubkey, 2);

	/* Create direct mini-path */
	path[0] = ci->local_id;
	path[1] = ci->dst;

	return ci->cb(cmd, path, ci->arg);
}

static struct command_result *command_failed(struct command *cmd,
					     const char *method,
					     const char *buf,
					     const jsmntok_t *result,
					     struct connect_info *ci)
{
	return ci->fail(cmd, json_strdup(tmpctx, buf, result), ci->arg);
}

static struct command_result *connect_direct(struct command *cmd,
					     struct connect_info *ci)
{
	struct out_req *req;

	if (ci->connect_disable) {
		return ci->fail(cmd, "fetchinvoice-noconnect set: not initiating a new connection",
				ci->arg);
	}
	plugin_log(cmd->plugin, LOG_DBG, "connecting directly to %s",
		   fmt_pubkey(tmpctx, &ci->dst));

	req = jsonrpc_request_start(cmd,
				    "connect", connect_ok, command_failed, ci);
	json_add_pubkey(req->js, "id", &ci->dst);
	return send_outreq(req);
}

static bool can_carry_onionmsg(const struct gossmap *map,
			       const struct gossmap_chan *c,
			       int dir,
			       struct amount_msat amount UNUSED,
			       void *arg UNUSED)
{
	const struct gossmap_node *n;

	/* Don't try to use disabled channels! */
	if (!c->half[dir].enabled)
		return false;

	/* Our local additions are always fine, since we checked features then */
	if (gossmap_chan_is_localmod(map, c))
		return true;

	/* Check features of recipient */
	n = gossmap_nth_node(map, c, !dir);
	return gossmap_node_get_feature(map, n, OPT_ONION_MESSAGES) != -1;
}

static void local_mods_remove_channels(struct gossmap_localmods *mods,
				       const struct gossmap *gossmap,
				       const struct node_id *selfid,
				       const struct node_id *peerid)
{
	const struct gossmap_node *peer = gossmap_find_node(gossmap, peerid);
	const struct gossmap_node *self = gossmap_find_node(gossmap, selfid);
	const bool enabled_off = false;

	if (!peer || !self)
		return;

	for (size_t i = 0; i < self->num_chans; i++) {
		int dir;
		struct short_channel_id_dir scidd;
		struct gossmap_chan *c = gossmap_nth_chan(gossmap, self, i, &dir);

		if (gossmap_nth_node(gossmap, c, !dir) != peer)
			continue;
		scidd.scid = gossmap_chan_scid(gossmap, c);
		scidd.dir = dir;

		/* Set enabled -> false for this channel */
		gossmap_local_updatechan(mods, &scidd, &enabled_off,
					 NULL, NULL, NULL, NULL, NULL);
	}
}

/* We add fake channels to gossmap to represent current outgoing connections.
 * This allows dijkstra to find transient connections as well. */
static struct gossmap_localmods *
gossmods_from_listpeers(const tal_t *ctx,
			struct plugin *plugin,
			const struct gossmap *gossmap,
			const struct node_id *self,
			const char *buf,
			const jsmntok_t *toks)
{
	struct gossmap_localmods *mods = gossmap_localmods_new(ctx);
	const jsmntok_t *peers, *peer;
	size_t i;

	peers = json_get_member(buf, toks, "peers");
	json_for_each_arr(i, peer, peers) {
		bool connected;
		struct node_id peer_id;
		const char *err;
		u8 *features = NULL;
		struct short_channel_id_dir fake_scidd;
		bool enabled = true;

		err = json_scan(tmpctx, buf, peer,
				"{connected:%,"
				"id:%,"
				"features?:%}",
				JSON_SCAN(json_to_bool, &connected),
				JSON_SCAN(json_to_node_id, &peer_id),
				JSON_SCAN_TAL(tmpctx, json_tok_bin_from_hex, &features));
		if (err) {
			plugin_err(plugin, "Bad listpeers.peers %zu: %s", i, err);
		}

		if (!feature_offered(features, OPT_ONION_MESSAGES))
			continue;

		if (!connected) {
			/* Discard any channels we have with that. */
			local_mods_remove_channels(mods, gossmap, self, &peer_id);
			continue;
		}

		/* Add a fake channel */
		fake_scidd.scid.u64 = i;
		fake_scidd.dir = node_id_idx(self, &peer_id);

		gossmap_local_addchan(mods, self, &peer_id, fake_scidd.scid,
				      AMOUNT_MSAT(1000), NULL);
		gossmap_local_updatechan(mods, &fake_scidd, &enabled,
					 NULL, NULL, NULL, NULL, NULL);
	}
	return mods;
}

static const struct pubkey *path_to_node(const tal_t *ctx,
					 struct command *cmd,
					 struct gossmap *gossmap,
					 struct plugin *plugin,
					 const char *buf,
					 const jsmntok_t *listpeers,
					 const struct pubkey *local_id,
					 const struct pubkey *dst_key)
{
	struct route_hop *r;
	const struct dijkstra *dij;
	const struct gossmap_node *src;
	const struct gossmap_node *dst;
	struct pubkey *nodes;
	struct gossmap_localmods *mods;
	struct node_id local_nodeid, dst_nodeid;

	node_id_from_pubkey(&local_nodeid, local_id);
	node_id_from_pubkey(&dst_nodeid, dst_key);
	mods = gossmods_from_listpeers(tmpctx, cmd->plugin, gossmap, &local_nodeid, buf, listpeers);

	gossmap_apply_localmods(gossmap, mods);
	dst = gossmap_find_node(gossmap, &dst_nodeid);
	if (!dst)
		goto fail;

	/* If we don't exist in gossip, routing can't happen. */
	src = gossmap_find_node(gossmap, &local_nodeid);
	if (!src)
		goto fail;

	dij = dijkstra(tmpctx, gossmap, dst, AMOUNT_MSAT(0), 0,
		       can_carry_onionmsg, route_score_shorter, NULL);

	r = route_from_dijkstra(tmpctx, gossmap, dij, src, AMOUNT_MSAT(0), 0);
	if (!r)
		goto fail;

	nodes = tal_arr(ctx, struct pubkey, tal_count(r) + 1);
	nodes[0] = *local_id;
	plugin_log(plugin, LOG_DBG, "Found path to %s: %s(us)",
		   fmt_node_id(tmpctx, &dst_nodeid),
		   fmt_pubkey(tmpctx, local_id));
	for (size_t i = 0; i < tal_count(r); i++) {
		if (!pubkey_from_node_id(&nodes[i+1], &r[i].node_id)) {
			plugin_err(plugin, "Could not convert nodeid %s",
				   fmt_node_id(tmpctx, &r[i].node_id));
		}
		plugin_log(plugin, LOG_DBG, "-> %s",
			   fmt_node_id(tmpctx, &r[i].node_id));
	}

	gossmap_remove_localmods(gossmap, mods);
	return nodes;

fail:
	gossmap_remove_localmods(gossmap, mods);
	return NULL;
}

static struct command_result *listpeers_done(struct command *cmd,
					     const char *method,
					     const char *buf,
					     const jsmntok_t *result,
					     struct connect_info *ci)
{
	const struct pubkey *path;

	path = path_to_node(tmpctx, cmd,
			    ci->gossmap, cmd->plugin, buf, result,
			    &ci->local_id, &ci->dst);
	if (!path)
		return connect_direct(cmd, ci);

	return ci->cb(cmd, path, ci->arg);
}

struct command_result *establish_onion_path_(struct command *cmd,
					     struct gossmap *gossmap,
					     const struct pubkey *local_id,
					     const struct pubkey *dst,
					     bool connect_disable,
					     struct command_result *(*success)(struct command *,
									       const struct pubkey *,
									       void *arg),
					     struct command_result *(*fail)(struct command *,
									    const char *why,
									    void *arg),
					     void *arg)
{
	struct connect_info *ci = tal(cmd, struct connect_info);
	struct out_req *req;

	/* Talking to ourselves is trivial. */
	if (pubkey_eq(local_id, dst)) {
		struct pubkey *path = tal_dup_arr(tmpctx, struct pubkey, local_id, 1, 0);
		return success(cmd, path, arg);
	}

	ci->local_id = *local_id;
	ci->dst = *dst;
	ci->cb = success;
	ci->fail = fail;
	ci->arg = arg;
	ci->connect_disable = connect_disable;
	ci->gossmap = gossmap;

	/* We use listpeers here: we don't actually care about channels, just connections! */
	req = jsonrpc_request_start(cmd, "listpeers",
				    listpeers_done,
				    command_failed,
				    ci);
	return send_outreq(req);
}
