// Email → claude PTY bridge.
//
// Track B. The email-auto-respond cron script dispatches each
// inbound email through this module: find-or-spawn a per-sender PTY
// session via the loopback claude-session-manager, write the email text
// to the session's PTY stdin, follow claude's per-session JSONL until
// `stop_reason: "end_turn"`, return the assistant turn text. The cron
// script then SMTPs the response.
//
// This module lives inside the email plugin's MCP source (not under
// platform/ui/app/lib/) because the cron script is a separate Node
// process — it cannot share in-process state with the platform/ui
// channel-pty-bridge instance. Each cron invocation queries the manager
// for the existing session via `GET /list?senderId=...` rather than
// caching it in memory.
//
// Doctrine references:
// - Manager HTTP contract: ../../../../services/claude-session-manager/src/http-server.ts
// - Shared bridge core (in-process, used by whatsapp / webchat):
//   ../../../../../ui/app/lib/channel-pty-bridge/
// brief: maxy-code/.tasks/004-channel-adaptors-rest.md
import { requirePortEnv } from "../../../../../lib/require-port-env/dist/index.js";
const TAG = "[email-adaptor]";
function managerBase() {
    const port = requirePortEnv("CLAUDE_SESSION_MANAGER_PORT", { tag: "email-adaptor" });
    return `http://127.0.0.1:${port}`;
}
// ─── Manager HTTP ─────────────────────────────────────────────────────
async function findExistingSession(senderId) {
    try {
        const res = await fetch(`${managerBase()}/list?senderId=${encodeURIComponent(senderId)}`);
        if (!res.ok)
            return null;
        const sessions = (await res.json());
        const alive = sessions.find((s) => s.status === "alive" && s.channel === "email");
        return alive?.sessionId ?? null;
    }
    catch {
        return null;
    }
}
async function spawnSession(opts) {
    try {
        const res = await fetch(`${managerBase()}/spawn`, {
            method: "POST",
            headers: { "content-type": "application/json" },
            body: JSON.stringify({
                senderId: opts.senderId,
                role: "public",
                channel: "email",
                accountId: opts.accountId,
            }),
        });
        if (!res.ok) {
            console.error(`${TAG} reject reason=spawn-failed senderId=${opts.senderId} status=${res.status}`);
            return null;
        }
        const body = (await res.json());
        return body.sessionId;
    }
    catch (err) {
        const m = err instanceof Error ? err.message : String(err);
        console.error(`${TAG} reject reason=spawn-unreachable senderId=${opts.senderId} message=${m}`);
        return null;
    }
}
async function writeInput(sessionId, text) {
    try {
        const res = await fetch(`${managerBase()}/${sessionId}/input`, {
            method: "POST",
            headers: { "content-type": "application/json" },
            body: JSON.stringify({ text }),
        });
        return res.ok;
    }
    catch {
        return false;
    }
}
// ─── JSONL follower (one-shot, waits for next end_turn) ──────────────
async function awaitNextTurn(sessionId, timeoutMs) {
    const abort = new AbortController();
    const timeoutHandle = setTimeout(() => abort.abort(), timeoutMs);
    try {
        const res = await fetch(`${managerBase()}/${sessionId}/log?follow=1`, { signal: abort.signal });
        if (!res.ok || !res.body) {
            return { error: `follow-status-${res.status}` };
        }
        const reader = res.body.getReader();
        const decoder = new TextDecoder("utf8");
        let buffered = "";
        let turnText = "";
        while (!abort.signal.aborted) {
            const { value, done } = await reader.read();
            if (done)
                break;
            buffered += decoder.decode(value, { stream: true });
            let nl = buffered.indexOf("\n");
            while (nl !== -1) {
                const line = buffered.slice(0, nl);
                buffered = buffered.slice(nl + 1);
                nl = buffered.indexOf("\n");
                if (!line)
                    continue;
                let event;
                try {
                    event = JSON.parse(line);
                }
                catch {
                    continue;
                }
                if (event.type === "user") {
                    turnText = "";
                    continue;
                }
                if (event.type !== "assistant")
                    continue;
                const msg = event.message;
                if (!msg)
                    continue;
                if (Array.isArray(msg.content)) {
                    for (const block of msg.content) {
                        if (block?.type === "text" && typeof block.text === "string") {
                            turnText += block.text;
                        }
                    }
                }
                if (msg.stop_reason === "end_turn") {
                    if (turnText.trim()) {
                        // Abort the stream so the manager closes the follower
                        // immediately; we have our turn.
                        abort.abort();
                        return { turnText };
                    }
                    turnText = "";
                }
            }
        }
        return { error: abort.signal.aborted ? "timeout" : "stream-closed" };
    }
    catch (err) {
        if (abort.signal.aborted)
            return { error: "timeout" };
        return { error: err instanceof Error ? err.message : String(err) };
    }
    finally {
        clearTimeout(timeoutHandle);
    }
}
// ─── Public dispatch ─────────────────────────────────────────────────
/** Send an email body to the per-sender claude PTY session, await the
 *  next assistant end_turn, return the response text. Reuses the session
 *  across cron invocations via the manager's senderId index. */
export async function dispatchEmailToClaude(input) {
    if (!input.text.trim())
        return { error: "empty-text" };
    const bytes = Buffer.byteLength(input.text, "utf8");
    console.error(`${TAG} inbound from=${input.senderEmail} bytes=${bytes} accountId=${input.accountId}`);
    let sessionId = await findExistingSession(input.senderEmail);
    let action = "reuse";
    if (!sessionId) {
        action = "spawn";
        sessionId = await spawnSession({ senderId: input.senderEmail, accountId: input.accountId });
        if (!sessionId)
            return { error: "spawn-failed" };
    }
    console.error(`${TAG} session-resolved senderId=${input.senderEmail} action=${action} sessionId=${sessionId.slice(0, 8)}`);
    const writeOk = await writeInput(sessionId, input.text);
    if (!writeOk) {
        console.error(`${TAG} reject reason=input-failed senderId=${input.senderEmail} sessionId=${sessionId.slice(0, 8)}`);
        return { error: "input-failed" };
    }
    const startedAt = Date.now();
    const result = await awaitNextTurn(sessionId, input.timeoutMs);
    const idleMs = Date.now() - startedAt;
    if ("error" in result) {
        console.error(`${TAG} reject reason=turn-${result.error} senderId=${input.senderEmail} sessionId=${sessionId.slice(0, 8)} idle-ms=${idleMs}`);
        return result;
    }
    console.error(`${TAG} outbound to=${input.senderEmail} bytes=${Buffer.byteLength(result.turnText, "utf8")} sessionId=${sessionId.slice(0, 8)} idle-ms=${idleMs}`);
    return result;
}
export function emailTurnTimeoutMs() {
    return Number(process.env.EMAIL_POLL_TURN_TIMEOUT_MS ?? String(10 * 60_000));
}
//# sourceMappingURL=claude-bridge.js.map