import { once } from 'node:events';
import { execSync, spawn } from 'node:child_process';
import { createInterface } from 'node:readline';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { PROTOCOL_VERSION, } from './protocol.js';
export class AgentRelayProtocolError extends Error {
    code;
    retryable;
    data;
    constructor(payload) {
        super(payload.message);
        this.name = 'AgentRelayProtocolError';
        this.code = payload.code;
        this.retryable = payload.retryable;
        this.data = payload.data;
    }
}
export class AgentRelayProcessError extends Error {
    constructor(message) {
        super(message);
        this.name = 'AgentRelayProcessError';
    }
}
function isHeadlessProvider(value) {
    return value === 'claude' || value === 'opencode';
}
export class AgentRelayClient {
    options;
    child;
    stdoutRl;
    stderrRl;
    lastStderrLine;
    requestSeq = 0;
    pending = new Map();
    startingPromise;
    eventListeners = new Set();
    stderrListeners = new Set();
    eventBuffer = [];
    maxBufferSize = 1000;
    exitPromise;
    /** The workspace key returned by the broker in its hello_ack response. */
    workspaceKey;
    constructor(options = {}) {
        this.options = {
            binaryPath: options.binaryPath ?? resolveDefaultBinaryPath(),
            binaryArgs: options.binaryArgs ?? [],
            brokerName: options.brokerName ?? (path.basename(options.cwd ?? process.cwd()) || 'project'),
            channels: options.channels ?? ['general'],
            cwd: options.cwd ?? process.cwd(),
            env: options.env ?? process.env,
            requestTimeoutMs: options.requestTimeoutMs ?? 10_000,
            shutdownTimeoutMs: options.shutdownTimeoutMs ?? 3_000,
            clientName: options.clientName ?? '@agent-relay/sdk',
            clientVersion: options.clientVersion ?? '0.1.0',
        };
    }
    static async start(options = {}) {
        const client = new AgentRelayClient(options);
        await client.start();
        return client;
    }
    onEvent(listener) {
        this.eventListeners.add(listener);
        return () => {
            this.eventListeners.delete(listener);
        };
    }
    queryEvents(filter) {
        let events = [...this.eventBuffer];
        if (filter?.kind) {
            events = events.filter((event) => event.kind === filter.kind);
        }
        if (filter?.name) {
            events = events.filter((event) => 'name' in event && event.name === filter.name);
        }
        const since = filter?.since;
        if (since !== undefined) {
            events = events.filter((event) => 'timestamp' in event && typeof event.timestamp === 'number' && event.timestamp >= since);
        }
        const limit = filter?.limit;
        if (limit !== undefined) {
            events = events.slice(-limit);
        }
        return events;
    }
    getLastEvent(kind, name) {
        for (let i = this.eventBuffer.length - 1; i >= 0; i -= 1) {
            const event = this.eventBuffer[i];
            if (event.kind === kind && (!name || ('name' in event && event.name === name))) {
                return event;
            }
        }
        return undefined;
    }
    onBrokerStderr(listener) {
        this.stderrListeners.add(listener);
        return () => {
            this.stderrListeners.delete(listener);
        };
    }
    async start() {
        if (this.child) {
            return;
        }
        if (this.startingPromise) {
            return this.startingPromise;
        }
        this.startingPromise = this.startInternal();
        try {
            await this.startingPromise;
        }
        finally {
            this.startingPromise = undefined;
        }
    }
    /**
     * Pre-register a batch of agents with Relaycast before their steps execute.
     * The broker warms its token cache in parallel; subsequent spawn_agent calls
     * hit the cache rather than waiting on individual HTTP registrations.
     * Fire-and-forget from the caller's perspective — broker responds immediately
     * and registers in the background.
     */
    async preflightAgents(agents) {
        if (agents.length === 0)
            return;
        await this.start();
        await this.requestOk('preflight_agents', { agents });
    }
    async spawnPty(input) {
        await this.start();
        const args = buildPtyArgsWithModel(input.cli, input.args ?? [], input.model);
        const agent = {
            name: input.name,
            runtime: 'pty',
            cli: input.cli,
            args,
            channels: input.channels ?? [],
            model: input.model,
            cwd: input.cwd ?? this.options.cwd,
            team: input.team,
            shadow_of: input.shadowOf,
            shadow_mode: input.shadowMode,
            restart_policy: input.restartPolicy,
        };
        const result = await this.requestOk('spawn_agent', {
            agent,
            ...(input.task != null ? { initial_task: input.task } : {}),
            ...(input.idleThresholdSecs != null ? { idle_threshold_secs: input.idleThresholdSecs } : {}),
            ...(input.continueFrom != null ? { continue_from: input.continueFrom } : {}),
        });
        return result;
    }
    async spawnHeadless(input) {
        await this.start();
        const agent = {
            name: input.name,
            runtime: 'headless',
            provider: input.provider,
            args: input.args ?? [],
            channels: input.channels ?? [],
        };
        const result = await this.requestOk('spawn_agent', {
            agent,
            ...(input.task != null ? { initial_task: input.task } : {}),
        });
        return result;
    }
    async spawnProvider(input) {
        const transport = input.transport ?? (input.provider === 'opencode' ? 'headless' : 'pty');
        if (transport === 'headless') {
            if (!isHeadlessProvider(input.provider)) {
                throw new AgentRelayProcessError(`provider '${input.provider}' does not support headless transport (supported: claude, opencode)`);
            }
            return this.spawnHeadless({
                name: input.name,
                provider: input.provider,
                args: input.args,
                channels: input.channels,
                task: input.task,
            });
        }
        return this.spawnPty({
            name: input.name,
            cli: input.provider,
            args: input.args,
            channels: input.channels,
            task: input.task,
            model: input.model,
            cwd: input.cwd,
            team: input.team,
            shadowOf: input.shadowOf,
            shadowMode: input.shadowMode,
            idleThresholdSecs: input.idleThresholdSecs,
            restartPolicy: input.restartPolicy,
            continueFrom: input.continueFrom,
        });
    }
    async spawnClaude(input) {
        return this.spawnProvider({ ...input, provider: 'claude' });
    }
    async spawnOpencode(input) {
        return this.spawnProvider({ ...input, provider: 'opencode' });
    }
    async release(name, reason) {
        await this.start();
        return this.requestOk('release_agent', { name, reason });
    }
    async sendInput(name, data) {
        await this.start();
        return this.requestOk('send_input', { name, data });
    }
    async setModel(name, model, opts) {
        await this.start();
        return this.requestOk('set_model', {
            name,
            model,
            timeout_ms: opts?.timeoutMs,
        });
    }
    async getMetrics(agent) {
        await this.start();
        return this.requestOk('get_metrics', { agent });
    }
    async getCrashInsights() {
        await this.start();
        return this.requestOk('get_crash_insights', {});
    }
    async sendMessage(input) {
        await this.start();
        try {
            return await this.requestOk('send_message', {
                to: input.to,
                text: input.text,
                from: input.from,
                thread_id: input.threadId,
                priority: input.priority,
                data: input.data,
            });
        }
        catch (error) {
            if (error instanceof AgentRelayProtocolError && error.code === 'unsupported_operation') {
                return { event_id: 'unsupported_operation', targets: [] };
            }
            throw error;
        }
    }
    async listAgents() {
        await this.start();
        const result = await this.requestOk('list_agents', {});
        return result.agents;
    }
    async getStatus() {
        await this.start();
        return this.requestOk('get_status', {});
    }
    async shutdown() {
        if (!this.child) {
            return;
        }
        try {
            await this.requestOk('shutdown', {});
        }
        catch {
            // Continue shutdown path if broker is already unhealthy.
        }
        const child = this.child;
        const wait = this.exitPromise ?? Promise.resolve();
        const timeout = setTimeout(() => {
            if (!child.killed) {
                child.kill('SIGTERM');
            }
        }, this.options.shutdownTimeoutMs);
        try {
            await wait;
        }
        finally {
            clearTimeout(timeout);
            if (this.child) {
                this.child.kill('SIGKILL');
            }
        }
    }
    async waitForExit() {
        if (!this.child) {
            return;
        }
        await this.exitPromise;
    }
    async startInternal() {
        const resolvedBinary = expandTilde(this.options.binaryPath);
        if (isExplicitPath(this.options.binaryPath) && !fs.existsSync(resolvedBinary)) {
            throw new AgentRelayProcessError(`broker binary not found: ${this.options.binaryPath}`);
        }
        this.lastStderrLine = undefined;
        const args = [
            'init',
            '--name',
            this.options.brokerName,
            '--channels',
            this.options.channels.join(','),
            ...this.options.binaryArgs,
        ];
        // Ensure the SDK bin directory (containing agent-relay-broker + relay_send) is on
        // PATH so spawned workers can find relay_send without any user setup.
        const env = { ...this.options.env };
        if (isExplicitPath(this.options.binaryPath)) {
            const binDir = path.dirname(path.resolve(resolvedBinary));
            const currentPath = env.PATH ?? env.Path ?? '';
            if (!currentPath.split(path.delimiter).includes(binDir)) {
                env.PATH = `${binDir}${path.delimiter}${currentPath}`;
            }
        }
        console.log(`[broker] Starting: ${resolvedBinary} ${args.join(' ')}`);
        const child = spawn(resolvedBinary, args, {
            cwd: this.options.cwd,
            env,
            stdio: 'pipe',
        });
        this.child = child;
        this.stdoutRl = createInterface({ input: child.stdout, crlfDelay: Infinity });
        this.stderrRl = createInterface({ input: child.stderr, crlfDelay: Infinity });
        this.stdoutRl.on('line', (line) => {
            this.handleStdoutLine(line);
        });
        this.stderrRl.on('line', (line) => {
            const trimmed = line.trim();
            if (trimmed) {
                this.lastStderrLine = trimmed;
            }
            for (const listener of this.stderrListeners) {
                listener(line);
            }
        });
        this.exitPromise = new Promise((resolve) => {
            child.once('exit', (code, signal) => {
                const detail = this.lastStderrLine ? `: ${this.lastStderrLine}` : '';
                const error = new AgentRelayProcessError(`broker exited (code=${code ?? 'null'}, signal=${signal ?? 'null'})${detail}`);
                this.failAllPending(error);
                this.disposeProcessHandles();
                resolve();
            });
            child.once('error', (error) => {
                this.failAllPending(error);
                this.disposeProcessHandles();
                resolve();
            });
        });
        const helloAck = await this.requestHello();
        console.log('[broker] Broker ready (hello handshake complete)');
        if (helloAck.workspace_key) {
            this.workspaceKey = helloAck.workspace_key;
        }
    }
    disposeProcessHandles() {
        this.stdoutRl?.close();
        this.stderrRl?.close();
        this.stdoutRl = undefined;
        this.stderrRl = undefined;
        this.lastStderrLine = undefined;
        this.child = undefined;
        this.exitPromise = undefined;
    }
    failAllPending(error) {
        for (const pending of this.pending.values()) {
            clearTimeout(pending.timeout);
            pending.reject(error);
        }
        this.pending.clear();
    }
    handleStdoutLine(line) {
        let parsed;
        try {
            parsed = JSON.parse(line);
        }
        catch {
            // Non-protocol output should not crash the SDK.
            return;
        }
        if (!parsed || typeof parsed !== 'object') {
            return;
        }
        if (parsed.v !== PROTOCOL_VERSION || typeof parsed.type !== 'string') {
            return;
        }
        const envelope = {
            v: parsed.v,
            type: parsed.type,
            request_id: parsed.request_id,
            payload: parsed.payload,
        };
        if (envelope.type === 'event') {
            const payload = envelope.payload;
            this.eventBuffer.push(payload);
            if (this.eventBuffer.length > this.maxBufferSize) {
                this.eventBuffer.shift();
            }
            for (const listener of this.eventListeners) {
                listener(payload);
            }
            return;
        }
        if (!envelope.request_id) {
            return;
        }
        const pending = this.pending.get(envelope.request_id);
        if (!pending) {
            return;
        }
        if (envelope.type === 'error') {
            clearTimeout(pending.timeout);
            this.pending.delete(envelope.request_id);
            pending.reject(new AgentRelayProtocolError(envelope.payload));
            return;
        }
        if (envelope.type !== pending.expectedType) {
            clearTimeout(pending.timeout);
            this.pending.delete(envelope.request_id);
            pending.reject(new AgentRelayProcessError(`unexpected response type '${envelope.type}' for request '${envelope.request_id}' (expected '${pending.expectedType}')`));
            return;
        }
        clearTimeout(pending.timeout);
        this.pending.delete(envelope.request_id);
        pending.resolve(envelope);
    }
    async requestHello() {
        const payload = {
            client_name: this.options.clientName,
            client_version: this.options.clientVersion,
        };
        const frame = await this.sendRequest('hello', payload, 'hello_ack');
        return frame.payload;
    }
    async requestOk(type, payload) {
        const frame = await this.sendRequest(type, payload, 'ok');
        const result = frame.payload;
        return result.result;
    }
    async sendRequest(type, payload, expectedType) {
        if (!this.child) {
            throw new AgentRelayProcessError('broker is not running');
        }
        const requestId = `req_${++this.requestSeq}`;
        const message = {
            v: PROTOCOL_VERSION,
            type,
            request_id: requestId,
            payload,
        };
        const responsePromise = new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
                this.pending.delete(requestId);
                reject(new AgentRelayProcessError(`request timed out after ${this.options.requestTimeoutMs}ms (type='${type}', request_id='${requestId}')`));
            }, this.options.requestTimeoutMs);
            this.pending.set(requestId, {
                expectedType,
                resolve,
                reject,
                timeout,
            });
        });
        const line = `${JSON.stringify(message)}\n`;
        if (!this.child.stdin.write(line)) {
            await once(this.child.stdin, 'drain');
        }
        return responsePromise;
    }
}
const CLI_MODEL_FLAG_CLIS = new Set(['claude', 'codex', 'gemini', 'goose', 'aider']);
const CLI_DEFAULT_ARGS = {
    codex: ['-c', 'check_for_update_on_startup=false'],
};
function buildPtyArgsWithModel(cli, args, model) {
    const cliName = cli.split(':')[0].trim().toLowerCase();
    const defaultArgs = CLI_DEFAULT_ARGS[cliName] ?? [];
    const baseArgs = [...defaultArgs, ...args];
    if (!model) {
        return baseArgs;
    }
    if (!CLI_MODEL_FLAG_CLIS.has(cliName)) {
        return baseArgs;
    }
    if (hasModelArg(baseArgs)) {
        return baseArgs;
    }
    return ['--model', model, ...baseArgs];
}
function hasModelArg(args) {
    for (let i = 0; i < args.length; i += 1) {
        const arg = args[i];
        if (arg === '--model') {
            return true;
        }
        if (arg.startsWith('--model=')) {
            return true;
        }
    }
    return false;
}
function expandTilde(p) {
    if (p === '~' || p.startsWith('~/') || p.startsWith('~\\')) {
        const home = os.homedir();
        return path.join(home, p.slice(2));
    }
    return p;
}
function isExplicitPath(binaryPath) {
    return (binaryPath.includes('/') ||
        binaryPath.includes('\\') ||
        binaryPath.startsWith('.') ||
        binaryPath.startsWith('~'));
}
function detectPlatformSuffix() {
    const platformMap = {
        darwin: { arm64: 'darwin-arm64', x64: 'darwin-x64' },
        linux: { arm64: 'linux-arm64', x64: 'linux-x64' },
    };
    return platformMap[process.platform]?.[process.arch] ?? null;
}
function getLatestVersionSync() {
    try {
        const result = execSync('curl -fsSL https://api.github.com/repos/AgentWorkforce/relay/releases/latest', {
            timeout: 15_000,
            stdio: ['pipe', 'pipe', 'pipe'],
        }).toString();
        const match = result.match(/"tag_name"\s*:\s*"v?([^"]+)"/);
        return match?.[1] ?? null;
    }
    catch {
        return null;
    }
}
function installBrokerBinary() {
    const suffix = detectPlatformSuffix();
    if (!suffix) {
        throw new AgentRelayProcessError(`Unsupported platform: ${process.platform}-${process.arch}`);
    }
    const homeDir = process.env.HOME || process.env.USERPROFILE || '';
    const installDir = path.join(homeDir, '.agent-relay', 'bin');
    const brokerExe = process.platform === 'win32' ? 'agent-relay-broker.exe' : 'agent-relay-broker';
    const targetPath = path.join(installDir, brokerExe);
    console.log(`[agent-relay] Broker binary not found, installing for ${suffix}...`);
    const version = getLatestVersionSync();
    if (!version) {
        throw new AgentRelayProcessError('Failed to fetch latest agent-relay version from GitHub.\n' +
            'Install manually: curl -fsSL https://raw.githubusercontent.com/AgentWorkforce/relay/main/install.sh | bash');
    }
    const binaryName = `agent-relay-broker-${suffix}`;
    const downloadUrl = `https://github.com/AgentWorkforce/relay/releases/download/v${version}/${binaryName}`;
    console.log(`[agent-relay] Downloading v${version} from ${downloadUrl}`);
    try {
        fs.mkdirSync(installDir, { recursive: true });
        execSync(`curl -fsSL "${downloadUrl}" -o "${targetPath}"`, {
            timeout: 60_000,
            stdio: ['pipe', 'pipe', 'pipe'],
        });
        fs.chmodSync(targetPath, 0o755);
        // macOS: re-sign to avoid Gatekeeper issues
        if (process.platform === 'darwin') {
            try {
                execSync(`codesign --force --sign - "${targetPath}"`, {
                    timeout: 10_000,
                    stdio: ['pipe', 'pipe', 'pipe'],
                });
            }
            catch {
                // Non-fatal
            }
        }
        // Verify
        execSync(`"${targetPath}" --help`, { timeout: 10_000, stdio: ['pipe', 'pipe', 'pipe'] });
    }
    catch (err) {
        try {
            fs.unlinkSync(targetPath);
        }
        catch {
            /* ignore */
        }
        const message = err instanceof Error ? err.message : String(err);
        throw new AgentRelayProcessError(`Failed to install broker binary: ${message}\n` +
            'Install manually: curl -fsSL https://raw.githubusercontent.com/AgentWorkforce/relay/main/install.sh | bash');
    }
    console.log(`[agent-relay] Broker installed to ${targetPath}`);
    return targetPath;
}
function resolveDefaultBinaryPath() {
    const brokerExe = process.platform === 'win32' ? 'agent-relay-broker.exe' : 'agent-relay-broker';
    const moduleDir = path.dirname(fileURLToPath(import.meta.url));
    // 1. In a source checkout, prefer Cargo's release binary to avoid stale bundled
    // copies when local dev rebuilds happen while broker processes are running.
    const workspaceRelease = path.resolve(moduleDir, '..', '..', '..', 'target', 'release', brokerExe);
    if (fs.existsSync(workspaceRelease)) {
        return workspaceRelease;
    }
    // 2. Check for bundled broker binary in SDK package (npm install)
    //    Try platform-specific name first (CI publishes per-platform binaries),
    //    then fall back to the generic name (local dev / postinstall copy).
    const binDir = path.resolve(moduleDir, '..', 'bin');
    const suffix = detectPlatformSuffix();
    if (suffix) {
        const platformBinary = path.join(binDir, `agent-relay-broker-${suffix}`);
        if (fs.existsSync(platformBinary)) {
            return platformBinary;
        }
    }
    const bundled = path.join(binDir, brokerExe);
    if (fs.existsSync(bundled)) {
        return bundled;
    }
    // 3. Check for standalone broker binary in ~/.agent-relay/bin/ (install.sh)
    const homeDir = process.env.HOME || process.env.USERPROFILE || '';
    const standaloneBroker = path.join(homeDir, '.agent-relay', 'bin', brokerExe);
    if (fs.existsSync(standaloneBroker)) {
        return standaloneBroker;
    }
    // 4. Auto-install from GitHub releases
    return installBrokerBinary();
}
//# sourceMappingURL=client.js.map