/**
 * Remote CLI Provider — proxies provider operations to a remote desktop node via WS.
 *
 * Instead of spawning a local child process, this adapter sends operations
 * (start-session, send-turn, stop-session, list-models) to a remote Electron
 * desktop client that actually runs the CLI provider (codex / claude-code).
 *
 * Events from the child process are relayed back through the WS control plane
 * via the `provider.event` message type.
 */
import { EventEmitter } from "node:events";
import { uuidv7 } from "../db/uuidv7.js";
import { mapCodexNotification } from "./codex-event-mapper.js";
import { NO_PROVIDER_AUTH, unsupportedLogin, unsupportedLogout } from "./provider-auth.js";
const remoteProviderEventDispatchers = new WeakMap();
function registerRemoteProviderEventListener(ws, listener) {
    let dispatcher = remoteProviderEventDispatchers.get(ws);
    if (!dispatcher) {
        const previous = ws.onRemoteProviderEvent;
        dispatcher = {
            previous,
            listeners: new Set(),
        };
        ws.onRemoteProviderEvent = (sessionId, event, metadata) => {
            previous?.(sessionId, event, metadata);
            const currentDispatcher = remoteProviderEventDispatchers.get(ws);
            if (!currentDispatcher)
                return;
            for (const currentListener of Array.from(currentDispatcher.listeners)) {
                currentListener(sessionId, event, metadata);
            }
        };
        remoteProviderEventDispatchers.set(ws, dispatcher);
    }
    dispatcher.listeners.add(listener);
    return () => {
        dispatcher?.listeners.delete(listener);
    };
}
export class RemoteCliProvider {
    ws;
    nodeId;
    id;
    info;
    emitter = new EventEmitter();
    sessions = new Map();
    remoteEventUnsubscribe = null;
    remoteEventListener = (sessionId, event) => {
        this.handleRemoteEvent(sessionId, event);
    };
    constructor(ws, nodeId, providerId) {
        this.ws = ws;
        this.nodeId = nodeId;
        this.id = providerId;
        this.info = {
            id: providerId,
            name: `Remote ${providerId}`,
            description: `${providerId} running on remote device ${nodeId}`,
            available: true,
            modes: ["full-access", "supervised"],
            auth: NO_PROVIDER_AUTH,
        };
    }
    async checkAvailability() {
        const node = this.ws.findNodeByDeviceId(this.nodeId);
        if (!node) {
            this.info.available = false;
            this.info.unavailableReason = `Device ${this.nodeId} is not connected`;
            return false;
        }
        const hasProvider = node.providers?.includes(this.id === "claude-code" ? "claude-code" : this.id);
        this.info.available = !!hasProvider;
        if (!hasProvider) {
            this.info.unavailableReason = `Provider ${this.id} not available on device ${this.nodeId}`;
        }
        return this.info.available;
    }
    async getAuthStatus() {
        return {
            ...NO_PROVIDER_AUTH,
            authenticated: null,
            detail: "Remote provider login is managed on the remote device.",
        };
    }
    async startLogin() {
        return unsupportedLogin(this.id, "Remote provider login is managed on the remote device.");
    }
    async logout() {
        return unsupportedLogout(this.id, "Remote provider login is managed on the remote device.");
    }
    async listModels() {
        try {
            const result = await this.ws.proxyProviderOp(this.nodeId, "list-models", { providerId: this.id }, 90_000);
            return Array.isArray(result) ? result : [];
        }
        catch {
            return [];
        }
    }
    async startSession(options) {
        const sessionId = uuidv7();
        const session = {
            id: sessionId,
            providerId: this.id,
            threadId: options.threadId,
            status: "starting",
            runtimeMode: options.mode,
            startedAt: new Date().toISOString(),
        };
        this.sessions.set(sessionId, session);
        this.ensureRemoteEventSubscription();
        try {
            const result = await this.ws.proxyProviderOp(this.nodeId, "start-session", {
                sessionId,
                providerId: this.id,
                workingDirectory: options.workingDirectory,
                mode: options.mode,
                model: options.model,
                env: options.env,
                mcpServers: options.mcpServers,
            }, 90_000);
            session.status = "running";
            this.emit({ type: "session.started", sessionId });
            // Store the remote provider thread ID on the session for send-turn
            if (result?.providerThreadId) {
                session.providerThreadId = result.providerThreadId;
            }
            return session;
        }
        catch (err) {
            session.status = "error";
            session.error = err instanceof Error ? err.message : "Remote session start failed";
            this.sessions.delete(sessionId);
            this.detachRemoteEventSubscriptionIfIdle();
            this.emit({ type: "session.error", sessionId, error: session.error });
            throw err;
        }
    }
    async sendTurn(sessionId, message) {
        const session = this.sessions.get(sessionId);
        if (!session)
            throw new Error(`Session ${sessionId} not found`);
        let cleanupCompletion = () => { };
        const completion = new Promise((resolve) => {
            const timeout = setTimeout(() => {
                cleanupCompletion();
                this.emit({
                    type: "session.error",
                    sessionId,
                    error: `Remote ${this.id} turn timed out waiting for completion`,
                });
                resolve();
            }, 30 * 60 * 1000);
            const handler = (event) => {
                if (event.sessionId !== sessionId)
                    return;
                if (event.type === "turn.completed" || event.type === "session.completed" || event.type === "session.error") {
                    cleanupCompletion();
                    resolve();
                }
            };
            cleanupCompletion = () => {
                clearTimeout(timeout);
                this.emitter.off("event", handler);
            };
            this.emitter.on("event", handler);
        });
        let completedFromEvent = false;
        const eventCompletion = completion.then(() => { completedFromEvent = true; });
        try {
            const result = await this.ws.proxyProviderOp(this.nodeId, "send-turn", {
                sessionId,
                message,
                providerThreadId: session.providerThreadId ?? sessionId,
            }, 30 * 60 * 1000 + 30_000);
            if (result?.completed && !completedFromEvent) {
                cleanupCompletion();
                this.emit({ type: "turn.completed", sessionId });
                return;
            }
        }
        catch (err) {
            cleanupCompletion();
            throw err;
        }
        await eventCompletion;
    }
    async interruptTurn(sessionId) {
        // Send a stop and mark as interrupted
        await this.stopSession(sessionId);
        const session = this.sessions.get(sessionId);
        if (session)
            session.status = "interrupted";
    }
    async respondToApproval(_sessionId, _requestId, _approved) {
        // Remote approval not yet supported — could be proxied in the future
    }
    async stopSession(sessionId) {
        const session = this.sessions.get(sessionId);
        try {
            await this.ws.proxyProviderOp(this.nodeId, "stop-session", { sessionId }, 15_000);
        }
        catch {
            // Ignore — child may already be dead
        }
        if (session) {
            session.status = "completed";
            session.completedAt = new Date().toISOString();
        }
        this.sessions.delete(sessionId);
        this.detachRemoteEventSubscriptionIfIdle();
    }
    onEvent(handler) {
        this.emitter.on("event", handler);
        return () => this.emitter.off("event", handler);
    }
    emit(event) {
        this.emitter.emit("event", event);
    }
    ensureRemoteEventSubscription() {
        if (this.remoteEventUnsubscribe)
            return;
        this.remoteEventUnsubscribe = registerRemoteProviderEventListener(this.ws, this.remoteEventListener);
    }
    detachRemoteEventSubscriptionIfIdle() {
        if (this.sessions.size > 0 || !this.remoteEventUnsubscribe)
            return;
        this.remoteEventUnsubscribe();
        this.remoteEventUnsubscribe = null;
    }
    /**
     * Handle a remote event relayed from the client via WS.
     * Uses the shared codex event mapper to translate raw JSON-RPC notifications.
     */
    handleRemoteEvent(sessionId, event) {
        if (!this.sessions.has(sessionId))
            return;
        const directEvent = this.parseDirectProviderEvent(event, sessionId);
        if (directEvent) {
            this.emit(directEvent);
            if (directEvent.type === "session.completed" || directEvent.type === "session.error") {
                this.sessions.delete(sessionId);
                this.detachRemoteEventSubscriptionIfIdle();
            }
            return;
        }
        const e = event;
        if (!e.method)
            return;
        const params = (e.params ?? {});
        const events = mapCodexNotification(e.method, params, sessionId);
        for (const evt of events) {
            this.emit(evt);
            if (evt.type === "session.completed") {
                this.sessions.delete(sessionId);
                this.detachRemoteEventSubscriptionIfIdle();
            }
        }
    }
    parseDirectProviderEvent(event, sessionId) {
        if (!event || typeof event !== "object")
            return null;
        const candidate = event;
        if (typeof candidate.type !== "string")
            return null;
        if (typeof candidate.sessionId === "string" && candidate.sessionId !== sessionId)
            return null;
        return { ...candidate, sessionId };
    }
}
//# sourceMappingURL=remote-cli-provider.js.map