/**
 * Channel manager — lifecycle, persistence, and the inbound→agent→outbound pipeline.
 *
 * Connectors are registered once; the manager starts/stops them, persists their
 * config, decides which inbound messages warrant a reply, runs the agent loop to
 * produce that reply, and sends it back through the connector.
 */
import { runAgentLoop, buildToolSchemas, SteeringController, } from "../tools/agent-loop.js";
import { uuidv7 } from "../db/uuidv7.js";
const DEFAULT_SYSTEM_PROMPT = "You are Jait, a helpful AI assistant replying over a messaging channel (e.g. WhatsApp). " +
    "Keep replies concise and conversational — they are read on a phone. " +
    "Do not use markdown headings or code fences unless explicitly asked.";
const DEFAULT_MAX_HISTORY = 20;
/** Normalize a WhatsApp-style id to its bare number for allowlist comparison. */
export function normalizeSenderId(id) {
    // "<number>@s.whatsapp.net" / "<number>:<device>@..." / "+<number>" → "<number>"
    return id.split("@")[0].split(":")[0].replace(/[^0-9]/g, "");
}
/** Decide whether the agent should reply to an inbound message. */
export function shouldRespond(msg, config) {
    // Never reply to our own outgoing messages to other people.
    if (msg.fromMe && !msg.isSelfChat)
        return false;
    if (config.respondToAll)
        return !msg.fromMe || msg.isSelfChat;
    const allow = (config.allowedSenders ?? []).map(normalizeSenderId).filter(Boolean);
    if (allow.length > 0) {
        return allow.includes(normalizeSenderId(msg.senderId));
    }
    // No allowlist configured → only respond in the self-chat (safe default).
    return msg.isSelfChat;
}
export class ChannelManager {
    deps;
    channels = new Map();
    /** Per-conversation message history (keyed by `${channelId}:${conversationId}`). */
    histories = new Map();
    /** Per-conversation serialization so messages are answered in order. */
    locks = new Map();
    replyGenerator;
    constructor(deps) {
        this.deps = deps;
        this.replyGenerator = deps.replyGenerator ?? new AgentLoopReplyGenerator(deps);
        this.ensureTable();
    }
    log(msg, ...args) {
        (this.deps.log ?? ((m, ...a) => console.log("[channels]", m, ...a)))(msg, ...args);
    }
    /* ── DB ─────────────────────────────────────────────────────────── */
    ensureTable() {
        this.deps.sqlite.exec(`
      CREATE TABLE IF NOT EXISTS channels (
        id TEXT PRIMARY KEY,
        enabled INTEGER NOT NULL DEFAULT 0,
        config TEXT NOT NULL DEFAULT '{}',
        updated_at TEXT NOT NULL
      )
    `);
    }
    getConfig(id) {
        const row = this.deps.sqlite
            .prepare("SELECT enabled, config FROM channels WHERE id = ?")
            .get(id);
        if (!row)
            return {};
        const parsed = JSON.parse(row.config || "{}");
        return { ...parsed, enabled: Boolean(row.enabled) };
    }
    setConfig(id, patch) {
        const current = this.getConfig(id);
        const next = { ...current, ...patch };
        const enabled = next.enabled ? 1 : 0;
        const { enabled: _e, ...rest } = next;
        this.deps.sqlite
            .prepare(`INSERT INTO channels (id, enabled, config, updated_at) VALUES (?, ?, ?, ?)
         ON CONFLICT(id) DO UPDATE SET enabled = excluded.enabled, config = excluded.config, updated_at = excluded.updated_at`)
            .run(id, enabled, JSON.stringify(rest), new Date().toISOString());
        return next;
    }
    /* ── Registration / lifecycle ───────────────────────────────────── */
    register(connector) {
        const existing = this.channels.get(connector.id);
        if (existing) {
            if (existing.connector === connector)
                return;
            throw new Error(`Channel '${connector.id}' is already registered`);
        }
        this.channels.set(connector.id, { connector, status: "stopped", detail: {} });
    }
    /** Remove a connector from the available channel list. Config is preserved. */
    async unregister(id) {
        const managed = this.channels.get(id);
        if (!managed)
            return;
        try {
            await managed.connector.stop();
        }
        finally {
            this.channels.delete(id);
        }
    }
    list() {
        return [...this.channels.values()].map((c) => {
            const config = this.getConfig(c.connector.id);
            return {
                id: c.connector.id,
                label: c.connector.label,
                status: c.status,
                enabled: Boolean(config.enabled),
                qr: c.detail.qr ?? c.connector.currentQr(),
                config,
            };
        });
    }
    get(id) {
        return this.channels.get(id);
    }
    /** Start a connector and persist enabled=true. */
    async start(id) {
        const managed = this.channels.get(id);
        if (!managed)
            throw new Error(`Channel '${id}' not found`);
        this.setConfig(id, { enabled: true });
        if (managed.status === "connected" || managed.status === "connecting" || managed.status === "qr") {
            return;
        }
        await managed.connector.start({
            onInbound: (msg) => { void this.handleInbound(msg); },
            onStatus: (status, detail) => {
                managed.status = status;
                managed.detail = detail ?? {};
                this.log(`${id}: status=${status}${detail?.error ? ` error=${detail.error}` : ""}`);
            },
        });
    }
    /** Stop a connector and persist enabled=false. */
    async stop(id) {
        const managed = this.channels.get(id);
        if (!managed)
            throw new Error(`Channel '${id}' not found`);
        this.setConfig(id, { enabled: false });
        await managed.connector.stop();
        managed.status = "stopped";
        managed.detail = {};
    }
    /** Start every connector whose persisted config has enabled=true. */
    async startEnabled() {
        for (const { connector } of this.channels.values()) {
            if (this.getConfig(connector.id).enabled) {
                try {
                    await this.start(connector.id);
                }
                catch (err) {
                    this.log(`Failed to auto-start ${connector.id}:`, err);
                }
            }
        }
    }
    async dispose() {
        for (const { connector } of this.channels.values()) {
            try {
                await connector.stop();
            }
            catch { /* best effort */ }
        }
    }
    /* ── Inbound → agent → outbound ─────────────────────────────────── */
    async handleInbound(msg) {
        const managed = this.channels.get(msg.channelId);
        if (!managed)
            return;
        if (!msg.text.trim())
            return;
        const config = this.getConfig(msg.channelId);
        if (!shouldRespond(msg, config))
            return;
        const key = `${msg.channelId}:${msg.conversationId}`;
        // Serialize replies per conversation.
        const prev = this.locks.get(key) ?? Promise.resolve();
        const next = prev
            .catch(() => { })
            .then(() => this.replyTo(managed, msg, config));
        this.locks.set(key, next.then(() => { }, () => { }));
        await next;
    }
    async replyTo(managed, msg, config) {
        const key = `${msg.channelId}:${msg.conversationId}`;
        const maxHistory = this.deps.maxHistory ?? DEFAULT_MAX_HISTORY;
        let history = this.histories.get(key);
        if (!history) {
            history = [{ role: "system", content: this.deps.systemPrompt ?? DEFAULT_SYSTEM_PROMPT }];
            this.histories.set(key, history);
        }
        history.push({ role: "user", content: msg.text });
        // Resolve allowed tools (filtered to those that actually exist).
        const allowedTools = new Set((config.tools ?? []).filter((t) => this.deps.toolRegistry?.has(t)));
        try {
            const reply = (await this.replyGenerator.generate(history, {
                channelId: msg.channelId,
                sessionId: `channel:${key}`,
                allowedTools,
            })).trim();
            if (reply) {
                history.push({ role: "assistant", content: reply });
                // Trim history (keep system prompt + last N turns).
                if (history.length > maxHistory + 1) {
                    const system = history[0];
                    this.histories.set(key, [system, ...history.slice(history.length - maxHistory)]);
                }
                await managed.connector.send({ conversationId: msg.conversationId, text: reply });
            }
        }
        catch (err) {
            this.log(`reply failed for ${key}:`, err);
            try {
                await managed.connector.send({
                    conversationId: msg.conversationId,
                    text: "⚠️ Sorry — I hit an error generating a reply.",
                });
            }
            catch { /* connector may be down */ }
        }
    }
}
/* ------------------------------------------------------------------ */
/*  Default reply generator — runs the agent loop                      */
/* ------------------------------------------------------------------ */
export class AgentLoopReplyGenerator {
    deps;
    constructor(deps) {
        this.deps = deps;
    }
    async generate(history, ctx) {
        const { toolRegistry, audit, auth } = this.deps;
        const allowedTools = ctx.allowedTools;
        const toolSchemas = toolRegistry && allowedTools.size > 0
            ? buildToolSchemas(toolRegistry, allowedTools)
            : [];
        const hasTools = toolSchemas.length > 0;
        const abort = new AbortController();
        const steering = new SteeringController();
        const executor = async (name, args, sid, _auth, onChunk, signal) => {
            if (!allowedTools.has(name) || !toolRegistry) {
                return { ok: false, message: `Tool '${name}' is not available to this channel` };
            }
            return toolRegistry.execute(name, args, {
                sessionId: sid,
                actionId: uuidv7(),
                projectRoot: this.deps.projectRoot ?? process.cwd(),
                requestedBy: `channel:${ctx.channelId}`,
                userId: auth?.userId,
                apiKeys: auth?.apiKeys,
                jaitBackend: auth?.jaitBackend,
                model: auth?.model,
                onOutputChunk: onChunk,
                signal,
            }, audit);
        };
        const result = await runAgentLoop({
            llm: this.deps.resolveLLM(),
            history,
            toolSchemas,
            hasTools,
            sessionId: ctx.sessionId,
            auth: auth?.userId
                ? { userId: auth.userId, apiKeys: auth.apiKeys, jaitBackend: auth.jaitBackend, model: auth.model }
                : undefined,
            abort,
            maxRounds: hasTools ? 6 : 1,
            maxRetries: 1,
            parallel: true,
            toolRegistry,
            allowedTools: hasTools ? allowedTools : undefined,
        }, executor, steering);
        return result.content ?? "";
    }
}
//# sourceMappingURL=manager.js.map