// The Construct
//
// Copyright (C) The Construct Developers, Authors & Contributors
// Copyright (C) 2016-2020 Jason Volk <jason@zemos.net>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice is present in all copies. The
// full license for this software is available in the LICENSE file.

namespace ircd::m::fed::well_known
{
	static net::hostport make_remote(const string_view &);
	static void submit(request &);
	static void receive(request &);
	static void finish(request &);
	static bool redirect(request &);
	static bool handle(request &);
	static void worker();

	static server::request request_skip;
	extern ctx::dock worker_dock;
	extern ctx::context worker_context;
	extern run::changed handle_quit;
	extern log::log log;
}

template<>
decltype(ircd::util::instance_list<ircd::m::fed::well_known::request>::allocator)
ircd::util::instance_list<ircd::m::fed::well_known::request>::allocator
{};

template<>
decltype(ircd::util::instance_list<ircd::m::fed::well_known::request>::list)
ircd::util::instance_list<ircd::m::fed::well_known::request>::list
{
    allocator
};

decltype(ircd::m::fed::well_known::log)
ircd::m::fed::well_known::log
{
	"m.well-known"
};

decltype(ircd::m::fed::well_known::cache_default)
ircd::m::fed::well_known::cache_default
{
	{ "name",     "ircd.m.fed.well-known.cache.default" },
	{ "default",  24 * 60 * 60L                         },
};

decltype(ircd::m::fed::well_known::cache_error)
ircd::m::fed::well_known::cache_error
{
	{ "name",     "ircd.m.fed.well-known.cache.error" },
	{ "default",  36 * 60 * 60L                       },
};

///NOTE: not yet used until HTTP cache headers in response are respected.
decltype(ircd::m::fed::well_known::cache_max)
ircd::m::fed::well_known::cache_max
{
	{ "name",     "ircd.m.fed.well-known.cache.max" },
	{ "default",  48 * 60 * 60L                     },
};

decltype(ircd::m::fed::well_known::request::path)
ircd::m::fed::well_known::request::path
{
	"/.well-known/matrix/server"
};

decltype(ircd::m::fed::well_known::request::type)
ircd::m::fed::well_known::request::type
{
	"well-known.matrix.server"
};

decltype(ircd::m::fed::well_known::request::sopts)
ircd::m::fed::well_known::request::sopts
{
	false // http_exceptions
};

decltype(ircd::m::fed::well_known::request::timeout)
ircd::m::fed::well_known::request::timeout
{
	{ "name",     "ircd.m.fed.well-known.request.timeout" },
	{ "default",  15L                                     },
};

decltype(ircd::m::fed::well_known::request::redirects_max)
ircd::m::fed::well_known::request::redirects_max
{
	{ "name",     "ircd.m.fed.well-known.request.redirects.max" },
	{ "default",  2L                                            },
};

decltype(ircd::m::fed::well_known::request::id_ctr)
ircd::m::fed::well_known::request::id_ctr;

decltype(ircd::m::fed::well_known::request::mutex)
ircd::m::fed::well_known::request::mutex;

decltype(ircd::m::fed::well_known::worker_dock)
ircd::m::fed::well_known::worker_dock;

decltype(ircd::m::fed::well_known::worker_context)
ircd::m::fed::well_known::worker_context
{
	"m.fed.well_known",
	512_KiB,
	&worker,
	context::POST
};

decltype(ircd::m::fed::well_known::handle_quit)
ircd::m::fed::well_known::handle_quit
{
	run::level::QUIT, []() noexcept
	{
		worker_dock.notify_all();
	}
};

ircd::ctx::future<ircd::string_view>
ircd::m::fed::well_known::get(const mutable_buffer &buf,
                              const string_view &target,
                              const opts &opts)
try
{
	const m::room::id::buf cache_room_id
	{
		"dns", m::my_host()
	};

	const m::room cache_room
	{
		cache_room_id
	};

	const m::event::idx event_idx
	{
		likely(opts.cache_check)?
			cache_room.get(std::nothrow, request::type, target):
			0UL
	};

	const milliseconds origin_server_ts
	{
		m::get<time_t>(std::nothrow, event_idx, "origin_server_ts", time_t(0))
	};

	const json::object content
	{
		m::get(std::nothrow, event_idx, "content", buf)
	};

	const seconds ttl
	{
		content.get<time_t>("ttl", time_t(86400))
	};

	const system_point expires
	{
		origin_server_ts + ttl
	};

	const bool expired
	{
		ircd::now<system_point>() > expires
	};

	const json::string cached
	{
		content["m.server"]
	};

	const bool valid
	{
		// entry must not be blank
		!empty(cached)

		// entry must not be expired unless options allow expired hits
		&& (!expired || opts.expired)
	};

	// Branch to return cache hit
	if(likely(valid))
		return ctx::future<string_view>
		{
			ctx::already, string_view
			{
				data(buf), move(buf, cached)
			}
		};

	const net::hostport remote
	{
		make_remote(target)
	};

	const bool fetch
	{
		// options must allow network request
		opts.request

		// check if the peer already has a cached error in server::
		&& !server::errant(remote)
	};

	// Branch if won't fetch; return target itself as result
	if(!fetch)
		return ctx::future<string_view>
		{
			ctx::already, string_view
			{
				data(buf), move(buf, target)
			}
		};

	if(opts.cache_check)
	{
		char tmbuf[48];
		log::dwarning
		{
			log, "%s cache invalid %s event_idx:%u expires %s",
			target,
			cached?: json::string{"<not found>"},
			event_idx,
			cached? timef(tmbuf, expires, localtime): "<never>"_sv,
		};
	}

	// Synchronize modification of the request::list
	const std::lock_guard request_lock
	{
		request::mutex
	};

	// Start request
	auto req
	{
		std::make_unique<request>()
	};

	// req->target is a copy in the request struct so the caller can disappear.
	req->target = string_view
	{
		req->tgtbuf[0], copy(req->tgtbuf[0], target)
	};

	// req->m_server is a copy in the request struct so the caller can disappear.
	req->m_server = string_view
	{
		req->tgtbuf[1], copy(req->tgtbuf[1], cached)
	};

	// but req->out is not safe once the caller destroys their future, which
	// is indicated by req->promise's invalidation. Do not write to this
	// unless the promise is valid.
	req->out = buf;

	// all other properties are independent of the caller
	req->opts = opts;
	req->expires = expires;
	req->uri.path = request::path;
	req->uri.remote = req->target;
	ctx::future<string_view> ret{req->promise}; try
	{
		submit(*req);
		req.release();
	}
	catch(const std::exception &e)
	{
		log::derror
		{
			log, "request submit for %s :%s",
			target,
			e.what(),
		};

		const ctx::exception_handler eh;
		finish(*req);
	}

	return ret;
}
catch(const ctx::interrupted &)
{
	throw;
}
catch(const std::exception &e)
{
	log::error
	{
		log, "get %s :%s",
		target,
		e.what(),
	};

	return ctx::future<string_view>
	{
		ctx::already, string_view
		{
			data(buf), move(buf, target)
		}
	};
}

void
ircd::m::fed::well_known::worker()
try
{
	// Wait for runlevel RUN before proceeding...
	run::barrier<ctx::interrupted>{};
	while(!request::list.empty() || run::level == run::level::RUN)
	{
		worker_dock.wait([]() noexcept
		{
			return !request::list.empty() || run::level != run::level::RUN;
		});

		if(request::list.empty())
			break;

		auto next
		{
			ctx::when_any(std::begin(request::list), std::end(request::list), []
			(auto &it) -> server::request &
			{
				return !(*it)->req? request_skip: (*it)->req;
			})
		};

		const ctx::uninterruptible::nothrow ui;
		if(!next.wait(milliseconds(250), std::nothrow))
			continue;

		const auto it
		{
			next.get()
		};

		assert(it != std::end(request::list));
		if(unlikely(it == std::end(request::list)))
			continue;

		const std::lock_guard request_lock
		{
			request::mutex
		};

		std::unique_ptr<request> req
		{
			*it
		};

		if(!handle(*req)) // redirect
			req.release();
		else
			finish(*req);
	}

	assert(request::list.empty());
}
catch(const ctx::interrupted &)
{
	throw;
}
catch(const std::exception &e)
{
	log::critical
	{
		log, "Worker unhandled :%s",
		e.what(),
	};
}

/// Returns true if the request is done with either success or error; false
/// if the request resubmitted itself.
bool
ircd::m::fed::well_known::handle(request &req)
try
{
	receive(req);

	// Handle redirection.
	if(http::category(req.code) == http::category::REDIRECT)
		return redirect(req);

	return true;
}
catch(const std::exception &e)
{
	log::derror
	{
		log, "%s handling :%s",
		req.target,
		e.what(),
	};

	return true;
}

bool
ircd::m::fed::well_known::redirect(request &req)
{
	// Indirection code, but no location response header
	if(!req.location)
		return true;

	// Redirection; carry over the new target by copying it because it's
	// in the buffer which we'll be overwriting for the new request.
	req.carry = unique_mutable_buffer{req.location};
	req.uri = string_view{req.carry};

	// Indirection code, bad location header.
	if(!req.uri.path || !req.uri.remote)
		return true;

	if(req.redirects++ >= request::redirects_max)
		return true;

	// Redirect
	submit(req);
	return false;
}

void
ircd::m::fed::well_known::finish(request &req)
try
{
	json::string result;
	const unwind resolve{[&req, &result]
	{
		if(req.promise.valid())
			req.promise.set_value(string_view
			{
				data(req.out), move(req.out, result?: req.target)
			});
	}};

	if(req.code == 200 && json::valid(req.response, std::nothrow))
		result = req.response["m.server"];

	if(!result)
		result = req.m_server;

	if(!result)
		result = req.target;

	if(!rfc3986::valid_remote(std::nothrow, result))
		result = req.target;

	if(result != req.target)
		log::debug
		{
			log, "query to %s for %s resolved delegation to %s",
			req.uri.remote,
			req.target,
			result,
		};

	const bool cache_expired
	{
		req.expires < now<system_point>()
	};

	const bool cache_result
	{
		result
		&& req.opts.cache_result
		&& req.opts.request
		&& (cache_expired || result != req.m_server)
	};

	req.m_server = result;
	if(!cache_result)
		return;

	// Any time the well-known result is the same as the req.target (that
	// includes legitimate errors where fetch_well_known() returns the
	// req.target to default) we consider that an error and use the error
	// TTL value. Sorry, no exponential backoff implemented yet.
	const auto cache_ttl
	{
		req.target == req.m_server?
			seconds(cache_error).count():
			seconds(cache_default).count()
	};

	const m::room::id::buf cache_room_id
	{
		"dns", m::my_host()
	};

	// Write our record to the cache room; note that this doesn't really
	// match the format of other DNS records in this room since it's a bit
	// simpler, but we don't share the ircd.dns.rr type prefix anyway.
	const auto cache_id
	{
		m::send(cache_room_id, m::me(), request::type, req.target, json::members
		{
			{ "ttl",       cache_ttl    },
			{ "m.server",  req.m_server },
		})
	};

	log::debug
	{
		log, "%s cached delegation to %s with %s ttl:%ld",
		req.target,
		req.m_server,
		string_view{cache_id},
		cache_ttl,
	};
}
catch(const std::exception &e)
{
	log::error
	{
		log, "%s completion :%s",
		req.target,
		e.what(),
	};
}

void
ircd::m::fed::well_known::receive(request &req)
{
	assert(req.sopts.http_exceptions == false);
	req.code = req.req.get(seconds(request::timeout));
	req.head = req.req.in.gethead(req.req);
	req.location = req.head.location;
	req.response = json::object
	{
		req.req.in.content
	};

	log::debug
	{
		log, "fetch to %s %s :%u %s",
		req.uri.remote,
		req.uri.path,
		uint(req.code),
		http::status(req.code),
	};
}

void
ircd::m::fed::well_known::submit(request &req)
{
	const net::hostport target
	{
		make_remote(req.uri.remote)
	};

	const http::header headers[]
	{
		{ "User-Agent", info::user_agent },
	};

	window_buffer window
	{
		req.buf
	};

	http::request
	{
		window, host(target), "GET", req.uri.path, 0, {}, headers
	};

	server::out out;
	out.head = window.completed();

	// Remaining space in buffer is used for received head; note that below
	// we specify this same buffer for in.content, but that's a trick
	// recognized by ircd::server to place received content directly after
	// head in this buffer without any additional dynamic allocation.
	server::in in;
	in.head = mutable_buffer{req.buf} + size(out.head);
	in.content = in.head;

	req.code = http::code(0);
	req.head = {};
	req.response = {};
	req.location = {};
	req.req = server::request
	{
		target, std::move(out), std::move(in), &req.sopts
	};

	worker_dock.notify();
}

ircd::net::hostport
ircd::m::fed::well_known::make_remote(const string_view &target)
{
	const net::hostport remote
	{
		target
	};

	// Hard target https service; do not inherit any matrix service from remote.
	const net::hostport ret
	{
		net::host(remote), "https", net::port(remote)
	};

	return ret;
}