"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.StartState = void 0;
const Setting_1 = require("./models/Setting");
const Logger_1 = require("@joplin/utils/Logger");
const Api_1 = require("./services/rest/Api");
const ApiResponse_1 = require("./services/rest/ApiResponse");
const urlParser = require('url');
const { randomClipperPort, startPort } = require('./randomClipperPort');
const enableServerDestroy = require('server-destroy');
const multiparty = require('multiparty');
var StartState;
(function (StartState) {
    StartState["Idle"] = "idle";
    StartState["Starting"] = "starting";
    StartState["Started"] = "started";
})(StartState || (exports.StartState = StartState = {}));
class ClipperServer {
    constructor() {
        this.startState_ = StartState.Idle;
        // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
        this.server_ = null;
        this.port_ = null;
        this.api_ = null;
        this.enabled_ = true;
        this.logger_ = new Logger_1.default();
    }
    static instance() {
        if (this.instance_)
            return this.instance_;
        this.instance_ = new ClipperServer();
        return this.instance_;
    }
    get api() {
        return this.api_;
    }
    enabled() {
        return this.enabled_;
    }
    setEnabled(v) {
        this.enabled_ = v;
        if (!this.enabled_ && this.isRunning()) {
            void this.stop();
        }
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
    initialize(actionApi = null) {
        this.api_ = new Api_1.default(() => {
            return Setting_1.default.value('api.token');
            // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
        }, (action) => { this.dispatch(action); }, actionApi);
    }
    setLogger(l) {
        this.logger_ = l;
    }
    logger() {
        return this.logger_;
    }
    // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
    setDispatch(d) {
        this.dispatch_ = d;
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
    dispatch(action) {
        if (!this.dispatch_)
            throw new Error('dispatch not set!');
        this.dispatch_(action);
    }
    setStartState(v) {
        if (this.startState_ === v)
            return;
        this.startState_ = v;
        this.dispatch({
            type: 'CLIPPER_SERVER_SET',
            startState: v,
        });
    }
    setPort(v) {
        if (this.port_ === v)
            return;
        this.port_ = v;
        this.dispatch({
            type: 'CLIPPER_SERVER_SET',
            port: v,
        });
    }
    async findAvailablePort() {
        const tcpPortUsed = require('tcp-port-used');
        let state = null;
        for (let i = 0; i < 10000; i++) {
            state = randomClipperPort(state, Setting_1.default.value('env'));
            const inUse = await tcpPortUsed.check(state.port);
            if (!inUse)
                return state.port;
        }
        throw new Error('All potential ports are in use or not available.');
    }
    async isRunning() {
        const tcpPortUsed = require('tcp-port-used');
        const port = Setting_1.default.value('api.port') ? Setting_1.default.value('api.port') : startPort(Setting_1.default.value('env'));
        const inUse = await tcpPortUsed.check(port);
        return inUse ? port : 0;
    }
    async start() {
        if (!this.enabled())
            throw new Error('Cannot start clipper server because it is disabled');
        this.setPort(null);
        this.setStartState(StartState.Starting);
        const settingPort = Setting_1.default.value('api.port');
        try {
            const p = settingPort ? settingPort : await this.findAvailablePort();
            this.setPort(p);
        }
        catch (error) {
            this.setStartState(StartState.Idle);
            this.logger().error(error);
            return null;
        }
        this.server_ = require('http').createServer();
        // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
        this.server_.on('request', async (request, response) => {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
            const writeCorsHeaders = (code, contentType = 'application/json', additionalHeaders = null) => {
                const headers = Object.assign({ 'Content-Type': contentType, 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE', 'Access-Control-Allow-Headers': 'X-Requested-With,content-type' }, (additionalHeaders ? additionalHeaders : {}));
                response.writeHead(code, headers);
            };
            // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
            const writeResponseJson = (code, object) => {
                writeCorsHeaders(code);
                response.write(JSON.stringify(object));
                response.end();
            };
            // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
            const writeResponseText = (code, text) => {
                writeCorsHeaders(code, 'text/plain');
                response.write(text);
                response.end();
            };
            // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
            const writeResponseInstance = (code, instance) => {
                if (instance.type === 'attachment') {
                    const filename = instance.attachmentFilename ? instance.attachmentFilename : 'file';
                    writeCorsHeaders(code, instance.contentType ? instance.contentType : 'application/octet-stream', {
                        'Content-disposition': `attachment; filename=${filename}`,
                        'Content-Length': instance.body.length,
                    });
                    response.end(instance.body);
                }
                else {
                    throw new Error('Not implemented');
                }
            };
            // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
            const writeResponse = (code, response) => {
                if (response instanceof ApiResponse_1.default) {
                    writeResponseInstance(code, response);
                }
                else if (typeof response === 'string') {
                    writeResponseText(code, response);
                }
                else if (response === null || response === undefined) {
                    writeResponseText(code, '');
                }
                else {
                    writeResponseJson(code, response);
                }
            };
            this.logger().info(`Request: ${request.method} ${request.url}`);
            const url = urlParser.parse(request.url, true);
            // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
            const execRequest = async (request, body = '', files = []) => {
                try {
                    const response = await this.api_.route(request.method, url.pathname, url.query, body, files);
                    writeResponse(200, response);
                }
                catch (error) {
                    this.logger().error(error);
                    const httpCode = error.httpCode ? error.httpCode : 500;
                    const msg = [];
                    if (httpCode >= 500)
                        msg.push('Internal Server Error');
                    if (error.message)
                        msg.push(error.message);
                    if (error.stack)
                        msg.push(`\n\n${error.stack}`);
                    writeResponse(httpCode, { error: msg.join(': ') });
                }
            };
            const contentType = request.headers['content-type'] ? request.headers['content-type'] : '';
            if (request.method === 'OPTIONS') {
                writeCorsHeaders(200);
                response.end();
            }
            else {
                if (contentType.indexOf('multipart/form-data') === 0) {
                    const form = new multiparty.Form();
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
                    form.parse(request, (error, fields, files) => {
                        if (error) {
                            writeResponse(error.httpCode ? error.httpCode : 500, error.message);
                            return;
                        }
                        else {
                            void execRequest(request, fields && fields.props && fields.props.length ? fields.props[0] : '', files && files.data ? files.data : []);
                        }
                    });
                }
                else {
                    if (request.method === 'POST' || request.method === 'PUT') {
                        let body = '';
                        // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
                        request.on('data', (data) => {
                            body += data;
                        });
                        request.on('end', async () => {
                            void execRequest(request, body);
                        });
                    }
                    else {
                        void execRequest(request);
                    }
                }
            }
        });
        enableServerDestroy(this.server_);
        this.logger().info(`Starting Clipper server on port ${this.port_}`);
        this.server_.listen(this.port_, '127.0.0.1');
        this.setStartState(StartState.Started);
        // We return an empty promise that never resolves so that it's possible to `await` the server indefinitely.
        // This is used only in command-server.js
        return new Promise(() => { });
    }
    async stop() {
        if (this.server_) {
            this.server_.destroy();
            this.server_ = null;
        }
        this.setStartState(StartState.Idle);
        this.setPort(null);
    }
}
ClipperServer.instance_ = null;
exports.default = ClipperServer;
//# sourceMappingURL=ClipperServer.js.map