/**
 * LLUAV Developer SDK - JavaScript Edition (Enhanced)
 *
 * Features:
 *   - Automatic login after connect
 *   - Auto-reconnect with exponential backoff
 *   - Heartbeat keep-alive
 *   - Strict command queue (one command in flight at a time)
 *   - Per-command timeout and retry
 *   - Periodic task scheduler
 *
 * Protocol: text-based commands over WebSocket, JSON responses.
 * Default port: 53001.
 */

'use strict';

let WebSocketImpl;
try {
    WebSocketImpl = require('ws');
} catch (e) {
    if (typeof WebSocket !== 'undefined') {
        WebSocketImpl = WebSocket;
    } else {
        throw new Error('WebSocket implementation not found. Install "ws" for Node.js or use a modern browser.');
    }
}

const WS_OPEN = WebSocketImpl.OPEN || 1;
const WS_CONNECTING = WebSocketImpl.CONNECTING || 0;

class LLUAVError extends Error {
    constructor(message) {
        super(message);
        this.name = 'LLUAVError';
    }
}

class LLUAVCommandError extends LLUAVError {
    constructor(message, retCode, response) {
        super(message);
        this.name = 'LLUAVCommandError';
        this.retCode = retCode;
        this.response = response;
    }
}

class LLUAVConnectionError extends LLUAVError {
    constructor(message) {
        super(message);
        this.name = 'LLUAVConnectionError';
    }
}

class LLUAVClient {
    /**
     * @param {string} host
     * @param {number} port
     * @param {string|null} password
     * @param {number} timeout - Default request timeout in ms
     * @param {object} options
     * @param {boolean} options.autoReconnect
     * @param {number} options.reconnectInterval - Initial reconnect delay in ms
     * @param {number} options.maxReconnectInterval - Max reconnect delay in ms
     * @param {number} options.maxRetries - Max retries per command
     * @param {number} options.heartbeatInterval - Heartbeat interval in ms (0 to disable)
     * @param {string} options.heartbeatCmd - Command used for heartbeat
     */
    constructor(host = 'localhost', port = 53001, password = null, timeout = 10000,
                options = {}) {
        this.host = host;
        this.port = port;
        this.password = password;
        this.timeout = timeout;
        this.autoReconnect = options.autoReconnect !== false;
        this.reconnectInterval = options.reconnectInterval || 1000;
        this.maxReconnectInterval = options.maxReconnectInterval || 30000;
        this.maxRetries = options.maxRetries !== undefined ? options.maxRetries : 3;
        this.heartbeatInterval = options.heartbeatInterval !== undefined ? options.heartbeatInterval : 5000;
        this.heartbeatCmd = options.heartbeatCmd || 'state';

        this.ws = null;
        this.connected = false;
        this.shutdown = false;
        this._queue = [];
        this._currentRequest = null;
        this._reconnectDelay = this.reconnectInterval;
        this._reconnectTimer = null;
        this._heartbeatTimer = null;
        this._periodicTimer = null;
        this._periodicTasks = new Map(); // name -> {intervalSec, cmd, args, lastRun}
    }

    get url() {
        return `ws://${this.host}:${this.port}`;
    }

    // ---------------- Connection Management ----------------

    connect() {
        return new Promise((resolve, reject) => {
            if (this.ws && (this.ws.readyState === WS_OPEN || this.ws.readyState === WS_CONNECTING)) {
                if (this.connected) return resolve();
                // Still connecting, wait a bit
                setTimeout(() => {
                    if (this.connected) resolve();
                    else reject(new LLUAVConnectionError('Connection timeout'));
                }, this.timeout);
                return;
            }
            this.shutdown = false;
            const ws = new WebSocketImpl(this.url);
            this.ws = ws;

            const connectTimer = setTimeout(() => {
                ws.close();
                reject(new LLUAVConnectionError(`Connection timeout to ${this.url}`));
            }, this.timeout);

            const onOpen = () => {
                clearTimeout(connectTimer);
                this.connected = true;
                this._reconnectDelay = this.reconnectInterval;
                this._bindEvents(ws);
                this._startHeartbeat();
                this._startPeriodic();
                this._processQueue();
                if (this.password) {
                    this._enqueue('login', this.password, { timeout: this.timeout })
                        .then(resolve)
                        .catch(reject);
                } else {
                    resolve();
                }
            };

            const onError = (err) => {
                clearTimeout(connectTimer);
                const msg = err && err.message ? err.message : 'Connection error';
                reject(new LLUAVConnectionError(msg));
                if (this.autoReconnect && !this.shutdown) {
                    this._scheduleReconnect();
                }
            };

            if (ws.on) {
                ws.once('open', onOpen);
                ws.once('error', onError);
            } else {
                ws.onopen = onOpen;
                ws.onerror = onError;
            }
        });
    }

    _bindEvents(ws) {
        const onMessage = (data) => {
            let raw = data;
            if (typeof data === 'object' && data.data) raw = data.data;
            this._onMessage(raw);
        };

        const onClose = () => {
            this._onClose();
        };

        const onError = (err) => {
            // Error usually precedes close; let onClose handle reconnect
            this.connected = false;
        };

        if (ws.on) {
            ws.on('message', onMessage);
            ws.on('close', onClose);
            ws.on('error', onError);
        } else {
            ws.onmessage = onMessage;
            ws.onclose = onClose;
            ws.onerror = onError;
        }
    }

    disconnect() {
        this.shutdown = true;
        this._stopReconnect();
        this._stopHeartbeat();
        this._stopPeriodic();

        // Reject all queued commands
        while (this._queue.length > 0) {
            const req = this._queue.shift();
            clearTimeout(req.timeoutId);
            req.reject(new LLUAVConnectionError('Disconnected by user'));
        }

        // Reject in-flight command
        if (this._currentRequest) {
            const req = this._currentRequest;
            this._currentRequest = null;
            clearTimeout(req.timeoutId);
            req.reject(new LLUAVConnectionError('Disconnected by user'));
        }

        if (this.ws) {
            try { this.ws.close(); } catch (e) {}
            this.ws = null;
        }
        this.connected = false;
        return Promise.resolve();
    }

    isConnected() {
        return this.connected && this.ws && this.ws.readyState === WS_OPEN;
    }

    // ---------------- Event Handlers ----------------

    _onMessage(raw) {
        let resp;
        try { resp = JSON.parse(raw); } catch (e) { return; }

        if (!this._currentRequest) return;

        const req = this._currentRequest;
        this._currentRequest = null;
        clearTimeout(req.timeoutId);

        if (resp.ret !== undefined && resp.ret !== 0) {
            req.reject(new LLUAVCommandError(
                resp.msg || `Command '${req.message}' failed`, resp.ret, resp));
        } else {
            req.resolve(resp);
        }

        // Continue with next command
        setTimeout(() => this._processQueue(), 0);
    }

    _onClose() {
        const wasConnected = this.connected;
        this.connected = false;
        this._stopHeartbeat();
        this._stopPeriodic();

        // Handle in-flight request: put back to queue head for retry
        if (this._currentRequest) {
            const req = this._currentRequest;
            this._currentRequest = null;
            clearTimeout(req.timeoutId);
            req.retryCount++;
            if (req.retryCount <= this.maxRetries) {
                this._queue.unshift(req);
            } else {
                req.reject(new LLUAVConnectionError('Connection closed before response'));
            }
        }

        if (this.autoReconnect && !this.shutdown && wasConnected) {
            this._scheduleReconnect();
        }
    }

    // ---------------- Reconnect ----------------

    _scheduleReconnect() {
        if (this._reconnectTimer || this.shutdown) return;
        this._reconnectTimer = setTimeout(() => {
            this._reconnectTimer = null;
            if (!this.shutdown) {
                this.connect().catch(() => {
                    // Connect failed, schedule next attempt with increased delay
                    if (this.autoReconnect && !this.shutdown) {
                        this._scheduleReconnect();
                    }
                });
            }
        }, this._reconnectDelay);
        this._reconnectDelay = Math.min(this._reconnectDelay * 2, this.maxReconnectInterval);
    }

    _stopReconnect() {
        if (this._reconnectTimer) {
            clearTimeout(this._reconnectTimer);
            this._reconnectTimer = null;
        }
    }

    // ---------------- Queue Processing ----------------

    _processQueue() {
        if (!this.connected || this._currentRequest || this._queue.length === 0) {
            return;
        }

        const req = this._queue.shift();

        if (Date.now() > req.deadline) {
            req.reject(new LLUAVCommandError('Command timeout'));
            setTimeout(() => this._processQueue(), 0);
            return;
        }

        this._currentRequest = req;
        req.timeoutId = setTimeout(() => {
            if (this._currentRequest === req) {
                this._currentRequest = null;
                req.retryCount++;
                if (req.retryCount <= this.maxRetries) {
                    this._queue.unshift(req);
                } else {
                    req.reject(new LLUAVCommandError('Max retries exceeded (timeout)'));
                }
                this._processQueue();
            }
        }, req.timeout);

        try {
            this.ws.send(req.message);
        } catch (e) {
            this.connected = false;
            if (this._currentRequest === req) {
                this._currentRequest = null;
                clearTimeout(req.timeoutId);
                req.retryCount++;
                if (req.retryCount <= this.maxRetries) {
                    this._queue.unshift(req);
                } else {
                    req.reject(new LLUAVConnectionError(e.message));
                }
                this._scheduleReconnect();
                this._processQueue();
            }
        }
    }

    _enqueue(cmd, args, options = {}) {
        return new Promise((resolve, reject) => {
            const message = args ? `${cmd} ${args}` : cmd;
            const to = options.timeout || this.timeout;
            const req = {
                message,
                resolve,
                reject,
                deadline: Date.now() + to,
                retryCount: 0,
                timeoutId: null,
                timeout: to,
            };
            this._queue.push(req);
            this._processQueue();
        });
    }

    // Used by heartbeat and periodic tasks (fire-and-forget)
    _enqueueFireAndForget(cmd, args) {
        this._enqueue(cmd, args, { timeout: this.timeout }).catch(() => {});
    }

    // ---------------- Heartbeat & Periodic ----------------

    _startHeartbeat() {
        if (this.heartbeatInterval > 0) {
            this._heartbeatTimer = setInterval(() => this._heartbeatTick(), this.heartbeatInterval);
        }
    }

    _stopHeartbeat() {
        if (this._heartbeatTimer) {
            clearInterval(this._heartbeatTimer);
            this._heartbeatTimer = null;
        }
    }

    _heartbeatTick() {
        if (this.connected && this._queue.length === 0) {
            this._enqueueFireAndForget(this.heartbeatCmd);
        }
    }

    _startPeriodic() {
        this._periodicTimer = setInterval(() => this._periodicTick(), 200);
    }

    _stopPeriodic() {
        if (this._periodicTimer) {
            clearInterval(this._periodicTimer);
            this._periodicTimer = null;
        }
    }

    _periodicTick() {
        if (!this.connected) return;
        const now = Date.now();
        for (const [name, task] of this._periodicTasks) {
            if (now - task.lastRun >= task.intervalSec * 1000) {
                task.lastRun = now;
                this._enqueueFireAndForget(task.cmd, task.args);
            }
        }
    }

    // ---------------- Periodic Task API ----------------

    addPeriodicTask(name, intervalSec, cmd, args = '') {
        this._periodicTasks.set(name, {
            intervalSec,
            cmd,
            args,
            lastRun: Date.now(),
        });
    }

    removePeriodicTask(name) {
        this._periodicTasks.delete(name);
    }

    // ---------------- Public Send API ----------------

    sendCmd(cmd, args = '', options = {}) {
        return this._enqueue(cmd, args, options);
    }

    // ---------------- High-level APIs ----------------

    async login(password) { return this.sendCmd('login', password); }
    async adminLock() { return this.sendCmd('admin_lock'); }
    async adminUnlock() { return this.sendCmd('admin_unlock'); }

    async getState() { return this.sendCmd('state'); }
    async getParam(name = '') { return name ? this.sendCmd('get_param', name) : this.sendCmd('param'); }
    async getVersion() { return this.sendCmd('ver'); }
    async getSysinfo() { return this.sendCmd('sysinfo'); }
    async getDiskUsage() { return this.sendCmd('diskusage'); }
    async getStatics() { return this.sendCmd('statics'); }
    async getMachineId() { return this.sendCmd('machineid'); }
    async getHud1() { return this.sendCmd('hud1'); }
    async getLidarPose() { return this.sendCmd('lidarpose'); }
    async getApronState() { return this.sendCmd('apron_state'); }
    async getMavlinkMsgs() { return this.sendCmd('mavlink_msgs'); }
    async getSubmapInfo() { return this.sendCmd('get_submap_info'); }

    async takeoff() { return this.sendCmd('takeoff'); }
    async land() { return this.sendCmd('land'); }
    async rtl() { return this.sendCmd('rtl'); }
    async missionRtl() { return this.sendCmd('mrtl'); }
    async tetherLand() { return this.sendCmd('tether_land'); }
    async setSpNed(x, y, z, yaw) { return this.sendCmd('set_sp_ned', `${x}:${y}:${z}:${yaw}`); }
    async relocatePos(x, y, z, yaw) { return this.sendCmd('relocate_pos', `${x},${y},${z},${yaw}`); }
    async tunnelFly(enable) { return this.sendCmd('tunnelfly', enable ? '1' : '0'); }
    async disarm() { return this.sendCmd('disarm'); }
    async sleep() { return this.sendCmd('sleep'); }
    async wakeup() { return this.sendCmd('wakeup'); }
    async autoSleepEnable(enable) { return this.sendCmd('autosleep_enable', enable ? '1' : '0'); }
    async zyhk(heights, landX = 0.0, landY = 0.0, landYaw = 0.0) {
        const hstr = heights.join(',');
        const args = (landX || landY || landYaw) ? `${hstr} ${landX},${landY},${landYaw}` : hstr;
        return this.sendCmd('zyhk', args);
    }

    async mission(extId, pathOrJson) { return this.sendCmd('mission', `${extId}:${pathOrJson}`); }
    async startPlan(planStr) { return this.sendCmd('plan', planStr); }
    async unlockPlan() { return this.sendCmd('unlock_plan'); }
    async missionWpList() { return this.sendCmd('mw_list'); }
    async missionWpExec(extId, wpIds) { return this.sendCmd('mw_exec', `${extId}:${wpIds}`); }
    async ai1Mission(taskId, missionFile) { return this.sendCmd('ai1_mission', `${taskId}:${missionFile}`); }
    async obsMission(enable) { return this.sendCmd('obs_mission', enable ? '1' : '0'); }
    async dumpMissionResult(extId = -1) { return this.sendCmd('dump_missionresult', extId >= 0 ? String(extId) : ''); }
    async dumpMissionList() { return this.sendCmd('dump_missionlist'); }
    async abortReport(abortId = -1) { return this.sendCmd('abort_report', String(abortId)); }
    async setTaskname(name) { return this.sendCmd('set_taskname', name); }

    async saveMap(path) { return this.sendCmd('savemap', path); }
    async setMapLocate(enable) { return this.sendCmd('set_maplocate', enable ? '1' : '0'); }
    async setMapPath(path) { return this.sendCmd('set_mappath', path); }
    async listMaps() { return this.sendCmd('list_maps'); }
    async rmMap(name) { return this.sendCmd('rm_map', name); }

    async capture() { return this.sendCmd('capture'); }
    async snapshot() { return this.sendCmd('snapshot'); }
    async recordToggle() { return this.sendCmd('record'); }
    async savebag(start) { return this.sendCmd('savebag', start ? '1' : '0'); }

    async siyiReboot() { return this.sendCmd('siyi_reboot'); }
    async siyiFormat() { return this.sendCmd('siyi_format'); }
    async siyiCenter() { return this.sendCmd('siyi_center'); }
    async siyiInit() { return this.sendCmd('siyi_init'); }
    async siyiDownvideo(vid = -1) { return this.sendCmd('siyi_downvideo', String(vid)); }
    async siyiSync() { return this.sendCmd('siyi_sync'); }

    async ledOn() { return this.sendCmd('ledon'); }
    async ledOff() { return this.sendCmd('ledoff'); }
    async ledEnable() { return this.sendCmd('ledenable'); }
    async ledDisable() { return this.sendCmd('leddisable'); }
    async ledPwm(pwm) { return this.sendCmd('ledpwm', String(pwm)); }
    async ledBlink() { return this.sendCmd('led_blink'); }

    async setParam(name, value) { return this.sendCmd('set_param', `${name}:${value}`); }
    async getParamValue(name) { return this.sendCmd('get_param', name); }
    async resetParams() { return this.sendCmd('reset_param'); }
    async saveParams(force = false) { return this.sendCmd('save_param', force ? '1' : '0'); }
    async changePassword(newPassword) { return this.sendCmd('passwd', newPassword); }

    async setApronVol(voltage) { return this.sendCmd('apron_vol', String(voltage)); }
    async setApronAmp(amperage) { return this.sendCmd('apron_amp', String(amperage)); }

    async rebootNode() { return this.sendCmd('reboot_node'); }
    async rebootAp() { return this.sendCmd('reboot_ap'); }
    async rebootSystem() { return this.sendCmd('reboot_system'); }
    async poweroffSystem() { return this.sendCmd('poweroff_system'); }
    async preparePoweroff() { return this.sendCmd('prepare_poweroff'); }
    async rebootDlo() { return this.sendCmd('reboot_dlo'); }
    async restartDlo() { return this.sendCmd('restart_dlo'); }
    async killLivox(script = '') { return this.sendCmd('kill_livox', script); }

    async lidarOnoff(on) { return this.sendCmd('lidaronoff', on ? '1' : '0'); }
    async lidarWarn() { return this.sendCmd('lidarwarn'); }
    async utgGet() { return this.sendCmd('utg_get'); }
    async utgSet(speed) { return this.sendCmd('utg_set', String(speed)); }

    async grabberCtrl(pwm) { return this.sendCmd('grabber_ctrl', String(pwm)); }
    async ara8Center() { return this.sendCmd('ara8_center'); }
    async ara8Pitch(angle) { return this.sendCmd('ara8_pitch', String(angle)); }
    async cvTest() { return this.sendCmd('cv_test'); }
    async preflightCheck() { return this.sendCmd('preflight_check'); }
    async preflightEnable(enable) { return this.sendCmd('preflight_enable', enable ? '1' : '0'); }
    async clearBags(minGb = -1) { return this.sendCmd('clear_bags', String(minGb)); }
    async clearImages(minGb = -1) { return this.sendCmd('clear_images', String(minGb)); }
    async listImagedir(size = -1) { return this.sendCmd('list_imagedir', String(size)); }
    async rmImagedir(name) { return this.sendCmd('rm_imagedir', name); }
    async testWp(x, y, z) { return this.sendCmd('test_wp', `${x}:${y}:${z}`); }
    async testWpList(filePath) { return this.sendCmd('test_listwp', filePath); }
    async showMsg(count = -1) { return this.sendCmd('showmsg', String(count)); }
    async saveMsg() { return this.sendCmd('savemsg'); }
    async bmsSetpower(on) { return this.sendCmd('bms_setpower', on ? '1' : '0'); }
    async bmsSetcharge(on) { return this.sendCmd('bms_setcharge', on ? '1' : '0'); }
    async showSetpoint() { return this.sendCmd('show_sp'); }
    async showApGuid2() { return this.sendCmd('show_apguid2'); }
    async help() { return this.sendCmd('help'); }
}

module.exports = {
    LLUAVClient,
    LLUAVError,
    LLUAVCommandError,
    LLUAVConnectionError,
};
