#!/usr/bin/env node

// AntiEtcd server application
// (c) Vitaliy Filippov, 2024
// License: Mozilla Public License 2.0 or Vitastor Network Public License 1.1

const AntiEtcd = require('./antietcd.js');

const help_text = `Miniature etcd replacement based on TinyRaft
(c) Vitaliy Filippov, 2024
License: Mozilla Public License 2.0 or Vitastor Network Public License 1.1

Usage:

${process.argv[0]} ${process.argv[1]} \n\
    [--cert ssl.crt] [--key ssl.key] [--ca ca.crt] [--peer_ca ssl.crt] [--port 12379] \n\
    [--data data.gz] [--persist-filter ./filter.js] [--persist_interval 500] \n\
    [--node_id node1 --cluster_key abcdef --cluster node1=http://localhost:12379,node2=http://localhost:12380,node3=http://localhost:12381] \n\
    [other options]

Supported etcd REST APIs:

    /v3/kv/txn /v3/kv/put /v3/kv/range /v3/kv/deleterange
    /v3/lease/grant /v3/lease/keepalive /v3/lease/revoke /v3/kv/lease/revoke
    websocket-based watch API (create_request, cancel_request, progress_request)

Options:

HTTP:

--port 2379
    Listen port
--ip 192.168.0.10
    Listen address (by default listen on all interfaces)
--cert <cert>
    Use TLS with this certificate file (PEM format)
--key <key>
    Use TLS with this key file (PEM format)
--ca <ca>
    Use trusted root certificates from this file.
--peer_ca <cert>
    Use this CA to verify peer antietcd certificates (defaults to the same --cert).
--client_cert_auth 1
    Require TLS client certificates signed by <ca> or by default CA to connect.
--ws_keepalive_interval 30000
    Client websocket ping (keepalive) interval in milliseconds
--use_base64 1
    Use base64 encoding of keys and values, like in etcd (enabled by default).

Consistency:

--use_locks 1
    Enable lock-based transaction isolation to prevent G1a anomaly (aborted read).
--merge_watches 1
    Antietcd merges all watcher events into a single websocket message to provide
    more ordering/transaction guarantees. Set to 0 to disable this behaviour.
--stale_read 1
    Allow to serve reads from replicas (potentially stale). Specify 0 to always
    contact leader on every read.

Persistence:

--data <filename>
    Store persistent data in <filename>
--persist_interval <milliseconds>
    Persist data on disk after this interval, not immediately after change
--persist_filter ./filter.js
    Use persistence filter from ./filter.js (or a module).
    Persistence filter is a function(cfg) returning function(key, value) ran
    for every change and returning a new value or undefined to skip persistence.
--compact_revisions 1000
    Number of previous revisions to keep deletion information in memory

Clustering:

--node_id <id>
    ID of this cluster node
--cluster <id1>=<url1>,<id2>=<url2>,...
    All other cluster nodes
--cluster_key <key>
    Shared cluster key for identification
--election_timeout 5000
    Raft election timeout
--heartbeat_timeout 1000
    Raft leader heartbeat timeout
--wait_quorum_timeout 30000
    Timeout for requests to wait for quorum to come up
--leadership_timeout 3000
    TinyRaft leadership timeout
--leader_priority <number>
    Raft leader priority for this node (optional)
--reconnect_interval 1000
    Unavailable peer connection retry interval
--dump_timeout 5000
    Timeout for dump command in milliseconds
--load_timeout 5000
    Timeout for load command in milliseconds
--forward_timeout 1000
    Timeout for forwarding requests from follower to leader in milliseconds
--replication_timeout 1000
    Timeout for replicating requests from leader to follower in milliseconds
--compact_timeout 1000
    Timeout for compaction requests from leader to follower in milliseconds

Logging:

--logs keyword1,keyword2,...
    Enable logs by keywords:
    access: log all requests and incoming websocket messages.
    watch: log outgoing websocket watch messages.
    cluster: log clustering (TinyRaft) events.
`;

function parse()
{
    const options = { stale_read: 1 };
    for (let i = 2; i < process.argv.length; i++)
    {
        const arg = process.argv[i].toLowerCase().replace(/^--(.+)$/, (m, m1) => '--'+m1.replace(/-/g, '_'));
        if (arg === '-h' || arg === '--help')
        {
            process.stderr.write(help_text);
            process.exit();
        }
        else if (arg.substr(0, 2) == '--')
        {
            options[arg.substr(2)] = process.argv[++i];
        }
    }
    if (options['persist_filter'])
    {
        options['persist_filter'] = require(options['persist_filter'])(options);
    }
    return options;
}

const antietcd = new AntiEtcd(parse());

// Set exit hook
const on_stop_cb = async () => { await antietcd.stop(); process.exit(0); };
process.on('SIGINT', on_stop_cb);
process.on('SIGTERM', on_stop_cb);
process.on('SIGQUIT', on_stop_cb);

antietcd.start().catch(console.error);
