"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ensurePiWorkerRuntimeAvailable = ensurePiWorkerRuntimeAvailable;
exports.runPiWorkerPrompt = runPiWorkerPrompt;
exports.resolvePiMcpAdapterExtensionPaths = resolvePiMcpAdapterExtensionPaths;
const node_fs_1 = __importDefault(require("node:fs"));
const node_os_1 = __importDefault(require("node:os"));
const node_path_1 = __importDefault(require("node:path"));
const node_module_1 = require("node:module");
const env_template_1 = require("../config/env-template");
const paths_1 = require("../core/paths");
const catalog_1 = require("../i18n/catalog");
const errors_1 = require("./errors");
const pi_worker_request_params_1 = require("./pi-worker-request-params");
const pi_capability_review_bash_1 = require("./pi-capability-review-bash");
const PI_AGENT_DIRECTORY_ENV_KEY = "PI_CODING_AGENT_DIR";
const requireFromHere = (0, node_module_1.createRequire)(__filename);
const importEsm = new Function("specifier", "return import(specifier)");
const MINIMUM_PI_NODE_VERSION = [22, 19, 0];
const defaultPiWorkerRuntimeDependencies = {
    ensureAvailable: ensurePiWorkerRuntimeAvailable,
    importPiCodingAgent,
    resolvePiMcpAdapterExtensionPaths,
};
let piAgentDirectoryEnvLock = Promise.resolve();
async function ensurePiWorkerRuntimeAvailable(locale) {
    if (!isCurrentNodeVersionAtLeast(MINIMUM_PI_NODE_VERSION)) {
        throw new errors_1.WorkerUnavailableError(`Pi custom workers require Node.js ${MINIMUM_PI_NODE_VERSION.join(".")} or newer.`, {
            publicMessage: (0, catalog_1.translate)(locale, "worker.error.unavailable"),
        });
    }
    try {
        await importPiCodingAgent();
        resolvePiMcpAdapterExtensionPaths();
    }
    catch (error) {
        throw new errors_1.WorkerUnavailableError(error instanceof Error ? error.message : String(error), {
            cause: error instanceof Error ? error : undefined,
            publicMessage: (0, catalog_1.translate)(locale, "worker.error.unavailable"),
        });
    }
}
async function runPiWorkerPrompt(input, dependencies = defaultPiWorkerRuntimeDependencies) {
    await dependencies.ensureAvailable(input.locale);
    const startedAtMs = Date.now();
    const module = (await dependencies.importPiCodingAgent());
    const authStorageFactory = readPiExport(module, "AuthStorage");
    const modelRegistryFactory = readPiExport(module, "ModelRegistry");
    const resourceLoaderConstructor = readPiExport(module, "DefaultResourceLoader");
    const sessionManagerFactory = readPiExport(module, "SessionManager");
    const settingsManagerFactory = readPiExport(module, "SettingsManager");
    const createSession = readPiExport(module, "createAgentSession");
    const createBashToolDefinition = readOptionalPiExport(module, "createBashToolDefinition");
    const createLocalBashOperations = readOptionalPiExport(module, "createLocalBashOperations");
    const authStorage = authStorageFactory.inMemory();
    const settingsManager = settingsManagerFactory.inMemory();
    const resolvedConfig = resolveCustomWorkerRuntimeConfig({
        config: input.config,
        env: input.env,
    });
    if (resolvedConfig.apiKey !== null) {
        authStorage.setRuntimeApiKey(input.workerName, resolvedConfig.apiKey);
    }
    const modelRegistry = modelRegistryFactory.inMemory(authStorage);
    const authHeader = resolvedConfig.apiKey !== null &&
        !hasHeader(resolvedConfig.headers, "authorization");
    const providerHeaders = buildCustomWorkerProviderHeaders(resolvedConfig);
    modelRegistry.registerProvider(input.workerName, {
        name: input.workerName,
        baseUrl: resolvedConfig.baseUrl,
        apiKey: "unused",
        api: "openai-completions",
        authHeader,
        ...(Object.keys(providerHeaders).length > 0
            ? { headers: providerHeaders }
            : {}),
        models: [
            {
                id: resolvedConfig.model,
                name: resolvedConfig.model,
                api: "openai-completions",
                reasoning: false,
                input: ["text"],
                cost: {
                    input: 0,
                    output: 0,
                    cacheRead: 0,
                    cacheWrite: 0,
                },
                contextWindow: 128_000,
                maxTokens: 16_384,
            },
        ],
    });
    const model = modelRegistry.find(input.workerName, resolvedConfig.model);
    if (model === undefined) {
        throw new errors_1.WorkerUnavailableError(`Pi custom worker model is not registered: ${input.workerName}/${resolvedConfig.model}`, {
            publicMessage: (0, catalog_1.translate)(input.locale, "worker.error.unavailable"),
        });
    }
    const agentDir = getPiAgentDir(input.homeDirectory, input.workerName);
    return withPiAgentDirectoryEnv(agentDir, async () => runPiWorkerPromptSession({
        input,
        startedAtMs,
        agentDir,
        authStorage,
        modelRegistry,
        model,
        resourceLoaderConstructor,
        sessionManagerFactory,
        settingsManager,
        createSession,
        createBashToolDefinition,
        createLocalBashOperations,
        extensionPaths: dependencies.resolvePiMcpAdapterExtensionPaths(),
        requestParams: resolvedConfig.requestParams,
    }));
}
async function runPiWorkerPromptSession(input) {
    const promptInput = input.input;
    const resourceLoader = new input.resourceLoaderConstructor({
        cwd: promptInput.workspacePath,
        agentDir: input.agentDir,
        settingsManager: input.settingsManager,
        additionalExtensionPaths: input.extensionPaths,
        additionalSkillPaths: collectPiSkillPaths(promptInput.workspacePath, input.agentDir),
    });
    await resourceLoader.reload();
    const { session } = await input.createSession({
        cwd: promptInput.workspacePath,
        agentDir: input.agentDir,
        authStorage: input.authStorage,
        modelRegistry: input.modelRegistry,
        model: input.model,
        resourceLoader,
        sessionManager: input.sessionManagerFactory.inMemory(promptInput.workspacePath),
        settingsManager: input.settingsManager,
        ...resolvePiToolPolicy({
            commandPolicy: promptInput.commandPolicy,
            workspacePath: promptInput.workspacePath,
            createBashToolDefinition: input.createBashToolDefinition,
            createLocalBashOperations: input.createLocalBashOperations,
        }),
    });
    await session.bindExtensions({});
    (0, pi_worker_request_params_1.applyRequestParamsPayloadOverride)(session, input.requestParams);
    let streamedOutput = false;
    const promptTimeout = createPiPromptTimeout({
        session,
        locale: promptInput.locale,
        timeoutMs: promptInput.timeoutMs,
        noOutputTimeoutMs: promptInput.noOutputTimeoutMs,
    });
    const unsubscribe = session.subscribe?.((event) => {
        const text = extractPiEventText(event);
        if (text) {
            streamedOutput = true;
            promptTimeout.recordOutput();
            promptInput.onStdoutChunk?.(text);
        }
    });
    try {
        promptTimeout.start();
        await Promise.race([
            session.prompt(promptInput.prompt),
            promptTimeout.promise,
        ]);
        const assistantError = extractLastAssistantError(session);
        if (assistantError !== null) {
            throw classifyPiAssistantError(assistantError, promptInput.locale);
        }
        const stdout = extractLastAssistantText(session);
        if (stdout.length > 0 && !streamedOutput) {
            promptInput.onStdoutChunk?.(stdout);
        }
        return {
            stdout,
            workerDurationMs: Date.now() - input.startedAtMs,
        };
    }
    catch (error) {
        promptInput.onStderrChunk?.(`${error instanceof Error ? error.message : String(error)}\n`);
        throw error;
    }
    finally {
        promptTimeout.clear();
        unsubscribe?.();
        session.dispose();
    }
}
async function withPiAgentDirectoryEnv(agentDir, task) {
    const release = await acquirePiAgentDirectoryEnvLock();
    const env = process.env;
    const hadPreviousValue = Object.hasOwn(env, PI_AGENT_DIRECTORY_ENV_KEY);
    const previousValue = env[PI_AGENT_DIRECTORY_ENV_KEY];
    env[PI_AGENT_DIRECTORY_ENV_KEY] = agentDir;
    try {
        return await task();
    }
    finally {
        if (hadPreviousValue && previousValue !== undefined) {
            env[PI_AGENT_DIRECTORY_ENV_KEY] = previousValue;
        }
        else {
            delete env[PI_AGENT_DIRECTORY_ENV_KEY];
        }
        release();
    }
}
async function acquirePiAgentDirectoryEnvLock() {
    const previous = piAgentDirectoryEnvLock.catch(() => undefined);
    let releaseCurrent;
    const current = new Promise((resolve) => {
        releaseCurrent = resolve;
    });
    piAgentDirectoryEnvLock = previous.then(() => current);
    await previous;
    return releaseCurrent;
}
function createPiPromptTimeout(input) {
    let totalTimer;
    let idleTimer;
    let rejectTimeout;
    const promise = new Promise((_resolve, reject) => {
        rejectTimeout = reject;
    });
    const fail = (timeoutKind) => {
        input.session.dispose();
        rejectTimeout?.(new errors_1.WorkerTimeoutError((0, catalog_1.translate)(input.locale, "worker.error.commandTimedOut", {
            command: "pi",
        }), {
            attempted: true,
            timeoutKind,
            publicMessage: (0, catalog_1.translate)(input.locale, "worker.error.timeout"),
        }));
    };
    const resetIdleTimer = () => {
        if (input.noOutputTimeoutMs === null) {
            return;
        }
        if (idleTimer !== undefined) {
            clearTimeout(idleTimer);
        }
        idleTimer = setTimeout(() => fail("idle"), input.noOutputTimeoutMs);
    };
    return {
        promise,
        start() {
            totalTimer = setTimeout(() => fail("total"), input.timeoutMs);
            resetIdleTimer();
        },
        recordOutput() {
            resetIdleTimer();
        },
        clear() {
            if (totalTimer !== undefined) {
                clearTimeout(totalTimer);
            }
            if (idleTimer !== undefined) {
                clearTimeout(idleTimer);
            }
        },
    };
}
function resolvePiToolPolicy(input) {
    switch (input.commandPolicy) {
        case "capability-review-read-only":
            return {
                tools: ["read", "grep", "find", "ls", "bash"],
                customTools: [
                    (0, pi_capability_review_bash_1.createCapabilityReviewBashTool)({
                        workspacePath: input.workspacePath,
                        createBashToolDefinition: input.createBashToolDefinition,
                        createLocalBashOperations: input.createLocalBashOperations,
                    }),
                ],
            };
        case "catalog-search-read-only":
        case "read-only":
            return { tools: ["read", "grep", "find", "ls"] };
        case undefined:
            return {};
    }
}
function resolveCustomWorkerRuntimeConfig(input) {
    const baseUrl = resolveTemplateValue("baseUrl", input.config.baseUrl, {
        env: input.env,
        rejectEmpty: true,
    });
    assertValidCustomWorkerBaseUrl(baseUrl);
    const model = resolveTemplateValue("model", input.config.model, {
        env: input.env,
        rejectEmpty: true,
    });
    const apiKey = input.config.apiKey === undefined
        ? null
        : resolveTemplateValue("apiKey", input.config.apiKey, {
            env: input.env,
            rejectEmpty: true,
        });
    return {
        baseUrl,
        model,
        apiKey,
        headers: Object.fromEntries(Object.entries(input.config.headers ?? {}).map(([key, value]) => [
            key,
            resolveTemplateValue(`headers.${key}`, value, {
                env: input.env,
                rejectEmpty: true,
            }),
        ])),
        requestParams: (0, pi_worker_request_params_1.resolveCustomWorkerRequestParams)({
            requestParams: input.config.requestParams,
            env: input.env,
        }),
    };
}
function resolveTemplateValue(label, value, input) {
    let resolved;
    try {
        resolved = (0, env_template_1.resolveConfigEnvTemplate)(value, input.env);
    }
    catch (error) {
        if (error instanceof env_template_1.MissingConfigEnvTemplateValueError) {
            throw new errors_1.WorkerUnavailableError(`customWorkers.${label} references missing environment variable: ${error.envName}`);
        }
        if (error instanceof env_template_1.EmptyConfigEnvTemplateValueError) {
            throw new errors_1.WorkerUnavailableError(`customWorkers.${label} references empty environment variable: ${error.envName}`);
        }
        if (error instanceof env_template_1.InvalidConfigEnvTemplateError) {
            throw new errors_1.WorkerUnavailableError(`customWorkers.${label} has invalid environment template: ${error.expression}`);
        }
        throw error;
    }
    if (input.rejectEmpty && resolved.trim().length === 0) {
        throw new errors_1.WorkerUnavailableError(`customWorkers.${label} resolved to an empty value.`);
    }
    return resolved;
}
function assertValidCustomWorkerBaseUrl(baseUrl) {
    let parsed;
    try {
        parsed = new URL(baseUrl);
    }
    catch {
        throw new errors_1.WorkerUnavailableError(`Invalid custom worker baseUrl: ${baseUrl}`);
    }
    if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
        throw new errors_1.WorkerUnavailableError(`Invalid custom worker baseUrl protocol: ${parsed.protocol}`);
    }
}
function hasHeader(headers, name) {
    return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
}
function collectPiSkillPaths(workspacePath, agentDir) {
    return [
        node_path_1.default.join(workspacePath, ".agents", "skills"),
        node_path_1.default.join(workspacePath, "skills"),
        node_path_1.default.join(agentDir, "skills"),
    ].filter((directory) => node_fs_1.default.existsSync(directory));
}
function buildCustomWorkerProviderHeaders(input) {
    const headers = { ...input.headers };
    if (input.apiKey === null && !hasHeader(input.headers, "authorization")) {
        // Pi requires an apiKey for custom providers, and the OpenAI SDK uses that
        // value for Authorization unless the default header explicitly disables it.
        const authorizationHeader = "Authorization";
        headers[authorizationHeader] = "";
    }
    return headers;
}
function getPiAgentDir(homeDirectory, workerName) {
    return node_path_1.default.join((0, paths_1.getControlPlaneHomePath)(homeDirectory ?? node_os_1.default.homedir()), "pi-workers", workerName);
}
function resolvePiMcpAdapterExtensionPaths() {
    const packageJsonPath = requireFromHere.resolve("pi-mcp-adapter/package.json");
    const packageJson = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, "utf8"));
    const extensions = packageJson.pi?.extensions;
    if (!Array.isArray(extensions) || extensions.length === 0) {
        throw new errors_1.WorkerUnavailableError("pi-mcp-adapter package.json does not declare pi.extensions.");
    }
    return extensions.map((extensionPath) => {
        if (typeof extensionPath !== "string" || extensionPath.trim() === "") {
            throw new errors_1.WorkerUnavailableError("pi-mcp-adapter package.json has an invalid pi.extensions entry.");
        }
        return node_path_1.default.resolve(node_path_1.default.dirname(packageJsonPath), extensionPath);
    });
}
async function importPiCodingAgent() {
    return importEsm("@earendil-works/pi-coding-agent");
}
function readPiExport(module, key) {
    const value = Reflect.get(module, key);
    if (value === undefined) {
        throw new errors_1.WorkerUnavailableError(`Pi export is unavailable: ${key}`);
    }
    return value;
}
function readOptionalPiExport(module, key) {
    return Reflect.get(module, key);
}
function isCurrentNodeVersionAtLeast(minimum) {
    const [major = 0, minor = 0, patch = 0] = process.versions.node
        .split(".")
        .map((segment) => Number(segment));
    return (major > minimum[0] ||
        (major === minimum[0] &&
            (minor > minimum[1] || (minor === minimum[1] && patch >= minimum[2]))));
}
function extractPiEventText(event) {
    if (event === null || typeof event !== "object") {
        return null;
    }
    const record = event;
    if (record.type === "message_update") {
        return extractAssistantMessageEventText(record.assistantMessageEvent);
    }
    if (record.type !== "message_delta" && record.type !== "content_delta") {
        return null;
    }
    const text = record.text ?? record.delta;
    return typeof text === "string" && text.length > 0 ? text : null;
}
function extractAssistantMessageEventText(event) {
    if (event === null || typeof event !== "object") {
        return null;
    }
    const record = event;
    return record.type === "text_delta" &&
        typeof record.delta === "string" &&
        record.delta.length > 0
        ? record.delta
        : null;
}
function extractLastAssistantText(session) {
    const messages = session.state?.messages;
    if (!Array.isArray(messages)) {
        return "";
    }
    for (let index = messages.length - 1; index >= 0; index -= 1) {
        const text = extractAssistantMessageText(messages[index]);
        if (text.length > 0) {
            return text;
        }
    }
    return "";
}
function extractLastAssistantError(session) {
    const messages = session.state?.messages;
    if (!Array.isArray(messages)) {
        return null;
    }
    for (let index = messages.length - 1; index >= 0; index -= 1) {
        const errorMessage = extractAssistantMessageError(messages[index]);
        if (errorMessage !== null) {
            return errorMessage;
        }
    }
    return null;
}
function extractAssistantMessageError(message) {
    if (message === null || typeof message !== "object") {
        return null;
    }
    const record = message;
    if (record.role !== "assistant" || record.stopReason !== "error") {
        return null;
    }
    return typeof record.errorMessage === "string" &&
        record.errorMessage.length > 0
        ? record.errorMessage
        : "Pi provider returned an error.";
}
function classifyPiAssistantError(errorMessage, locale) {
    const sanitized = sanitizePiProviderErrorMessage(errorMessage);
    const normalized = sanitized.toLowerCase();
    if (normalized.includes("429") ||
        normalized.includes("rate limit") ||
        normalized.includes("too many requests") ||
        normalized.includes("quota") ||
        normalized.includes("insufficient balance") ||
        normalized.includes("usage limit")) {
        return new errors_1.WorkerQuotaExhaustedError((0, catalog_1.translate)(locale, "worker.error.commandQuotaExhausted", {
            command: "pi",
        }), {
            attempted: true,
            publicMessage: (0, catalog_1.translate)(locale, "worker.error.quotaExhausted"),
        });
    }
    if (normalized.includes("401") ||
        normalized.includes("403") ||
        normalized.includes("unauthorized") ||
        normalized.includes("forbidden") ||
        normalized.includes("invalid api key") ||
        normalized.includes("invalid token") ||
        normalized.includes("authentication failed")) {
        return new errors_1.WorkerAuthInvalidError((0, catalog_1.translate)(locale, "worker.error.commandAuthInvalid", { command: "pi" }), {
            attempted: true,
            publicMessage: (0, catalog_1.translate)(locale, "worker.error.authInvalid"),
        });
    }
    return new errors_1.WorkerExecutionError(`Pi provider returned an error: ${sanitized}`, {
        attempted: true,
        publicMessage: (0, catalog_1.translate)(locale, "worker.error.executionFailed"),
    });
}
function sanitizePiProviderErrorMessage(message) {
    return message
        .replace(/sk-[A-Za-z0-9_-]{12,}/g, "sk-***")
        .replace(/<ak-[^>]+>/gi, "<redacted>");
}
function extractAssistantMessageText(message) {
    if (message === null || typeof message !== "object") {
        return "";
    }
    const record = message;
    if (record.role !== "assistant") {
        return "";
    }
    if (typeof record.content === "string") {
        return record.content;
    }
    if (!Array.isArray(record.content)) {
        return "";
    }
    return record.content
        .map((block) => {
        if (block === null || typeof block !== "object") {
            return "";
        }
        const textBlock = block;
        return textBlock.type === "text" && typeof textBlock.text === "string"
            ? textBlock.text
            : "";
    })
        .join("");
}
