/***********************************************************************************************************************************
SFTP Storage File Write
***********************************************************************************************************************************/
#include <build.h>

#ifdef HAVE_LIBSSH2

#include "common/debug.h"
#include "common/log.h"
#include "common/user.h"
#include "storage/sftp/write.h"
#include "storage/write.h"

/***********************************************************************************************************************************
Object type
***********************************************************************************************************************************/
struct StorageWriteSftp
{
    const StorageWriteInterface *interface;                         // Interface
    const String *name;                                             // File name
    mode_t modeFile;                                                // File mode
    mode_t modePath;                                                // Path mode
    bool createPath;                                                // Create path if missing
    bool syncFile;                                                  // Sync file on close
    bool atomic;                                                    // Atomic write
    StorageSftp *storage;                                           // Storage that created this object

    const String *nameTmp;                                          // Temporary filename utilized for atomic ops
    const String *path;                                             // Utilized for path operations
    LIBSSH2_SFTP_HANDLE *sftpHandle;                                // LibSsh2 session sftp handle
};

/***********************************************************************************************************************************
Open the file
***********************************************************************************************************************************/
static void
storageWriteSftpOpen(THIS_VOID)
{
    THIS(StorageWriteSftp);

    FUNCTION_LOG_BEGIN(logLevelTrace);
        FUNCTION_LOG_PARAM(STORAGE_WRITE_SFTP, this);
    FUNCTION_LOG_END();

    ASSERT(this != NULL);
    ASSERT(storageSftpSessionSftp(this->storage) != NULL);

    const unsigned long int flags = LIBSSH2_FXF_CREAT | LIBSSH2_FXF_WRITE | LIBSSH2_FXF_TRUNC;

    // Retry the open once if the connection was lost (e.g. the server dropped an idle connection) and reopened successfully
    unsigned int retry = 0;
    int connErrno;

    do
    {
        // Open the file
        do
        {
            this->sftpHandle = libssh2_sftp_open_ex(
                storageSftpSessionSftp(this->storage), strZ(this->nameTmp), (unsigned int)strSize(this->nameTmp), flags,
                (int)this->modeFile, LIBSSH2_SFTP_OPENFILE);
        }
        while (this->sftpHandle == NULL &&
               storageSftpWaitFd(this->storage, (connErrno = libssh2_session_last_errno(storageSftpSession(this->storage)))));

        // Attempt to create the path if it is missing
        if (this->sftpHandle == NULL && libssh2_session_last_errno(storageSftpSession(this->storage)) == LIBSSH2_ERROR_SFTP_PROTOCOL
            && libssh2_sftp_last_error(storageSftpSessionSftp(this->storage)) == LIBSSH2_FX_NO_SUCH_FILE)
        {
            // Create the path
            storageInterfacePathCreateP(this->storage, this->path, false, false, this->modePath);

            // Open file again
            do
            {
                this->sftpHandle = libssh2_sftp_open_ex(
                    storageSftpSessionSftp(this->storage), strZ(this->nameTmp), (unsigned int)strSize(this->nameTmp), flags,
                    (int)this->modeFile, LIBSSH2_SFTP_OPENFILE);
            }
            while (this->sftpHandle == NULL &&
                   storageSftpWaitFd(this->storage, (connErrno = libssh2_session_last_errno(storageSftpSession(this->storage)))));
        }
    }
    while (this->sftpHandle == NULL && retry++ == 0 && storageSftpReconnect(this->storage, connErrno));

    // Handle error
    if (this->sftpHandle == NULL)
    {
        const int rc = libssh2_session_last_errno(storageSftpSession(this->storage));

        if (rc == LIBSSH2_ERROR_EAGAIN)
            THROW_FMT(FileOpenError, "timeout while opening file '%s'", strZ(this->name));

        const uint64_t sftpErr = libssh2_sftp_last_error(storageSftpSessionSftp(this->storage));

        if (rc == LIBSSH2_ERROR_SFTP_PROTOCOL && sftpErr == LIBSSH2_FX_NO_SUCH_FILE)
        {
            THROW_FMT(FileMissingError, STORAGE_ERROR_WRITE_MISSING, strZ(this->name));
        }
        else
            storageSftpEvalLibSsh2Error(rc, sftpErr, &FileOpenError, strNewFmt(STORAGE_ERROR_WRITE_OPEN, strZ(this->name)), NULL);
    }

    FUNCTION_LOG_RETURN_VOID();
}

/***********************************************************************************************************************************
Write to the file
***********************************************************************************************************************************/
static void
storageWriteSftp(THIS_VOID, const Buffer *const buffer)
{
    THIS(StorageWriteSftp);

    FUNCTION_LOG_BEGIN(logLevelTrace);
        FUNCTION_LOG_PARAM(STORAGE_WRITE_SFTP, this);
        FUNCTION_LOG_PARAM(BUFFER, buffer);
    FUNCTION_LOG_END();

    ASSERT(this != NULL);
    ASSERT(buffer != NULL);
    ASSERT(this->sftpHandle != NULL);

    ssize_t rc;
    size_t remains = bufUsed(buffer);                               // Amount left to write
    size_t offset = 0;                                              // Offset into the buffer

    // Loop until all the data is written
    do
    {
        do
        {
            rc = libssh2_sftp_write(this->sftpHandle, (const char *)bufPtrConst(buffer) + offset, remains);
        }
        while (storageSftpWaitFd(this->storage, rc));

        // Break on error. Error will be thrown below the loop.
        if (rc < 0)
            break;

        // Offset for next write start point
        offset += (size_t)rc;

        // Update amount left to write
        remains -= (size_t)rc;
    }
    while (remains);

    if (rc == LIBSSH2_ERROR_EAGAIN)
        THROW_FMT(FileWriteError, "timeout writing '%s'", strZ(this->nameTmp));

    if (rc < 0)
    {
        storageSftpEvalLibSsh2Error(
            (int)rc, libssh2_sftp_last_error(storageSftpSessionSftp(this->storage)), &FileWriteError,
            strNewFmt("unable to write '%s'", strZ(this->nameTmp)), NULL);
    }

    FUNCTION_LOG_RETURN_VOID();
}

/***********************************************************************************************************************************
Unlink already existing file
***********************************************************************************************************************************/
static void
storageWriteSftpUnlinkExisting(THIS_VOID)
{
    THIS(StorageWriteSftp);

    FUNCTION_LOG_BEGIN(logLevelTrace);
        FUNCTION_LOG_PARAM(STORAGE_WRITE_SFTP, this);
    FUNCTION_LOG_END();

    ASSERT(this != NULL);

    int rc;

    do
    {
        rc = libssh2_sftp_unlink_ex(storageSftpSessionSftp(this->storage), strZ(this->name), (unsigned int)strSize(this->name));
    }
    while (storageSftpWaitFd(this->storage, rc));

    if (rc != 0)
    {
        if (rc == LIBSSH2_ERROR_EAGAIN)
            THROW_FMT(FileWriteError, "timeout unlinking '%s'", strZ(this->name));

        else
            storageSftpEvalLibSsh2Error(
                rc, libssh2_sftp_last_error(storageSftpSessionSftp(this->storage)), &FileRemoveError,
                strNewFmt("unable to remove existing '%s'", strZ(this->name)), NULL);
    }

    FUNCTION_LOG_RETURN_VOID();
}

/***********************************************************************************************************************************
Rename a file
***********************************************************************************************************************************/
static void
storageWriteSftpRename(THIS_VOID)
{
    THIS(StorageWriteSftp);

    FUNCTION_LOG_BEGIN(logLevelTrace);
        FUNCTION_LOG_PARAM(STORAGE_WRITE_SFTP, this);
    FUNCTION_LOG_END();

    ASSERT(this != NULL);

    int rc;

    do
    {
        rc = libssh2_sftp_rename_ex(
            storageSftpSessionSftp(this->storage), strZ(this->nameTmp), (unsigned int)strSize(this->nameTmp), strZ(this->name),
            (unsigned int)strSize(this->name),
            LIBSSH2_SFTP_RENAME_OVERWRITE | LIBSSH2_SFTP_RENAME_ATOMIC | LIBSSH2_SFTP_RENAME_NATIVE);
    }
    while (storageSftpWaitFd(this->storage, rc));

    if (rc != 0)
    {
        if (rc == LIBSSH2_ERROR_EAGAIN)
            THROW_FMT(FileWriteError, "timeout moving '%s' to '%s'", strZ(this->nameTmp), strZ(this->name));
        else
            storageSftpEvalLibSsh2Error(
                rc, libssh2_sftp_last_error(storageSftpSessionSftp(this->storage)), &FileRemoveError,
                strNewFmt("unable to move '%s' to '%s'", strZ(this->nameTmp), strZ(this->name)), NULL);
    }

    FUNCTION_LOG_RETURN_VOID();
}

/***********************************************************************************************************************************
Close the file
***********************************************************************************************************************************/
static void
storageWriteSftpClose(THIS_VOID)
{
    THIS(StorageWriteSftp);

    FUNCTION_LOG_BEGIN(logLevelTrace);
        FUNCTION_LOG_PARAM(STORAGE_WRITE_SFTP, this);
    FUNCTION_LOG_END();

    ASSERT(this != NULL);

    // Close if the file has not already been closed
    if (this->sftpHandle != NULL)
    {
        int rc;

        if (this->syncFile)
        {
            do
            {
                rc = libssh2_sftp_fsync(this->sftpHandle);
            }
            while (storageSftpWaitFd(this->storage, rc));

            if (rc != 0)
            {
                if (rc == LIBSSH2_ERROR_EAGAIN)
                    THROW_FMT(FileSyncError, "timeout syncing file '%s'", strZ(this->nameTmp));
                else
                {
                    storageSftpEvalLibSsh2Error(
                        rc, libssh2_sftp_last_error(storageSftpSessionSftp(this->storage)), &FileSyncError,
                        strNewFmt(STORAGE_ERROR_WRITE_SYNC, strZ(this->nameTmp)), NULL);
                }
            }
        }

        // Close the file
        do
        {
            rc = libssh2_sftp_close(this->sftpHandle);
        }
        while (storageSftpWaitFd(this->storage, rc));

        if (rc != 0)
        {
            if (rc == LIBSSH2_ERROR_EAGAIN)
                THROW_FMT(FileCloseError, "timeout closing file '%s'", strZ(this->nameTmp));

            storageSftpEvalLibSsh2Error(
                rc, libssh2_sftp_last_error(storageSftpSessionSftp(this->storage)), &FileCloseError,
                strNewFmt(STORAGE_ERROR_WRITE_CLOSE, strZ(this->nameTmp)), NULL);
        }

        this->sftpHandle = NULL;

        // Rename from temp file
        if (this->atomic)
        {
            do
            {
                rc = libssh2_sftp_rename_ex(
                    storageSftpSessionSftp(this->storage), strZ(this->nameTmp), (unsigned int)strSize(this->nameTmp), strZ(this->name),
                    (unsigned int)strSize(this->name),
                    LIBSSH2_SFTP_RENAME_OVERWRITE | LIBSSH2_SFTP_RENAME_ATOMIC | LIBSSH2_SFTP_RENAME_NATIVE);
            }
            while (storageSftpWaitFd(this->storage, rc));

            if (rc != 0)
            {
                if (rc == LIBSSH2_ERROR_EAGAIN)
                    THROW_FMT(FileCloseError, "timeout renaming file '%s'", strZ(this->nameTmp));

                const uint64_t sftpErr = libssh2_sftp_last_error(storageSftpSessionSftp(this->storage));

                // Some/most sftp servers will not rename over an existing file, in testing this returned LIBSSH2_FX_FAILURE
                if (rc == LIBSSH2_ERROR_SFTP_PROTOCOL &&
                    (sftpErr == LIBSSH2_FX_FAILURE || sftpErr == LIBSSH2_FX_FILE_ALREADY_EXISTS))
                {
                    // Remove the existing file and retry the rename
                    storageWriteSftpUnlinkExisting(this);
                    storageWriteSftpRename(this);
                }
                else
                {
                    storageSftpEvalLibSsh2Error(
                        rc, sftpErr, &FileCloseError,
                        strNewFmt("unable to move '%s' to '%s'", strZ(this->nameTmp), strZ(this->name)), NULL);
                }
            }
        }
    }

    FUNCTION_LOG_RETURN_VOID();
}

/**********************************************************************************************************************************/
static const StorageWriteInterface storageWriteSftpInterface =
{
    .close = storageWriteSftpClose,
    .open = storageWriteSftpOpen,
    .write = storageWriteSftp,
};

FN_EXTERN StorageWriteSftp *
storageWriteSftpNew(
    StorageSftp *const storage, const String *const name, const mode_t modeFile, const mode_t modePath, const String *const user,
    const String *const group, const time_t timeModified, const bool createPath, const bool syncFile, const bool atomic,
    const bool truncate)
{
    FUNCTION_LOG_BEGIN(logLevelTrace);
        FUNCTION_LOG_PARAM(STORAGE_SFTP, storage);
        FUNCTION_LOG_PARAM(STRING, name);
        FUNCTION_LOG_PARAM(MODE, modeFile);
        FUNCTION_LOG_PARAM(MODE, modePath);
        FUNCTION_LOG_PARAM(STRING, user);
        FUNCTION_LOG_PARAM(STRING, group);
        FUNCTION_LOG_PARAM(TIME, timeModified);
        FUNCTION_LOG_PARAM(BOOL, createPath);
        FUNCTION_LOG_PARAM(BOOL, syncFile);
        FUNCTION_LOG_PARAM(BOOL, atomic);
        FUNCTION_LOG_PARAM(BOOL, truncate);
    FUNCTION_LOG_END();

    ASSERT(storage != NULL);
    ASSERT(name != NULL);
    ASSERT(modeFile != 0);
    ASSERT(modePath != 0);

    OBJ_NEW_BEGIN(StorageWriteSftp, .childQty = MEM_CONTEXT_QTY_MAX)
    {
        *this = (StorageWriteSftp)
        {
            .interface = &storageWriteSftpInterface,
            .name = strDup(name),
            .modeFile = modeFile,
            .modePath = modePath,
            .createPath = createPath,
            .syncFile = syncFile,
            .atomic = atomic,
            .storage = storage,
            .nameTmp = atomic ? strNewFmt("%s." STORAGE_FILE_TEMP_EXT, strZ(name)) : strDup(name),
            .path = strPath(name),
        };
    }
    OBJ_NEW_END();

    FUNCTION_LOG_RETURN(STORAGE_WRITE_SFTP, this);
}

#endif // HAVE_LIBSSH2
