"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isValidPort = isValidPort;
exports.parsePort = parsePort;
exports.generateKey = generateKey;
exports.setupConfigFile = setupConfigFile;
exports.setupRoutes = setupRoutes;
exports.setupServer = setupServer;
exports.startServer = startServer;
const crypto_1 = __importDefault(require("crypto"));
const fs_1 = __importDefault(require("fs"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const cors_1 = __importDefault(require("cors"));
const express_1 = __importDefault(require("express"));
const utils_1 = require("../utils");
/**
 * Validates if a port number is in the valid range
 */
function isValidPort(port) {
    return port > 0 && port < 65535;
}
/**
 * Parses and validates a port string
 */
function parsePort({ portStr, defaultPort = 9090, }) {
    if (!portStr) {
        return defaultPort;
    }
    const parsedPort = Number.parseInt(portStr, 10);
    if (!isValidPort(parsedPort)) {
        throw new Error(`${portStr} is not a valid port`);
    }
    return parsedPort;
}
/**
 * Generates a random alphanumeric string to serve as admin key
 */
function generateKey() {
    return crypto_1.default.randomBytes(5).toString('hex');
}
/**
 * Sets up the configuration file
 */
async function setupConfigFile({ root = '.', } = {}) {
    const output = root;
    const isDir = fs_1.default.lstatSync(output).isDirectory();
    const outFile = isDir ? `${output}/config.json` : output;
    const baseDir = path_1.default.dirname(outFile);
    if (fs_1.default.existsSync(outFile)) {
        (0, utils_1.debug)(`Found existing config file ${outFile}`);
    }
    else {
        (0, utils_1.debug)(`Creating config file ${outFile}`);
        await (0, utils_1.writeJsonFile)(outFile, {
            assemblies: [],
            configuration: {},
            connections: [],
            defaultSession: {
                name: 'New Session',
            },
            tracks: [],
        });
    }
    return { outFile, baseDir };
}
/**
 * Validates admin key and extracts config path from request
 */
function validateAndExtractParams({ req, key, baseDir, outFile, }) {
    const { body } = req;
    // Check for adminKey in both query parameters and request body for backward compatibility
    const adminKeyFromQuery = req.query.adminKey;
    const adminKeyFromBody = body?.adminKey;
    const adminKey = adminKeyFromBody || adminKeyFromQuery;
    if (adminKey !== key) {
        return { isValid: false, error: 'Invalid admin key' };
    }
    // Get configPath from either body or query parameters
    const configPathParam = body?.configPath || req.query.config;
    try {
        // Normalize the config path
        const configPath = configPathParam
            ? path_1.default.normalize(path_1.default.join(baseDir, configPathParam))
            : outFile;
        // Check for directory traversal attempts
        const normalizedBaseDir = path_1.default.normalize(baseDir);
        const relPath = path_1.default.relative(normalizedBaseDir, configPath);
        // Ensure the config path is within the base directory and doesn't contain path traversal
        if (relPath.startsWith('..') || path_1.default.isAbsolute(relPath)) {
            return { isValid: false, error: 'Cannot perform directory traversal' };
        }
        return { isValid: true, configPath };
    }
    catch (error) {
        return { isValid: false, error: 'Failed to validate config path' };
    }
}
/**
 * Sets up the API routes for the server
 */
function setupRoutes({ app, baseDir, outFile, key, serverRef, }) {
    // Root route
    app.get('/', (_req, res) => {
        res.setHeader('Content-Type', 'text/plain');
        res.send('JBrowse Admin Server');
    });
    // Update config route
    app.post('/updateConfig', (req, res) => {
        const { body } = req;
        const config = body.config;
        const validation = validateAndExtractParams({ req, key, baseDir, outFile });
        if (!validation.isValid) {
            res.status(401).setHeader('Content-Type', 'text/plain');
            res.send(`Error: ${validation.error}`);
            return;
        }
        // Remove adminKey from body before saving to config
        if (body.adminKey) {
            delete body.adminKey;
        }
        try {
            fs_1.default.writeFileSync(validation.configPath, JSON.stringify(config, null, 2));
            res.setHeader('Content-Type', 'text/plain');
            res.send('Config updated successfully');
        }
        catch (error) {
            res.status(500).setHeader('Content-Type', 'text/plain');
            res.send('Error: Failed to update config');
        }
    });
    // Get config route
    app.get('/config', (req, res) => {
        const validation = validateAndExtractParams({ req, key, baseDir, outFile });
        if (!validation.isValid) {
            res.status(401).setHeader('Content-Type', 'text/plain');
            res.send(`Error: ${validation.error}`);
            return;
        }
        try {
            if (fs_1.default.existsSync(validation.configPath)) {
                const config = fs_1.default.readFileSync(validation.configPath, 'utf8');
                res.setHeader('Content-Type', 'text/plain');
                res.send(config);
            }
            else {
                res.status(404).setHeader('Content-Type', 'text/plain');
                res.send('Error: Config file not found');
            }
        }
        catch (error) {
            console.error('Error reading config:', error);
            res.status(500).setHeader('Content-Type', 'text/plain');
            res.send('Error: Failed to read config');
        }
    });
    // Shutdown route for testing
    app.post('/shutdown', (req, res) => {
        const { body } = req;
        const adminKey = body?.adminKey;
        if (adminKey !== key) {
            res.status(401).setHeader('Content-Type', 'text/plain');
            res.send('Error: Invalid admin key');
            return;
        }
        res.setHeader('Content-Type', 'text/plain');
        res.send('Server shutting down');
        // Shutdown the server after sending response
        setImmediate(() => {
            if (serverRef.current) {
                serverRef.current.close();
            }
        });
    });
}
/**
 * Sets up the Express server with routes
 */
function setupServer({ baseDir, outFile, bodySizeLimit, }) {
    // Create Express application
    const app = (0, express_1.default)();
    // Configure middleware
    app.use(express_1.default.static(baseDir));
    app.use((0, cors_1.default)());
    app.use(express_1.default.json({ limit: bodySizeLimit }));
    // Add error handling middleware
    app.use((err, _req, res, next) => {
        if (err) {
            console.error('Server error:', err);
            res.status(500).setHeader('Content-Type', 'text/plain');
            res.send('Internal Server Error');
        }
        else {
            next();
        }
    });
    // Generate admin key and store it
    const key = generateKey();
    const keyPath = path_1.default.join(os_1.default.tmpdir(), `jbrowse-admin-${key}`);
    try {
        fs_1.default.writeFileSync(keyPath, key);
        (0, utils_1.debug)(`Admin key stored at ${keyPath}`);
    }
    catch (error) {
        console.error(`Failed to write admin key to ${keyPath}:`, error.message);
        // Continue anyway, as this is not critical
    }
    // Create server reference for shutdown route
    const serverRef = { current: null };
    // Set up routes
    setupRoutes({ app, baseDir, outFile, key, serverRef });
    return { app, key, keyPath, serverRef };
}
/**
 * Starts the server and sets up shutdown handlers
 */
function startServer({ app, port, key, outFile, keyPath, serverRef, }) {
    // Start the server
    const server = app.listen(port, () => {
        console.log(`Admin server started on port ${port}\n\n` +
            `To access the admin interface, open your browser to:\n` +
            `http://localhost:${port}?adminKey=${key}\n\n` +
            `Admin key: ${key}\n` +
            `Config file: ${outFile}\n\n` +
            `To stop the server, press Ctrl+C`);
    });
    // Store server reference for shutdown route
    serverRef.current = server;
    // Handle server errors
    server.on('error', (error) => {
        if (error.code === 'EADDRINUSE') {
            console.error(`Error: Port ${port} is already in use`);
        }
        else {
            console.error('Server error:', error.message);
        }
        process.exit(1);
    });
    // Common shutdown handler
    const shutdownHandler = () => {
        console.log('\nShutting down admin server...');
        server.close(() => {
            // Clean up admin key file
            try {
                fs_1.default.unlinkSync(keyPath);
                (0, utils_1.debug)(`Removed admin key file: ${keyPath}`);
            }
            catch (error) {
                // Ignore errors when cleaning up
            }
            process.exit(0);
        });
    };
    // Handle server shutdown
    process.on('SIGINT', shutdownHandler);
    process.on('SIGTERM', shutdownHandler);
}
