import { i as listCustomerSlugs } from "./customer-dir-CkMMXhb0.js";
import { i as readInteractions, n as appendInteraction } from "./interactions-writer-B8XAzdqR.js";
import { n as logger } from "./logger-Dyl4VcLO.js";
import { t as chunkText } from "./chunk-BhUZmQg5.js";
import { r as persistAttachment } from "./attachments-BddHbCt8.js";
import { n as htmlToMarkdown } from "./html-BaeOCZKE.js";
import path from "path";
import fs from "fs";
import matter from "gray-matter";
//#region src/sync/email-ingest.ts
/**
* Ingest one normalized email into a customer: convert + index its
* attachments, summarize it, append the interaction (with attachment links),
* and index the full body (chunked) for semantic search. Caller is responsible
* for deduplication (skip messages whose sourceRef is already logged).
*/
async function ingestEmail(dataDir, slug, msg, options = {}) {
	const includeAttachments = options.includeAttachments ?? true;
	const maxBytes = options.maxAttachmentBytes ?? 26214400;
	const attachmentLinks = [];
	if (includeAttachments) for (const att of msg.attachments) {
		if (att.content.length > maxBytes) {
			logger.warn("email-ingest", "skipping oversized attachment", {
				filename: att.filename,
				bytes: att.content.length
			});
			continue;
		}
		try {
			const saved = await persistAttachment({
				dataDir,
				slug,
				messageId: msg.messageId,
				source: msg.sourceRef,
				date: msg.date,
				filename: att.filename,
				mimeType: att.mimeType,
				buffer: att.content
			});
			attachmentLinks.push(saved.markdownName);
		} catch (err) {
			logger.warn("email-ingest", "attachment failed", {
				filename: att.filename,
				error: err.message
			});
		}
	}
	const { summarizeEmail } = await import("./llm-DSX1-wFu.js");
	const summary = await summarizeEmail(msg.subject, msg.bodyMarkdown, msg.from);
	await appendInteraction(dataDir, slug, {
		date: msg.date,
		type: "Email",
		direction: options.direction ?? "inbound",
		with: msg.from,
		subject: msg.subject,
		summary: summary.summary,
		nextSteps: summary.nextSteps,
		...attachmentLinks.length > 0 ? { attachments: attachmentLinks } : {},
		sourceRef: msg.sourceRef,
		synced: (/* @__PURE__ */ new Date()).toISOString()
	});
	const { indexInLanceDB } = await import("./lancedb-CswQEE5K.js");
	const bodyChunks = chunkText(`${msg.subject}\n${msg.bodyMarkdown}`);
	for (let i = 0; i < bodyChunks.length; i++) {
		const ref = i === 0 ? msg.sourceRef : `${msg.sourceRef}#${i}`;
		await indexInLanceDB(dataDir, slug, bodyChunks[i], ref, {
			date: msg.date,
			type: "Email"
		}).catch((err) => {
			logger.error("email-ingest", "LanceDB index failed", { error: err.message });
		});
	}
	return {
		attachments: attachmentLinks.length,
		chunks: bodyChunks.length
	};
}
//#endregion
//#region src/sync/email-router.ts
/** The domain part of an email address, lowercased (empty string if malformed). */
function domainOf(email) {
	const at = email.lastIndexOf("@");
	return at >= 0 ? email.slice(at + 1).trim().toLowerCase() : "";
}
/** Read just the routing-relevant fields from a customer's main_facts (tolerant). */
function readRoutingFields(dataDir, slug) {
	const file = path.join(dataDir, "customers", slug, "main_facts.md");
	if (!fs.existsSync(file)) return {};
	try {
		const data = matter(fs.readFileSync(file, "utf-8")).data;
		return {
			domain: typeof data["domain"] === "string" ? data["domain"] : void 0,
			email: typeof data["email"] === "string" ? data["email"] : void 0,
			primary_contact: typeof data["primary_contact"] === "string" ? data["primary_contact"] : void 0
		};
	} catch {
		return {};
	}
}
/**
* Build the routing table from every customer's main_facts. A customer is
* identified by its `domain`, `email`, and `primary_contact` (when it looks
* like an email). Customers without any identifier are still listed (empty
* arrays) so callers can see them, but they never match.
*/
function buildRoutingTable(dataDir) {
	return listCustomerSlugs(dataDir).map((slug) => {
		const facts = readRoutingFields(dataDir, slug);
		const domains = /* @__PURE__ */ new Set();
		const emails = /* @__PURE__ */ new Set();
		if (facts.domain) domains.add(facts.domain.trim().toLowerCase());
		for (const candidate of [facts.email, facts.primary_contact]) if (candidate && candidate.includes("@")) {
			const addr = candidate.trim().toLowerCase();
			emails.add(addr);
			const d = domainOf(addr);
			if (d) domains.add(d);
		}
		return {
			slug,
			domains: [...domains],
			emails: [...emails]
		};
	});
}
/**
* Route a message to a customer slug by matching any of its addresses
* (from/to/cc) against the routing table. Exact email matches win over domain
* matches. Returns the matched slug, or null when nothing matches (the message
* is "unrouted").
*/
function routeMessage(addresses, table) {
	const addrs = addresses.map((a) => a.trim().toLowerCase()).filter((a) => a.includes("@"));
	if (addrs.length === 0) return null;
	const domains = new Set(addrs.map(domainOf).filter(Boolean));
	for (const c of table) if (c.emails.some((e) => addrs.includes(e))) return c.slug;
	for (const c of table) if (c.domains.some((d) => domains.has(d))) return c.slug;
	return null;
}
//#endregion
//#region src/sync/connectors/imap.ts
/** Build a real ImapFlow client. Loaded lazily so the dep stays off hot paths. */
async function defaultClientFactory(config) {
	const { ImapFlow } = await import("imapflow");
	const auth = config.auth.accessToken ? {
		user: config.auth.user,
		accessToken: config.auth.accessToken
	} : {
		user: config.auth.user,
		pass: config.auth.pass ?? ""
	};
	return new ImapFlow({
		host: config.host,
		port: config.port ?? 993,
		secure: config.secure ?? true,
		auth,
		logger: false
	});
}
/** Normalize extracted email fields into the provider-independent email shape. */
async function normalizeParsedEmail(parsed, ctx) {
	const toAddresses = (parsed.toAddresses ?? []).map((a) => a.toLowerCase()).filter((a) => a.includes("@"));
	const plain = (parsed.text ?? "").trim();
	const bodyMarkdown = plain ? plain : parsed.html ? (await htmlToMarkdown(parsed.html)).trim() : "";
	return {
		messageId: (parsed.messageId ?? "").replace(/[<>]/g, "").trim() || `uid-${ctx.uid}`,
		from: parsed.fromText ?? "",
		toAddresses,
		subject: parsed.subject ?? "(no subject)",
		date: (parsed.date ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10),
		bodyMarkdown,
		attachments: (parsed.attachments ?? []).filter((a) => a.filename).map((a) => ({
			filename: a.filename,
			mimeType: a.contentType ?? "application/octet-stream",
			content: a.content
		})),
		sourceRef: `imap://${ctx.user}@${ctx.host}/${ctx.mailbox}/${ctx.uid}`
	};
}
/** Flatten mailparser's AddressObject | AddressObject[] | undefined to addresses. */
function flattenAddresses(field) {
	if (!field) return [];
	return (Array.isArray(field) ? field : [field]).flatMap((o) => o.value ?? []).map((a) => (a.address ?? "").toLowerCase()).filter((a) => a.includes("@"));
}
/**
* Sync a whole IMAP mailbox (any provider). Each message is parsed, routed to a
* customer — by a fixed `slug` or auto-routed by sender/recipient domain — and
* ingested through the shared pipeline (attachments→Markdown, summary, index).
* Messages that match no customer are counted as `unrouted` and skipped.
*/
async function syncImapMailbox(opts) {
	const result = {
		synced: 0,
		skipped: 0,
		unrouted: 0
	};
	const mailbox = opts.config.mailbox ?? "INBOX";
	const { simpleParser } = await import("mailparser");
	const client = opts.clientFactory ? opts.clientFactory(opts.config) : await defaultClientFactory(opts.config);
	const table = opts.slug ? null : buildRoutingTable(opts.dataDir);
	const dedupCache = /* @__PURE__ */ new Map();
	const seen = async (slug, sourceRef) => {
		let content = dedupCache.get(slug);
		if (content === void 0) {
			content = await readInteractions(opts.dataDir, slug).catch(() => "");
			dedupCache.set(slug, content);
		}
		return content.includes(sourceRef);
	};
	await client.connect();
	const lock = await client.getMailboxLock(mailbox);
	try {
		const range = opts.since ? { since: opts.since } : { all: true };
		for await (const message of client.fetch(range, {
			uid: true,
			source: true
		})) try {
			const parsed = await simpleParser(message.source);
			const msg = await normalizeParsedEmail({
				messageId: parsed.messageId,
				fromText: parsed.from?.text,
				toAddresses: [...flattenAddresses(parsed.to), ...flattenAddresses(parsed.cc)],
				subject: parsed.subject,
				date: parsed.date,
				text: parsed.text,
				html: parsed.html,
				attachments: parsed.attachments
			}, {
				user: opts.config.auth.user,
				host: opts.config.host,
				mailbox,
				uid: message.uid
			});
			let slug = opts.slug ?? null;
			if (!slug && table) slug = routeMessage([(msg.from.match(/<([^>]+)>/)?.[1] ?? msg.from).toLowerCase(), ...msg.toAddresses].filter((a) => domainOf(a)), table);
			if (!slug) {
				result.unrouted++;
				continue;
			}
			if (await seen(slug, msg.sourceRef)) {
				result.skipped++;
				continue;
			}
			await ingestEmail(opts.dataDir, slug, msg, {
				...opts.includeAttachments !== void 0 ? { includeAttachments: opts.includeAttachments } : {},
				...opts.maxAttachmentBytes !== void 0 ? { maxAttachmentBytes: opts.maxAttachmentBytes } : {},
				direction: directionFor(msg, opts.config.auth.user)
			});
			dedupCache.set(slug, (dedupCache.get(slug) ?? "") + msg.sourceRef);
			result.synced++;
		} catch (err) {
			logger.warn("imap-sync", "message failed", {
				uid: message.uid,
				error: err.message
			});
			result.skipped++;
		}
	} finally {
		lock.release();
		await client.logout().catch(() => void 0);
	}
	return result;
}
/** Inbound unless the mailbox owner is the sender. */
function directionFor(msg, user) {
	return (msg.from.match(/<([^>]+)>/)?.[1] ?? msg.from).toLowerCase() === user.toLowerCase() ? "outbound" : "inbound";
}
//#endregion
export { syncImapMailbox };

//# sourceMappingURL=imap-o6PRuBvm.js.map