/**
 * LocalTransport — In-process SQLite transport for fully offline agent messaging.
 *
 * Reads and writes messages directly to conduit.db via node:sqlite.
 * No network calls. Works fully offline. Messages are stored in the
 * project-local conduit.db (ADR-037), keeping agent messaging isolated
 * from the global-identity signaldock.db.
 *
 * Priority: LocalTransport is preferred over HttpTransport when
 * conduit.db is available (see factory.ts).
 *
 * @see docs/specs/SIGNALDOCK-UNIFIED-AGENT-REGISTRY.md Section 4.4
 * @task T213
 * @task T356
 * @epic T310
 */
import { randomUUID } from 'node:crypto';
import { existsSync } from 'node:fs';
import { createRequire } from 'node:module';
import { getConduitDbPath } from '../store/conduit-sqlite.js';
const _require = createRequire(import.meta.url);
const { DatabaseSync: DatabaseSyncClass } = _require('node:sqlite');
/** In-process SQLite transport for fully offline agent messaging. */
export class LocalTransport {
    name = 'local';
    state = null;
    /**
     * Connect to conduit.db for in-process messaging.
     *
     * Opens the database, sets WAL mode pragmas, and verifies
     * the messages table exists. Throws if conduit.db is missing
     * or uninitialized (run `cleo init` first).
     *
     * @task T356
     * @epic T310
     */
    async connect(config) {
        const dbPath = getConduitDbPath(process.cwd());
        if (!existsSync(dbPath)) {
            throw new Error(`LocalTransport: conduit.db not found at ${dbPath}. Run: cleo init`);
        }
        const db = new DatabaseSyncClass(dbPath);
        db.exec('PRAGMA journal_mode = WAL');
        db.exec('PRAGMA busy_timeout = 5000');
        db.exec('PRAGMA foreign_keys = ON');
        // Verify the messages table exists
        const hasMessages = db
            .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages'")
            .get();
        if (!hasMessages) {
            db.close();
            throw new Error('LocalTransport: conduit.db exists but messages table missing — run cleo init or allow auto-migration (T358)');
        }
        this.state = {
            agentId: config.agentId,
            db,
            dbPath,
            subscribers: new Set(),
            pollTimer: null,
        };
    }
    /** Close the database connection and stop any subscriber polling. */
    async disconnect() {
        if (!this.state)
            return;
        if (this.state.pollTimer) {
            clearInterval(this.state.pollTimer);
        }
        this.state.subscribers.clear();
        this.state.db.close();
        this.state = null;
    }
    /**
     * Store a message in conduit.db.
     *
     * Inserts into the messages table with status 'pending'.
     * For conversation messages, also links via conversation_participants
     * if not already present.
     */
    async push(to, content, options) {
        this.ensureConnected();
        const { db, agentId } = this.state;
        const messageId = randomUUID();
        const nowUnix = Math.floor(Date.now() / 1000);
        if (options?.conversationId) {
            db.prepare(`INSERT INTO messages (id, conversation_id, from_agent_id, to_agent_id, content, content_type, status, created_at)
         VALUES (?, ?, ?, ?, ?, 'text', 'pending', ?)`).run(messageId, options.conversationId, agentId, to, content, nowUnix);
        }
        else {
            // Direct message — create or reuse a DM conversation
            const convId = this.ensureDmConversation(agentId, to);
            db.prepare(`INSERT INTO messages (id, conversation_id, from_agent_id, to_agent_id, content, content_type, status, created_at)
         VALUES (?, ?, ?, ?, ?, 'text', 'pending', ?)`).run(messageId, convId, agentId, to, content, nowUnix);
        }
        // Notify local subscribers
        this.notifySubscribers({
            id: messageId,
            from: agentId,
            content,
            threadId: options?.conversationId,
            timestamp: new Date(nowUnix * 1000).toISOString(),
        });
        return { messageId };
    }
    /**
     * Poll for messages addressed to this agent.
     *
     * Returns messages with status 'pending' where to_agent_id matches
     * the connected agent. Messages are returned oldest-first.
     */
    async poll(options) {
        this.ensureConnected();
        const { db, agentId } = this.state;
        const limit = options?.limit ?? 50;
        let query;
        let params;
        if (options?.since) {
            query = `SELECT id, from_agent_id, content, conversation_id, created_at
               FROM messages
               WHERE to_agent_id = ? AND status = 'pending' AND created_at > ?
               ORDER BY created_at ASC
               LIMIT ?`;
            params = [agentId, options.since, limit];
        }
        else {
            query = `SELECT id, from_agent_id, content, conversation_id, created_at
               FROM messages
               WHERE to_agent_id = ? AND status = 'pending'
               ORDER BY created_at ASC
               LIMIT ?`;
            params = [agentId, limit];
        }
        const rows = db.prepare(query).all(...params);
        return rows.map((r) => ({
            id: r.id,
            from: r.from_agent_id,
            content: r.content,
            threadId: r.conversation_id ?? undefined,
            timestamp: new Date(r.created_at * 1000).toISOString(),
        }));
    }
    /**
     * Acknowledge messages by marking them as 'delivered'.
     *
     * Updates the status and delivered_at timestamp for each message ID.
     */
    async ack(messageIds) {
        this.ensureConnected();
        if (messageIds.length === 0)
            return;
        const { db } = this.state;
        const nowUnix = Math.floor(Date.now() / 1000);
        const placeholders = messageIds.map(() => '?').join(', ');
        db.prepare(`UPDATE messages SET status = 'delivered', delivered_at = ? WHERE id IN (${placeholders})`).run(nowUnix, ...messageIds);
    }
    /**
     * Subscribe to real-time local messages.
     *
     * Since this is in-process, subscribers are notified synchronously
     * when push() is called. Additionally, a polling interval checks
     * for messages inserted by other processes (e.g., Rust CLI).
     *
     * @returns Unsubscribe function.
     */
    subscribe(handler) {
        this.ensureConnected();
        this.state.subscribers.add(handler);
        // Start cross-process polling if not already running
        if (!this.state.pollTimer && this.state.subscribers.size === 1) {
            this.state.pollTimer = setInterval(() => {
                void this.pollAndNotify();
            }, 1000);
        }
        return () => {
            this.state?.subscribers.delete(handler);
            if (this.state?.subscribers.size === 0 && this.state.pollTimer) {
                clearInterval(this.state.pollTimer);
                this.state.pollTimer = null;
            }
        };
    }
    /**
     * Check whether conduit.db is available for local transport.
     *
     * Used by factory.ts to decide whether to use LocalTransport.
     *
     * @task T356
     * @epic T310
     * @param cwd - Optional working directory override (defaults to process.cwd()).
     * @returns `true` if conduit.db exists at the expected path.
     */
    static isAvailable(cwd) {
        const dbPath = getConduitDbPath(cwd ?? process.cwd());
        return existsSync(dbPath);
    }
    /** Poll for new messages and notify subscribers (cross-process sync). */
    async pollAndNotify() {
        if (!this.state || this.state.subscribers.size === 0)
            return;
        const messages = await this.poll({ limit: 20 });
        for (const msg of messages) {
            this.notifySubscribers(msg);
        }
        if (messages.length > 0) {
            await this.ack(messages.map((m) => m.id));
        }
    }
    /** Notify all active subscribers of a new message. */
    notifySubscribers(message) {
        if (!this.state)
            return;
        for (const handler of this.state.subscribers) {
            try {
                handler(message);
            }
            catch {
                // Subscriber errors must not break the transport
            }
        }
    }
    /**
     * Ensure a DM conversation exists between two agents.
     *
     * Conversations store participants as a comma-separated TEXT field.
     * We search for existing private conversations containing both agents.
     *
     * @returns The conversation ID.
     */
    ensureDmConversation(fromAgentId, toAgentId) {
        const { db } = this.state;
        // Participants are stored as comma-separated text, sorted alphabetically
        const sortedParticipants = [fromAgentId, toAgentId].sort().join(',');
        // Check for existing DM conversation with these exact participants
        const existing = db
            .prepare(`SELECT id FROM conversations
         WHERE visibility = 'private' AND participants = ?
         LIMIT 1`)
            .get(sortedParticipants);
        if (existing)
            return existing.id;
        // Create new DM conversation
        const convId = randomUUID();
        const nowUnix = Math.floor(Date.now() / 1000);
        db.prepare(`INSERT INTO conversations (id, participants, visibility, message_count, created_at, updated_at)
       VALUES (?, ?, 'private', 0, ?, ?)`).run(convId, sortedParticipants, nowUnix, nowUnix);
        return convId;
    }
    /** Throw if not connected. */
    ensureConnected() {
        if (!this.state) {
            throw new Error('LocalTransport not connected. Call connect() first.');
        }
    }
}
//# sourceMappingURL=local-transport.js.map