/**
 * Pubs module — markdown → print-ready pocket books via Typst.
 *
 * Ported from pocket-press (Next.js) to Hono routes.
 * Stateless — no database needed. Artifacts stored in /tmp.
 */

import { Hono } from "hono";
import { resolve, join } from "node:path";
import { mkdir, writeFile, readFile, readdir, stat } from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { createTransport, type Transporter } from "nodemailer";
import { parseMarkdown } from "./parse-document";
import { compileDocument, compileDocumentToPages } from "./typst-compile";
import { buildReflowableEpub, buildFixedLayoutEpub } from "./epub-gen";
import { getFormat, FORMATS, listFormats } from "./formats";
import {
	listPublications,
	getPublication,
	getPublicationFile,
	savePublication,
	slugify as slugifyPub,
} from "./publications-store";
import type { BookFormat } from "./formats";
import { generateImposition } from "./imposition";
import { discoverPrinters } from "./printer-discovery";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing";
import * as Automerge from "@automerge/automerge";
import { verifyToken, extractToken } from "../../server/auth";
import type { SyncServer } from '../../server/local-first/sync-server';
import { pubsDraftSchema, pubsDocId } from './schemas';
import type { PubsDoc } from './schemas';

const ARTIFACTS_DIR = process.env.ARTIFACTS_DIR || "/tmp/rpubs-artifacts";

// ── SMTP ──

let _smtpTransport: Transporter | null = null;

function getSmtpTransport(): Transporter | null {
	if (_smtpTransport) return _smtpTransport;
	const host = process.env.SMTP_HOST || "mail.rmail.online";
	const isInternal = host.includes('mailcow') || host.includes('postfix');
	if (!process.env.SMTP_PASS && !isInternal) return null;
	_smtpTransport = createTransport({
		host,
		port: isInternal ? 25 : (Number(process.env.SMTP_PORT) || 587),
		secure: !isInternal && Number(process.env.SMTP_PORT) === 465,
		...(isInternal ? {} : {
			auth: {
				user: process.env.SMTP_USER || "noreply@rmail.online",
				pass: process.env.SMTP_PASS!,
			},
		}),
		tls: { rejectUnauthorized: false },
	});
	return _smtpTransport;
}

// ── Email rate limiter (5/hour per IP) ──

const emailRateMap = new Map<string, number[]>();

function checkEmailRate(ip: string): boolean {
	const now = Date.now();
	const hour = 60 * 60 * 1000;
	const attempts = (emailRateMap.get(ip) || []).filter((t) => now - t < hour);
	if (attempts.length >= 5) return false;
	attempts.push(now);
	emailRateMap.set(ip, attempts);
	return true;
}

// rCart internal URL
const RCART_URL = process.env.RCART_URL || "http://localhost:3000";

// ── Types ──

interface ArtifactRequest {
	content: string;
	title?: string;
	author?: string;
	format: string;
	creator_id?: string;
	creator_name?: string;
	creator_wallet?: string;
	source_space?: string;
	license?: string;
	tags?: string[];
	description?: string;
	pricing?: {
		creator_share_pct?: number;
		community_share_pct?: number;
		creator_min?: { amount: number; currency?: string };
		suggested_retail?: { amount: number; currency?: string };
	};
}

// ── Helpers ──

function buildArtifactEnvelope(opts: {
	id: string;
	format: BookFormat;
	pageCount: number;
	pdfSizeBytes: number;
	parsedTitle: string;
	req: ArtifactRequest;
	baseUrl: string;
}) {
	const { id, format, pageCount, pdfSizeBytes, parsedTitle, req, baseUrl } = opts;

	const isBook = pageCount > 48;
	const productType = isBook ? "book" : "zine";
	const binding = isBook ? "perfect-bind" : "saddle-stitch";

	const substrates = isBook
		? ["paper-100gsm", "paper-100gsm-recycled", "cover-250gsm"]
		: ["paper-100gsm-recycled", "paper-100gsm", "paper-80gsm"];

	const requiredCapabilities = isBook
		? ["laser-print", "perfect-bind"]
		: ["laser-print", "saddle-stitch"];

	const pdfUrl = `${baseUrl}/api/artifact/${id}/pdf`;

	return {
		id,
		schema_version: "1.0" as const,
		type: "print-ready" as const,
		origin: "rpubs.online",
		source_space: req.source_space || null,
		creator: {
			id: req.creator_id || "anonymous",
			...(req.creator_name ? { name: req.creator_name } : {}),
			...(req.creator_wallet ? { wallet: req.creator_wallet } : {}),
		},
		created_at: new Date().toISOString(),
		...(req.license ? { license: req.license } : {}),
		payload: {
			title: parsedTitle,
			...(req.description
				? { description: req.description }
				: { description: `A ${pageCount}-page ${productType} in ${format.name} format, generated by rPubs.` }),
			...(req.tags && req.tags.length > 0 ? { tags: req.tags } : {}),
		},
		spec: {
			product_type: productType,
			dimensions: {
				width_mm: format.widthMm,
				height_mm: format.heightMm,
				bleed_mm: 3,
			},
			pages: pageCount,
			color_space: "grayscale",
			dpi: 300,
			binding,
			finish: "uncoated",
			substrates,
			required_capabilities: requiredCapabilities,
		},
		render_targets: {
			[format.id]: {
				url: pdfUrl,
				format: "pdf",
				dpi: 300,
				file_size_bytes: pdfSizeBytes,
				notes: `${format.name} format, grayscale, ${binding}. ${pageCount} pages.`,
			},
		},
		pricing: {
			creator_share_pct: req.pricing?.creator_share_pct ?? 30,
			community_share_pct: req.pricing?.community_share_pct ?? 10,
			...(req.pricing?.creator_min
				? { creator_min: { amount: req.pricing.creator_min.amount, currency: req.pricing.creator_min.currency || "USD" } }
				: {}),
			...(req.pricing?.suggested_retail
				? { suggested_retail: { amount: req.pricing.suggested_retail.amount, currency: req.pricing.suggested_retail.currency || "USD" } }
				: {}),
		},
		next_actions: [
			{ tool: "rcart.online", action: "list-for-sale", label: "Sell in community shop", endpoint: "/api/catalog/ingest", method: "POST" },
			{ tool: "rpubs.online", action: "reformat", label: "Generate another format", endpoint: "/api/reformat", method: "POST" },
			{ tool: "rfiles.online", action: "archive", label: "Save to files", endpoint: "/api/v1/files/import", method: "POST" },
		],
	};
}

function escapeAttr(s: string): string {
	return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

function formatBytes(n: number): string {
	if (n < 1024) return `${n} B`;
	if (n < 1024 * 1024) return `${(n / 1024).toFixed(0)} KB`;
	return `${(n / (1024 * 1024)).toFixed(1)} MB`;
}

function renderReaderPage(opts: { record: import("./publications-store").PublicationRecord; spaceSlug: string }): string {
	const { record, spaceSlug } = opts;
	// Always emit canonical subdomain absolute URLs so links are correct
	// regardless of whether the user arrived via {space}.rspace.online/...
	// or rspace.online/{space}/... (internal path-scope rewriting form).
	const base = `https://${encodeURIComponent(record.space)}.rspace.online/rpubs/publications/${encodeURIComponent(record.slug)}`;
	const pdfUrl = `${base}/pdf`;
	const epubUrl = `${base}/epub`;
	const epubFixedUrl = record.fixedEpubBytes ? `${base}/epub-fixed` : null;

	return `
<style>
	.reader-wrap { max-width: 980px; margin: 0 auto; padding: 28px 20px 80px; color: var(--rs-text, #e5e7eb); }
	.reader-hero { margin-bottom: 20px; }
	.reader-title { font-size: 1.6rem; font-weight: 700; margin: 0 0 4px; }
	.reader-author { color: var(--rs-text-muted, #94a3b8); font-size: 0.95rem; }
	.reader-meta { color: var(--rs-text-muted, #94a3b8); font-size: 0.82rem; margin-top: 6px; }
	.reader-description { margin: 12px 0 0; line-height: 1.6; color: var(--rs-text, #cbd5e1); }
	.reader-flipbook { margin: 20px 0 28px; min-height: 420px; }
	.reader-downloads { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
	.dl-card { display: flex; flex-direction: column; gap: 4px; padding: 14px; background: var(--rs-surface, #1e293b); border: 1px solid var(--rs-border, #334155); border-radius: 8px; text-decoration: none; color: inherit; transition: all 0.15s; }
	.dl-card:hover { border-color: var(--rs-accent, #14b8a6); transform: translateY(-1px); }
	.dl-head { display: flex; justify-content: space-between; align-items: baseline; }
	.dl-title { font-weight: 600; font-size: 0.98rem; }
	.dl-size { color: var(--rs-text-muted, #94a3b8); font-size: 0.78rem; }
	.dl-sub { color: var(--rs-text-muted, #94a3b8); font-size: 0.82rem; }
	.reader-share { margin-top: 20px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
	.reader-share input { flex: 1; min-width: 200px; padding: 8px 10px; background: var(--rs-surface-alt, #0f172a); color: inherit; border: 1px solid var(--rs-border, #334155); border-radius: 6px; font-family: inherit; font-size: 0.85rem; }
	.reader-share button { padding: 8px 14px; background: var(--rs-accent, #14b8a6); color: #fff; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem; }
	.reader-share button:hover { background: var(--rs-accent-hover, #0d9488); }
</style>

<div class="reader-wrap">
	<header class="reader-hero">
		<h1 class="reader-title">${escapeAttr(record.title)}</h1>
		${record.author ? `<div class="reader-author">by ${escapeAttr(record.author)}</div>` : ""}
		<div class="reader-meta">${escapeAttr(record.formatName)} · ${record.pageCount} pages · ${new Date(record.createdAt).toLocaleDateString()}</div>
		${record.description ? `<p class="reader-description">${escapeAttr(record.description)}</p>` : ""}
	</header>

	<div class="reader-flipbook">
		<folk-pubs-flipbook pdf-url="${pdfUrl}"></folk-pubs-flipbook>
	</div>

	<div class="reader-downloads">
		<a class="dl-card" href="${pdfUrl}" download>
			<div class="dl-head"><span class="dl-title">Download PDF</span><span class="dl-size">${formatBytes(record.pdfBytes)}</span></div>
			<span class="dl-sub">Print-ready, preserves design</span>
		</a>
		<a class="dl-card" href="${epubUrl}" download>
			<div class="dl-head"><span class="dl-title">EPUB · reflowable</span><span class="dl-size">${formatBytes(record.reflowableEpubBytes)}</span></div>
			<span class="dl-sub">Resizable text, works on any e-reader</span>
		</a>
		${epubFixedUrl ? `
		<a class="dl-card" href="${epubFixedUrl}" download>
			<div class="dl-head"><span class="dl-title">EPUB · fixed layout</span><span class="dl-size">${record.fixedEpubBytes ? formatBytes(record.fixedEpubBytes) : ""}</span></div>
			<span class="dl-sub">Preserves page design, for Kindle/iPad</span>
		</a>` : ""}
	</div>

	<div class="reader-share">
		<input type="text" id="share-url" readonly value="" />
		<button onclick="(() => { const el = document.getElementById('share-url'); el.select(); navigator.clipboard.writeText(el.value); const btn = event.target; const orig = btn.textContent; btn.textContent = 'Copied!'; setTimeout(() => btn.textContent = orig, 1500); })()">Copy Link</button>
	</div>
	<script>document.getElementById('share-url').value = window.location.href;</script>
</div>`;
}

// ── Routes ──

let _syncServer: SyncServer | null = null;

const routes = new Hono();

// ── API: List available formats ──
routes.get("/api/formats", (c) => {
	return c.json({ formats: listFormats() });
});

// ── API: Generate PDF (direct download) ──
routes.post("/api/generate", async (c) => {
	try {
		const body = await c.req.json();
		const { content, title, author, format: formatId } = body;

		if (!content || typeof content !== "string" || content.trim().length === 0) {
			return c.json({ error: "Content is required" }, 400);
		}

		if (!formatId || !getFormat(formatId)) {
			return c.json({ error: `Invalid format. Available: ${Object.keys(FORMATS).join(", ")}` }, 400);
		}

		const document = parseMarkdown(content, title, author);
		const result = await compileDocument({ document, formatId });

		const filename = `${document.title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${formatId}.pdf`;

		return new Response(new Uint8Array(result.pdf), {
			status: 200,
			headers: {
				"Content-Type": "application/pdf",
				"Content-Disposition": `attachment; filename="${filename}"`,
				"X-Page-Count": String(result.pageCount),
			},
		});
	} catch (error) {
		console.error("[Pubs] Generation error:", error);
		return c.json({ error: error instanceof Error ? error.message : "Generation failed" }, 500);
	}
});

// ── API: Create persistent artifact ──
routes.post("/api/artifact", async (c) => {
	try {
		const body: ArtifactRequest = await c.req.json();
		const { content, title, author, format: formatId } = body;

		if (!content || typeof content !== "string" || content.trim().length === 0) {
			return c.json({ error: "Content is required" }, 400);
		}

		const format = getFormat(formatId);
		if (!formatId || !format) {
			return c.json({ error: `Invalid format. Available: ${Object.keys(FORMATS).join(", ")}` }, 400);
		}

		if (body.tags && !Array.isArray(body.tags)) {
			return c.json({ error: "tags must be an array of strings" }, 400);
		}

		const document = parseMarkdown(content, title, author);
		const result = await compileDocument({ document, formatId });

		// Store artifact
		const artifactId = randomUUID();
		const artifactDir = join(ARTIFACTS_DIR, artifactId);
		await mkdir(artifactDir, { recursive: true });

		await writeFile(join(artifactDir, `${formatId}.pdf`), result.pdf);
		await writeFile(join(artifactDir, "source.md"), content);

		// Build envelope
		const proto = c.req.header("x-forwarded-proto") || "https";
		const host = c.req.header("host") || "rpubs.online";
		const baseUrl = `${proto}://${host}`;

		const artifact = buildArtifactEnvelope({
			id: artifactId,
			format,
			pageCount: result.pageCount,
			pdfSizeBytes: result.pdf.length,
			parsedTitle: document.title,
			req: body,
			baseUrl,
		});

		await writeFile(join(artifactDir, "artifact.json"), JSON.stringify(artifact, null, 2));

		return c.json(artifact, 201);
	} catch (error) {
		console.error("[Pubs] Artifact error:", error);
		return c.json({ error: error instanceof Error ? error.message : "Artifact generation failed" }, 500);
	}
});

// ── API: Get artifact (metadata or files) ──
routes.get("/api/artifact/:id", async (c) => {
	const id = c.req.param("id");

	if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) {
		return c.json({ error: "Invalid artifact ID" }, 400);
	}

	const artifactDir = join(ARTIFACTS_DIR, id);

	try {
		await stat(artifactDir);
	} catch {
		return c.json({ error: "Artifact not found" }, 404);
	}

	const fileType = c.req.query("file");

	if (fileType === "source") {
		try {
			const source = await readFile(join(artifactDir, "source.md"));
			return new Response(source, {
				headers: {
					"Content-Type": "text/markdown; charset=utf-8",
					"Content-Disposition": `inline; filename="source.md"`,
				},
			});
		} catch {
			return c.json({ error: "Source file not found" }, 404);
		}
	}

	if (fileType === "pdf") {
		const files = await readdir(artifactDir);
		const pdfFile = files.find((f) => f.endsWith(".pdf"));
		if (!pdfFile) return c.json({ error: "PDF not found" }, 404);

		const pdf = await readFile(join(artifactDir, pdfFile));
		return new Response(new Uint8Array(pdf), {
			headers: {
				"Content-Type": "application/pdf",
				"Content-Disposition": `inline; filename="${pdfFile}"`,
				"Content-Length": String(pdf.length),
			},
		});
	}

	// Default: return artifact JSON
	try {
		const artifactJson = await readFile(join(artifactDir, "artifact.json"), "utf-8");
		return new Response(artifactJson, {
			headers: { "Content-Type": "application/json" },
		});
	} catch {
		return c.json({ error: "Artifact metadata not found" }, 404);
	}
});

// ── API: Direct PDF access ──
routes.get("/api/artifact/:id/pdf", async (c) => {
	const id = c.req.param("id");

	if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) {
		return c.json({ error: "Invalid artifact ID" }, 400);
	}

	const artifactDir = join(ARTIFACTS_DIR, id);

	try {
		await stat(artifactDir);
	} catch {
		return c.json({ error: "Artifact not found" }, 404);
	}

	const files = await readdir(artifactDir);
	const pdfFile = files.find((f) => f.endsWith(".pdf"));
	if (!pdfFile) return c.json({ error: "PDF not found" }, 404);

	const pdf = await readFile(join(artifactDir, pdfFile));
	return new Response(new Uint8Array(pdf), {
		headers: {
			"Content-Type": "application/pdf",
			"Content-Disposition": `inline; filename="${pdfFile}"`,
			"Content-Length": String(pdf.length),
		},
	});
});

// ── API: Generate EPUB (reflowable or fixed-layout) ──
routes.post("/api/generate-epub", async (c) => {
	try {
		const body = await c.req.json();
		const { content, title, author, format: formatId, mode = "reflowable", description } = body;

		if (!content || typeof content !== "string" || content.trim().length === 0) {
			return c.json({ error: "Content is required" }, 400);
		}

		const epubMode = mode === "fixed" ? "fixed" : "reflowable";

		// Format is only required for fixed-layout (used by Typst).
		if (epubMode === "fixed" && (!formatId || !getFormat(formatId))) {
			return c.json({ error: `Invalid format. Available: ${Object.keys(FORMATS).join(", ")}` }, 400);
		}

		const document = parseMarkdown(content, title, author);
		const slug = (document.title || "document").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "document";

		let epubBuf: Buffer;
		let filename: string;
		let pageCount = 0;

		if (epubMode === "fixed") {
			const { pages } = await compileDocumentToPages({ document, formatId, ppi: 144 });
			pageCount = pages.length;
			epubBuf = await buildFixedLayoutEpub({ document, pages, formatId, description });
			filename = `${slug}-${formatId}-fixed.epub`;
		} else {
			epubBuf = await buildReflowableEpub({ document, description });
			filename = `${slug}.epub`;
		}

		return new Response(new Uint8Array(epubBuf), {
			status: 200,
			headers: {
				"Content-Type": "application/epub+zip",
				"Content-Disposition": `attachment; filename="${filename}"`,
				"Content-Length": String(epubBuf.length),
				"X-Epub-Mode": epubMode,
				...(pageCount ? { "X-Page-Count": String(pageCount) } : {}),
			},
		});
	} catch (error) {
		console.error("[Pubs] EPUB error:", error);
		return c.json({ error: error instanceof Error ? error.message : "EPUB generation failed" }, 500);
	}
});

// ── API: Publish to a space (persistent, publicly addressable) ──
routes.post("/api/publish", async (c) => {
	try {
		const body = await c.req.json();
		const {
			content,
			title,
			author,
			format: formatId,
			description,
			include_fixed_epub = false,
		} = body;

		if (!content || typeof content !== "string" || content.trim().length === 0) {
			return c.json({ error: "Content is required" }, 400);
		}

		const format = getFormat(formatId);
		if (!formatId || !format) {
			return c.json({ error: `Invalid format. Available: ${Object.keys(FORMATS).join(", ")}` }, 400);
		}

		const space = c.req.param("space") || c.get("effectiveSpace") || "personal";

		const document = parseMarkdown(content, title, author);

		// Compile PDF (always) + reflowable EPUB (always).
		const pdfResult = await compileDocument({ document, formatId });
		const reflowable = await buildReflowableEpub({ document, description });

		// Fixed EPUB only if requested — it's the costly path.
		let fixedEpub: Buffer | undefined;
		if (include_fixed_epub) {
			const { pages } = await compileDocumentToPages({ document, formatId, ppi: 144 });
			fixedEpub = await buildFixedLayoutEpub({ document, pages, formatId, description });
		}

		const record = await savePublication({
			space,
			title: document.title,
			author: document.author || author || "Unknown",
			description,
			formatId,
			formatName: format.name,
			pageCount: pdfResult.pageCount,
			sourceMarkdown: content,
			pdf: pdfResult.pdf,
			reflowableEpub: reflowable,
			fixedEpub,
		});

		// Canonical subdomain form: {space}.rspace.online/rpubs/publications/{slug}.
		// The server rewrites subdomain → path-scope internally, so this URL works
		// even though Hono routes are mounted at `/:space/rpubs/...`.
		const hostedUrl = `https://${record.space}.rspace.online/rpubs/publications/${record.slug}`;

		return c.json({
			...record,
			hosted_url: hostedUrl,
			hosted_path: `/rpubs/publications/${record.slug}`,
			pdf_url: `${hostedUrl}/pdf`,
			epub_url: `${hostedUrl}/epub`,
			epub_fixed_url: fixedEpub ? `${hostedUrl}/epub-fixed` : null,
		}, 201);
	} catch (error) {
		console.error("[Pubs] Publish error:", error);
		return c.json({ error: error instanceof Error ? error.message : "Publish failed" }, 500);
	}
});

// ── API: List publications in a space ──
routes.get("/api/publications", async (c) => {
	const space = c.req.param("space") || c.get("effectiveSpace") || "personal";
	const records = await listPublications(space);
	return c.json({ publications: records });
});

// ── API: Get publication metadata ──
routes.get("/api/publications/:slug", async (c) => {
	const space = c.req.param("space") || c.get("effectiveSpace") || "personal";
	const slug = c.req.param("slug");
	const record = await getPublication(space, slug);
	if (!record) return c.json({ error: "Publication not found" }, 404);
	return c.json(record);
});

// ── Public: Reader page ──
routes.get("/publications/:slug", async (c) => {
	const spaceSlug = c.req.param("space") || "personal";
	const dataSpace = c.get("effectiveSpace") || spaceSlug;
	const slug = c.req.param("slug");

	const record = await getPublication(dataSpace, slug);
	if (!record) {
		return c.html(`<!doctype html><meta charset="utf-8"><title>Not found</title><body style="font:14px system-ui;padding:40px;color:#ddd;background:#111"><h1>Publication not found</h1><p>No publication exists at this URL.</p><p><a style="color:#14b8a6" href="https://${encodeURIComponent(spaceSlug)}.rspace.online/rpubs">Back to rPubs</a></p></body>`, 404);
	}

	return c.html(renderShell({
		title: `${record.title} — ${spaceSlug} | rSpace`,
		moduleId: "rpubs",
		spaceSlug,
		modules: getModuleInfoList(),
		theme: "dark",
		body: renderReaderPage({ record, spaceSlug }),
		scripts: `<script type="module" src="/modules/rpubs/folk-pubs-flipbook.js?v=3"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rpubs/pubs.css">`,
	}));
});

// ── Public: Download PDF ──
routes.get("/publications/:slug/pdf", async (c) => {
	const space = c.req.param("space") || c.get("effectiveSpace") || "personal";
	const slug = c.req.param("slug");
	const buf = await getPublicationFile(space, slug, "pdf");
	if (!buf) return c.json({ error: "PDF not found" }, 404);
	return new Response(new Uint8Array(buf), {
		headers: {
			"Content-Type": "application/pdf",
			"Content-Disposition": `inline; filename="${slugifyPub(slug)}.pdf"`,
			"Content-Length": String(buf.length),
		},
	});
});

// ── Public: Download EPUB (reflowable default) ──
routes.get("/publications/:slug/epub", async (c) => {
	const space = c.req.param("space") || c.get("effectiveSpace") || "personal";
	const slug = c.req.param("slug");
	const buf = await getPublicationFile(space, slug, "reflowable.epub");
	if (!buf) return c.json({ error: "EPUB not found" }, 404);
	return new Response(new Uint8Array(buf), {
		headers: {
			"Content-Type": "application/epub+zip",
			"Content-Disposition": `attachment; filename="${slugifyPub(slug)}.epub"`,
			"Content-Length": String(buf.length),
		},
	});
});

// ── Public: Download fixed-layout EPUB (if available) ──
routes.get("/publications/:slug/epub-fixed", async (c) => {
	const space = c.req.param("space") || c.get("effectiveSpace") || "personal";
	const slug = c.req.param("slug");
	const buf = await getPublicationFile(space, slug, "fixed.epub");
	if (!buf) return c.json({ error: "Fixed-layout EPUB not available for this publication" }, 404);
	return new Response(new Uint8Array(buf), {
		headers: {
			"Content-Type": "application/epub+zip",
			"Content-Disposition": `attachment; filename="${slugifyPub(slug)}-fixed.epub"`,
			"Content-Length": String(buf.length),
		},
	});
});

// ── API: Generate imposition PDF ──
routes.post("/api/imposition", async (c) => {
	try {
		const body = await c.req.json();
		const { content, title, author, format: formatId } = body;

		if (!content || typeof content !== "string" || content.trim().length === 0) {
			return c.json({ error: "Content is required" }, 400);
		}

		if (!formatId || !getFormat(formatId)) {
			return c.json({ error: `Invalid format. Available: ${Object.keys(FORMATS).join(", ")}` }, 400);
		}

		const document = parseMarkdown(content, title, author);
		const result = await compileDocument({ document, formatId });

		const imposition = await generateImposition(result.pdf, formatId);
		const filename = `${document.title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${formatId}-imposition.pdf`;

		return new Response(new Uint8Array(imposition.pdf), {
			status: 200,
			headers: {
				"Content-Type": "application/pdf",
				"Content-Disposition": `attachment; filename="${filename}"`,
				"X-Sheet-Count": String(imposition.sheetCount),
				"X-Page-Count": String(imposition.pageCount),
			},
		});
	} catch (error) {
		console.error("[Pubs] Imposition error:", error);
		return c.json({ error: error instanceof Error ? error.message : "Imposition generation failed" }, 500);
	}
});

// ── API: Email PDF ──
routes.post("/api/email-pdf", async (c) => {
	try {
		const body = await c.req.json();
		const { content, title, author, format: formatId, email } = body;

		if (!content || typeof content !== "string" || content.trim().length === 0) {
			return c.json({ error: "Content is required" }, 400);
		}
		if (!email || typeof email !== "string" || !email.includes("@")) {
			return c.json({ error: "Valid email is required" }, 400);
		}

		const format = getFormat(formatId);
		if (!formatId || !format) {
			return c.json({ error: `Invalid format. Available: ${Object.keys(FORMATS).join(", ")}` }, 400);
		}

		const ip = c.req.header("x-forwarded-for") || c.req.header("x-real-ip") || "unknown";
		if (!checkEmailRate(ip)) {
			return c.json({ error: "Rate limit exceeded (5 emails/hour). Try again later." }, 429);
		}

		const transport = getSmtpTransport();
		if (!transport) {
			return c.json({ error: "Email service not configured" }, 503);
		}

		const document = parseMarkdown(content, title, author);
		const result = await compileDocument({ document, formatId });

		const slug = (title || document.title || "document")
			.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");

		const fromAddr = process.env.SMTP_FROM || process.env.SMTP_USER || "noreply@rmail.online";

		await transport.sendMail({
			from: `"rPubs Press" <${fromAddr}>`,
			to: email,
			subject: `Your publication: ${title || document.title || "Untitled"}`,
			text: [
				`Here's your publication from rPubs Pocket Press.`,
				``,
				`Title: ${title || document.title || "Untitled"}`,
				author ? `Author: ${author}` : null,
				`Format: ${format.name} (${format.widthMm}\u00D7${format.heightMm}mm)`,
				`Pages: ${result.pageCount}`,
				``,
				`---`,
				`rPubs \u00B7 Community pocket press`,
				`https://rpubs.online`,
			].filter(Boolean).join("\n"),
			html: [
				`<div style="font-family: system-ui, sans-serif; max-width: 480px; margin: 0 auto; padding: 24px;">`,
				`<h2 style="margin: 0 0 8px; font-size: 18px;">Your publication is ready</h2>`,
				`<p style="color: #64748b; margin: 0 0 16px; font-size: 14px;">`,
				`<strong>${title || document.title || "Untitled"}</strong>`,
				author ? ` by ${author}` : "",
				`</p>`,
				`<table style="font-size: 13px; color: #475569; margin-bottom: 16px;">`,
				`<tr><td style="padding: 2px 12px 2px 0; color: #94a3b8;">Format</td><td>${format.name}</td></tr>`,
				`<tr><td style="padding: 2px 12px 2px 0; color: #94a3b8;">Pages</td><td>${result.pageCount}</td></tr>`,
				`</table>`,
				`<p style="font-size: 13px; color: #64748b;">The PDF is attached below.</p>`,
				`<hr style="border: none; border-top: 1px solid #e2e8f0; margin: 20px 0;" />`,
				`<p style="font-size: 11px; color: #94a3b8;">rPubs &middot; Community pocket press &middot; <a href="https://rpubs.online" style="color: #5a9a7a;">rpubs.online</a></p>`,
				`</div>`,
			].join("\n"),
			attachments: [{
				filename: `${slug}-${formatId}.pdf`,
				content: Buffer.from(result.pdf),
				contentType: "application/pdf",
			}],
		});

		return c.json({ ok: true, message: `PDF sent to ${email}` });
	} catch (error) {
		console.error("[Pubs] Email error:", error);
		return c.json({ error: error instanceof Error ? error.message : "Failed to send email" }, 500);
	}
});

// ── API: Discover printers ──
routes.get("/api/printers", async (c) => {
	try {
		const lat = parseFloat(c.req.query("lat") || "");
		const lng = parseFloat(c.req.query("lng") || "");

		if (isNaN(lat) || isNaN(lng)) {
			return c.json({ error: "lat and lng are required" }, 400);
		}

		const radiusKm = parseFloat(c.req.query("radius") || "100");
		const formatId = c.req.query("format") || undefined;

		const providers = await discoverPrinters({ lat, lng, radiusKm, formatId });
		return c.json({ providers });
	} catch (error) {
		console.error("[Pubs] Printer discovery error:", error);
		return c.json({ error: error instanceof Error ? error.message : "Discovery failed" }, 500);
	}
});

// ── API: Place order (forward to rCart) ──
routes.post("/api/order", async (c) => {
	try {
		const body = await c.req.json();
		const { provider_id, total_price } = body;

		if (!provider_id || total_price === undefined) {
			return c.json({ error: "provider_id and total_price are required" }, 400);
		}

		// Generate artifact first if content is provided
		let artifactId = body.artifact_id;
		if (!artifactId && body.content) {
			const document = parseMarkdown(body.content, body.title, body.author);
			const formatId = body.format || "digest";
			const result = await compileDocument({ document, formatId });

			artifactId = randomUUID();
			const artifactDir = join(ARTIFACTS_DIR, artifactId);
			await mkdir(artifactDir, { recursive: true });
			await writeFile(join(artifactDir, `${formatId}.pdf`), result.pdf);
			await writeFile(join(artifactDir, "source.md"), body.content);
		}

		const orderRes = await fetch(`${RCART_URL}/api/orders`, {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify({
				catalog_entry_id: body.catalog_entry_id,
				artifact_id: artifactId,
				provider_id,
				provider_name: body.provider_name,
				provider_distance_km: body.provider_distance_km,
				quantity: body.quantity || 1,
				production_cost: body.production_cost,
				creator_payout: body.creator_payout,
				community_payout: body.community_payout,
				total_price,
				currency: body.currency || "USD",
				payment_method: "manual",
				buyer_contact: body.buyer_contact,
				buyer_location: body.buyer_location,
			}),
		});

		if (!orderRes.ok) {
			const err = await orderRes.json().catch(() => ({}));
			console.error("[Pubs] rCart order failed:", err);
			return c.json({ error: "Failed to create order" }, 502 as any);
		}

		const order = await orderRes.json();
		return c.json(order, 201);
	} catch (error) {
		console.error("[Pubs] Order error:", error);
		return c.json({ error: error instanceof Error ? error.message : "Order creation failed" }, 500);
	}
});

// ── API: Batch / group buy ──
routes.post("/api/batch", async (c) => {
	try {
		const body = await c.req.json();
		const { artifact_id, catalog_entry_id, provider_id, provider_name, buyer_contact, buyer_location, quantity = 1 } = body;

		if (!artifact_id && !catalog_entry_id) {
			return c.json({ error: "artifact_id or catalog_entry_id required" }, 400);
		}
		if (!provider_id) {
			return c.json({ error: "provider_id required" }, 400);
		}

		// Check for existing open batch
		const searchParams = new URLSearchParams({
			artifact_id: artifact_id || catalog_entry_id,
			status: "open",
			...(provider_id && { provider_id }),
		});

		const existingRes = await fetch(`${RCART_URL}/api/batches?${searchParams}`);
		const existingData = await existingRes.json();
		const openBatches = (existingData.batches || []).filter(
			(b: { provider_id: string }) => b.provider_id === provider_id
		);

		if (openBatches.length > 0) {
			const batch = openBatches[0];
			const joinRes = await fetch(`${RCART_URL}/api/batches/${batch.id}/join`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ buyer_contact, buyer_location, quantity }),
			});

			if (!joinRes.ok) {
				const err = await joinRes.json().catch(() => ({}));
				return c.json({ error: (err as any).error || "Failed to join batch" }, 502 as any);
			}

			const result = await joinRes.json();
			return c.json({ action: "joined", ...result });
		}

		// Create new batch
		const createRes = await fetch(`${RCART_URL}/api/batches`, {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify({ catalog_entry_id, artifact_id, provider_id, provider_name }),
		});

		if (!createRes.ok) {
			const err = await createRes.json().catch(() => ({}));
			return c.json({ error: (err as any).error || "Failed to create batch" }, 502 as any);
		}

		const batch = await createRes.json();
		const joinRes = await fetch(`${RCART_URL}/api/batches/${batch.id}/join`, {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify({ buyer_contact, buyer_location, quantity }),
		});

		if (!joinRes.ok) {
			return c.json({ action: "created", batch, member: null }, 201);
		}

		const joinResult = await joinRes.json();
		return c.json({ action: "created", ...joinResult }, 201);
	} catch (error) {
		console.error("[Pubs] Batch error:", error);
		return c.json({ error: error instanceof Error ? error.message : "Batch operation failed" }, 500);
	}
});

routes.get("/api/batch", async (c) => {
	const artifactId = c.req.query("artifact_id");
	const providerId = c.req.query("provider_id");

	if (!artifactId) {
		return c.json({ error: "artifact_id required" }, 400);
	}

	const params = new URLSearchParams({ artifact_id: artifactId, status: "open" });
	if (providerId) params.set("provider_id", providerId);

	try {
		const res = await fetch(`${RCART_URL}/api/batches?${params}`);
		if (!res.ok) return c.json({ batches: [] });
		const data = await res.json();
		return c.json(data);
	} catch {
		return c.json({ batches: [] });
	}
});

// ── CRUD: Drafts (Automerge) ──

routes.get("/api/drafts", (c) => {
    if (!_syncServer) return c.json({ drafts: [] });
    const space = c.req.param("space") || "demo";
    const prefix = `${space}:pubs:drafts:`;
    const drafts: any[] = [];
    for (const docId of _syncServer.listDocs()) {
        if (!docId.startsWith(prefix)) continue;
        const doc = _syncServer.getDoc<PubsDoc>(docId);
        if (!doc?.draft) continue;
        drafts.push({ id: doc.draft.id, title: doc.draft.title, author: doc.draft.author, format: doc.draft.format, createdAt: doc.draft.createdAt, updatedAt: doc.draft.updatedAt });
    }
    return c.json({ drafts: drafts.sort((a, b) => b.updatedAt - a.updatedAt) });
});

routes.post("/api/drafts", async (c) => {
    const authToken = extractToken(c.req.raw.headers);
    if (!authToken) return c.json({ error: "Authentication required" }, 401);
    try { await verifyToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); }
    if (!_syncServer) return c.json({ error: "Not initialized" }, 503);
    const space = c.req.param("space") || "demo";
    const { title = "Untitled", author = "", format = "digest", content = "" } = await c.req.json();
    const id = crypto.randomUUID();
    const docId = pubsDocId(space, id);
    const now = Date.now();
    const doc = Automerge.change(Automerge.init<PubsDoc>(), 'create draft', (d) => {
        const init = pubsDraftSchema.init();
        Object.assign(d, init);
        d.meta.spaceSlug = space;
        d.draft.id = id;
        d.draft.title = title;
        d.draft.author = author;
        d.draft.format = format;
        d.draft.createdAt = now;
        d.draft.updatedAt = now;
        d.content = content;
    });
    _syncServer.setDoc(docId, doc);
    return c.json({ id, title, author, format }, 201);
});

routes.delete("/api/drafts/:id", async (c) => {
    const authToken = extractToken(c.req.raw.headers);
    if (!authToken) return c.json({ error: "Authentication required" }, 401);
    try { await verifyToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); }
    if (!_syncServer) return c.json({ error: "Not initialized" }, 503);
    const space = c.req.param("space") || "demo";
    const id = c.req.param("id");
    const docId = pubsDocId(space, id);
    const doc = _syncServer.getDoc<PubsDoc>(docId);
    if (!doc) return c.json({ error: "Not found" }, 404);
    _syncServer.changeDoc<PubsDoc>(docId, `delete draft ${id}`, (d) => {
        d.draft.title = '[deleted]';
        d.content = '';
    });
    return c.json({ ok: true });
});

// ── Page: Zine Generator (redirect to canvas with auto-spawn) ──
routes.get("/zine", (c) => {
	const spaceSlug = c.req.param("space") || "personal";
	return c.redirect(`/${spaceSlug}?tool=folk-zine-gen`);
});

// ── Page: Editor (also served at /press) ──
routes.get("/press", (c) => {
	const spaceSlug = c.req.param("space") || "personal";
	const dataSpace = c.get("effectiveSpace") || spaceSlug;
	return c.html(renderShell({
		title: `${spaceSlug} — rPubs Press | rSpace`,
		moduleId: "rpubs",
		spaceSlug,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-pubs-editor space="${spaceSlug}"></folk-pubs-editor>`,
		scripts: `<script type="module" src="/modules/rpubs/folk-pubs-editor.js?v=3"></script>
			<script type="module" src="/modules/rpubs/folk-pubs-flipbook.js?v=3"></script>
			<script type="module" src="/modules/rpubs/folk-pubs-publish-panel.js?v=3"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rpubs/pubs.css">`,
	}));
});

routes.get("/", (c) => {
	const spaceSlug = c.req.param("space") || "personal";
	const dataSpace = c.get("effectiveSpace") || spaceSlug;
	return c.html(renderShell({
		title: `${spaceSlug} — rPubs Editor | rSpace`,
		moduleId: "rpubs",
		spaceSlug,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-pubs-editor space="${spaceSlug}"></folk-pubs-editor>`,
		scripts: `<script type="module" src="/modules/rpubs/folk-pubs-editor.js?v=3"></script>
			<script type="module" src="/modules/rpubs/folk-pubs-flipbook.js?v=3"></script>
			<script type="module" src="/modules/rpubs/folk-pubs-publish-panel.js?v=3"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rpubs/pubs.css">`,
	}));
});

export function getRecentPublicationsForMI(space: string, limit = 5): { id: string; title: string; author: string; format: string; updatedAt: number }[] {
    if (!_syncServer) return [];
    const prefix = `${space}:pubs:drafts:`;
    const items: { id: string; title: string; author: string; format: string; updatedAt: number }[] = [];
    for (const docId of _syncServer.listDocs()) {
        if (!docId.startsWith(prefix)) continue;
        const doc = _syncServer.getDoc<PubsDoc>(docId);
        if (!doc?.draft || doc.draft.title === '[deleted]') continue;
        items.push({ id: doc.draft.id, title: doc.draft.title, author: doc.draft.author, format: doc.draft.format, updatedAt: doc.draft.updatedAt });
    }
    return items.sort((a, b) => b.updatedAt - a.updatedAt).slice(0, limit);
}

// ── Module export ──

export const pubsModule: RSpaceModule = {
	id: "rpubs",
	name: "rPubs",
	icon: "📖",
	description: "Drop in a document, get a pocket book",
	scoping: { defaultScope: 'global', userConfigurable: true },
	routes,
	docSchemas: [{ pattern: '{space}:pubs:drafts:{draftId}', description: 'One doc per publication draft', init: pubsDraftSchema.init }],
	async onInit(ctx) { _syncServer = ctx.syncServer; },
	publicWrite: true,
	standaloneDomain: "rpubs.online",
	landingPage: renderLanding,
	feeds: [
		{
			id: "publications",
			name: "Publications",
			kind: "data",
			description: "Print-ready artifacts generated from markdown documents",
			filterable: true,
		},
		{
			id: "citations",
			name: "Citations",
			kind: "data",
			description: "Citation and reference data extracted from documents",
		},
	],
	acceptsFeeds: ["data"],
	outputPaths: [
		{ path: "publications", name: "Publications", icon: "📖", description: "Published pocket books and zines" },
		{ path: "drafts", name: "Drafts", icon: "📝", description: "Work-in-progress documents" },
	],
};
