//
// docker.cpp
//

#if defined(__linux__)

#include "sinsp.h"
#include "sinsp_int.h"
#include "docker.h"
#include "user_event.h"

const std::string docker::DOCKER_SOCKET_FILE = "/var/run/docker.sock";

docker::docker(std::string url,
	const std::string& path,
	const std::string& http_version,
	int timeout_ms,
	bool is_captured,
	bool verbose,
	event_filter_ptr_t event_filter): m_id("docker"),
		m_timeout_ms(timeout_ms),
		m_is_captured(is_captured),
		m_verbose(verbose),
		m_event_filter(event_filter),
		m_container_events{"attach", "commit", "copy", "create",
							"destroy", "die", "exec_create", "exec_start",
							"export", "kill", "oom", "pause", "rename", "resize",
							"restart", "start", "stop", "top", "unpause", "update"},
		m_image_events{"delete", "import", "pull", "push", "tag", "untag"},
		m_volume_events{"create", "mount", "unmount", "destroy"},
		m_network_events{"create", "connect", "disconnect", "destroy"},
		m_severity_map
		{
			// container
			{ "attach",      sinsp_logger::SEV_EVT_INFORMATION },
			{ "commit",      sinsp_logger::SEV_EVT_INFORMATION },
			{ "copy",        sinsp_logger::SEV_EVT_INFORMATION },
			{ "create",      sinsp_logger::SEV_EVT_INFORMATION },
			{ "destroy",     sinsp_logger::SEV_EVT_WARNING     },
			{ "die",         sinsp_logger::SEV_EVT_WARNING     },
			{ "exec_create", sinsp_logger::SEV_EVT_INFORMATION },
			{ "exec_start",  sinsp_logger::SEV_EVT_INFORMATION },
			{ "export",      sinsp_logger::SEV_EVT_INFORMATION },
			{ "kill",        sinsp_logger::SEV_EVT_WARNING     },
			{ "oom",         sinsp_logger::SEV_EVT_WARNING     },
			{ "pause",       sinsp_logger::SEV_EVT_INFORMATION },
			{ "rename",      sinsp_logger::SEV_EVT_INFORMATION },
			{ "resize",      sinsp_logger::SEV_EVT_INFORMATION },
			{ "restart",     sinsp_logger::SEV_EVT_WARNING     },
			{ "start",       sinsp_logger::SEV_EVT_INFORMATION },
			{ "stop",        sinsp_logger::SEV_EVT_INFORMATION },
			{ "top",         sinsp_logger::SEV_EVT_INFORMATION },
			{ "unpause",     sinsp_logger::SEV_EVT_INFORMATION },
			{ "update",      sinsp_logger::SEV_EVT_INFORMATION },

			// image
			{ "delete", sinsp_logger::SEV_EVT_INFORMATION },
			{ "import", sinsp_logger::SEV_EVT_INFORMATION },
			{ "pull",   sinsp_logger::SEV_EVT_INFORMATION },
			{ "push",   sinsp_logger::SEV_EVT_INFORMATION },
			{ "tag",    sinsp_logger::SEV_EVT_INFORMATION },
			{ "untag",  sinsp_logger::SEV_EVT_INFORMATION },

			// volume
			{ "mount",   sinsp_logger::SEV_EVT_INFORMATION },
			{ "unmount", sinsp_logger::SEV_EVT_INFORMATION },

			// network
			{ "connect",    sinsp_logger::SEV_EVT_INFORMATION },
			{ "disconnect", sinsp_logger::SEV_EVT_INFORMATION }
		},
		m_name_translation
		{
			// Container
			{ "attach",      "Attached"      },
			{ "commit",      "Committed"     },
			{ "copy",        "Copied"        },
			{ "create",      "Created"       },
			{ "destroy",     "Destroyed"     },
			{ "die",         "Died"          },
			{ "exec_create", "Exec Created"  },
			{ "exec_start",  "Exec Started"  },
			{ "export",      "Exported"      },
			{ "kill",        "Killed"        },
			{ "oom",         "Out of Memory" },
			{ "pause",       "Paused"        },
			{ "rename",      "Renamed"       },
			{ "resize",      "Resized"       },
			{ "restart",     "Restarted"     },
			{ "start",       "Started"       },
			{ "stop",        "Stopped"       },
			{ "top",         "Top"           },
			{ "unpause",     "Unpaused"      },
			{ "update",      "Updated"       },

			// Image
			{ "delete", "Deleted"  },
			{ "import", "Imported" },
			{ "pull",   "Pulled"   },
			{ "push",   "Pushed"   },
			{ "tag",    "Tagged"   },
			{ "untag",  "Untagged" },

			// Volume
			// { "create",  "Created" }, duplicate
			{ "mount",   "Mounted"   },
			{ "unmount", "Unmounted" },
			// { "destroy", "Destroyed" }, duplicate

			// Network
			// { "create",     "Created"      }, duplicate
			{ "connect",    "Connected"    },
			{ "disconnect", "Disconnected" }
			// { "destroy"     "Destroyed"    } duplicate
		}
{
	g_logger.log(std::string("Creating Docker object for " +
							(url.empty() ? std::string("capture replay") : url)),
				 sinsp_logger::SEV_DEBUG);
#ifdef HAS_CAPTURE
	if(url.empty())
	{
		url = std::string("file://").append(scap_get_host_root()).append(DOCKER_SOCKET_FILE);
	}
	m_event_http = std::make_shared<handler_t>(*this, "docker", url, path, http_version,
					timeout_ms, nullptr, nullptr, true, false, 524288u, false);
	m_event_http->set_json_callback(&docker::set_event_json);
	m_event_http->add_json_filter(".");
	m_collector.add(m_event_http);
	m_collector.set_steady_state(true);
	send_data_request();
#endif
}

docker::~docker()
{
}

#ifdef HAS_CAPTURE
void docker::send_event_data_request()
{
	if(m_event_http)
	{
		m_event_http->send_request();
	}
	else
	{
		throw sinsp_exception("Docker event HTTP client is null.");
	}
}

void docker::connect()
{
	if(!connect(m_event_http, &docker::set_event_json, 1))
	{
		throw sinsp_exception("Connection to Docker API failed.");
	}
}
#endif // HAS_CAPTURE

bool docker::is_alive() const
{
#ifdef HAS_CAPTURE
	if(m_event_http && !m_event_http->is_connected())
	{
		g_logger.log("Docker state connection loss.", sinsp_logger::SEV_WARNING);
		return false;
	}
#endif // HAS_CAPTURE
	return true;
}

#ifdef HAS_CAPTURE

void docker::check_collector_status()
{
	if(!m_collector.is_healthy(m_event_http))
	{
		throw sinsp_exception("Docker collector not healthy, "
							  "giving up on data collection in this cycle ...");
	}
}

void docker::send_data_request(bool collect)
{
	if(m_events.size()) { return; }
	connect();
	send_event_data_request();
	g_logger.log("Docker event request sent.", sinsp_logger::SEV_DEBUG);
	if(collect) { collect_data(); }
}

void docker::collect_data()
{
	if(m_collector.subscription_count())
	{
		if(!m_event_http->is_enabled())
		{
			m_event_http->enable();
		}
		m_collector.get_data();
		if(m_events.size())
		{
			for(auto evt : m_events)
			{
				if(evt && !evt->isNull())
				{
					handle_event(std::move(*evt));
				}
				else
				{
					g_logger.log(std::string("Docker event error: ") +
								(!evt ? "event is null." : (evt->isNull() ? "JSON is null." : "Unknown")),
								sinsp_logger::SEV_ERROR);
				}
			}
			m_events.clear();
		}
	}
}
#endif // HAS_CAPTURE

void docker::set_event_json(json_ptr_t json, const std::string&)
{
	if(m_event_filter)
	{
		m_events.emplace_back(json);
	}
}

#ifdef HAS_CAPTURE
void docker::emit_event(Json::Value& root, std::string type, std::string status, bool send_to_backend)
{
	++m_event_counter;
	std::string::size_type delim_pos = status.find(':');
	if(delim_pos != std::string::npos)
	{
		status = status.substr(0, delim_pos);
	}
	g_logger.log("Docker EVENT: handling " + status + " of " + type, sinsp_logger::SEV_DEBUG);
	severity_map_t::const_iterator it = m_severity_map.find(status);

	severity_t severity;
	std::string event_name = status;
	std::string id = get_json_string(root, "id");
	if(id.length() > 7 && id.substr(0, 7) == "sha256:") // untag and delete have "sha256:id" format
	{
		id.clear(); // ignore that (will be displayed in event description)
	}
	severity = it->second;
	g_logger.log("Docker EVENT: severity for " + status + '=' + std::to_string(severity - sinsp_logger::SEV_EVT_MIN), sinsp_logger::SEV_DEBUG);
	uint64_t epoch_time_s = static_cast<uint64_t>(~0);
	Json::Value t = root["time"];
	if(!t.isNull() && t.isConvertibleTo(Json::uintValue))
	{
		epoch_time_s = t.asUInt64();
	}
	g_logger.log("Docker EVENT: name=" + event_name + ", id=" + id +
				", status=" + status + ", time=" + std::to_string(epoch_time_s),
				sinsp_logger::SEV_DEBUG);
	if(m_verbose)
	{
		std::cout << Json::FastWriter().write(root) << std::endl;
	}

	Json::Value no_value = Json::nullValue;
	const Json::Value& actor = root["Actor"];
	const Json::Value& attrib = actor.isNull() ? no_value : actor["Attributes"];
	const Json::Value& img = attrib.isNull() ? no_value : attrib["image"];
	std::string image;
	if(!img.isNull() && img.isConvertibleTo(Json::stringValue))
	{
		image = img.asString();
	}
	event_scope scope;
	if(m_machine_id.length())
	{
		scope.add("host.mac", m_machine_id);
	}
	if(is_image_event(event_name))
	{
		bool id_was_empty = false;
		if(id.empty())
		{
			id = get_json_string(root, "id");
			id_was_empty = true;
		}
		if(!id.empty())
		{
			scope.add("container.image", id);
		}
		else if(!image.empty())
		{
			scope.add("container.image", image);
		}
		else
		{
			g_logger.log("Cannot determine container image for Docker event.", sinsp_logger::SEV_WARNING);
		}
		if(id_was_empty) { id.clear(); }
	}
	else if(is_container_event(event_name))
	{
		if(id.length() >= 12)
		{
			scope.add("container.id", id.substr(0, 12));
		}
	}
	if(status.length())
	{
		status.insert(0, "Event: ", 7);
	}
	if(!actor.isNull() && actor.isObject())
	{
		if(!attrib.isNull() && attrib.isObject())
		{
			if(!image.empty())
			{
				status.append("; Image: ").append(image);
			}
			if(!id.empty() && id != image)
			{
				status.append("; ID: ").append(id);
			}
			const Json::Value& name = attrib["name"];
			if(!name.isNull() && name.isConvertibleTo(Json::stringValue))
			{
				status.append("; Name: ").append(name.asString());
			}
		}
	}
	sinsp_user_event::tag_map_t tags;
	tags["source"] = "docker";
	if(event_name.length())
	{
		if(type.length())
		{
			type[0] = toupper(type[0]);
			event_name = type.append(1, ' ').append(translate_name(event_name));
		}
		else // older docker versions don't tell type
		{
			event_name[0] = toupper(event_name[0]);
			event_name.insert(0, "Docker ");
		}
	}
	std::string evt = sinsp_user_event::to_string(epoch_time_s, std::move(event_name),
						std::move(status), std::move(scope), std::move(tags));

	if(send_to_backend)
	{
		if(it != m_severity_map.end())
		{
			//
			// This is where the event is sent to the backend
			//
			g_logger.log(evt, severity);

			if(g_logger.get_severity() >= sinsp_logger::SEV_TRACE)
			{
				g_logger.log("Docker EVENT: scheduled for sending\n" + evt, sinsp_logger::SEV_TRACE);
			}
		}
		else
		{
			g_logger.log("Docker EVENT: status not supported: " + status, sinsp_logger::SEV_ERROR);
			if(g_logger.get_severity() >= sinsp_logger::SEV_DEBUG)
			{
				g_logger.log(Json::FastWriter().write(root), sinsp_logger::SEV_DEBUG);
			}
		}
	}

	//
	// This is where the event is sent to the memdumper
	//
	g_logger.log(std::move(evt), (severity_t)sinsp_logger::SEV_EVT_MDUMP);
}

void docker::handle_event(Json::Value&& root)
{
	if(m_event_filter && (m_event_counter < sinsp_user_event::max_events_per_cycle()))
	{
		std::string type = get_json_string(root, "Type");
		std::string status = get_json_string(root, "Action");
		if(status.empty())
		{
			status = get_json_string(root, "status");
		}
		g_logger.log("Docker EVENT: type=" + type + ", status=" + status + ", "
					 "queued events count=" + std::to_string(m_event_counter), sinsp_logger::SEV_DEBUG);
		bool is_allowed = m_event_filter->allows_all();
		if(!is_allowed)
		{
			if(!type.empty())
			{
				is_allowed = m_event_filter->allows_all(type);
				if(!is_allowed && !status.empty())
				{
					is_allowed = m_event_filter->has(type, status);
				}
				// status for exec_* events is different, eg.:
				//   "container:exec_create: ls -l"
				if(!is_allowed)
				{
					std::string exec_create = "exec_create";
					std::string exec_start = "exec_start";
					std::string::size_type pos = status.find(exec_create);
					if(pos != std::string::npos)
					{
						status = exec_create;
						g_logger.log("Docker EVENT: found exec_create status=" + status, sinsp_logger::SEV_TRACE);
					}
					else
					{
						pos = status.find(exec_start);
						if(pos != std::string::npos)
						{
							status = exec_start;
							g_logger.log("Docker EVENT: found exec_start status=" + status, sinsp_logger::SEV_TRACE);
						}
					}
					if(pos != std::string::npos)
					{
						is_allowed = m_event_filter->has(type, status);
						g_logger.log("Docker EVENT: status=" + status + (is_allowed ? " is " : " is not ") + "allowed",
									 sinsp_logger::SEV_TRACE);
					}
				}
			}
			else // older docker versions don't tell type, so there will be some overlap of duplicates
			{
				is_allowed = m_event_filter->has("container", status);
				if(!is_allowed && !status.empty())
				{
					is_allowed = m_event_filter->has("image", status);
				}
				if(!is_allowed && !status.empty())
				{
					is_allowed = m_event_filter->has("volume", status);
				}
				if(!is_allowed && !status.empty())
				{
					is_allowed = m_event_filter->has("volume", status);
				}
			}
		}

		if(is_allowed)
		{
			emit_event(root, type, status, true);
		}
		else
		{
			emit_event(root, type, status, false);

			if(g_logger.get_severity() >= sinsp_logger::SEV_TRACE)
			{
				g_logger.log("Docker EVENT: status not permitted by filter: " + type +':' + status, sinsp_logger::SEV_TRACE);
				g_logger.log(Json::FastWriter().write(root), sinsp_logger::SEV_TRACE);
			}
		}
		m_event_limit_exceeded = false;
	}
	else if(!m_event_limit_exceeded) // only get in here once per cycle, to send event overflow warning
	{
		sinsp_user_event::emit_event_overflow("Docker", get_machine_id());
		m_event_limit_exceeded = true;
	}
}

std::string docker::get_socket_file()
{
	string sock_file = scap_get_host_root();
	std::string::size_type len = sock_file.length();
	if(len && sock_file[len - 1] == '/')
	{
		if((len - 1) > 0)
		{
			sock_file = sock_file.substr(0, len - 1);
		}
		else
		{
			sock_file.clear();
		}
	}
	sock_file.append(DOCKER_SOCKET_FILE);
	return sock_file;
}
#endif // HAS_CAPTURE

#endif // __linux__
