// SPDX-License-Identifier: LGPL-3.0-or-later
/*

   Copyright 2017 Skytechnology sp. z o.o.
   Copyright 2023 Leil Storage OÜ

   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 3 of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with this library; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
   02110-1301 USA
*/

#include "fsal.h"
#include "fsal_api.h"
#include "fsal_types.h"
#include "../FSAL/fsal_private.h"
#include "nfs_exports.h"

#include "nfs-ganesha/context_wrap.h"
#include "nfs-ganesha/fileinfo_cache.h"
#include "nfs-ganesha/saunafs_fsal_types.h"
#include "nfs-ganesha/saunafs_internal.h"

/**
 * @brief Remove count expired instances from cache.
 *
 * @param[in] export     export that store the cache
 * @param[in] count      number of instances to be removed
 */
static void clearFileInfoCache(struct SaunaFSExport *export, int count) {
	if (export == NULL) {
		return;
	}

	for (int i = 0; i < count; ++i) {
		FileInfoEntry_t *cacheHandle = NULL;

		cacheHandle = popExpiredFileInfoCache(export->cache);

		if (cacheHandle == NULL) {
			break;
		}

		fileinfo_t *fileHandle = extractFileInfo(cacheHandle);

		sau_release(export->fsInstance, fileHandle);
		fileInfoEntryFree(cacheHandle);
	}
}

/**
 * @brief Clean up a DS handle.
 *
 * DS handle Lifecycle management.
 * This function cleans up private resources associated with a filehandle
 * and deallocates it. Implement this method or you will leak. This
 * function should not be called directly.
 *
 * @param[in] dataServerHandle     Handle to release
 */
static void dsh_release(struct fsal_ds_handle *const dataServerHandle) {
	struct SaunaFSExport *export;
	struct DataServerHandle *dataServer;

	export = container_of(op_ctx->ctx_pnfs_ds->mds_fsal_export, struct SaunaFSExport, export);

	dataServer = container_of(dataServerHandle, struct DataServerHandle, handle);

	assert(export->cache);

	if (dataServer->cacheHandle != NULL) {
		releaseFileInfoCache(export->cache, dataServer->cacheHandle);
	}

	gsh_free(dataServer);
	clearFileInfoCache(export, 5);
}

/**
 * @brief Open a file from DataServerHandle.
 *
 * Auxiliary function to open files in the syscalls related with Data Server.
 *
 * @param[in] export         Handle to release
 * @param[in] dataServer     Data Server handle
 *
 * @returns: nfsstat4 status returned after opening the file
 */
static nfsstat4 openfile(struct SaunaFSExport *export, struct DataServerHandle *dataServer) {
	if (export == NULL) {
		return NFS4ERR_IO;
	}

	if (dataServer->cacheHandle != NULL) {
		return NFS4_OK;
	}

	clearFileInfoCache(export, 2);

	struct FileInfoEntry *entry = NULL;

	entry = acquireFileInfoCache(export->cache, dataServer->inode);

	dataServer->cacheHandle = entry;
	if (dataServer->cacheHandle == NULL) {
		return NFS4ERR_IO;
	}

	fileinfo_t *fileHandle = extractFileInfo(dataServer->cacheHandle);
	if (fileHandle != NULL) {
		return NFS4_OK;
	}

	fileHandle = saunafs_open(export->fsInstance, NULL, dataServer->inode, O_RDWR);

	if (fileHandle == NULL) {
		eraseFileInfoCache(export->cache, dataServer->cacheHandle);
		dataServer->cacheHandle = NULL;
		return NFS4ERR_IO;
	}

	attachFileInfo(dataServer->cacheHandle, fileHandle);
	return NFS4_OK;
}

/**
 * @brief Read from a data-server handle.
 *
 * DS handle I/O Functions
 *
 * NFSv4.1 data server handles are disjount from normal filehandles (in
 * Ganesha, there is a ds_flag in the filehandle_v4_t structure) and do not
 * get loaded into mdcache or processed the normal way.
 *
 * @param[in] dataServerHandle      Handle to release
 * @param[in] stateid               The stateid supplied with the READ
 *                                  operation, for validation
 * @param[in] offset                The offset at which to read
 * @param[in] requestedLength       Length of read requested (and size of
 *                                  buffer)
 * @param[out] buffer               The buffer to which to store read data
 * @param[out] suppliedLength       Length of data read
 * @param[out] eof                  true on end of file
 *
 * @returns: An NFSv4.1 status code.
 */
static nfsstat4 dsh_read(struct fsal_ds_handle *const dataServerHandle, const stateid4 *stateid,
                         const offset4 offset, const count4 requestedLength, void *const buffer,
                         count4 *const suppliedLength, bool *const eof) {
	(void) stateid;

	struct SaunaFSExport *export = NULL;
	struct DataServerHandle *dataServer = NULL;
	fileinfo_t *fileHandle = NULL;

	export = container_of(op_ctx->ctx_pnfs_ds->mds_fsal_export, struct SaunaFSExport, export);

	dataServer = container_of(dataServerHandle, struct DataServerHandle, handle);

	LogFullDebug(COMPONENT_FSAL,
	             "export=%" PRIu16 " inode=%" PRIiNode " offset=%" PRIu64
	             " size=%" PRIu32, export->export.export_id,
	             dataServer->inode, offset, requestedLength);

	nfsstat4 status = openfile(export, dataServer);

	if (status != NFS4_OK) {
		return status;
	}

	fileHandle = extractFileInfo(dataServer->cacheHandle);
	ssize_t bytes =
	    saunafs_read(export->fsInstance, NULL, fileHandle, offset, requestedLength, buffer);

	if (bytes < 0) {
		return nfs4LastError();
	}

	*suppliedLength = bytes;
	*eof = (bytes == 0);

	return NFS4_OK;
}

/**
 * @brief Write to a data-server handle.
 *
 * NFSv4.1 data server filehandles are disjount from normal filehandles (in
 * Ganesha, there is a ds_flag in the filehandle_v4_t structure) and do not
 * get loaded into mdcache or processed the normal way.
 *
 * @param[in] dataServerHandle      FSAL DS handle
 * @param[in] stateid               The stateid supplied with the READ
 *                                  operation, for validation
 * @param[in] offset                The offset at which to read
 * @param[in] writeLength           Length of write requested (and size of
 *                                  buffer)
 * @param[out] buffer               The buffer to which to store read data
 * @param[in] stability             wanted Stability of write
 * @param[out] writtenLength        Length of data written
 * @param[out] writeVerifier        Write verifier
 * @param[out] stabilityGot         Stability used for write (must be as or
 *                                  more stable than request)
 *
 * @returns: An NFSv4.1 status code.
 */
static nfsstat4 dsh_write(struct fsal_ds_handle *const dataServerHandle, const stateid4 *stateid,
                          const offset4 offset, const count4 writeLength, const void *buffer,
                          const stable_how4 stability, count4 *const writtenLength,
                          verifier4 *const writeVerifier, stable_how4 *const stabilityGot) {
	(void) stateid;
	(void) writeVerifier;

	struct SaunaFSExport *export = NULL;
	struct DataServerHandle *dataServer = NULL;

	fileinfo_t *fileHandle = NULL;
	int status = 0;

	export = container_of(op_ctx->ctx_pnfs_ds->mds_fsal_export, struct SaunaFSExport, export);

	dataServer = container_of(dataServerHandle, struct DataServerHandle, handle);

	LogFullDebug(COMPONENT_FSAL,
	             "export=%" PRIu16 " inode=%" PRIiNode " offset=%" PRIu64
	             " size=%" PRIu32, export->export.export_id,
	             dataServer->inode, offset, writeLength);

	nfsstat4 nfsStatus = openfile(export, dataServer);
	if (nfsStatus != NFS4_OK) {
		return nfsStatus;
	}

	fileHandle = extractFileInfo(dataServer->cacheHandle);
	ssize_t bytes =
	    saunafs_write(export->fsInstance, NULL, fileHandle, offset, writeLength, buffer);

	if (bytes < 0) {
		return nfs4LastError();
	}

	if (stability != UNSTABLE4) {
		status = saunafs_flush(export->fsInstance, NULL, fileHandle);
	}

	*writtenLength = bytes;
	*stabilityGot = (status < 0) ? UNSTABLE4 : stability;

	return NFS4_OK;
}

/**
 * @brief Commit a byte range to a DS handle.
 *
 * NFSv4.1 data server filehandles are disjount from normal filehandles (in
 * Ganesha, there is a ds_flag in the filehandle_v4_t structure) and do not
 * get loaded into mdcache or processed the normal way.
 *
 * @param[in] dataServerHandle      FSAL DS handle
 * @param[in] offset                Start of commit window
 * @param[in] count                 Length of commit window
 * @param[out] writeVerifier        Write verifier
 *
 * @returns: An NFSv4.1 status code.
 */
static nfsstat4 dsh_commit(struct fsal_ds_handle *const dataServerHandle, const offset4 offset,
                           const count4 count, verifier4 *const writeVerifier) {
	struct SaunaFSExport *export = NULL;
	struct DataServerHandle *dataServer = NULL;
	fileinfo_t *fileHandle = NULL;

	memset(writeVerifier, 0, NFS4_VERIFIER_SIZE);

	export = container_of(op_ctx->ctx_pnfs_ds->mds_fsal_export, struct SaunaFSExport, export);

	dataServer = container_of(dataServerHandle, struct DataServerHandle, handle);

	LogFullDebug(COMPONENT_FSAL,
	             "export=%" PRIu16 " inode=%" PRIiNode " offset=%" PRIu64
	             " size=%" PRIu32, export->export.export_id,
	             dataServer->inode, offset, count);

	nfsstat4 nfsStatus = openfile(export, dataServer);

	if (nfsStatus != NFS4_OK) {
		/* If we failed here then there is no opened SaunaFS file
		 * descriptor, which implies that we don't need to flush
		 * anything */
		return NFS4_OK;
	}

	fileHandle = extractFileInfo(dataServer->cacheHandle);
	int status = saunafs_flush(export->fsInstance, NULL, fileHandle);

	if (status < 0) {
		LogMajor(COMPONENT_PNFS, "ds_commit() failed  '%s'",
		         sau_error_string(sau_last_err()));
		return NFS4ERR_INVAL;
	}

	return NFS4_OK;
}

/**
 * @brief Read plus from a data-server handle.
 *
 * NFSv4.2 data server handles are disjount from normal filehandles (in
 * Ganesha, there is a ds_flag in the filehandle_v4_t structure) and do not
 * get loaded into mdcache or processed the normal way.
 *
 * @param[in]  dataServerHandle      FSAL DS handle
 * @param[in]  stateid               Stateid supplied with the READ
 *                                   operation, for validation
 * @param[in]  offset                The offset at which to read
 * @param[in]  requestedLength       Length of read requested (and size of
 *                                   buffer)
 * @param[out] buffer                Buffer to which to store read data
 * @param[out] suppliedLength        Length of data read
 * @param[out] eof                   true on end of file
 * @param[out] info                  IO info
 *
 * @returns: An NFSv4.2 status code.
 */
static nfsstat4 dsh_read_plus(struct fsal_ds_handle *const dataServerHandle,
                              const stateid4 *stateid, const offset4 offset,
                              const count4 requestedLength, void *const buffer,
                              const count4 suppliedLength, bool *const eof, struct io_info *info) {
	(void) dataServerHandle;
	(void) stateid;
	(void) offset;
	(void) requestedLength;
	(void) buffer;
	(void) suppliedLength;
	(void) eof;
	(void) info;

	LogCrit(COMPONENT_PNFS, "Unimplemented DS read_plus!");
	return NFS4ERR_NOTSUPP;
}

/**
 * @brief Create a FSAL data server handle from a wire handle.
 *
 * This function creates a FSAL data server handle from a client supplied
 * "wire" handle.
 *
 * @param[in]  pnfsDataServer       FSAL pNFS DS
 * @param[in]  buffer               Buffer from which to create the struct
 * @param[out] handle               FSAL DS handle
 * @param[out] flags                Flags used to create the FSAL data
 *                                  server handle
 *
 * @returns: NFSv4.1 error codes.
 */
static nfsstat4 make_ds_handle(struct fsal_pnfs_ds *const pnfsDataServer,
                               const struct gsh_buffdesc *const buffer,
                               struct fsal_ds_handle **const handle, int flags) {
	(void) pnfsDataServer;

	struct DSWire *dataServerWire = NULL;

	dataServerWire = (struct DSWire *)buffer->addr;

	struct DataServerHandle *dataServerHandle = NULL;
	*handle = NULL;

	if (buffer->len != sizeof(struct DSWire)) {
		return NFS4ERR_BADHANDLE;
	}

	if (dataServerWire->inode == 0) {
		return NFS4ERR_BADHANDLE;
	}

	dataServerHandle = gsh_calloc(1, sizeof(struct DataServerHandle));
	*handle = &dataServerHandle->handle;

	if (flags & FH_FSAL_BIG_ENDIAN) {
#if (BYTE_ORDER != BIG_ENDIAN)
		dataServerHandle->inode = bswap_32(dataServerWire->inode);
#else
		dataServerHandle->inode = dataServerWire->inode;
#endif
	} else {
#if (BYTE_ORDER == BIG_ENDIAN)
		dataServerHandle->inode = bswap_32(dataServerWire->inode);
#else
		dataServerHandle->inode = dataServerWire->inode;
#endif
	}

	return NFS4_OK;
}

/**
 * @brief Initialize FSAL specific permissions per pNFS DS.
 *
 * @param[in] pnfsDataServer     FSAL pNFS DS
 * @param[in] request            Incoming request
 *
 * @returns: NFSv4.1 error codes: NFS4_OK, NFS4ERR_ACCESS, NFS4ERR_WRONGSEC
 */
static nfsstat4 ds_permissions(struct fsal_pnfs_ds *const pnfsDataServer, struct svc_req *request) {
	(void) pnfsDataServer;
	(void) request;

	op_ctx->export_perms.set = root_op_export_set;
	op_ctx->export_perms.options = root_op_export_options;
	return NFS4_OK;
}

/**
 * @brief Initialize FSAL specific values for pNFS Data Server.
 *
 * @param[in] ops     FSAL pNFS Data Server operations vector
 */
void pnfsDsOperationsInit(struct fsal_pnfs_ds_ops *ops) {
	memcpy(ops, &def_pnfs_ds_ops, sizeof(struct fsal_pnfs_ds_ops));
	ops->make_ds_handle = make_ds_handle;
	ops->dsh_release = dsh_release;
	ops->dsh_read = dsh_read;
	ops->dsh_write = dsh_write;
	ops->dsh_commit = dsh_commit;
	ops->dsh_read_plus = dsh_read_plus;
	ops->ds_permissions = ds_permissions;
}
