import { EventEmitter } from "node:events";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { logger } from "../logger.js";
import { createGmailClient, createRawEmail, fetchReplyHeaders, buildReplySubject, } from "../email/gmail-utils.js";
const MAX_INLINE_ATTACHMENT_BYTES = 10 * 1024 * 1024; // 10MB
const STARTUP_UNREAD_SCAN_MAX = 20;
/**
 * Gmail channel adapter.
 * Uses the agent's dedicated Gmail account to receive and send emails.
 *
 * On startup, scans for recent unread INBOX emails to catch messages
 * received during downtime, then captures the current historyId and
 * polls forward using the Gmail History API.
 */
export class GmailAdapter extends EventEmitter {
    name = "gmail";
    config;
    gmail;
    pollInterval = null;
    lastHistoryId = null;
    processedMessageIds = new Set();
    initialized = false;
    constructor(config) {
        super();
        this.config = config;
        this.gmail = createGmailClient(config);
    }
    async start() {
        // 1. Scan for unread INBOX messages that arrived while the agent was offline.
        //    Since we mark every processed email as read, anything still UNREAD was missed.
        await this.processUnreadOnStartup();
        // 2. Capture current historyId as our baseline for forward polling.
        try {
            const profile = await this.gmail.users.getProfile({ userId: "me" });
            this.lastHistoryId = profile.data.historyId ?? null;
            this.initialized = true;
            logger.channel("gmail", `Baseline historyId: ${this.lastHistoryId}`);
        }
        catch (err) {
            logger.warn(`Gmail: Could not get initial history ID: ${err instanceof Error ? err.message : String(err)}`);
        }
        // 3. Poll for new messages every 30 seconds
        this.pollInterval = setInterval(() => {
            this.pollForMessages().catch((err) => {
                this.emit("error", err instanceof Error ? err : new Error(String(err)));
            });
        }, 30_000);
    }
    stop() {
        if (this.pollInterval) {
            clearInterval(this.pollInterval);
            this.pollInterval = null;
        }
        return Promise.resolve();
    }
    async sendMessage(to, message, attachments, replyTo) {
        const threadId = replyTo?.meta?.threadId;
        const gmailMessageId = replyTo?.meta?.messageId;
        const subject = replyTo?.meta?.subject;
        let inReplyTo;
        let references;
        let replySubject = "Message from VisionClaw";
        if (threadId && gmailMessageId) {
            try {
                const replyHeaders = await fetchReplyHeaders(this.gmail, gmailMessageId);
                inReplyTo = replyHeaders.inReplyTo;
                references = replyHeaders.references;
            }
            catch {
                // Fall through — send as new thread if header fetch fails
            }
            if (subject) {
                replySubject = buildReplySubject(subject);
            }
        }
        const raw = createRawEmail({
            from: this.config.gmail,
            to,
            subject: replySubject,
            body: message,
            attachments,
            inReplyTo,
            references,
        });
        await this.gmail.users.messages.send({
            userId: "me",
            requestBody: { raw, threadId: threadId ?? undefined },
        });
    }
    /**
     * One-time scan for unread INBOX messages on startup.
     * Catches emails received while the agent was offline —
     * since we mark processed emails as read, anything still
     * UNREAD must have been missed during downtime.
     */
    async processUnreadOnStartup() {
        try {
            const listRes = await this.gmail.users.messages.list({
                userId: "me",
                q: "is:unread in:inbox",
                maxResults: STARTUP_UNREAD_SCAN_MAX,
            });
            const messages = listRes.data.messages ?? [];
            if (messages.length === 0) {
                logger.channel("gmail", "Startup scan: no unread messages");
                return;
            }
            logger.channel("gmail", `Startup scan: ${messages.length} unread message(s) found`);
            for (const stub of messages) {
                const msgId = stub.id;
                if (!msgId)
                    continue;
                this.processedMessageIds.add(msgId);
                try {
                    const full = await this.gmail.users.messages.get({
                        userId: "me",
                        id: msgId,
                        format: "full",
                    });
                    // Mark as read to prevent re-processing
                    await this.gmail.users.messages.modify({
                        userId: "me",
                        id: msgId,
                        requestBody: { removeLabelIds: ["UNREAD"] },
                    });
                    const commandMessage = await parseGmailMessage(full.data, msgId, this.gmail, this.config.gmail);
                    if (commandMessage) {
                        this.emit("message", commandMessage);
                    }
                }
                catch (err) {
                    logger.warn(`Gmail: Startup scan error for ${msgId}: ${err instanceof Error ? err.message : String(err)}`);
                }
            }
        }
        catch (err) {
            logger.warn(`Gmail: Startup unread scan failed: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    /**
     * Poll for new messages using the Gmail History API.
     * Only fetches messages added since the last known historyId,
     * so old unread emails are never processed.
     */
    async pollForMessages() {
        if (!this.initialized || !this.lastHistoryId) {
            return;
        }
        try {
            // Use History API to get only messages added since our last checkpoint
            const historyRes = await this.gmail.users.history.list({
                userId: "me",
                startHistoryId: this.lastHistoryId,
                historyTypes: ["messageAdded"],
                labelId: "INBOX",
            });
            // Update historyId for next poll
            if (historyRes.data.historyId) {
                this.lastHistoryId = historyRes.data.historyId;
            }
            const history = historyRes.data.history ?? [];
            // Extract new message IDs from history records
            const newMessageIds = [];
            for (const record of history) {
                const added = record.messagesAdded ?? [];
                for (const item of added) {
                    const msgId = item.message?.id;
                    if (msgId && !this.processedMessageIds.has(msgId)) {
                        newMessageIds.push(msgId);
                    }
                }
            }
            if (newMessageIds.length === 0) {
                return;
            }
            logger.channel("gmail", `${newMessageIds.length} new message(s) detected`);
            for (const msgId of newMessageIds) {
                this.processedMessageIds.add(msgId);
                try {
                    const full = await this.gmail.users.messages.get({
                        userId: "me",
                        id: msgId,
                        format: "full",
                    });
                    // Always mark as read to prevent re-processing
                    await this.gmail.users.messages.modify({
                        userId: "me",
                        id: msgId,
                        requestBody: { removeLabelIds: ["UNREAD"] },
                    });
                    const commandMessage = await parseGmailMessage(full.data, msgId, this.gmail, this.config.gmail);
                    if (commandMessage) {
                        this.emit("message", commandMessage);
                    }
                }
                catch (err) {
                    logger.warn(`Gmail: Error processing message ${msgId}: ${err instanceof Error ? err.message : String(err)}`);
                }
            }
            // Keep the set from growing unbounded (once per poll, outside the per-message loop)
            if (this.processedMessageIds.size > 1000) {
                const entries = Array.from(this.processedMessageIds);
                this.processedMessageIds = new Set(entries.slice(-500));
            }
        }
        catch (err) {
            const errMsg = err instanceof Error ? err.message : String(err);
            // If historyId is too old, Gmail returns 404. Reset baseline.
            if (errMsg.includes("404") || errMsg.includes("historyId")) {
                logger.warn("Gmail: History expired, resetting baseline");
                try {
                    const profile = await this.gmail.users.getProfile({ userId: "me" });
                    this.lastHistoryId = profile.data.historyId ?? null;
                }
                catch {
                    // noop
                }
                return;
            }
            logger.warn(`Gmail poll error: ${errMsg}`);
        }
    }
}
/**
 * Parse a Gmail API message into a CommandMessage.
 * Downloads attachment data and stores it inline as base64.
 */
async function parseGmailMessage(msg, id, gmail, selfAddress) {
    const headers = msg.payload?.headers ?? [];
    const from = headers.find((h) => h.name?.toLowerCase() === "from")?.value ?? "unknown";
    const subject = headers.find((h) => h.name?.toLowerCase() === "subject")?.value ?? "";
    const date = headers.find((h) => h.name?.toLowerCase() === "date")?.value ?? "";
    // Ignore emails sent by the agent itself (from address may contain a display name)
    if (from.toLowerCase().includes(selfAddress.toLowerCase())) {
        logger.debug(`Gmail: Ignoring self-sent email (from: ${from})`);
        return null;
    }
    // Extract body text and attachments from parts
    let body = "";
    const attachments = [];
    if (msg.payload?.body?.data) {
        body = Buffer.from(msg.payload.body.data, "base64url").toString("utf-8");
    }
    if (msg.payload?.parts) {
        for (const part of msg.payload.parts) {
            // Text body
            if (part.mimeType === "text/plain" && part.body?.data && !body) {
                body = Buffer.from(part.body.data, "base64url").toString("utf-8");
                continue;
            }
            // Attachment with attachmentId
            const attachmentId = part.body?.attachmentId;
            const filename = part.filename ?? "";
            const mimeType = part.mimeType ?? "application/octet-stream";
            if (attachmentId && filename) {
                try {
                    const attRes = await gmail.users.messages.attachments.get({
                        userId: "me",
                        messageId: id,
                        id: attachmentId,
                    });
                    if (attRes.data.data) {
                        const rawBuf = Buffer.from(attRes.data.data, "base64url");
                        const type = mimeType.startsWith("image/")
                            ? "image"
                            : mimeType.startsWith("audio/")
                                ? "audio"
                                : mimeType.startsWith("video/")
                                    ? "video"
                                    : "file";
                        if (rawBuf.length > MAX_INLINE_ATTACHMENT_BYTES) {
                            const dlDir = path.join(os.homedir(), "Downloads");
                            fs.mkdirSync(dlDir, { recursive: true });
                            const filePath = path.join(dlDir, `${id}-${filename}`);
                            fs.writeFileSync(filePath, rawBuf);
                            logger.info(`Gmail: Large attachment saved to disk (${rawBuf.length} bytes): ${filePath}`);
                            attachments.push({ type, localPath: filePath, filename, mimeType });
                        }
                        else {
                            const base64 = rawBuf.toString("base64");
                            attachments.push({ type, base64, filename, mimeType });
                        }
                    }
                }
                catch (err) {
                    logger.warn(`Gmail: Failed to download attachment ${filename}: ${err instanceof Error ? err.message : String(err)}`);
                }
            }
        }
    }
    // If no plain text body, summarize from subject only
    if (!body.trim()) {
        if (!subject.trim() && attachments.length === 0) {
            return null;
        }
        body = attachments.length > 0 ? "(attachments only)" : "(no text body)";
    }
    return {
        id,
        channel: "gmail",
        sender: from,
        text: subject ? `[Subject: ${subject}]\n${body}` : body,
        attachments,
        timestamp: date.trim() ? date : new Date().toISOString(),
        meta: { subject, messageId: msg.id, threadId: msg.threadId },
    };
}
//# sourceMappingURL=gmail.js.map