import { spawn } from "node:child_process";
import os from "node:os";
import path from "node:path";
import fs from "node:fs/promises";
import { registerChannel } from "./registry.js";
const CHAT_DB = path.join(os.homedir(), "Library", "Messages", "chat.db");
export class IMessageChannel {
    name = "imessage";
    timer;
    lastRowId = 0;
    handler;
    polling = false;
    intervalMs;
    constructor(opts = {}) {
        this.intervalMs = opts.intervalMs ?? 5_000;
    }
    async start(handler) {
        try {
            await fs.access(CHAT_DB);
        }
        catch {
            throw new Error("iMessage chat.db not accessible. Grant Full Disk Access to your terminal in System Settings → Privacy.");
        }
        this.handler = handler;
        this.lastRowId = await this.maxRowId();
        this.timer = setInterval(() => this.tick(), this.intervalMs);
    }
    async tick() {
        if (this.polling)
            return;
        this.polling = true;
        try {
            const messages = await this.fetchSince(this.lastRowId);
            for (const msg of messages) {
                if (msg.rowId > this.lastRowId)
                    this.lastRowId = msg.rowId;
                if (msg.fromMe)
                    continue;
                await this.handler?.({
                    channel: this.name,
                    from: msg.handle ?? "unknown",
                    text: msg.text,
                    threadId: msg.threadId,
                    receivedAt: new Date(),
                });
            }
        }
        catch (err) {
            console.error(`[imessage] poll error: ${err.message}`);
        }
        finally {
            this.polling = false;
        }
    }
    async send(msg) {
        const args = buildOsascriptArgs(msg.to, msg.text);
        await new Promise((resolve, reject) => {
            const child = spawn("/usr/bin/osascript", args);
            let stderr = "";
            child.stderr.on("data", (b) => (stderr += b.toString("utf8")));
            child.on("error", reject);
            child.on("close", (code) => code === 0
                ? resolve()
                : reject(new Error(`osascript exited ${code}: ${stderr.trim()}`)));
        });
    }
    async stop() {
        if (this.timer)
            clearInterval(this.timer);
    }
    async maxRowId() {
        const out = await this.runSqlite("SELECT MAX(ROWID) FROM message;");
        const n = parseInt(out.trim(), 10);
        return isNaN(n) ? 0 : n;
    }
    async fetchSince(rowId) {
        const sql = `SELECT m.ROWID, h.id, m.text, m.is_from_me, m.cache_roomnames
                 FROM message m
                 LEFT JOIN handle h ON m.handle_id = h.ROWID
                 WHERE m.ROWID > ${rowId} AND m.text IS NOT NULL
                 ORDER BY m.ROWID ASC LIMIT 50;`;
        const out = await this.runSqlite(sql);
        const rows = [];
        for (const line of out.split("\n")) {
            if (!line.trim())
                continue;
            const parts = line.split("|");
            if (parts.length < 5)
                continue;
            rows.push({
                rowId: parseInt(parts[0], 10),
                handle: parts[1] || null,
                text: parts[2] ?? "",
                fromMe: parts[3] === "1",
                threadId: parts[4] ?? "",
            });
        }
        return rows;
    }
    async runSqlite(sql) {
        return await new Promise((resolve, reject) => {
            const child = spawn("sqlite3", ["-readonly", CHAT_DB, sql]);
            let stdout = "";
            let stderr = "";
            child.stdout.on("data", (b) => (stdout += b.toString("utf8")));
            child.stderr.on("data", (b) => (stderr += b.toString("utf8")));
            child.on("error", reject);
            child.on("close", (code) => code === 0
                ? resolve(stdout)
                : reject(new Error(`sqlite3 exited ${code}: ${stderr.trim()}`)));
        });
    }
}
/**
 * Build the osascript argv for sending an iMessage WITHOUT interpolating
 * user/LLM text into the AppleScript source.
 *
 * The old approach (`send "${text}"`) only escaped double-quotes, so a
 * message containing a newline — or a crafted `" & (do shell script "...")`
 * payload — could break out of the string literal and inject AppleScript.
 * Inbound iMessage text is untrusted (anyone who can text the user), so this
 * was a real injection vector.
 *
 * Instead the script is a STATIC `on run argv` program; recipient and text
 * arrive as positional argv items (`item 1`/`item 2 of argv`) that AppleScript
 * never parses as source. No escaping needed; newlines/quotes/backslashes
 * survive verbatim.
 */
export function buildOsascriptArgs(recipient, text) {
    const script = `on run argv
  set theRecipient to item 1 of argv
  set theText to item 2 of argv
  tell application "Messages"
    set targetService to first service whose service type = iMessage
    set targetBuddy to buddy theRecipient of targetService
    send theText to targetBuddy
  end tell
end run`;
    return ["-e", script, recipient, text];
}
registerChannel("imessage", (cfg) => {
    const intervalMs = typeof cfg.intervalMs === "number" ? cfg.intervalMs : undefined;
    return new IMessageChannel(intervalMs ? { intervalMs } : {});
});
//# sourceMappingURL=imessage.js.map