"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RouterOSApiClient = exports.extractRouterOSText = void 0;
const net_1 = __importDefault(require("net"));
const tls_1 = __importDefault(require("tls"));
const DEFAULT_TIMEOUT_MS = 15000;
const waitMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const encodeLength = (length) => {
    if (length < 0x80)
        return Buffer.from([length]);
    if (length < 0x4000)
        return Buffer.from([(length >> 8) | 0x80, length & 0xff]);
    if (length < 0x200000)
        return Buffer.from([(length >> 16) | 0xc0, (length >> 8) & 0xff, length & 0xff]);
    if (length < 0x10000000) {
        return Buffer.from([(length >> 24) | 0xe0, (length >> 16) & 0xff, (length >> 8) & 0xff, length & 0xff]);
    }
    return Buffer.from([0xf0, (length >> 24) & 0xff, (length >> 16) & 0xff, (length >> 8) & 0xff, length & 0xff]);
};
const encodeSentence = (words) => {
    const chunks = [];
    for (const word of words) {
        const wordBuffer = Buffer.from(String(word ?? ''), 'utf8');
        chunks.push(encodeLength(wordBuffer.length));
        chunks.push(wordBuffer);
    }
    chunks.push(Buffer.from([0]));
    return Buffer.concat(chunks);
};
const decodeLength = (buffer, offset) => {
    if (offset >= buffer.length)
        return null;
    const first = buffer[offset];
    if ((first & 0x80) === 0x00) {
        return { length: first, consumed: 1 };
    }
    if ((first & 0xc0) === 0x80) {
        if (offset + 1 >= buffer.length)
            return null;
        return {
            length: ((first & 0x3f) << 8) | buffer[offset + 1],
            consumed: 2,
        };
    }
    if ((first & 0xe0) === 0xc0) {
        if (offset + 2 >= buffer.length)
            return null;
        return {
            length: ((first & 0x1f) << 16) | (buffer[offset + 1] << 8) | buffer[offset + 2],
            consumed: 3,
        };
    }
    if ((first & 0xf0) === 0xe0) {
        if (offset + 3 >= buffer.length)
            return null;
        return {
            length: (((first & 0x0f) << 24) >>> 0) | (buffer[offset + 1] << 16) | (buffer[offset + 2] << 8) | buffer[offset + 3],
            consumed: 4,
        };
    }
    if ((first & 0xf8) === 0xf0) {
        if (offset + 4 >= buffer.length)
            return null;
        return {
            length: ((buffer[offset + 1] << 24) >>> 0) | (buffer[offset + 2] << 16) | (buffer[offset + 3] << 8) | buffer[offset + 4],
            consumed: 5,
        };
    }
    throw new Error('Unsupported RouterOS word length encoding');
};
const parseWordMap = (words) => {
    const values = {};
    for (const word of words.slice(1)) {
        if (!word)
            continue;
        const normalized = word.startsWith('=') ? word.slice(1) : word;
        const sepIndex = normalized.indexOf('=');
        if (sepIndex === -1) {
            values[normalized] = '';
            continue;
        }
        const key = normalized.slice(0, sepIndex);
        const value = normalized.slice(sepIndex + 1);
        values[key] = value;
    }
    return values;
};
const extractRouterOSText = (result) => {
    const chunks = [];
    for (const sentence of result.sentences) {
        for (const candidateKey of ['ret', 'contents', 'output', 'value', 'message']) {
            const candidate = sentence.values[candidateKey];
            if (candidate)
                chunks.push(candidate);
        }
    }
    if (chunks.length > 0)
        return chunks.join('\n').trim();
    if (result.records.length > 0) {
        return result.records
            .map((row) => Object.entries(row).map(([key, value]) => `${key}=${value}`).join(' '))
            .join('\n')
            .trim();
    }
    return String(result.rawText || '').trim();
};
exports.extractRouterOSText = extractRouterOSText;
class RouterOSApiClient {
    constructor(options) {
        this.socket = null;
        this.buffer = Buffer.alloc(0);
        this.currentSentenceWords = [];
        this.activeResponse = null;
        this.connected = false;
        this.loggedIn = false;
        this.options = options;
    }
    async open() {
        await this.connect();
        if (!this.loggedIn) {
            const loginResult = await this.executeWords([
                '/login',
                `=name=${this.options.username}`,
                `=password=${this.options.password}`,
            ]);
            if (!loginResult.success) {
                throw new Error(loginResult.error || 'RouterOS login failed');
            }
            this.loggedIn = true;
        }
    }
    async close() {
        const socket = this.socket;
        const pending = this.activeResponse;
        this.activeResponse = null;
        this.socket = null;
        this.connected = false;
        this.loggedIn = false;
        this.buffer = Buffer.alloc(0);
        this.currentSentenceWords = [];
        if (pending) {
            clearTimeout(pending.timer);
            pending.reject(new Error('RouterOS connection closed'));
        }
        if (!socket)
            return;
        await new Promise((resolve) => {
            socket.once('close', () => resolve());
            socket.destroy();
            setTimeout(resolve, 50);
        });
    }
    async executeWords(words, timeoutMs = this.options.timeoutMs || DEFAULT_TIMEOUT_MS) {
        await this.connect();
        if (!this.socket)
            throw new Error('RouterOS socket is not connected');
        if (this.activeResponse) {
            throw new Error('RouterOS command already in progress');
        }
        return await new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                this.failActive(new Error('RouterOS command timed out'));
            }, timeoutMs);
            this.activeResponse = { sentences: [], resolve, reject, timer };
            try {
                this.socket?.write(encodeSentence(words));
            }
            catch (error) {
                this.failActive(error instanceof Error ? error : new Error(String(error)));
            }
        });
    }
    async probeDevice() {
        const identityResult = await this.executeWords(['/system/identity/print']);
        if (!identityResult.success)
            throw new Error(identityResult.error || 'Failed to read RouterOS identity');
        const resourceResult = await this.executeWords(['/system/resource/print']);
        if (!resourceResult.success)
            throw new Error(resourceResult.error || 'Failed to read RouterOS resources');
        let routerboard = null;
        try {
            const routerboardResult = await this.executeWords(['/system/routerboard/print']);
            if (routerboardResult.success) {
                routerboard = routerboardResult.records[0] || routerboardResult.done || null;
            }
        }
        catch {
            routerboard = null;
        }
        return {
            identity: identityResult.records[0] || identityResult.done || null,
            resource: resourceResult.records[0] || resourceResult.done || null,
            routerboard,
            rawText: [(0, exports.extractRouterOSText)(identityResult), (0, exports.extractRouterOSText)(resourceResult)].filter(Boolean).join('\n'),
        };
    }
    async exportConfig(baseName, timeoutMs = 30000) {
        const sanitizedBase = String(baseName || `emtelle-export-${Date.now()}`).replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/\.rsc$/i, '');
        const exportResult = await this.executeWords(['/export', '=show-sensitive=', '=terse=', `=file=${sanitizedBase}`], timeoutMs);
        if (!exportResult.success) {
            throw new Error(exportResult.error || 'RouterOS export failed');
        }
        const immediateText = (0, exports.extractRouterOSText)(exportResult);
        if (immediateText)
            return immediateText;
        for (const fileName of [`${sanitizedBase}.rsc`, sanitizedBase]) {
            for (let attempt = 0; attempt < 5; attempt += 1) {
                const contents = await this.readFileContents(fileName, timeoutMs).catch(() => '');
                if (contents) {
                    await this.deleteFile(fileName, timeoutMs).catch(() => undefined);
                    return contents;
                }
                await waitMs(800);
            }
        }
        throw new Error('RouterOS export returned no config text');
    }
    async printLogs(timeoutMs = 20000) {
        return await this.executeWords(['/log/print'], timeoutMs);
    }
    async runInlineScript(scriptBody, prefix = 'emtelle-job', timeoutMs = 30000) {
        const trimmed = String(scriptBody || '').trim();
        if (!trimmed)
            throw new Error('RouterOS script body is required');
        const scriptName = `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
        const addResult = await this.executeWords([
            '/system/script/add',
            `=name=${scriptName}`,
            '=policy=read,write,test,api,password,sensitive,policy,ftp,reboot,romon',
            `=source=${trimmed}`,
        ], timeoutMs);
        if (!addResult.success) {
            throw new Error(addResult.error || 'Failed to create temporary RouterOS script');
        }
        const scriptRef = addResult.done.ret || addResult.records[0]?.['.id'] || scriptName;
        try {
            return await this.executeWords(['/system/script/run', `=.id=${scriptRef}`], timeoutMs);
        }
        finally {
            await this.executeWords(['/system/script/remove', `=.id=${scriptRef}`], timeoutMs).catch(() => undefined);
        }
    }
    async readFileContents(fileName, timeoutMs = 15000) {
        const fileResult = await this.executeWords(['/file/print', `?name=${fileName}`], timeoutMs);
        if (!fileResult.success || fileResult.records.length === 0) {
            throw new Error(`RouterOS file not found: ${fileName}`);
        }
        const fileRow = fileResult.records[0];
        if (fileRow.contents)
            return fileRow.contents;
        const fileId = fileRow['.id'];
        if (fileId) {
            const getResult = await this.executeWords(['/file/get', `=.id=${fileId}`, '=value-name=contents'], timeoutMs);
            const text = (0, exports.extractRouterOSText)(getResult);
            if (text)
                return text;
        }
        throw new Error(`RouterOS file is empty or unreadable: ${fileName}`);
    }
    async deleteFile(fileName, timeoutMs = 15000) {
        const fileResult = await this.executeWords(['/file/print', `?name=${fileName}`], timeoutMs);
        if (!fileResult.success || fileResult.records.length === 0)
            return;
        const fileId = fileResult.records[0]['.id'];
        if (fileId) {
            await this.executeWords(['/file/remove', `=.id=${fileId}`], timeoutMs);
        }
    }
    async connect() {
        if (this.connected && this.socket)
            return;
        await new Promise((resolve, reject) => {
            const timeoutMs = this.options.timeoutMs || DEFAULT_TIMEOUT_MS;
            const onConnect = () => {
                this.connected = true;
                resolve();
            };
            const onError = (error) => {
                reject(error);
            };
            const socket = this.options.useSsl
                ? tls_1.default.connect({
                    host: this.options.host,
                    port: this.options.port || 8729,
                    rejectUnauthorized: false,
                    servername: net_1.default.isIP(this.options.host) ? undefined : this.options.host,
                }, onConnect)
                : net_1.default.createConnection({
                    host: this.options.host,
                    port: this.options.port || 8728,
                }, onConnect);
            socket.setTimeout(timeoutMs);
            socket.setNoDelay(true);
            socket.once('error', onError);
            socket.once('timeout', () => onError(new Error('RouterOS connection timed out')));
            socket.on('data', (chunk) => this.handleData(chunk));
            socket.on('close', () => {
                this.connected = false;
                this.loggedIn = false;
                this.socket = null;
                this.failActive(new Error('RouterOS connection closed'));
            });
            socket.on('error', (error) => {
                this.connected = false;
                this.loggedIn = false;
                this.failActive(error);
            });
            this.socket = socket;
        });
    }
    handleData(chunk) {
        this.buffer = Buffer.concat([this.buffer, chunk]);
        while (true) {
            const decoded = this.tryReadWord();
            if (!decoded)
                return;
            if (decoded.word === null) {
                const completedSentence = this.currentSentenceWords.splice(0);
                this.onSentence(completedSentence);
            }
            else {
                this.currentSentenceWords.push(decoded.word);
            }
        }
    }
    tryReadWord() {
        const decodedLength = decodeLength(this.buffer, 0);
        if (!decodedLength)
            return null;
        const totalLength = decodedLength.consumed + decodedLength.length;
        if (this.buffer.length < totalLength)
            return null;
        const word = decodedLength.length === 0
            ? null
            : this.buffer.slice(decodedLength.consumed, totalLength).toString('utf8');
        this.buffer = this.buffer.slice(totalLength);
        return { word };
    }
    onSentence(words) {
        if (!this.activeResponse)
            return;
        const type = words[0] || '!done';
        this.activeResponse.sentences.push(words);
        if (type === '!done' || type === '!trap' || type === '!fatal') {
            const pending = this.activeResponse;
            this.activeResponse = null;
            clearTimeout(pending.timer);
            pending.resolve(this.buildResult(pending.sentences));
        }
    }
    buildResult(sentences) {
        const normalized = sentences.map((words) => ({
            type: words[0] || '!done',
            words,
            values: parseWordMap(words),
        }));
        const records = normalized.filter((sentence) => sentence.type === '!re').map((sentence) => sentence.values);
        const done = normalized.find((sentence) => sentence.type === '!done')?.values || {};
        const trapValues = normalized.filter((sentence) => sentence.type === '!trap' || sentence.type === '!fatal').map((sentence) => sentence.values);
        const error = trapValues.length > 0
            ? trapValues.map((entry) => entry.message || entry.category || JSON.stringify(entry)).join(' | ')
            : undefined;
        const rawText = normalized.map((sentence) => sentence.words.join(' ')).join('\n');
        return {
            success: !error,
            sentences: normalized,
            records,
            done,
            rawText,
            error,
        };
    }
    failActive(error) {
        if (!this.activeResponse)
            return;
        const pending = this.activeResponse;
        this.activeResponse = null;
        clearTimeout(pending.timer);
        pending.reject(error);
    }
}
exports.RouterOSApiClient = RouterOSApiClient;
//# sourceMappingURL=routeros-api.js.map