// AntiEtcd to Etcd gRPC upstream adapter
// (c) Vitaliy Filippov, 2026
// License: Mozilla Public License 2.0 or Vitastor Network Public License 1.1

const { CompareTarget, CompareResult, Etcd3 } = require('etcd3');

const { RequestError } = require('./common.js');
const EtcTree = require('./etctree.js');

class EtcdProxy
{
    constructor(antietcd)
    {
        this.cfg = antietcd.cfg;
        this.antietcd = antietcd;
        this.tls = {};
        this.watch_revision = undefined;
        this.keepalive_stream = null;
        this.keepalive_requests = {};
        this.antietcd.etctree.disable_leases = true;
        this.antietcd.etctree.pause_leases();
    }

    async init()
    {
        if (this.cfg.etcd_cert)
        {
            this.tls.certChain = await this.antietcd.readPEM(this.cfg.etcd_cert);
        }
        if (this.cfg.etcd_key)
        {
            this.tls.privateKey = await this.antietcd.readPEM(this.cfg.etcd_key);
        }
        if (this.cfg.etcd_ca)
        {
            this.tls.rootCertificate = await this.antietcd.readPEM(this.cfg.etcd_ca);
        }
        this.client = new Etcd3({
            hosts: this.cfg.etcd_proxy instanceof Array ? this.cfg.etcd_proxy : this.cfg.etcd_proxy.split(','),
            credentials: this.tls,
        });
        this.ns = this.client.namespace('');
        this.stream_on_data = (item) => this._on_data(item).catch(console.error);
        this.stream_on_end = () => this._on_end();
        this.stream_on_error = (err) => this._on_error(err);
        await this._start_watcher();
    }

    async _start_watcher()
    {
        const snapshot = await this.ns.kv.range({
            key: Buffer.from('00', 'hex'),
            range_end: Buffer.from('ff', 'hex'),
        });
        const newtree = new EtcTree();
        for (const kv of snapshot.kvs)
        {
            kv.key = kv.key.toString();
            kv.value = kv.value.toString();
            kv.lease = kv.lease || null;
            newtree.raw_put(kv);
        }
        this.watch_revision = snapshot.header.revision;
        this.antietcd.etctree.load({
            mod_revision: snapshot.header.revision,
            compact_revision: 0,
            state: newtree.state,
        }, false /*update_only*/);
        this.stream = await this.ns.watchClient.watch();
        this.stream.on('data', this.stream_on_data);
        this.stream.on('end', this.stream_on_end);
        this.stream.on('error', this.stream_on_error);
        this.stream.write({ create_request: {
            key: Buffer.from('00', 'hex'),
            range_end: Buffer.from('ff', 'hex'),
            start_revision: this.watch_revision,
            watch_id: 'A',
            progress_notify: true,
        } });
    }

    async _on_data(item)
    {
        if (item.canceled)
        {
            this.stream.cancel();
            console.log('Etcd proxy watch canceled, reason: '+item.cancel_reason+', restarting');
            this._start_watcher();
            return;
        }
        if (item.events)
        {
            await this.antietcd.etctree.apply_replication({ header: item.header, events: item.events.map(ev => ev.kv) });
        }
    }

    _on_error(err)
    {
        console.log('Etcd proxy watch error: '+err);
        this.stream.cancel();
        this._start_watcher();
    }

    _on_end()
    {
        console.log('Etcd proxy watch ended');
        this.stream.cancel();
        this._start_watcher();
    }

    // all functions are async, but the underlying Promise is just passed as is

    api_txn(txn)
    {
        txn = { ...txn };
        if (txn.compare)
        {
            txn.compare = txn.compare.map(req =>
            {
                let conv;
                if (req.target == 'MOD')
                {
                    conv = {
                        key: Buffer.from(req.key),
                        target: CompareTarget.Mod,
                        mod_revision: req.mod_revision,
                    };
                }
                else if (req.target == 'CREATE')
                {
                    conv = {
                        key: Buffer.from(req.key),
                        target: CompareTarget.Create,
                        create_revision: req.create_revision,
                    };
                }
                else if (req.target == 'VERSION')
                {
                    conv = {
                        key: Buffer.from(req.key),
                        target: CompareTarget.Version,
                        version: req.version,
                    };
                }
                else if (req.target == 'VALUE')
                {
                    conv = {
                        key: Buffer.from(req.key),
                        target: CompareTarget.Value,
                        value: Buffer.from(req.value),
                    };
                }
                else if (req.target == 'LEASE')
                {
                    conv = {
                        key: Buffer.from(req.key),
                        target: CompareTarget.Lease,
                        lease: req.lease,
                    };
                }
                else
                {
                    throw new RequestError(400, 'Invalid comparison target: '+req.target);
                }
                if (req.result == "EQUAL" || req.result == null)
                {
                    conv.result = CompareResult.Equal;
                }
                else if (req.result == "GREATER")
                {
                    conv.result = CompareResult.Greater;
                }
                else if (req.result == "LESS")
                {
                    conv.result = CompareResult.Less;
                }
                else if (req.result == "NOT_EQUAL")
                {
                    conv.result = CompareResult.NotEqual;
                }
                else
                {
                    throw new RequestError(400, 'Invalid comparison result: '+req.result);
                }
                return conv;
            });
        }
        for (const r of [ 'success', 'failure' ])
        {
            if (!txn[r])
            {
                continue;
            }
            txn[r] = txn[r].map(req =>
            {
                if (req.request_range)
                {
                    req = { request_range: { ...req.request_range, key: Buffer.from(req.request_range.key) } };
                    if (req.request_range.range_end != null)
                        req.request_range.range_end = Buffer.from(req.request_range.range_end);
                }
                else if (req.request_put)
                {
                    req = { request_put: {
                        ...req.request_put,
                        key: Buffer.from(req.request_put.key),
                        value: Buffer.from(req.request_put.value),
                    } };
                }
                else if (req.request_delete_range)
                {
                    req = { request_delete_range: { ...req.request_delete_range, key: Buffer.from(req.request_delete_range.key) } };
                    if (req.request_delete_range.range_end != null)
                        req.request_delete_range.range_end = Buffer.from(req.request_delete_range.range_end);
                }
                return req;
            });
        }
        return this.ns.kv.txn(txn);
    }

    lease_grant(params)
    {
        return this.ns.leaseClient.leaseGrant(params);
    }

    lease_revoke(params)
    {
        return this.ns.leaseClient.leaseRevoke(params);
    }

    async lease_keepalive(params)
    {
        if (!this.keepalive_stream)
        {
            // No idea why, but leaseKeepAlive in a streaming API in etcd grpc
            this.keepalive_stream = await this.ns.leaseClient.leaseKeepAlive();
            this.keepalive_stream.on('data', res =>
            {
                const req = this.keepalive_requests[res.ID];
                if (req)
                {
                    delete this.keepalive_requests[res.ID];
                    req.ok({ result: res });
                }
            });
            this.keepalive_stream.on('error', err =>
            {
                const cbs = this.keepalive_requests;
                this.keepalive_requests = {};
                this.keepalive_stream.end();
                this.keepalive_stream = null;
                for (const id in cbs)
                {
                    cbs[id].no(err);
                }
            });
        }
        return await new Promise((ok, no) =>
        {
            this.keepalive_requests[params.ID] = { id: params.ID, ok, no };
            this.keepalive_stream.write(params);
        });
    }

    async maintenance_status()
    {
        return await this.client.maintenance.status();
    }
}

module.exports = EtcdProxy;
