/**
 * Inbox module — collaborative email with multisig approval.
 *
 * Shared mailboxes with role-based access, threaded comments,
 * and Gnosis Safe multisig approval for outgoing emails.
 *
 * Storage: Automerge documents via SyncServer (one doc per mailbox).
 * IMAP credentials and sync state kept in module-scoped Maps (not in CRDT).
 */

import { Hono } from "hono";
import * as Automerge from "@automerge/automerge";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server';
import {
	mailboxSchema,
	mailboxDocId,
	type MailboxDoc,
	type MailboxMeta,
	type ThreadItem,
	type ThreadComment,
	type ApprovalItem,
	type ApprovalSignature,
} from './schemas';

let _syncServer: SyncServer | null = null;

const routes = new Hono();

// ── In-memory stores for data not in Automerge schemas ──

/** Workspace metadata (no Automerge schema — lightweight index) */
interface WorkspaceInfo {
	id: string;
	slug: string;
	name: string;
	description: string | null;
	ownerDid: string;
	createdAt: number;
}
const _workspaces = new Map<string, WorkspaceInfo>(); // slug → info

/** IMAP credentials per mailbox (not stored in CRDT for security) */
interface ImapConfig {
	imapUser: string | null;
	imapHost: string | null;
	imapPort: number | null;
}
const _imapConfigs = new Map<string, ImapConfig>(); // mailboxId → config

/** IMAP sync state per mailbox (transient server state) */
interface ImapSyncState {
	mailboxId: string;
	lastUid: number;
	uidValidity: number | null;
	lastSyncAt: number | null;
	error: string | null;
}
const _syncStates = new Map<string, ImapSyncState>(); // mailboxId → state

// ── Helpers ──

/** Default space used when no space param is provided */
const DEFAULT_SPACE = "global";

function generateId(): string {
	return crypto.randomUUID();
}

/**
 * Ensure a mailbox Automerge doc exists for the given space + mailboxId.
 * Returns the current doc state.
 */
function ensureMailboxDoc(space: string, mailboxId: string): MailboxDoc {
	const docId = mailboxDocId(space, mailboxId);
	let doc = _syncServer!.getDoc<MailboxDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<MailboxDoc>(), 'init mailbox', (d) => {
			const init = mailboxSchema.init();
			d.meta = init.meta;
			d.meta.spaceSlug = space;
			d.mailbox = init.mailbox;
			d.mailbox.id = mailboxId;
			d.members = [];
			d.threads = {};
			d.approvals = {};
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

/**
 * Find a mailbox doc by slug across all docs on the SyncServer.
 * Returns [space, mailboxId, doc] or null.
 */
function findMailboxBySlug(slug: string): [string, string, MailboxDoc] | null {
	for (const id of _syncServer!.getDocIds()) {
		// Match pattern: {space}:inbox:mailboxes:{mailboxId}
		const parts = id.split(':');
		if (parts.length === 4 && parts[1] === 'inbox' && parts[2] === 'mailboxes') {
			const doc = _syncServer!.getDoc<MailboxDoc>(id);
			if (doc && doc.mailbox && doc.mailbox.slug === slug) {
				return [parts[0], parts[3], doc];
			}
		}
	}
	return null;
}

/**
 * Find a mailbox doc by mailbox ID across all docs on the SyncServer.
 * Returns [space, docId, doc] or null.
 */
function findMailboxById(mailboxId: string): [string, string, MailboxDoc] | null {
	for (const id of _syncServer!.getDocIds()) {
		const parts = id.split(':');
		if (parts.length === 4 && parts[1] === 'inbox' && parts[2] === 'mailboxes' && parts[3] === mailboxId) {
			const doc = _syncServer!.getDoc<MailboxDoc>(id);
			if (doc) return [parts[0], id, doc];
		}
	}
	return null;
}

/**
 * Get all mailbox docs from the SyncServer.
 */
function getAllMailboxDocs(): Array<{ space: string; docId: string; doc: MailboxDoc }> {
	const results: Array<{ space: string; docId: string; doc: MailboxDoc }> = [];
	for (const id of _syncServer!.getDocIds()) {
		const parts = id.split(':');
		if (parts.length === 4 && parts[1] === 'inbox' && parts[2] === 'mailboxes') {
			const doc = _syncServer!.getDoc<MailboxDoc>(id);
			if (doc) results.push({ space: parts[0], docId: id, doc });
		}
	}
	return results;
}

/**
 * Find a thread by ID across all mailbox docs.
 * Returns [docId, threadId, thread, doc] or null.
 */
function findThreadById(threadId: string): [string, string, ThreadItem, MailboxDoc] | null {
	for (const { docId, doc } of getAllMailboxDocs()) {
		const thread = doc.threads[threadId];
		if (thread) return [docId, threadId, thread, doc];
	}
	return null;
}

/**
 * Find an approval by ID across all mailbox docs.
 * Returns [docId, approvalId, approval, doc] or null.
 */
function findApprovalById(approvalId: string): [string, string, ApprovalItem, MailboxDoc] | null {
	for (const { docId, doc } of getAllMailboxDocs()) {
		const approval = doc.approvals[approvalId];
		if (approval) return [docId, approvalId, approval, doc];
	}
	return null;
}

/** Convert MailboxMeta to REST response format (snake_case) */
function mailboxToRest(mb: MailboxMeta) {
	return {
		id: mb.id,
		workspace_id: mb.workspaceId,
		slug: mb.slug,
		name: mb.name,
		email: mb.email,
		description: mb.description,
		visibility: mb.visibility,
		owner_did: mb.ownerDid,
		safe_address: mb.safeAddress,
		safe_chain_id: mb.safeChainId,
		approval_threshold: mb.approvalThreshold,
		created_at: new Date(mb.createdAt).toISOString(),
		imap_user: _imapConfigs.get(mb.id)?.imapUser || null,
	};
}

/** Convert ThreadItem to REST response format */
function threadToRest(t: ThreadItem) {
	return {
		id: t.id,
		mailbox_id: t.mailboxId,
		message_id: t.messageId,
		subject: t.subject,
		from_address: t.fromAddress,
		from_name: t.fromName,
		to_addresses: t.toAddresses,
		cc_addresses: t.ccAddresses,
		body_text: t.bodyText,
		body_html: t.bodyHtml,
		tags: t.tags,
		status: t.status,
		is_read: t.isRead,
		is_starred: t.isStarred,
		assigned_to: t.assignedTo,
		has_attachments: t.hasAttachments,
		received_at: new Date(t.receivedAt).toISOString(),
		created_at: new Date(t.createdAt).toISOString(),
		comment_count: t.comments.length,
	};
}

/** Convert ThreadComment to REST response format */
function commentToRest(c: ThreadComment) {
	return {
		id: c.id,
		thread_id: c.threadId,
		author_id: c.authorId,
		author_did: c.authorId, // In Automerge, authorId IS the DID
		username: null as string | null,
		body: c.body,
		mentions: c.mentions,
		created_at: new Date(c.createdAt).toISOString(),
	};
}

/** Convert ApprovalItem to REST response format */
function approvalToRest(a: ApprovalItem) {
	return {
		id: a.id,
		mailbox_id: a.mailboxId,
		thread_id: a.threadId,
		author_id: a.authorId,
		subject: a.subject,
		body_text: a.bodyText,
		body_html: a.bodyHtml,
		to_addresses: a.toAddresses,
		cc_addresses: a.ccAddresses,
		status: a.status,
		required_signatures: a.requiredSignatures,
		safe_tx_hash: a.safeTxHash,
		created_at: new Date(a.createdAt).toISOString(),
		resolved_at: a.resolvedAt ? new Date(a.resolvedAt).toISOString() : null,
		signature_count: a.signatures.length,
	};
}

// ── Mailboxes API ──

// GET /api/mailboxes — list mailboxes
routes.get("/api/mailboxes", async (c) => {
	const { workspace } = c.req.query();
	const allDocs = getAllMailboxDocs();

	let mailboxes: ReturnType<typeof mailboxToRest>[];

	if (workspace) {
		const ws = _workspaces.get(workspace);
		if (!ws) return c.json({ mailboxes: [] });

		mailboxes = allDocs
			.filter(({ doc }) => doc.mailbox.workspaceId === ws.id)
			.map(({ doc }) => mailboxToRest(doc.mailbox))
			.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
	} else {
		mailboxes = allDocs
			.map(({ doc }) => mailboxToRest(doc.mailbox))
			.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
			.slice(0, 50);
	}

	return c.json({ mailboxes });
});

// POST /api/mailboxes — create mailbox
routes.post("/api/mailboxes", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const body = await c.req.json();
	const { slug, name, email, description, visibility = "private", imap_user, imap_password } = body;
	if (!slug || !name || !email) return c.json({ error: "slug, name, email required" }, 400);
	if (!/^[a-z0-9-]+$/.test(slug)) return c.json({ error: "Invalid slug" }, 400);

	// Check uniqueness
	const existing = findMailboxBySlug(slug);
	if (existing) return c.json({ error: "Mailbox already exists" }, 409);

	const space = (c.req.query("space") || c.req.param("space") || DEFAULT_SPACE);
	const mailboxId = generateId();
	const now = Date.now();

	// Create Automerge doc
	const doc = Automerge.change(Automerge.init<MailboxDoc>(), 'Create mailbox', (d) => {
		d.meta = {
			module: 'inbox',
			collection: 'mailboxes',
			version: 1,
			spaceSlug: space,
			createdAt: now,
		};
		d.mailbox = {
			id: mailboxId,
			workspaceId: null,
			slug,
			name,
			email,
			description: description || '',
			visibility,
			ownerDid: claims.sub,
			safeAddress: null,
			safeChainId: null,
			approvalThreshold: 1,
			createdAt: now,
		};
		d.members = [];
		d.threads = {};
		d.approvals = {};
	});
	_syncServer!.setDoc(mailboxDocId(space, mailboxId), doc);

	// Store IMAP config separately
	if (imap_user) {
		_imapConfigs.set(mailboxId, { imapUser: imap_user, imapHost: null, imapPort: null });
	}

	return c.json(mailboxToRest(doc.mailbox), 201);
});

// GET /api/mailboxes/:slug — mailbox detail
routes.get("/api/mailboxes/:slug", async (c) => {
	const slug = c.req.param("slug");
	const found = findMailboxBySlug(slug);
	if (!found) return c.json({ error: "Mailbox not found" }, 404);

	const [, , doc] = found;

	// Compute thread counts by status
	const threadCounts: Record<string, number> = {};
	for (const thread of Object.values(doc.threads)) {
		threadCounts[thread.status] = (threadCounts[thread.status] || 0) + 1;
	}

	return c.json({ ...mailboxToRest(doc.mailbox), threadCounts });
});

// ── Threads API ──

// GET /api/mailboxes/:slug/threads — list threads
routes.get("/api/mailboxes/:slug/threads", async (c) => {
	const slug = c.req.param("slug");
	const { status, search, limit = "50", offset = "0" } = c.req.query();

	const found = findMailboxBySlug(slug);
	if (!found) return c.json({ error: "Mailbox not found" }, 404);

	const [, , doc] = found;
	let threads = Object.values(doc.threads);

	// Filter by status
	if (status) {
		threads = threads.filter((t) => t.status === status);
	}

	// Filter by search (case-insensitive on subject and from_address)
	if (search) {
		const q = search.toLowerCase();
		threads = threads.filter((t) =>
			(t.subject && t.subject.toLowerCase().includes(q)) ||
			(t.fromAddress && t.fromAddress.toLowerCase().includes(q))
		);
	}

	// Sort by receivedAt descending
	threads.sort((a, b) => b.receivedAt - a.receivedAt);

	// Paginate
	const lim = Math.min(parseInt(limit), 100);
	const off = parseInt(offset) || 0;
	const page = threads.slice(off, off + lim);

	return c.json({ threads: page.map(threadToRest) });
});

// GET /api/threads/:id — thread detail with comments
routes.get("/api/threads/:id", async (c) => {
	const id = c.req.param("id");
	const found = findThreadById(id);
	if (!found) return c.json({ error: "Thread not found" }, 404);

	const [, , thread] = found;
	const comments = [...thread.comments]
		.sort((a, b) => a.createdAt - b.createdAt)
		.map(commentToRest);

	return c.json({ ...threadToRest(thread), comments });
});

// PATCH /api/threads/:id — update thread metadata
routes.patch("/api/threads/:id", async (c) => {
	const id = c.req.param("id");
	const body = await c.req.json();
	const allowed = ["status", "is_read", "is_starred", "tags", "assigned_to"] as const;

	// Check at least one valid field
	const hasUpdate = allowed.some((key) => key in body);
	if (!hasUpdate) return c.json({ error: "No valid fields" }, 400);

	const found = findThreadById(id);
	if (!found) return c.json({ error: "Thread not found" }, 404);

	const [docId] = found;

	const updated = _syncServer!.changeDoc<MailboxDoc>(docId, `Update thread ${id}`, (d) => {
		const t = d.threads[id];
		if (!t) return;
		if ("status" in body) t.status = body.status;
		if ("is_read" in body) t.isRead = body.is_read;
		if ("is_starred" in body) t.isStarred = body.is_starred;
		if ("tags" in body) {
			// Replace tags array
			t.tags.length = 0;
			for (const tag of body.tags) t.tags.push(tag);
		}
		if ("assigned_to" in body) t.assignedTo = body.assigned_to;
	});

	if (!updated) return c.json({ error: "Thread not found" }, 404);
	const thread = updated.threads[id];
	return c.json(threadToRest(thread));
});

// POST /api/threads/:id/comments — add comment
routes.post("/api/threads/:id/comments", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const threadId = c.req.param("id");
	const body = await c.req.json();
	const { text, mentions } = body;
	if (!text) return c.json({ error: "text required" }, 400);

	const found = findThreadById(threadId);
	if (!found) return c.json({ error: "Thread not found" }, 404);

	const [docId] = found;
	const commentId = generateId();
	const now = Date.now();

	_syncServer!.changeDoc<MailboxDoc>(docId, `Add comment to thread ${threadId}`, (d) => {
		const t = d.threads[threadId];
		if (!t) return;
		t.comments.push({
			id: commentId,
			threadId,
			authorId: claims.sub,
			body: text,
			mentions: mentions || [],
			createdAt: now,
		});
	});

	const comment: ThreadComment = {
		id: commentId,
		threadId,
		authorId: claims.sub,
		body: text,
		mentions: mentions || [],
		createdAt: now,
	};

	return c.json(commentToRest(comment), 201);
});

// ── Approvals API ──

// GET /api/approvals — list pending approvals
routes.get("/api/approvals", async (c) => {
	const { mailbox, status = "PENDING" } = c.req.query();

	if (mailbox) {
		const found = findMailboxBySlug(mailbox);
		if (!found) return c.json({ error: "Mailbox not found" }, 404);
		const [, , doc] = found;

		const approvals = Object.values(doc.approvals)
			.filter((a) => a.status === status)
			.sort((a, b) => b.createdAt - a.createdAt)
			.map(approvalToRest);

		return c.json({ approvals });
	} else {
		const allApprovals: ReturnType<typeof approvalToRest>[] = [];
		for (const { doc } of getAllMailboxDocs()) {
			for (const a of Object.values(doc.approvals)) {
				if (a.status === status) allApprovals.push(approvalToRest(a));
			}
		}
		allApprovals.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
		return c.json({ approvals: allApprovals.slice(0, 50) });
	}
});

// POST /api/approvals — create approval draft
routes.post("/api/approvals", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const body = await c.req.json();
	const { mailbox_slug, thread_id, subject, body_text, to_addresses } = body;
	if (!mailbox_slug || !subject) return c.json({ error: "mailbox_slug and subject required" }, 400);

	const found = findMailboxBySlug(mailbox_slug);
	if (!found) return c.json({ error: "Mailbox not found" }, 404);

	const [space, mailboxId, doc] = found;
	const docId = mailboxDocId(space, mailboxId);
	const approvalId = generateId();
	const now = Date.now();

	_syncServer!.changeDoc<MailboxDoc>(docId, `Create approval draft`, (d) => {
		d.approvals[approvalId] = {
			id: approvalId,
			mailboxId: doc.mailbox.id,
			threadId: thread_id || null,
			authorId: claims.sub,
			subject,
			bodyText: body_text || '',
			bodyHtml: '',
			toAddresses: to_addresses || [],
			ccAddresses: [],
			status: 'PENDING',
			requiredSignatures: doc.mailbox.approvalThreshold || 1,
			safeTxHash: null,
			createdAt: now,
			resolvedAt: 0,
			signatures: [],
		};
	});

	const approval: ApprovalItem = {
		id: approvalId,
		mailboxId: doc.mailbox.id,
		threadId: thread_id || null,
		authorId: claims.sub,
		subject,
		bodyText: body_text || '',
		bodyHtml: '',
		toAddresses: to_addresses || [],
		ccAddresses: [],
		status: 'PENDING',
		requiredSignatures: doc.mailbox.approvalThreshold || 1,
		safeTxHash: null,
		createdAt: now,
		resolvedAt: 0,
		signatures: [],
	};

	return c.json(approvalToRest(approval), 201);
});

// POST /api/approvals/:id/sign — sign an approval
routes.post("/api/approvals/:id/sign", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const id = c.req.param("id");
	const body = await c.req.json();
	const { vote = "APPROVE" } = body;
	if (!["APPROVE", "REJECT"].includes(vote)) return c.json({ error: "Invalid vote" }, 400);

	const found = findApprovalById(id);
	if (!found) return c.json({ error: "Approval not found" }, 404);

	const [docId, , approval] = found;
	if (approval.status !== "PENDING") return c.json({ error: "Approval not pending" }, 400);

	const now = Date.now();

	const updated = _syncServer!.changeDoc<MailboxDoc>(docId, `Sign approval ${id}`, (d) => {
		const a = d.approvals[id];
		if (!a) return;

		// Upsert signature: replace if signer already voted
		const existingIdx = a.signatures.findIndex((s) => s.signerId === claims.sub);
		const sig: ApprovalSignature = {
			id: existingIdx >= 0 ? a.signatures[existingIdx].id : generateId(),
			approvalId: id,
			signerId: claims.sub,
			vote,
			signedAt: now,
		};

		if (existingIdx >= 0) {
			a.signatures[existingIdx].vote = vote;
			a.signatures[existingIdx].signedAt = now;
		} else {
			a.signatures.push(sig);
		}

		// Count approvals and rejections
		const approveCount = a.signatures.filter((s) => s.vote === 'APPROVE').length;
		const rejectCount = a.signatures.filter((s) => s.vote === 'REJECT').length;

		if (approveCount >= a.requiredSignatures) {
			a.status = 'APPROVED';
			a.resolvedAt = now;
		} else if (rejectCount > 0) {
			a.status = 'REJECTED';
			a.resolvedAt = now;
		}
	});

	if (!updated) return c.json({ error: "Approval not found" }, 404);

	const finalApproval = updated.approvals[id];
	const approveCount = finalApproval.signatures.filter((s) => s.vote === 'APPROVE').length;

	return c.json({
		ok: true,
		status: finalApproval.status,
		signatures: approveCount,
		...(finalApproval.status === 'PENDING' ? { required: finalApproval.requiredSignatures } : {}),
	});
});

// ── Workspaces API ──

// GET /api/workspaces
routes.get("/api/workspaces", async (c) => {
	const workspaces = Array.from(_workspaces.values())
		.sort((a, b) => b.createdAt - a.createdAt)
		.slice(0, 50)
		.map((ws) => ({
			id: ws.id,
			slug: ws.slug,
			name: ws.name,
			description: ws.description,
			owner_did: ws.ownerDid,
			created_at: new Date(ws.createdAt).toISOString(),
		}));
	return c.json({ workspaces });
});

// POST /api/workspaces
routes.post("/api/workspaces", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const body = await c.req.json();
	const { slug, name, description } = body;
	if (!slug || !name) return c.json({ error: "slug and name required" }, 400);

	if (_workspaces.has(slug)) return c.json({ error: "Workspace already exists" }, 409);

	const ws: WorkspaceInfo = {
		id: generateId(),
		slug,
		name,
		description: description || null,
		ownerDid: claims.sub,
		createdAt: Date.now(),
	};
	_workspaces.set(slug, ws);

	return c.json({
		id: ws.id,
		slug: ws.slug,
		name: ws.name,
		description: ws.description,
		owner_did: ws.ownerDid,
		created_at: new Date(ws.createdAt).toISOString(),
	}, 201);
});

// GET /api/health
routes.get("/api/health", (c) => c.json({ ok: true, imapSync: IMAP_HOST !== "" }));

// GET /api/sync-status — show IMAP sync state per mailbox
routes.get("/api/sync-status", async (c) => {
	const syncStates: any[] = [];

	for (const [mailboxId, state] of _syncStates) {
		const found = findMailboxById(mailboxId);
		if (!found) continue;
		const [, , doc] = found;

		syncStates.push({
			mailbox_id: mailboxId,
			last_uid: state.lastUid,
			uid_validity: state.uidValidity,
			last_sync_at: state.lastSyncAt ? new Date(state.lastSyncAt).toISOString() : null,
			error: state.error,
			slug: doc.mailbox.slug,
			name: doc.mailbox.name,
			email: doc.mailbox.email,
		});
	}

	syncStates.sort((a, b) => {
		if (!a.last_sync_at && !b.last_sync_at) return 0;
		if (!a.last_sync_at) return 1;
		if (!b.last_sync_at) return -1;
		return new Date(b.last_sync_at).getTime() - new Date(a.last_sync_at).getTime();
	});

	return c.json({ syncStates });
});

// ── IMAP Sync Worker ──
const IMAP_HOST = process.env.IMAP_HOST || "";
const IMAP_PORT = parseInt(process.env.IMAP_PORT || "993");
const IMAP_TLS_REJECT = process.env.IMAP_TLS_REJECT_UNAUTHORIZED !== "false";
const SYNC_INTERVAL = 30_000; // 30 seconds

interface SyncableMailbox {
	id: string;
	slug: string;
	email: string;
	space: string;
	imap_user: string | null;
	imap_host: string | null;
	imap_port: number | null;
}

async function syncMailbox(mailbox: SyncableMailbox) {
	let ImapFlow: any;
	let simpleParser: any;
	try {
		ImapFlow = (await import("imapflow")).ImapFlow;
		simpleParser = (await import("mailparser")).simpleParser;
	} catch {
		console.error("[Inbox] imapflow/mailparser not installed — skipping IMAP sync");
		return;
	}

	const host = mailbox.imap_host || IMAP_HOST;
	const port = mailbox.imap_port || IMAP_PORT;
	const user = mailbox.imap_user;
	if (!host || !user) return;

	// Get or create sync state
	let syncState = _syncStates.get(mailbox.id);
	if (!syncState) {
		syncState = {
			mailboxId: mailbox.id,
			lastUid: 0,
			uidValidity: null,
			lastSyncAt: null,
			error: null,
		};
		_syncStates.set(mailbox.id, syncState);
	}

	const client = new ImapFlow({
		host,
		port,
		secure: port === 993,
		auth: { user, pass: process.env[`IMAP_PASS_${mailbox.slug.toUpperCase().replace(/-/g, "_")}`] || "" },
		tls: { rejectUnauthorized: IMAP_TLS_REJECT },
		logger: false,
	});

	try {
		await client.connect();
		const lock = await client.getMailboxLock("INBOX");

		try {
			const status = client.mailbox;
			const uidValidity = status?.uidValidity;

			// UID validity changed — need full resync
			if (syncState.uidValidity && uidValidity && syncState.uidValidity !== uidValidity) {
				syncState.lastUid = 0;
				syncState.uidValidity = uidValidity;
			}

			// Fetch messages newer than last synced UID
			const lastUid = syncState.lastUid || 0;
			const range = lastUid > 0 ? `${lastUid + 1}:*` : "1:*";
			let maxUid = lastUid;
			let count = 0;

			const docId = mailboxDocId(mailbox.space, mailbox.id);

			for await (const msg of client.fetch(range, {
				envelope: true,
				source: true,
				uid: true,
			})) {
				if (msg.uid <= lastUid) continue;
				if (count >= 100) break; // Batch limit

				try {
					const parsed = await simpleParser(msg.source);
					const fromAddr = parsed.from?.value?.[0]?.address || msg.envelope?.from?.[0]?.address || "";
					const fromName = parsed.from?.value?.[0]?.name || msg.envelope?.from?.[0]?.name || "";
					const subject = parsed.subject || msg.envelope?.subject || "(no subject)";
					const toAddrs = (parsed.to?.value || []).map((a: any) => a.address || "");
					const ccAddrs = (parsed.cc?.value || []).map((a: any) => a.address || "");
					const messageId = parsed.messageId || msg.envelope?.messageId || null;
					const hasAttachments = (parsed.attachments?.length || 0) > 0;
					const receivedAt = parsed.date ? parsed.date.getTime() : Date.now();

					const threadId = generateId();

					// Check for duplicate messageId before inserting
					const currentDoc = _syncServer!.getDoc<MailboxDoc>(docId);
					const isDuplicate = messageId && currentDoc &&
						Object.values(currentDoc.threads).some((t) => t.messageId === messageId);

					if (!isDuplicate) {
						_syncServer!.changeDoc<MailboxDoc>(docId, `IMAP sync: ${subject}`, (d) => {
							d.threads[threadId] = {
								id: threadId,
								mailboxId: mailbox.id,
								messageId,
								subject,
								fromAddress: fromAddr,
								fromName: fromName,
								toAddresses: toAddrs,
								ccAddresses: ccAddrs,
								bodyText: parsed.text || '',
								bodyHtml: parsed.html || '',
								tags: [],
								status: 'open',
								isRead: false,
								isStarred: false,
								assignedTo: null,
								hasAttachments,
								receivedAt,
								createdAt: Date.now(),
								comments: [],
							};
						});
						count++;
					}
				} catch (parseErr) {
					console.error(`[Inbox] Parse error UID ${msg.uid}:`, parseErr);
				}

				if (msg.uid > maxUid) maxUid = msg.uid;
			}

			// Update sync state
			syncState.lastUid = maxUid;
			syncState.uidValidity = uidValidity || null;
			syncState.lastSyncAt = Date.now();
			syncState.error = null;

			if (count > 0) console.log(`[Inbox] Synced ${count} messages for ${mailbox.email}`);
		} finally {
			lock.release();
		}

		await client.logout();
	} catch (e: any) {
		console.error(`[Inbox] IMAP sync error for ${mailbox.email}:`, e.message);
		if (syncState) {
			syncState.error = e.message;
			syncState.lastSyncAt = Date.now();
		}
	}
}

function runSyncLoop() {
	if (!IMAP_HOST) {
		console.log("[Inbox] IMAP_HOST not set — IMAP sync disabled");
		return;
	}
	console.log(`[Inbox] IMAP sync enabled — polling every ${SYNC_INTERVAL / 1000}s`);

	const doSync = async () => {
		if (!_syncServer) return;

		try {
			// Find all mailboxes with IMAP config
			const mailboxes: SyncableMailbox[] = [];
			for (const { space, doc } of getAllMailboxDocs()) {
				const imapCfg = _imapConfigs.get(doc.mailbox.id);
				if (imapCfg?.imapUser) {
					mailboxes.push({
						id: doc.mailbox.id,
						slug: doc.mailbox.slug,
						email: doc.mailbox.email,
						space,
						imap_user: imapCfg.imapUser,
						imap_host: imapCfg.imapHost,
						imap_port: imapCfg.imapPort,
					});
				}
			}

			for (const mb of mailboxes) {
				await syncMailbox(mb);
			}
		} catch (e) {
			console.error("[Inbox] Sync loop error:", e);
		}
	};

	// Initial sync after 5s delay
	setTimeout(doSync, 5000);
	// Recurring sync
	setInterval(doSync, SYNC_INTERVAL);
}

// Start IMAP sync in background
runSyncLoop();

// ── About / use-cases landing ──
routes.get("/about", (c) => {
	const space = c.req.param("space") || "demo";
	return c.html(renderShell({
		title: `Multi-Sig Inbox — rInbox | rSpace`,
		moduleId: "rinbox",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `
<div style="max-width:56rem;margin:0 auto;padding:2rem 1.5rem;">
  <h1 style="font-size:2rem;font-weight:bold;text-align:center;margin-bottom:0.5rem;">What can you do with a multi-sig inbox?</h1>
  <p style="color:#94a3b8;text-align:center;margin-bottom:2.5rem;max-width:36rem;margin-left:auto;margin-right:auto;">
    When outbound email requires collective approval, entirely new coordination patterns become possible.
  </p>
  <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1.5rem;">
    <div style="background:rgba(30,41,59,.5);border:1px solid rgba(51,65,85,.5);border-radius:1rem;padding:1.5rem;">
      <h3 style="font-size:1.1rem;font-weight:600;margin-bottom:0.5rem;color:#22d3ee;">Governance &amp; Resolutions</h3>
      <p style="color:#94a3b8;font-size:.875rem;line-height:1.6;">Board decisions require 3-of-5 signers before the email sends. The message is the vote &mdash; no separate tooling needed.</p>
    </div>
    <div style="background:rgba(30,41,59,.5);border:1px solid rgba(51,65,85,.5);border-radius:1rem;padding:1.5rem;">
      <h3 style="font-size:1.1rem;font-weight:600;margin-bottom:0.5rem;color:#a78bfa;">Escrow &amp; Conditional Release</h3>
      <p style="color:#94a3b8;font-size:.875rem;line-height:1.6;">Hold sensitive documents in an inbox that only unlocks when N parties agree. Mediation where neither side can act alone.</p>
    </div>
    <div style="background:rgba(30,41,59,.5);border:1px solid rgba(51,65,85,.5);border-radius:1rem;padding:1.5rem;">
      <h3 style="font-size:1.1rem;font-weight:600;margin-bottom:0.5rem;color:#f87171;">Whistleblower Coordination</h3>
      <p style="color:#94a3b8;font-size:.875rem;line-height:1.6;">Evidence requires M-of-N co-signers before release. Dead man&rsquo;s switch if a signer goes silent. Nobody goes first alone.</p>
    </div>
    <div style="background:rgba(30,41,59,.5);border:1px solid rgba(51,65,85,.5);border-radius:1rem;padding:1.5rem;">
      <h3 style="font-size:1.1rem;font-weight:600;margin-bottom:0.5rem;color:#4ade80;">Social Key Recovery</h3>
      <p style="color:#94a3b8;font-size:.875rem;line-height:1.6;">Lost access? 3-of-5 trusted contacts co-sign your restoration. No phone number, no backup email &mdash; a trust network.</p>
    </div>
    <div style="background:rgba(30,41,59,.5);border:1px solid rgba(51,65,85,.5);border-radius:1rem;padding:1.5rem;">
      <h3 style="font-size:1.1rem;font-weight:600;margin-bottom:0.5rem;color:#fbbf24;">Tamper-Proof Audit Trails</h3>
      <p style="color:#94a3b8;font-size:.875rem;line-height:1.6;">Every email read and sent is co-signed. Cryptographic proof of who approved what, when. Built for compliance.</p>
    </div>
    <div style="background:rgba(30,41,59,.5);border:1px solid rgba(51,65,85,.5);border-radius:1rem;padding:1.5rem;">
      <h3 style="font-size:1.1rem;font-weight:600;margin-bottom:0.5rem;color:#60a5fa;">Treasury &amp; Payments</h3>
      <p style="color:#94a3b8;font-size:.875rem;line-height:1.6;">Invoice arrives, 2-of-3 finance team co-sign the reply authorizing payment. Bridges email to on-chain wallets.</p>
    </div>
  </div>
  <div style="text-align:center;margin-top:2.5rem;">
    <a href="/${space}/rinbox" style="display:inline-block;padding:0.75rem 2rem;background:#0891b2;color:white;border-radius:0.75rem;text-decoration:none;font-weight:600;">Open Inbox</a>
    <a href="https://rinbox.online" style="display:inline-block;padding:0.75rem 2rem;margin-left:1rem;border:1px solid rgba(51,65,85,.5);color:#94a3b8;border-radius:0.75rem;text-decoration:none;font-weight:600;">rinbox.online</a>
  </div>
</div>`,
		scripts: "",
		styles: "",
	}));
});

// ── Page route ──
routes.get("/", (c) => {
	const space = c.req.param("space") || "demo";
	return c.html(renderShell({
		title: `${space} — Inbox | rSpace`,
		moduleId: "rinbox",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-inbox-client space="${space}"></folk-inbox-client>`,
		scripts: `<script type="module" src="/modules/rinbox/folk-inbox-client.js"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rinbox/inbox.css">`,
	}));
});

// ── Seed template data ──

function seedTemplateInbox(space: string) {
	if (!_syncServer) return;
	// Skip if space already has mailboxes
	const prefix = `${space}:inbox:mailboxes:`;
	const existing = _syncServer.listDocs().filter((id) => id.startsWith(prefix));
	if (existing.length > 0) return;

	const mbId = crypto.randomUUID();
	const docId = mailboxDocId(space, mbId);
	const now = Date.now();
	const day = 86400000;

	const doc = Automerge.change(Automerge.init<MailboxDoc>(), 'seed template mailbox', (d) => {
		d.meta = { module: 'inbox', collection: 'mailboxes', version: 1, spaceSlug: space, createdAt: now };
		d.mailbox = {
			id: mbId, workspaceId: null, slug: 'commons-team', name: 'Commons Team',
			email: `commons-team@${space}.rspace.online`,
			description: 'Shared mailbox for the commons coordination team.',
			visibility: 'members', ownerDid: 'did:demo:seed',
			safeAddress: null, safeChainId: null, approvalThreshold: 1, createdAt: now,
		};
		d.members = [];
		d.threads = {};
		d.approvals = {};

		const threads: Array<{ subj: string; from: string; fromName: string; body: string; tags: string[]; age: number }> = [
			{
				subj: 'Grant Application Update — Gitcoin GG22',
				from: 'grants@gitcoin.co', fromName: 'Gitcoin Grants',
				body: 'Your application for the Cosmolocal Commons project has been accepted into GG22 Climate Solutions round. Matching pool opens April 1.',
				tags: ['grants', 'important'], age: 2,
			},
			{
				subj: 'Partnership Inquiry — Barcelona Maker Space',
				from: 'hello@bcnmakers.cat', fromName: 'BCN Makers',
				body: 'Hi! We saw your cosmolocal print network and would love to join as a provider. We have risograph, screen print, and laser cutting capabilities.',
				tags: ['partnerships'], age: 5,
			},
			{
				subj: 'Weekly Digest — rSpace Dev Updates',
				from: 'digest@rspace.online', fromName: 'rSpace Bot',
				body: '## This Week\n\n- 3 new modules deployed (rChoices, rSplat, rInbox)\n- EncryptID guardian recovery shipped\n- 2 new providers joined the cosmolocal network',
				tags: ['digest'], age: 1,
			},
		];

		for (const t of threads) {
			const tId = crypto.randomUUID();
			d.threads[tId] = {
				id: tId, mailboxId: mbId, messageId: `<${crypto.randomUUID()}@demo>`,
				subject: t.subj, fromAddress: t.from, fromName: t.fromName,
				toAddresses: [`commons-team@${space}.rspace.online`], ccAddresses: [],
				bodyText: t.body, bodyHtml: '', tags: t.tags, status: 'open',
				isRead: t.age > 3, isStarred: t.tags.includes('important'),
				assignedTo: null, hasAttachments: false,
				receivedAt: now - t.age * day, createdAt: now - t.age * day, comments: [],
			};
		}
	});

	_syncServer.setDoc(docId, doc);
	console.log(`[Inbox] Template seeded for "${space}": 1 mailbox, 3 threads`);
}

export const inboxModule: RSpaceModule = {
	id: "rinbox",
	name: "rInbox",
	icon: "\u{1F4E8}",
	description: "Collaborative email with multisig approval",
	scoping: { defaultScope: 'space', userConfigurable: false },
	docSchemas: [{ pattern: '{space}:inbox:mailboxes:{mailboxId}', description: 'Mailbox with threads and approvals', init: mailboxSchema.init }],
	routes,
	landingPage: renderLanding,
	seedTemplate: seedTemplateInbox,
	async onInit(ctx) {
		_syncServer = ctx.syncServer;
		console.log("[Inbox] Module initialized (Automerge-only, no PG)");
	},
	standaloneDomain: "rinbox.online",
	feeds: [
		{
			id: "messages",
			name: "Messages",
			kind: "data",
			description: "Email threads and messages across shared mailboxes",
			filterable: true,
		},
		{
			id: "notifications",
			name: "Notifications",
			kind: "attention",
			description: "New mail alerts, approval requests, and mention notifications",
		},
	],
	acceptsFeeds: ["data"],
	subPageInfos: [
		{
			path: "about",
			title: "About rInbox",
			icon: "📮",
			tagline: "rInbox",
			description: "Collaborative email for communities — shared mailboxes with multisig approval, threaded discussions, and team workflows.",
			features: [
				{ icon: "📬", title: "Shared Mailboxes", text: "Create shared inboxes that multiple team members can read and respond from." },
				{ icon: "✅", title: "Multisig Approval", text: "Require multiple approvals before sending sensitive emails on behalf of the group." },
				{ icon: "💬", title: "Internal Comments", text: "Discuss emails privately with your team before crafting a response." },
			],
		},
	],
};
