/*
 * \brief  Tool for managing the download of depot content
 * \author Norman Feske
 * \date   2017-12-08
 */

/*
 * Copyright (C) 2017 Genode Labs GmbH
 *
 * This file is part of the Genode OS framework, which is distributed
 * under the terms of the GNU Affero General Public License version 3.
 */

/* Genode includes */
#include <base/component.h>
#include <base/heap.h>
#include <base/attached_rom_dataspace.h>
#include <os/reporter.h>
#include <timer_session/connection.h>

/* local includes */
#include "xml.h"

namespace Depot_download_manager {
	using namespace Depot;
	struct Child_exit_state;
	struct Main;
}


struct Depot_download_manager::Child_exit_state
{
	bool exists = false;
	bool exited = false;
	int  code   = 0;

	typedef String<64> Name;

	Child_exit_state(Xml_node init_state, Name const &name)
	{
		init_state.for_each_sub_node("child", [&] (Xml_node child) {
			if (child.attribute_value("name", Name()) == name) {
				exists = true;
				if (child.has_attribute("exited")) {
					exited = true;
					code = child.attribute_value("exited", 0L);
				}
			}
		});
	}
};


struct Depot_download_manager::Main : Import::Download_progress
{
	Env &_env;

	Heap _heap { _env.ram(), _env.rm() };

	Attached_rom_dataspace _installation      { _env, "installation"      };
	Attached_rom_dataspace _dependencies      { _env, "dependencies"      };
	Attached_rom_dataspace _index             { _env, "index"             };
	Attached_rom_dataspace _init_state        { _env, "init_state"        };
	Attached_rom_dataspace _fetchurl_progress { _env, "fetchurl_progress" };

	/**
	 * User identity, from which current downloads are fetched
	 */
	Attached_rom_dataspace _current_user { _env, "user" };

	/**
	 * Result of signature verification, reported by the 'verify' component
	 */
	Attached_rom_dataspace _verified { _env, "verified" };

	class Invalid_download_url : Exception { };

	/**
	 * \throw Invalid_download_url
	 */
	Url _current_user_url() const;

	Archive::User _current_user_name() const
	{
		return _current_user.xml().attribute_value("name", Archive::User());
	}

	Path _current_user_path() const
	{
		return Path("/depot/", _current_user_name());
	}

	Expanding_reporter _init_config { _env, "config", "init_config" };

	Expanding_reporter _state_reporter { _env, "state", "state" };

	/**
	 * Version counters, used to enforce the restart or reconfiguration of
	 * components.
	 */
	Depot_query_version _depot_query_count { 1 };
	Fetchurl_version    _fetchurl_count    { 1 };

	unsigned const _fetchurl_max_attempts = 3;
	unsigned       _fetchurl_attempt      = 0;

	Archive::User _next_user { };

	List_model<Job> _jobs { };

	Constructible<Import> _import { };

	/**
	 * Download_progress interface
	 */
	Info download_progress(Archive::Path const &path) const override
	{
		Info result { Info::Bytes(), Info::Bytes() };
		try {
			Url const url_path(_current_user_url(), "/", Archive::download_file_path(path));

			/* search fetchurl progress report for matching 'url_path' */
			_fetchurl_progress.xml().for_each_sub_node("fetch", [&] (Xml_node fetch) {
				if (fetch.attribute_value("url", Url()) == url_path)
					result = { .total = fetch.attribute_value("total", Info::Bytes()),
					           .now   = fetch.attribute_value("now",   Info::Bytes()) }; });

		} catch (Invalid_download_url) { }
		return result;
	}

	void _update_state_report()
	{
		_state_reporter.generate([&] (Xml_generator &xml) {

			/* produce detailed reports while the installation is in progress */
			if (_import.constructed()) {
				xml.attribute("progress", "yes");
				_import->report(xml, *this);
			}

			/* once all imports have settled, present the final results */
			else {
				_jobs.for_each([&] (Job const &job) {

					if (!job.started)
						return;

					/*
					 * If a job has been started and has not failed, it must
					 * have succeeded at the time when the import is finished.
					 */
					char const *type = Archive::index(job.path) ? "index" : "archive";
					xml.node(type, [&] () {
						xml.attribute("path",  job.path);
						xml.attribute("state", job.failed ? "failed" : "done");
					});
				});
			}
		});
	}

	void _generate_init_config(Xml_generator &);

	void _generate_init_config()
	{
		_init_config.generate([&] (Xml_generator &xml) {
			_generate_init_config(xml); });
	}

	void _handle_installation()
	{
		_installation.update();

		Job::Update_policy policy(_heap);
		_jobs.update_from_xml(policy, _installation.xml());

		_depot_query_count.value++;

		_generate_init_config();
	}

	Signal_handler<Main> _installation_handler {
		_env.ep(), *this, &Main::_handle_installation };

	Signal_handler<Main> _query_result_handler {
		_env.ep(), *this, &Main::_handle_query_result };

	void _handle_query_result();

	Signal_handler<Main> _init_state_handler {
		_env.ep(), *this, &Main::_handle_init_state };

	void _handle_init_state();

	Signal_handler<Main> _fetchurl_progress_handler {
		_env.ep(), *this, &Main::_handle_fetchurl_progress };

	/* number of bytes downloaded by current fetchurl instance */
	uint64_t _downloaded_bytes { };

	void _handle_fetchurl_progress()
	{
		_fetchurl_progress.update();

		/* count sum of bytes downloaded by current fetchurl instance */
		_downloaded_bytes = 0;
		_fetchurl_progress.xml().for_each_sub_node("fetch", [&] (Xml_node fetch) {
			_downloaded_bytes += fetch.attribute_value("now", 0ULL); });

		if (_import.constructed()) {
			_import->apply_download_progress(*this);

			/* proceed with next import step if all downloads are done or failed */
			if (!_import->downloads_in_progress())
				_generate_init_config();
		}

		_update_state_report();
	}

	struct Fetchurl_watchdog
	{
		Main &_main;

		Timer::Connection _timer { _main._env };

		Signal_handler<Fetchurl_watchdog> _handler {
			_main._env.ep(), *this, &Fetchurl_watchdog::_handle };

		uint64_t _observed_downloaded_bytes = _main._downloaded_bytes;

		void _handle()
		{
			if (_main._downloaded_bytes != _observed_downloaded_bytes) {
				_observed_downloaded_bytes = _main._downloaded_bytes;
				return;
			}

			warning("fetchurl got stuck, respawning");

			/* downloads got stuck, try replacing fetchurl with new instance */
			_main._fetchurl_count.value++;
			_main._generate_init_config();
		}

		enum { PERIOD_SECONDS = 5UL };

		Fetchurl_watchdog(Main &main) : _main(main)
		{
			_timer.sigh(_handler);
			_timer.trigger_periodic((Genode::uint64_t)PERIOD_SECONDS*1000*1000);
		}
	};

	Constructible<Fetchurl_watchdog> _fetchurl_watchdog { };

	Main(Env &env) : _env(env)
	{
		_dependencies     .sigh(_query_result_handler);
		_index            .sigh(_query_result_handler);
		_current_user     .sigh(_query_result_handler);
		_init_state       .sigh(_init_state_handler);
		_verified         .sigh(_init_state_handler);
		_installation     .sigh(_installation_handler);
		_fetchurl_progress.sigh(_fetchurl_progress_handler);

		_handle_installation();
		_generate_init_config();
	}
};


Depot_download_manager::Url
Depot_download_manager::Main::_current_user_url() const
{
	if (!_current_user.xml().has_sub_node("url"))
		throw Invalid_download_url();

	Url const url = _current_user.xml().sub_node("url").decoded_content<Url>();

	/*
	 * Ensure that the URL does not contain any '"' character because it will
	 * be taken as an XML attribute value.
	 *
	 * XXX This should better be addressed by sanitizing the argument of
	 *     'Xml_generator::attribute' (issue #1757).
	 */
	for (char const *s = url.string(); *s; s++)
		if (*s == '"')
			throw Invalid_download_url();

	return url;
}


void Depot_download_manager::Main::_generate_init_config(Xml_generator &xml)
{
	xml.node("report", [&] () {
		xml.attribute("delay_ms", 500); });

	xml.node("parent-provides", [&] () {
		gen_parent_service<Rom_session>(xml);
		gen_parent_service<Cpu_session>(xml);
		gen_parent_service<Pd_session>(xml);
		gen_parent_service<Log_session>(xml);
		gen_parent_service<Timer::Session>(xml);
		gen_parent_service<Report::Session>(xml);
		gen_parent_service<Nic::Session>(xml);
		gen_parent_service<File_system::Session>(xml);
	});

	xml.node("start", [&] () {
		gen_depot_query_start_content(xml, _installation.xml(),
		                              _next_user, _depot_query_count, _jobs); });

	bool const fetchurl_running = _import.constructed()
	                           && _import->downloads_in_progress();
	if (fetchurl_running) {
		try {
			xml.node("start", [&] () {
				gen_fetchurl_start_content(xml, *_import, _current_user_url(),
				                           _fetchurl_count); });
		}
		catch (Invalid_download_url) {
			error("invalid download URL for depot user:", _current_user.xml());
		}
	}

	if (_import.constructed() && _import->unverified_archives_available())
		xml.node("start", [&] () {
			gen_verify_start_content(xml, *_import, _current_user_path()); });

	if (_import.constructed() && _import->verified_archives_available()) {

		xml.node("start", [&] () {
			gen_chroot_start_content(xml, _current_user_name());  });

		xml.node("start", [&] () {
			gen_extract_start_content(xml, *_import, _current_user_path(),
			                          _current_user_name()); });
	}

	_fetchurl_watchdog.conditional(fetchurl_running, *this);
}


void Depot_download_manager::Main::_handle_query_result()
{
	/* finish current import before starting a new one */
	if (_import.constructed())
		return;

	_dependencies.update();
	_index.update();
	_current_user.update();

	/* validate completeness of depot-user info */
	{
		Xml_node const user = _current_user.xml();

		Archive::User const name = user.attribute_value("name", Archive::User());

		bool const user_info_complete = user.has_sub_node("url")
		                             && user.has_sub_node("pubkey");

		if (name.valid() && !user_info_complete) {

			/* discard jobs that lack proper depot-user info */
			_jobs.for_each([&] (Job &job) {
				if (Archive::user(job.path) == name)
					job.failed = true; });

			/*
			 * Don't attempt importing content for an unknown user.
			 * Instead, trigger the depot query for the next pending job.
			 */
			if (name == _next_user) {
				_next_user = Archive::User();
				_generate_init_config();
				return;
			}
		}
	}

	Xml_node const dependencies = _dependencies.xml();
	Xml_node const index        = _index.xml();

	if (dependencies.num_sub_nodes() == 0 && index.num_sub_nodes() == 0)
		return;

	bool const missing_dependencies = dependencies.has_sub_node("missing");
	bool const missing_index_files  = index.has_sub_node("missing");

	if (!missing_dependencies && !missing_index_files) {
		log("installation complete.");
		_update_state_report();
		return;
	}

	/**
	 * Select depot user for next import
	 *
	 * Prefer the downloading of index files over archives because index files
	 * are quick to download and important for interactivity.
	 */
	auto select_next_user = [&] ()
	{
		Archive::User user { };

		if (missing_index_files)
			index.with_sub_node("missing", [&] (Xml_node missing) {
				user = missing.attribute_value("user", Archive::User()); });

		if (user.valid())
			return user;

		dependencies.with_sub_node("missing", [&] (Xml_node missing) {
			user = Archive::user(missing.attribute_value("path", Archive::Path())); });

		if (!user.valid())
			warning("unable to select depot user for next import");

		return user;
	};

	Archive::User const next_user = select_next_user();

	if (next_user != _current_user_name()) {
		_next_user = next_user;

		/* query user info from 'depot_query' */
		_generate_init_config();
		return;
	}

	/* start new import */
	_import.construct(_heap, _current_user_name(), dependencies, index);

	/* mark imported jobs as started */
	_import->for_each_download([&] (Archive::Path const &path) {
		_jobs.for_each([&] (Job &job) {
			if (job.path == path)
				job.started = true; }); });

	_fetchurl_attempt = 0;
	_update_state_report();

	/* spawn fetchurl */
	_generate_init_config();
}


void Depot_download_manager::Main::_handle_init_state()
{
	_init_state.update();
	_verified.update();

	bool reconfigure_init = false;

	if (!_import.constructed())
		return;

	Import &import = *_import;

	if (import.downloads_in_progress()) {

		Child_exit_state const fetchurl_state(_init_state.xml(), "fetchurl");

		if (fetchurl_state.exited && fetchurl_state.code != 0) {
			error("fetchurl failed with exit code ", fetchurl_state.code);

			/* retry by incrementing the version attribute of the start node */
			_fetchurl_count.value++;

			if (_fetchurl_attempt++ >= _fetchurl_max_attempts) {
				import.all_remaining_downloads_unavailable();
				_fetchurl_attempt = 0;
			}

			reconfigure_init = true;
		}

		if (fetchurl_state.exited && fetchurl_state.code == 0) {
			import.all_downloads_completed();

			/* kill fetchurl, start verify */
			reconfigure_init = true;
		}
	}

	if (!import.downloads_in_progress() && import.completed_downloads_available()) {
		import.verify_all_downloaded_archives();
		reconfigure_init = true;
	}

	if (import.unverified_archives_available()) {

		_verified.xml().for_each_sub_node([&] (Xml_node node) {

			/* path in the VFS name space of the 'verify' component */
			Path const abs_path = node.attribute_value("path", Archive::Path());

			/* determine matching archive path */
			Path path;
			import.for_each_unverified_archive([&] (Archive::Path const &archive) {
				if (abs_path == Path("/public/", Archive::download_file_path(archive)))
					path = archive; });

			if (path.valid()) {
				if (node.type() == "good") {
					import.archive_verified(path);
				} else {
					error("signature check failed for '", path, "' "
					      "(", node.attribute_value("reason", String<64>()), ")");
					import.archive_verification_failed(path);
				}
			}

			reconfigure_init = true;
		});
	}

	if (import.verified_archives_available()) {

		Child_exit_state const extract_state(_init_state.xml(), "extract");

		if (extract_state.exited && extract_state.code != 0)
			error("extract failed with exit code ", extract_state.code);

		if (extract_state.exited && extract_state.code == 0)
			import.all_verified_archives_extracted();
	}

	/* flag failed jobs to prevent re-attempts in subsequent import iterations */
	import.for_each_failed_archive([&] (Archive::Path const &path) {
		_jobs.for_each([&] (Job &job) {
			if (job.path == path)
				job.failed = true; }); });

	/* report before destructing '_import' to avoid empty intermediate reports */
	if (reconfigure_init)
		_update_state_report();

	if (!import.in_progress()) {
		_import.destruct();

		/* re-issue new depot query to start next iteration */
		_depot_query_count.value++;
		reconfigure_init = true;
	}

	if (reconfigure_init)
		_generate_init_config();
}


void Component::construct(Genode::Env &env)
{
	static Depot_download_manager::Main main(env);
}

