/** @file
 * @brief Support for glass database replication
 */
/* Copyright 2008 Lemur Consulting Ltd
 * Copyright 2009,2010,2011,2012,2013,2014,2015,2016 Olly Betts
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see
 * <https://www.gnu.org/licenses/>.
 */

#include <config.h>

#include "glass_databasereplicator.h"

#include "xapian/error.h"

#include "../flint_lock.h"
#include "glass_defs.h"
#include "glass_replicate_internal.h"
#include "glass_version.h"
#include "compression_stream.h"
#include "debuglog.h"
#include "fd.h"
#include "internaltypes.h"
#include "io_utils.h"
#include "pack.h"
#include "posixy_wrapper.h"
#include "net/remoteconnection.h"
#include "replicationprotocol.h"
#include "str.h"
#include "stringutils.h"

#include <algorithm>
#include <cerrno>

[[noreturn]]
static void
throw_connection_closed_unexpectedly()
{
    throw Xapian::NetworkError("Connection closed unexpectedly");
}

using namespace std;
using namespace Xapian;

static const char * dbnames =
        "/postlist." GLASS_TABLE_EXTENSION "\0"
        "/docdata." GLASS_TABLE_EXTENSION "\0\0"
        "/termlist." GLASS_TABLE_EXTENSION "\0"
        "/position." GLASS_TABLE_EXTENSION "\0"
        "/spelling." GLASS_TABLE_EXTENSION "\0"
        "/synonym." GLASS_TABLE_EXTENSION;

GlassDatabaseReplicator::GlassDatabaseReplicator(const string & db_dir_)
    : db_dir(db_dir_)
{
    std::fill_n(fds, sizeof(fds) / sizeof(fds[0]), -1);
}

void
GlassDatabaseReplicator::commit() const
{
    for (size_t i = 0; i != Glass::MAX_; ++i) {
        int fd = fds[i];
        if (fd >= 0) {
            io_sync(fd);
#if 0 // FIXME: close or keep open?
            close(fd);
            fds[i] = -1;
#endif
        }
    }
}

GlassDatabaseReplicator::~GlassDatabaseReplicator()
{
    for (size_t i = 0; i != Glass::MAX_; ++i) {
        int fd = fds[i];
        if (fd >= 0) {
            close(fd);
        }
    }
}

bool
GlassDatabaseReplicator::check_revision_at_least(const string & rev,
                                                 const string & target) const
{
    LOGCALL(DB, bool, "GlassDatabaseReplicator::check_revision_at_least", rev | target);

    glass_revision_number_t rev_val;
    glass_revision_number_t target_val;

    const char * ptr = rev.data();
    const char * end = ptr + rev.size();
    if (!unpack_uint(&ptr, end, &rev_val)) {
        throw NetworkError("Invalid revision string supplied to check_revision_at_least");
    }

    ptr = target.data();
    end = ptr + target.size();
    if (!unpack_uint(&ptr, end, &target_val)) {
        throw NetworkError("Invalid revision string supplied to check_revision_at_least");
    }

    RETURN(rev_val >= target_val);
}

void
GlassDatabaseReplicator::process_changeset_chunk_version(string & buf,
                                                         RemoteConnection & conn,
                                                         double end_time) const
{
    const char *ptr = buf.data();
    const char *end = ptr + buf.size();

    glass_revision_number_t rev;
    if (!unpack_uint(&ptr, end, &rev))
        throw NetworkError("Invalid revision in changeset");

    string::size_type size;
    if (!unpack_uint(&ptr, end, &size))
        throw NetworkError("Invalid version file size in changeset");

    // Get the new version file into buf.
    buf.erase(0, ptr - buf.data());
    int res = conn.get_message_chunk(buf, size, end_time);
    if (res <= 0) {
        if (res < 0)
            throw_connection_closed_unexpectedly();
        throw NetworkError("Unexpected end of changeset (6)");
    }

    // Write size bytes from start of buf to new version file.
    string tmpfile = db_dir;
    tmpfile += "/v.rtmp";
    int fd = posixy_open(tmpfile.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0666);
    if (fd == -1) {
        string msg = "Failed to open ";
        msg += tmpfile;
        throw DatabaseError(msg, errno);
    }
    {
        FD closer(fd);
        io_write(fd, buf.data(), size);
        io_sync(fd);
    }
    string version_file = db_dir;
    version_file += "/iamglass";
    if (!io_tmp_rename(tmpfile, version_file)) {
        string msg("Couldn't create new version file ");
        msg += version_file;
        throw DatabaseError(msg, errno);
    }

    buf.erase(0, size);
}

void
GlassDatabaseReplicator::process_changeset_chunk_blocks(Glass::table_type table,
                                                        unsigned v,
                                                        string & buf,
                                                        RemoteConnection & conn,
                                                        double end_time) const
{
    const char *ptr = buf.data();
    const char *end = ptr + buf.size();

    unsigned int changeset_blocksize = GLASS_MIN_BLOCKSIZE << v;
    if (changeset_blocksize > 65536 ||
        (changeset_blocksize & (changeset_blocksize - 1))) {
        throw NetworkError("Invalid blocksize in changeset");
    }
    uint4 block_number;
    if (!unpack_uint(&ptr, end, &block_number))
        throw NetworkError("Invalid block number in changeset");

    buf.erase(0, ptr - buf.data());

    int fd = fds[table];
    if (fd == -1) {
        string db_path = db_dir;
        db_path += dbnames + table * (11 + CONST_STRLEN(GLASS_TABLE_EXTENSION));
        fd = posixy_open(db_path.c_str(), O_WRONLY | O_CREAT | O_CLOEXEC, 0666);
        if (fd == -1) {
            string msg = "Failed to open ";
            msg += db_path;
            throw DatabaseError(msg, errno);
        }
        fds[table] = fd;
    }

    int res = conn.get_message_chunk(buf, changeset_blocksize, end_time);
    if (res <= 0) {
        if (res < 0)
            throw_connection_closed_unexpectedly();
        throw NetworkError("Unexpected end of changeset (4)");
    }

    io_write_block(fd, buf.data(), changeset_blocksize, block_number);
    buf.erase(0, changeset_blocksize);
}

string
GlassDatabaseReplicator::apply_changeset_from_conn(RemoteConnection & conn,
                                                   double end_time,
                                                   bool valid) const
{
    LOGCALL(DB, string, "GlassDatabaseReplicator::apply_changeset_from_conn", conn | end_time | valid);

    // Lock the database to perform modifications.
    FlintLock lock(db_dir);
    string explanation;
    FlintLock::reason why = lock.lock(true, false, explanation);
    if (why != FlintLock::SUCCESS) {
        lock.throw_databaselockerror(why, db_dir, explanation);
    }

    int type = conn.get_message_chunked(end_time);
    if (type < 0)
        throw_connection_closed_unexpectedly();
    AssertEq(type, REPL_REPLY_CHANGESET);

    string buf;
    // Read enough to be certain that we've got the header part of the
    // changeset.

    if (conn.get_message_chunk(buf, REASONABLE_CHANGESET_SIZE, end_time) < 0)
        throw_connection_closed_unexpectedly();
    const char *ptr = buf.data();
    const char *end = ptr + buf.size();
    // Check the magic string.
    if (!startswith(buf, CHANGES_MAGIC_STRING)) {
        throw NetworkError("Invalid ChangeSet magic string");
    }
    ptr += CONST_STRLEN(CHANGES_MAGIC_STRING);
    if (ptr == end)
        throw NetworkError("Couldn't read a valid version number from changeset");
    unsigned int changes_version = *ptr++;
    if (changes_version != CHANGES_VERSION)
        throw NetworkError("Unsupported changeset version");

    glass_revision_number_t startrev;
    glass_revision_number_t endrev;

    if (!unpack_uint(&ptr, end, &startrev))
        throw NetworkError("Couldn't read a valid start revision from changeset");
    if (!unpack_uint(&ptr, end, &endrev))
        throw NetworkError("Couldn't read a valid end revision from changeset");

    if (endrev <= startrev)
        throw NetworkError("End revision in changeset is not later than start revision");

    if (ptr == end)
        throw NetworkError("Unexpected end of changeset (1)");

    if (valid) {
        // Check the revision number.
        // If the database was not known to be valid, we cannot
        // reliably determine its revision number, so must skip this
        // check.
        GlassVersion version_file(db_dir);
        version_file.read();
        if (startrev != version_file.get_revision())
            throw NetworkError("Changeset supplied is for wrong revision number");
    }

    unsigned char changes_type = *ptr++;
    if (changes_type != 0) {
        throw NetworkError("Unsupported changeset type: " + str(changes_type));
        // FIXME - support changes of type 1, produced when DANGEROUS mode is
        // on.
    }

    // Clear the bits of the buffer which have been read.
    buf.erase(0, ptr - buf.data());

    // Read the items from the changeset.
    while (true) {
        if (conn.get_message_chunk(buf, REASONABLE_CHANGESET_SIZE, end_time) < 0)
            throw_connection_closed_unexpectedly();
        ptr = buf.data();
        end = ptr + buf.size();
        if (ptr == end)
            throw NetworkError("Unexpected end of changeset (3)");

        // Read the type of the next chunk of data
        // chunk type can be (in binary):
        //
        // 11111111 - last chunk
        // 11111110 - version file
        // 00BBBTTT - table block:
        //   Block size = (GLASS_MIN_BLOCKSIZE<<BBB) BBB=0..5
        //   Table TTT=0..(Glass::MAX_-1)
        unsigned char chunk_type = *ptr++;
        if (chunk_type == 0xff)
            break;
        if (chunk_type == 0xfe) {
            // Version file.
            buf.erase(0, ptr - buf.data());
            process_changeset_chunk_version(buf, conn, end_time);
            continue;
        }
        size_t table_code = (chunk_type & 0x07);
        if (table_code >= Glass::MAX_)
            throw NetworkError("Bad table code in changeset file");
        Glass::table_type table = static_cast<Glass::table_type>(table_code);
        unsigned char v = (chunk_type >> 3) & 0x0f;

        // Process the chunk
        buf.erase(0, ptr - buf.data());
        process_changeset_chunk_blocks(table, v, buf, conn, end_time);
    }

    if (ptr != end)
        throw NetworkError("Junk found at end of changeset");

    buf.resize(0);
    pack_uint(buf, endrev);

    commit();

    RETURN(buf);
}

string
GlassDatabaseReplicator::get_uuid() const
{
    LOGCALL(DB, string, "GlassDatabaseReplicator::get_uuid", NO_ARGS);
    GlassVersion version_file(db_dir);
    try {
        version_file.read();
    } catch (const Xapian::DatabaseError &) {
        RETURN(string());
    }
    RETURN(version_file.get_uuid_string());
}
