/***************************************************************
 *
 * Copyright (C) 2024, Pelican Project, Morgridge Institute for Research
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you
 * may not use this file except in compliance with the License.  You may
 * obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 ***************************************************************/

#include "HTTPFileSystem.hh"
#include "HTTPCommands.hh"
#include "HTTPDirectory.hh"
#include "HTTPFile.hh"
#include "logging.hh"

#include <XrdOuc/XrdOucEnv.hh>
#include <XrdOuc/XrdOucGatherConf.hh>
#include <XrdSec/XrdSecEntity.hh>
#include <XrdVersion.hh>

#include <memory>
#include <sstream>
#include <stdexcept>
#include <vector>

#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "stl_string_utils.hh"

using namespace XrdHTTPServer;

HTTPFileSystem::HTTPFileSystem(XrdSysLogger *lp, const char *configfn,
							   XrdOucEnv * /*envP*/)
	: m_log(lp, "httpserver_"), m_token("", &m_log) {
	m_log.Say("------ Initializing the HTTP filesystem plugin.");
	if (!Config(lp, configfn)) {
		throw std::runtime_error("Failed to configure HTTP filesystem plugin.");
	}
}

HTTPFileSystem::~HTTPFileSystem() {}

bool HTTPFileSystem::handle_required_config(const std::string &name_from_config,
											const char *desired_name,
											const std::string &source,
											std::string &target) {
	if (name_from_config != desired_name) {
		return true;
	}

	if (source.empty()) {
		std::string error;
		formatstr(error, "%s must specify a value", desired_name);
		m_log.Emsg("Config", error.c_str());
		return false;
	}

	std::stringstream ss;
	ss << "Setting " << desired_name << "=" << source;
	m_log.Log(LogMask::Debug, "Config", ss.str().c_str());
	target = source;
	return true;
}

bool HTTPFileSystem::Config(XrdSysLogger *lp, const char *configfn) {
	m_remote_flavor = "auto";

	XrdOucEnv myEnv;
	XrdOucGatherConf httpserver_conf("httpserver.", &m_log);
	int result;
	if ((result = httpserver_conf.Gather(configfn,
										 XrdOucGatherConf::full_lines)) < 0) {
		m_log.Emsg("Config", -result, "parsing config file", configfn);
		return false;
	}

	std::string attribute;
	std::string token_file;

	m_log.setMsgMask(0);

	while (httpserver_conf.GetLine()) {
		auto attribute = httpserver_conf.GetToken();
		if (!strcmp(attribute, "httpserver.trace")) {
			if (!XrdHTTPServer::ConfigLog(httpserver_conf, m_log)) {
				m_log.Emsg("Config", "Failed to configure the log level");
			}
			continue;
		}

		auto value = httpserver_conf.GetToken();
		if (!value) {
			continue;
		}

		if (!handle_required_config(attribute, "httpserver.host_name", value,
									http_host_name) ||
			!handle_required_config(attribute, "httpserver.host_url", value,
									http_host_url) ||
			!handle_required_config(attribute, "httpserver.url_base", value,
									m_url_base) ||
			!handle_required_config(attribute, "httpserver.remote_flavor",
									value, m_remote_flavor) ||
			!handle_required_config(attribute, "httpserver.storage_prefix",
									value, m_storage_prefix) ||
			!handle_required_config(attribute, "httpserver.token_file", value,
									token_file)) {
			return false;
		}
	}

	if (m_url_base.empty()) {
		if (http_host_name.empty()) {
			m_log.Emsg("Config", "httpserver.host_name not specified; this or "
								 "httpserver.url_base are required");
			return false;
		}
		if (http_host_url.empty()) {
			m_log.Emsg("Config", "httpserver.host_url not specified; this or "
								 "httpserver.url_base are required");
			return false;
		}
	}

	if (m_remote_flavor != "http" && m_remote_flavor != "webdav" &&
		m_remote_flavor != "auto") {
		m_log.Emsg("Config", "Invalid httpserver.remote_flavor specified; "
							 "must be one of: 'http', 'webdav', or 'auto'");
		return false;
	}

	if (!token_file.empty()) {
		m_token = TokenFile(token_file, &m_log);
	}

	// For the "auto" flavor, probe the remote once at startup so the first
	// directory listing does not pay the OPTIONS round-trip.  A failure here
	// (e.g. the remote is not yet reachable) is non-fatal: the flavor stays
	// unknown and is re-probed lazily on the next listing.
	if (m_remote_flavor == "auto") {
		maybeProbeFlavor();
	}

	return true;
}

RemoteFlavor HTTPFileSystem::detectRemoteFlavor() {
	std::string hostUrl =
		!getHTTPUrlBase().empty() ? getHTTPUrlBase() : getHTTPHostUrl();

	HTTPOptions options(hostUrl, "", m_log, &m_token);
	if (!options.SendRequest()) {
		m_log.Log(LogMask::Warning, "HTTPFileSystem",
				  "OPTIONS probe of remote failed; directory-listing flavor "
				  "remains undetermined");
		return RemoteFlavor::Unknown;
	}

	if (options.SupportsPropfind()) {
		m_log.Log(LogMask::Info, "HTTPFileSystem",
				  "Remote advertises WebDAV; using PROPFIND for listings");
		return RemoteFlavor::Webdav;
	}

	m_log.Log(LogMask::Info, "HTTPFileSystem",
			  "Remote does not advertise WebDAV; using HTML for listings");
	return RemoteFlavor::Http;
}

void HTTPFileSystem::maybeProbeFlavor() {
	if (m_resolved_flavor.load() != RemoteFlavor::Unknown) {
		return;
	}

	std::lock_guard<std::mutex> lock(m_probe_mtx);
	// Re-check under the lock; another thread may have resolved it.
	if (m_resolved_flavor.load() != RemoteFlavor::Unknown) {
		return;
	}

	auto now = std::chrono::steady_clock::now();
	if (m_probed_once && (now - m_last_probe) < m_probe_interval) {
		return;
	}
	m_probed_once = true;
	m_last_probe = now;

	auto detected = detectRemoteFlavor();
	if (detected != RemoteFlavor::Unknown) {
		m_resolved_flavor.store(detected);
	}
}

RemoteFlavor HTTPFileSystem::getResolvedFlavor() {
	if (m_remote_flavor == "http") {
		return RemoteFlavor::Http;
	}
	if (m_remote_flavor == "webdav") {
		return RemoteFlavor::Webdav;
	}
	// "auto": consult (and lazily refresh) the probed result.
	maybeProbeFlavor();
	return m_resolved_flavor.load();
}

// Object Allocation Functions
//
XrdOssDF *HTTPFileSystem::newDir(const char *user) {
	return new HTTPDirectory(m_log, *this);
}

XrdOssDF *HTTPFileSystem::newFile(const char *user) {
	return new HTTPFile(m_log, this);
}

int HTTPFileSystem::Stat(const char *path, struct stat *buff, int opts,
						 XrdOucEnv *env) {
	std::string error;

	// need to forward a HEAD request to the remote server

	HTTPFile httpFile(m_log, this);
	int rv = httpFile.Open(path, 0, (mode_t)0, *env);
	if (rv && rv != -EISDIR) {
		m_log.Emsg("Stat", "Failed to open path:", path);
		return rv;
	}
	// Assume that HTTPFile::FStat() doesn't write to buff unless it succeeds.
	return httpFile.Fstat(buff);
}

int HTTPFileSystem::Create(const char *tid, const char *path, mode_t mode,
						   XrdOucEnv &env, int opts) {
	// Is path valid?
	std::string object;
	std::string hostname = this->getHTTPHostName();
	int rv = parse_path(hostname, path, object);
	if (rv != 0) {
		return rv;
	}

	return 0;
}

int HTTPFileSystem::Mkdir(const char *path, mode_t mode, int mkpath,
						  XrdOucEnv *env) {
	m_log.Log(LogMask::Debug, "Mkdir", "Creating directory", path);

	// Validate and translate the incoming path into the remote object name.
	std::string object;
	if (parse_path(getStoragePrefix(), path, object) != 0) {
		m_log.Emsg("Mkdir", "Failed to parse path:", path);
		return -EINVAL;
	}

	std::string hostUrl =
		!getHTTPUrlBase().empty() ? getHTTPUrlBase() : getHTTPHostUrl();
	m_log.Log(LogMask::Debug, "Mkdir", "Object:", object.c_str());
	m_log.Log(LogMask::Debug, "Mkdir", "Host URL:", hostUrl.c_str());

	// Issue a WebDAV MKCOL against the remote server.  XRootD's XrdHttp
	// creates any missing intermediate parents itself, so the `mkpath`
	// argument requires no special handling here.
	HTTPMkcol mkcolCommand(hostUrl, object, m_log, &m_token);
	if (!mkcolCommand.SendRequest()) {
		// A 405 (Method Not Allowed) from MKCOL means a non-collection
		// resource already exists at this path; surface that as EEXIST so
		// callers can distinguish it from other failures.
		if (mkcolCommand.getResponseCode() == 405) {
			m_log.Log(LogMask::Warning, "Mkdir",
					  "Cannot create directory; a file already exists at path",
					  path);
			return -EEXIST;
		}
		return HTTPRequest::HandleHTTPError(mkcolCommand, m_log, "MKCOL",
											object.c_str());
	}

	return 0;
}

int HTTPFileSystem::Unlink(const char *path, int Opts, XrdOucEnv *env) {
	m_log.Log(LogMask::Debug, "Unlink", "Unlinking path", path);
	// make sure file exists
	HTTPFile httpFile(m_log, this);
	if (httpFile.Open(path, 0, (mode_t)0, *env) != 0) {
		m_log.Emsg("Unlink", "Failed to open path:", path);
		return -ENOENT;
	}

	std::string object;
	if (parse_path(getStoragePrefix(), path, object) != 0) {
		m_log.Emsg("Unlink", "Failed to parse path:", path);
		return -EIO;
	}
	// delete the file
	std::string hostUrl =
		!getHTTPUrlBase().empty() ? getHTTPUrlBase() : getHTTPHostUrl();
	m_log.Log(LogMask::Debug, "Unlink", "Object:", object.c_str());
	m_log.Log(LogMask::Debug, "Unlink", "Host URL:", hostUrl.c_str());
	HTTPDelete deleteCommand(hostUrl, object, m_log, &m_token);
	if (!deleteCommand.SendRequest()) {
		return HTTPRequest::HandleHTTPError(deleteCommand, m_log, "DELETE",
											object.c_str());
	}

	return 0;
}
