'use strict';
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;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.cliOperations = cliOperations;
exports.buildRequest = buildRequest;
const cliCredentials_ts_1 = require("./cliCredentials.js");
const tokenAuthentication_ts_1 = require("../security/tokenAuthentication.js");
const envMgr = __importStar(require("../utility/environment/environmentManager.js"));
envMgr.initSync();
const terms = __importStar(require("../utility/hdbTerms.js"));
const common_utils_ts_1 = require("../utility/common_utils.js");
const path = __importStar(require("path"));
const fs = __importStar(require("fs-extra"));
const YAML = __importStar(require("yaml"));
const packageComponent_ts_1 = require("../components/packageComponent.js");
const multipartBuilder_ts_1 = require("./multipartBuilder.js");
const sseConsumer_ts_1 = require("./sseConsumer.js");
const deployRenderer_ts_1 = require("./deployRenderer.js");
const processManagement_js_1 = require("../utility/processManagement/processManagement.js");
const configUtils_js_1 = require("../config/configUtils.js");
const OP_ALIASES = { deploy: 'deploy_component', package: 'package_component' };
// Operations whose responses should be consumed as text/event-stream so live phase events
// (prepare, load, replicate, restart) render as they happen instead of after the whole
// deploy completes. Add an operation here only after wiring its server-side
// SSE_PROGRESS_OPERATIONS entry — otherwise the server returns the buffered JSON path and
// the SSE parser sees no events.
const SSE_OPERATIONS = new Set(['deploy_component']);
// Properties on `req` that the CLI itself uses for transport/UX, not the operations API.
// They never get serialized into the request body.
const TRANSPORT_ONLY_FIELDS = new Set([
    'target',
    'username',
    'password',
    'rejectUnauthorized',
    'json',
    'skip_node_modules',
    'skip_symlinks',
]);
const PREPARE_OPERATION = {
    deploy_component: async (req) => {
        if (req.package) {
            return;
        }
        const projectPath = process.cwd();
        if (!req.project)
            req.project = path.basename(projectPath);
        const packageOptions = {
            skip_node_modules: req.skip_node_modules !== false,
            skip_symlinks: req.skip_symlinks === true,
        };
        // Store path + options for deferred stream creation after the renderer is set up,
        // so the pre-gzip onBytes callback can be wired directly to renderer.countUploadBytes.
        req._projectPath = projectPath;
        req._packageOptions = packageOptions;
        // Pre-walk the directory for an uncompressed-size estimate. Both the progress counter
        // and this total are in uncompressed units so the bar tracks to 100% naturally.
        req._uploadSizeEstimate = await (0, packageComponent_ts_1.getPackagedDirectorySize)(projectPath, packageOptions);
        req._multipart = true;
    },
};
/**
 * Builds an Op-API request object from CLI args
 */
function buildRequest() {
    const req = {};
    for (const arg of process.argv.slice(2)) {
        if (OP_ALIASES.hasOwnProperty(arg)) {
            req.operation = OP_ALIASES[arg];
        }
        else if (arg.includes('=')) {
            let [first, ...rest] = arg.split('=');
            let restStr = rest.join('=');
            try {
                restStr = JSON.parse(restStr);
            }
            catch {
                /* noop */
            }
            req[first] = restStr;
        }
        else {
            // operation should only be in the first arg
            req.operation ??= arg;
        }
    }
    return req;
}
/**
 * Resolves the target URL from various sources.
 * @param {Object} req The request object.
 * @param {Object} allCredentials Stored credentials.
 * @returns {string|null} The resolved target URL.
 */
function resolveTarget(req, allCredentials) {
    return (req.target ||
        process.env.HARPER_CLI_TARGET ||
        process.env.CLI_TARGET ||
        (allCredentials && allCredentials.last_target));
}
/**
 * Using a unix domain socket will send a request to hdb operations API server
 * @param req
 * @param skipResponseLog By default, the response is logged to the console. Set this to true to skip logging it, which can be useful for sensitive responses like login calls!
 * @returns {Promise<void>}
 */
async function cliOperations(req, skipResponseLog = false) {
    require('dotenv').config();
    const allCredentials = (0, cliCredentials_ts_1.loadCredentials)();
    req.target = (0, cliCredentials_ts_1.normalizeTarget)(resolveTarget(req, allCredentials));
    let target;
    if (req.target) {
        try {
            target = new URL(req.target);
        }
        catch (error) {
            try {
                target = new URL(`https://${req.target}:9925`);
            }
            catch {
                throw error;
            }
        }
        const resolvedTarget = req.target;
        target = {
            protocol: target.protocol,
            hostname: target.hostname,
            port: target.port,
            username: req.username || target.username || process.env.HARPER_CLI_USERNAME || process.env.CLI_TARGET_USERNAME,
            password: req.password || target.password || process.env.HARPER_CLI_PASSWORD || process.env.CLI_TARGET_PASSWORD,
            rejectUnauthorized: req.rejectUnauthorized,
            resolvedTarget,
        };
        console.error(`Connecting to ${resolvedTarget}`);
    }
    else {
        // if we aren't doing a targeted operation (like deploy), we initialize the config and verify that local harper
        // is running and that we can communicate with it.
        console.error('Connecting to local Harper instance');
        (0, configUtils_js_1.initConfig)();
        if (!(0, processManagement_js_1.getHdbPid)()) {
            console.error('Harper must be running to perform this operation');
            process.exit(1);
        }
        if (!fs.existsSync((0, configUtils_js_1.getConfigPath)(terms.CONFIG_PARAMS.OPERATIONSAPI_NETWORK_DOMAINSOCKET))) {
            console.error('No domain socket found, unable to perform this operation');
            process.exit(1);
        }
    }
    await PREPARE_OPERATION[req.operation]?.(req);
    try {
        let options = target ?? {
            protocol: 'http:',
            socketPath: (0, configUtils_js_1.getConfigPath)(terms.CONFIG_PARAMS.OPERATIONSAPI_NETWORK_DOMAINSOCKET),
        };
        options.method = 'POST';
        options.headers = { 'Content-Type': 'application/json' };
        if (target?.username) {
            options.headers.Authorization = `Basic ${Buffer.from(`${target.username}:${target.password}`).toString('base64')}`;
        }
        else if (allCredentials) {
            let tokens = null;
            let lookupKey = null;
            if (target && allCredentials.targets) {
                lookupKey = target.resolvedTarget;
                tokens = allCredentials.targets[lookupKey] ?? null;
            }
            if (tokens?.operation_token) {
                if (tokens.refresh_token && (0, tokenAuthentication_ts_1.isJWTExpired)(tokens.operation_token)) {
                    console.error('Operation token expired, attempting to refresh...');
                    try {
                        const refreshOptions = { ...options };
                        refreshOptions.headers = { ...options.headers, Authorization: `Bearer ${tokens.refresh_token}` };
                        const refreshResponse = await (0, common_utils_ts_1.httpRequest)(refreshOptions, {
                            operation: 'refresh_operation_token',
                        });
                        if (refreshResponse.statusCode === 200) {
                            const refreshData = JSON.parse(refreshResponse.body);
                            if (refreshData.operation_token) {
                                tokens.operation_token = refreshData.operation_token;
                                (0, cliCredentials_ts_1.saveCredentials)(lookupKey || target?.resolvedTarget, {
                                    operation_token: tokens.operation_token,
                                    refresh_token: tokens.refresh_token,
                                });
                                console.error('Operation token refreshed successfully.');
                                // Update the original request's authorization header with the new token
                                options.headers.Authorization = `Bearer ${tokens.operation_token}`;
                            }
                        }
                        else if (refreshResponse.statusCode === 401) {
                            console.error('Refresh token expired or invalid. Please run harper login again.');
                            process.exit(1);
                        }
                        else {
                            console.error(`Failed to refresh operation token: ${refreshResponse.statusCode}`);
                        }
                    }
                    catch (refreshErr) {
                        console.error(`Error refreshing operation token: ${refreshErr.message}`);
                    }
                }
                options.headers.Authorization = `Bearer ${tokens.operation_token}`;
            }
        }
        const useSse = SSE_OPERATIONS.has(req.operation);
        if (useSse) {
            options.headers.Accept = 'text/event-stream';
            options.streamResponse = true;
        }
        // One renderer owns the (future) upload bar and the SSE event rendering for a
        // multipart deploy. Created here so the upload-stream tap and the SSE consumer
        // below share the same instance.
        const renderer = req._multipart ? new deployRenderer_ts_1.DeployRenderer({ uploadTotal: req._uploadSizeEstimate ?? 0 }) : null;
        let body;
        if (req._multipart) {
            // Create the package stream here — after the renderer exists — so we can pass
            // renderer.countUploadBytes as the onBytes callback. Both progress and total are
            // uncompressed bytes, so the bar tracks accurately to 100% without premature snapping.
            const packageStream = (0, packageComponent_ts_1.streamPackagedDirectory)(req._projectPath, req._packageOptions, renderer ? (n) => renderer.countUploadBytes(n) : undefined);
            const fields = {};
            for (const [key, value] of Object.entries(req)) {
                if (key.startsWith('_') || TRANSPORT_ONLY_FIELDS.has(key))
                    continue;
                fields[key] = value;
            }
            const multipart = (0, multipartBuilder_ts_1.buildMultipartBody)(fields, {
                name: 'payload',
                filename: 'package.tar.gz',
                contentType: 'application/gzip',
                stream: packageStream,
            });
            options.headers['Content-Type'] = multipart.contentType;
            // Use chunked transfer-encoding: we don't know the total size up front because the
            // payload is streamed from `tar.pack` and never fully buffered.
            options.headers['Transfer-Encoding'] = 'chunked';
            // Tap the body so bytes flowing into the HTTP request advance the upload bar.
            // The renderer's Transform is identity — chunks pass through unmodified.
            body = renderer ? renderer.tapUploadStream(multipart.stream) : multipart.stream;
        }
        else {
            body = req;
        }
        let response = await (0, common_utils_ts_1.httpRequest)(options, body);
        // endUpload() is called from the counter Transform's flush callback in tapUploadStream
        // once all multipart bytes have flowed through. For SSE deploys, httpRequest resolves
        // when response headers arrive (streamResponse: true), which happens before the full
        // upload completes — calling endUpload() here would snap the bar prematurely.
        let responseData;
        if (useSse && response.headers['content-type']?.startsWith('text/event-stream')) {
            // Consume SSE: render phase events live, capture the final result from the `done`
            // event (or the error message from the `error` event). The HTTP status stays 200
            // until end-of-stream; failures are signaled in-band.
            let finalResult;
            let sseError;
            for await (const message of (0, sseConsumer_ts_1.parseSSE)(response)) {
                renderer?.renderEvent(message);
                if (message.event === 'done') {
                    try {
                        finalResult = JSON.parse(message.data)?.result;
                    }
                    catch {
                        finalResult = message.data;
                    }
                }
                else if (message.event === 'error') {
                    try {
                        sseError = JSON.parse(message.data);
                    }
                    catch {
                        sseError = { message: message.data };
                    }
                }
            }
            if (sseError) {
                const errMsg = sseError.message ?? (typeof sseError === 'object' ? JSON.stringify(sseError) : sseError);
                console.error(`error: ${errMsg}`);
                process.exit(1);
            }
            responseData = finalResult ?? { message: 'Deploy completed (no result payload).' };
        }
        else {
            // When useSse is true, httpRequest returns a raw IncomingMessage (streamResponse mode),
            // so .body is undefined. Drain the stream to get the text (e.g. a 401 error body).
            let bodyText;
            if (useSse) {
                const chunks = [];
                for await (const chunk of response)
                    chunks.push(Buffer.from(chunk));
                bodyText = Buffer.concat(chunks).toString('utf8');
            }
            else {
                bodyText = response.body;
            }
            try {
                responseData = JSON.parse(bodyText);
            }
            catch {
                responseData = {
                    status: response.statusCode + ' ' + (response.statusMessage || 'Unknown'),
                    body: bodyText,
                };
            }
        }
        let responseLog;
        if (req.json) {
            responseLog = JSON.stringify(responseData, null, 2);
        }
        else {
            responseLog = YAML.stringify(responseData).trim();
        }
        const { statusCode } = response;
        if (statusCode < 200 || (statusCode >= 300 && statusCode !== 304)) {
            const errorPrefix = responseLog.startsWith('error:') ? '' : 'error: ';
            console.error(`${errorPrefix}${responseLog}`);
            process.exit(1);
        }
        if (!skipResponseLog) {
            console.log(responseLog);
        }
        if (target) {
            responseData.resolvedTarget = target.resolvedTarget;
        }
        return responseData;
    }
    catch (err) {
        if (err.code === 'ENOENT' || err.code === 'ECONNREFUSED') {
            console.error(`error: Failed to connect to Harper (${err.code}): ${err.message}`);
        }
        else if (err.code === 'EACCES') {
            console.error(`error: Permission denied accessing the domain socket: ${err.message}`);
        }
        else if (err.code === 'ENOTFOUND') {
            console.error(`error: Host not found: "${err.hostname}" ${err.message}`);
        }
        else {
            console.error(`error: ${err.message ?? err}`);
        }
        process.exit(1);
    }
}
//# sourceMappingURL=cliOperations.js.map