"use strict";
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const perm_1 = require("./perm");
const config_1 = require("./config");
const lodash_1 = __importDefault(require("lodash"));
const update_1 = require("./update");
const listen_1 = require("./listen");
const yaml_1 = __importDefault(require("yaml"));
const const_1 = require("./const");
const node_readline_1 = require("node:readline");
const first_1 = require("./first");
const plugins_1 = require("./plugins");
const fileAttr_1 = require("./fileAttr");
const github_1 = require("./github");
const cross_1 = require("./cross");
const api_monitor_1 = __importStar(require("./api.monitor"));
const connections_1 = require("./connections");
const argv_1 = require("./argv");
const listen_2 = require("./listen");
let debugEnabled = argv_1.argv.debug || process.env.HFS_DEBUG || const_1.DEV;
if (!argv_1.argv.updating && !config_1.showHelp) {
    try {
        // Not sure if the try is necessary for when stdin is unavailable, but someone reported a problem using nohup https://github.com/rejetto/hfs/issues/74 and I've found this example try-catching https://github.com/DefinitelyTyped/DefinitelyTyped/blob/dda83a906914489e09ca28afea12948529015d4a/types/node/readline.d.ts#L489
        const tty = process.stdin.isTTY && process.stdout.isTTY || undefined;
        const prompter = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stdout, prompt: tty && 'command> ' })
            .on('line', x => parseCommandLine(x).then(showPrompt))
            .on('SIGINT', () => process.emit('SIGINT')); // readline swallows the first ctrl+c unless we forward it to process-level handlers
        let isClean = true;
        const showPrompt = tty && lodash_1.default.debounce(() => {
            if (first_1.quitting || !tty || !isClean)
                return;
            prompter.prompt(true);
            isClean = false;
        }, 100);
        function clean() {
            if (isClean)
                return;
            // keep console methods synchronous: reposition the cursor immediately and let stream ordering preserve output sequence
            (0, node_readline_1.cursorTo)(process.stdout, 0); // we don't need to clean as long as the prompt is never longer than the printed line
            isClean = true;
        }
        showPrompt?.();
        for (const k of ['log', 'warn', 'error', 'debug']) {
            const original = console[k];
            console[k] = (...args) => {
                if (k === 'debug' && !debugEnabled)
                    return;
                if (!first_1.quitting && tty)
                    clean();
                try {
                    original(...args);
                }
                finally {
                    showPrompt?.();
                }
            };
        }
    }
    catch {
        console.log("Console commands not available");
        const original = console.debug;
        console.debug = (...args) => debugEnabled && original(...args);
    }
}
async function parseCommandLine(line) {
    const tokens = Array.from(line.trim().matchAll(/"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|((?:\\.|[^\s"'\\])+)/g)).map(m => (m[1] ?? m[2] ?? m[3] ?? '').replace(/\\([\\ "'"])/g, '$1')); // unescape
    if (!tokens.length)
        return;
    let [name, ...params] = tokens;
    name = aliases[name] || name;
    let cmd = commands[name];
    if (cmd?.alias)
        cmd = commands[cmd.alias];
    if (!cmd)
        return console.error("Invalid command, try 'help'");
    if (cmd.cb.length > params.length)
        return console.error("Insufficient parameters, expected: " + cmd.params);
    try {
        await cmd.cb(...params);
        console.log("+++ Command executed");
    }
    catch (err) {
        if (typeof err !== 'string' && !err?.message)
            throw err;
        console.error("Command failed:", err.message || err);
    }
}
const aliases = { ver: 'version', exit: 'quit' };
const commands = {
    help: {
        params: '',
        cb() {
            console.log("Available commands:", ...lodash_1.default.map(commands, ({ params }, name) => '\n - ' + name + ' ' + params));
        }
    },
    'show-admin': {
        params: '',
        cb() {
            (0, listen_1.openAdmin)();
        }
    },
    'create-admin': {
        params: '<password> [<username>=admin]',
        cb: perm_1.createAdmin
    },
    'change-password': {
        params: '<user> <password>',
        async cb(user, password) {
            const acc = (0, perm_1.getAccount)(user);
            if (!acc)
                throw "user doesn't exist";
            await (0, perm_1.updateAccount)(acc, { password });
        }
    },
    config: {
        params: '<key> <value>',
        async cb(key, value) {
            if (!(0, config_1.configKeyExists)(key))
                throw "specified key doesn't exist";
            let v = value;
            try {
                v = JSON.parse(v);
            }
            catch { }
            await (0, config_1.setConfig)({ [key]: v });
        }
    },
    'get-config': {
        params: '[<key-mask>]',
        cb(key = '*') {
            const matcher = (0, cross_1.makeMatcher)(key);
            const filtered = lodash_1.default.pickBy((0, config_1.getWholeConfig)({}), (_v, k) => matcher(k));
            console.log('\n' + yaml_1.default.stringify(filtered, { lineWidth: 1000 }).trim());
        }
    },
    quit: {
        params: '',
        cb() {
            process.emit('SIGTERM');
        }
    },
    update: {
        params: '[<version>=latest]',
        cb: update_1.update,
    },
    'check-update': {
        params: '',
        async cb() {
            const update = await (0, update_1.getBestUpdate)();
            if (!update)
                throw "you already have the latest version: " + const_1.VERSION;
            console.log("New version available", update.name);
        }
    },
    version: {
        params: '',
        cb() {
            console.log(const_1.VERSION, 'build', const_1.BUILD_TIMESTAMP);
        }
    },
    debug: {
        params: '',
        cb() {
            debugEnabled = !debugEnabled;
            console.log(`Debug messages ${debugEnabled ? "on" : "off"}`);
        }
    },
    'start-plugin': {
        params: '<name>',
        cb: plugins_1.startPlugin,
    },
    'stop-plugin': {
        params: '<name>',
        cb: plugins_1.stopPlugin,
    },
    'download-plugin': {
        params: '<githubUser/repo>',
        cb: github_1.downloadPlugin,
    },
    'list-plugins': {
        params: '',
        cb() {
            (0, plugins_1.mapPlugins)(p => console.log('On:', p.id), false);
            (0, plugins_1.getInactivePlugins)().map(p => console.log('Off:', p.id));
        }
    },
    'purge-file-attr': {
        params: '',
        cb: fileAttr_1.purgeFileAttr,
    },
    transfers: {
        params: '',
        cb() {
            const transfers = (0, connections_1.getConnections)().map(api_monitor_1.serializeConnection).filter(x => x.op === 'upload' || x.op === 'download');
            if (!transfers.length)
                return console.log("No ongoing uploads/downloads");
            console.table(transfers.map(x => ({
                type: x.op,
                progress: (0, cross_1.with_)(x.opProgress ?? x.opOffset, v => v == null ? '' : (0, cross_1.formatPerc)(v)),
                transferred: (0, cross_1.formatBytes)(Math.max(x.sent || 0, x.got || 0)),
                total: x.opTotal == null ? '' : (0, cross_1.formatBytes)(x.opTotal),
                speed: (0, cross_1.formatSpeed)(Math.max(x.outSpeedKb || 0, x.inSpeedKb || 0) * 1000),
                user: x.user,
                path: x.path,
            })));
        }
    },
    status: {
        params: '',
        async cb() {
            const ports = await (0, listen_2.getServerStatus)(false);
            console.log(lodash_1.default.map(ports, (x, k) => `${k.toUpperCase()} ${x.configuredPort < 0 ? "disabled" : x.listening ? `on port ${x.port}` : (x.error || "not working")}`).join(" – "));
            const operations = lodash_1.default.countBy((0, connections_1.getConnections)(), x => x.ctx && (0, api_monitor_1.inferOperation)(x.ctx).op);
            console.log(`Active downloads ↑ ${operations.download || 0} – uploads ↓ ${operations.upload || 0}`);
            const conn = (await api_monitor_1.default.get_connection_stats().next()).value;
            if (conn) {
                const { sent_got: sg } = conn;
                console.log(`Speed ↑ ${(0, cross_1.formatSpeed)(conn.outSpeedKb * 1000)} ↓ ${(0, cross_1.formatSpeed)(conn.inSpeedKb * 1000)}`);
                console.log(`Transferred ↑ ${(0, cross_1.formatBytes)(sg[0])} ↓ ${(0, cross_1.formatBytes)(sg[1])} since ${(0, cross_1.formatTimestamp)(sg[2])}`);
                console.log(`Connections ${conn.connections} (${conn.ips} IPs)`);
            }
        }
    }
};
