import assert from 'node:assert';
import child_process from 'node:child_process';
import logger from './logger.js';
import path from 'node:path';
import stream from 'node:stream';
import { StringDecoder } from 'node:string_decoder';

const { log } = logger('logs');
const TransformStream = stream.Transform;

const LOGTAIL_CMD = path.join(import.meta.dirname, 'scripts/logtail.sh');
const KILL_CHILD_CMD = path.join(import.meta.dirname, 'scripts/kill-child.sh');

class LogStream extends TransformStream {
    constructor(options) {
        super();
        this._options = Object.assign({ source: 'unknown', format: 'json' }, options);
        this._decoder = new StringDecoder();
        this._soFar = '';
        this._lastknownTimestamp = 0;
    }

    _format(line) {
        if (this._options.format !== 'json') return line + '\n';

        const data = line.split(' '); // logs are <ISOtimestamp> <msg>
        let timestamp = (new Date(data[0])).getTime();
        let message;
        if (isNaN(timestamp)) {
            timestamp = this._lastknownTimestamp;
            message = line;
        } else {
            this._lastknownTimestamp = timestamp;
            message = line.slice(data[0].length+1);
        }

        return JSON.stringify({
            realtimeTimestamp: timestamp * 1000, // timestamp info can be missing (0) for app logs via logPaths
            message: message || line, // send the line if message parsing failed
            source: this._options.source
        }) + '\n';
    }

    _transform(chunk, encoding, callback) {
        const data = this._soFar + this._decoder.write(chunk);
        let start = this._soFar.length, end;
        while ((end = data.indexOf('\n', start)) !== -1) {
            const line = data.slice(start, end); // does not include end
            this.push(this._format(line));
            start = end + 1;
        }
        this._soFar = data.slice(start);
        callback(null);
    }

    _flush(callback) {
        const line = this._soFar + this._decoder.end();
        if (line) this.push(this._format(line));
        callback(null);
    }
}

function tail(filePaths, options) {
    assert(Array.isArray(filePaths));
    assert.strictEqual(typeof options, 'object');

    const lines = options.lines === -1 ? '+1' : options.lines;
    const args = [ `--lines=${lines}` ];
    if (options.follow) args.push('--follow');

    const cp = options.sudo
        ? child_process.spawn('/usr/bin/sudo', [ LOGTAIL_CMD, ...args, ...filePaths ])
        : child_process.spawn('/usr/bin/tail', args.concat(filePaths));

    cp.terminate = () => {
        child_process.execFile('/usr/bin/sudo', [ KILL_CHILD_CMD, cp.pid, process.pid ], { encoding: 'utf8' }, (error, stdout, stderr) => {
            if (error) log(`tail: failed to kill children`, stdout, stderr);
        });
    };

    return cp;
}

function journalctl(unit, options) {
    assert.strictEqual(typeof unit, 'string');
    assert.strictEqual(typeof options, 'object');

    const args = [
        '--lines=' + (options.lines === -1 ? 'all' : options.lines),
        `--unit=${unit}`,
        '--no-pager',
        '--output=short-iso'
    ];

    if (options.follow) args.push('--follow');

    const cp = child_process.spawn('journalctl', args);
    cp.terminate = () => cp.kill('SIGKILL');
    return cp;
}

export default {
    tail,
    journalctl,
    LogStream
};
