/**
 * EPUB3 generator — reflowable (from markdown) + fixed-layout (from Typst PNGs).
 *
 * Produces valid EPUB3 packages using JSZip (already a dep). No external tools.
 *
 * Reflowable mode: markdown sections → styled xhtml chapters. Resizable text,
 * tiny file, reads well on any device.
 *
 * Fixed-layout mode: each page of the compiled Typst PDF is embedded as a PNG
 * image in a pre-paginated xhtml page — preserves the pocket-book design 1:1.
 */

import JSZip from "jszip";
import { marked } from "marked";
import { randomUUID } from "node:crypto";
import type { ParsedDocument, DocumentSection, DocumentBlock } from "./parse-document";
import type { PageImage } from "./typst-compile";
import { getFormat } from "./formats";

// ── Shared helpers ──

function esc(s: string): string {
	return String(s)
		.replace(/&/g, "&amp;")
		.replace(/</g, "&lt;")
		.replace(/>/g, "&gt;")
		.replace(/"/g, "&quot;")
		.replace(/'/g, "&#39;");
}

function slug(s: string): string {
	return s
		.toLowerCase()
		.replace(/[^a-z0-9]+/g, "-")
		.replace(/^-+|-+$/g, "")
		.slice(0, 60) || "doc";
}

function containerXml(): string {
	return `<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
  <rootfiles>
    <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
  </rootfiles>
</container>`;
}

interface ManifestItem {
	id: string;
	href: string;
	mediaType: string;
	properties?: string;
	inSpine?: boolean;
	spineProperties?: string;
}

function buildPackageOpf(opts: {
	uuid: string;
	title: string;
	author: string;
	language: string;
	description?: string;
	items: ManifestItem[];
	fixedLayout?: boolean;
	coverImageId?: string;
}): string {
	const { uuid, title, author, language, description, items, fixedLayout, coverImageId } = opts;
	const now = new Date().toISOString().replace(/\.\d+Z$/, "Z");

	const manifest = items
		.map((i) => {
			const attrs = [
				`id="${esc(i.id)}"`,
				`href="${esc(i.href)}"`,
				`media-type="${esc(i.mediaType)}"`,
			];
			if (i.properties) attrs.push(`properties="${esc(i.properties)}"`);
			return `    <item ${attrs.join(" ")}/>`;
		})
		.join("\n");

	const spine = items
		.filter((i) => i.inSpine)
		.map((i) => {
			const attrs = [`idref="${esc(i.id)}"`];
			if (i.spineProperties) attrs.push(`properties="${esc(i.spineProperties)}"`);
			return `    <itemref ${attrs.join(" ")}/>`;
		})
		.join("\n");

	const layoutMeta = fixedLayout
		? `
    <meta property="rendition:layout">pre-paginated</meta>
    <meta property="rendition:orientation">auto</meta>
    <meta property="rendition:spread">auto</meta>`
		: "";

	const coverMeta = coverImageId
		? `\n    <meta name="cover" content="${esc(coverImageId)}"/>`
		: "";

	return `<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="pub-id" xml:lang="${esc(language)}">
  <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
    <dc:identifier id="pub-id">urn:uuid:${esc(uuid)}</dc:identifier>
    <dc:title>${esc(title)}</dc:title>
    <dc:creator>${esc(author)}</dc:creator>
    <dc:language>${esc(language)}</dc:language>
    ${description ? `<dc:description>${esc(description)}</dc:description>` : ""}
    <meta property="dcterms:modified">${now}</meta>${layoutMeta}${coverMeta}
  </metadata>
  <manifest>
${manifest}
  </manifest>
  <spine>
${spine}
  </spine>
</package>`;
}

function buildNav(items: { href: string; title: string }[], language: string): string {
	return `<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="${esc(language)}">
<head><meta charset="utf-8"/><title>Contents</title></head>
<body>
  <nav epub:type="toc" id="toc"><h1>Contents</h1>
    <ol>
${items.map((i) => `      <li><a href="${esc(i.href)}">${esc(i.title)}</a></li>`).join("\n")}
    </ol>
  </nav>
</body>
</html>`;
}

function buildNcx(opts: { uuid: string; title: string; items: { href: string; title: string }[] }): string {
	const { uuid, title, items } = opts;
	return `<?xml version="1.0" encoding="UTF-8"?>
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
  <head>
    <meta name="dtb:uid" content="urn:uuid:${esc(uuid)}"/>
    <meta name="dtb:depth" content="1"/>
    <meta name="dtb:totalPageCount" content="0"/>
    <meta name="dtb:maxPageNumber" content="0"/>
  </head>
  <docTitle><text>${esc(title)}</text></docTitle>
  <navMap>
${items
	.map(
		(it, i) => `    <navPoint id="np-${i + 1}" playOrder="${i + 1}">
      <navLabel><text>${esc(it.title)}</text></navLabel>
      <content src="${esc(it.href)}"/>
    </navPoint>`,
	)
	.join("\n")}
  </navMap>
</ncx>`;
}

async function finalizeZip(zip: JSZip): Promise<Buffer> {
	const data = await zip.generateAsync({
		type: "nodebuffer",
		compression: "DEFLATE",
		compressionOptions: { level: 9 },
		mimeType: "application/epub+zip",
	});
	return data as Buffer;
}

function seedEpubSkeleton(): JSZip {
	const zip = new JSZip();
	// mimetype MUST be first and uncompressed.
	zip.file("mimetype", "application/epub+zip", { compression: "STORE" });
	zip.folder("META-INF")!.file("container.xml", containerXml());
	return zip;
}

// ── Reflowable ──

function renderBlockToHtml(block: DocumentBlock): string {
	switch (block.type) {
		case "paragraph":
			return `<p>${marked.parseInline(block.text) as string}</p>`;
		case "quote": {
			const inner = marked.parseInline(block.text) as string;
			const attrib = block.attribution
				? `<footer>— ${esc(block.attribution)}</footer>`
				: "";
			return `<blockquote><p>${inner}</p>${attrib}</blockquote>`;
		}
		case "list": {
			const tag = block.ordered ? "ol" : "ul";
			const items = block.items
				.map((i) => `<li>${marked.parseInline(i) as string}</li>`)
				.join("");
			return `<${tag}>${items}</${tag}>`;
		}
		case "code": {
			const lang = block.language ? ` class="language-${esc(block.language)}"` : "";
			return `<pre><code${lang}>${esc(block.code)}</code></pre>`;
		}
		case "separator":
			return `<hr/>`;
	}
}

function renderSectionXhtml(section: DocumentSection, language: string): string {
	const body = [
		section.heading
			? `<h${Math.min(section.level || 2, 6)}>${esc(section.heading)}</h${Math.min(section.level || 2, 6)}>`
			: "",
		...section.blocks.map(renderBlockToHtml),
	]
		.filter(Boolean)
		.join("\n");

	return `<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="${esc(language)}">
<head>
  <meta charset="utf-8"/>
  <title>${esc(section.heading || "Section")}</title>
  <link rel="stylesheet" type="text/css" href="styles/book.css"/>
</head>
<body>
${body}
</body>
</html>`;
}

const REFLOWABLE_CSS = `@namespace epub "http://www.idpf.org/2007/ops";
html, body { margin: 0; padding: 0; }
body { font-family: Georgia, "Times New Roman", serif; line-height: 1.55; padding: 0 1em; }
h1, h2, h3, h4 { font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; line-height: 1.25; margin: 1.2em 0 0.5em; }
h1 { font-size: 1.6em; }
h2 { font-size: 1.35em; }
h3 { font-size: 1.15em; }
p { margin: 0.6em 0; text-align: justify; hyphens: auto; -webkit-hyphens: auto; }
blockquote { margin: 1em 1.5em; font-style: italic; color: #444; border-left: 2px solid #999; padding-left: 0.8em; }
blockquote footer { font-style: normal; font-size: 0.9em; color: #666; margin-top: 0.3em; }
pre { font-family: "SF Mono", Menlo, Consolas, monospace; font-size: 0.85em; background: #f4f4f4; padding: 0.6em 0.8em; border-radius: 3px; overflow-x: auto; white-space: pre-wrap; }
code { font-family: "SF Mono", Menlo, Consolas, monospace; font-size: 0.9em; }
ul, ol { margin: 0.6em 0 0.6em 1.5em; }
li { margin: 0.2em 0; }
hr { border: none; border-top: 1px solid #ccc; margin: 2em auto; width: 40%; }
img { max-width: 100%; height: auto; }
.title-page { text-align: center; padding-top: 20%; }
.title-page h1 { font-size: 2.2em; margin-bottom: 0.3em; }
.title-page .author { font-style: italic; color: #555; margin-top: 1em; }`;

export async function buildReflowableEpub(opts: {
	document: ParsedDocument;
	language?: string;
	description?: string;
}): Promise<Buffer> {
	const { document, language = "en", description } = opts;
	const uuid = randomUUID();
	const zip = seedEpubSkeleton();
	const oebps = zip.folder("OEBPS")!;

	oebps.folder("styles")!.file("book.css", REFLOWABLE_CSS);

	// Title page
	const titlePage = `<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="${esc(language)}">
<head><meta charset="utf-8"/><title>${esc(document.title)}</title><link rel="stylesheet" type="text/css" href="styles/book.css"/></head>
<body><div class="title-page"><h1>${esc(document.title)}</h1>${document.author ? `<div class="author">${esc(document.author)}</div>` : ""}</div></body>
</html>`;
	oebps.file("title.xhtml", titlePage);

	const navItems: { href: string; title: string }[] = [
		{ href: "title.xhtml", title: document.title },
	];
	const manifestItems: ManifestItem[] = [
		{ id: "css", href: "styles/book.css", mediaType: "text/css" },
		{ id: "title", href: "title.xhtml", mediaType: "application/xhtml+xml", inSpine: true },
	];

	document.sections.forEach((section, idx) => {
		const href = `sections/section-${String(idx + 1).padStart(3, "0")}.xhtml`;
		oebps.folder("sections")!.file(
			`section-${String(idx + 1).padStart(3, "0")}.xhtml`,
			renderSectionXhtml(section, language),
		);
		manifestItems.push({
			id: `sec${idx + 1}`,
			href,
			mediaType: "application/xhtml+xml",
			inSpine: true,
		});
		if (section.heading) navItems.push({ href, title: section.heading });
	});

	// nav.xhtml (EPUB3) + toc.ncx (back-compat)
	oebps.file("nav.xhtml", buildNav(navItems, language));
	manifestItems.push({
		id: "nav",
		href: "nav.xhtml",
		mediaType: "application/xhtml+xml",
		properties: "nav",
	});

	oebps.file("toc.ncx", buildNcx({ uuid, title: document.title, items: navItems }));
	manifestItems.push({ id: "ncx", href: "toc.ncx", mediaType: "application/x-dtbncx+xml" });

	oebps.file(
		"content.opf",
		buildPackageOpf({
			uuid,
			title: document.title,
			author: document.author || "Unknown",
			language,
			description,
			items: manifestItems,
		}),
	);

	return finalizeZip(zip);
}

// ── Fixed-layout ──

const FIXED_CSS = `@namespace epub "http://www.idpf.org/2007/ops";
html, body { margin: 0; padding: 0; overflow: hidden; }
.page { position: absolute; inset: 0; width: 100%; height: 100%; }
.page img { width: 100%; height: 100%; object-fit: contain; display: block; }`;

function renderFixedPageXhtml(opts: {
	imagePath: string;
	width: number;
	height: number;
	pageNum: number;
	language: string;
}): string {
	const { imagePath, width, height, pageNum, language } = opts;
	return `<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="${esc(language)}">
<head>
  <meta charset="utf-8"/>
  <title>Page ${pageNum}</title>
  <meta name="viewport" content="width=${width}, height=${height}"/>
  <link rel="stylesheet" type="text/css" href="styles/page.css"/>
</head>
<body>
  <div class="page"><img src="${esc(imagePath)}" alt="Page ${pageNum}"/></div>
</body>
</html>`;
}

export async function buildFixedLayoutEpub(opts: {
	document: ParsedDocument;
	pages: PageImage[];
	formatId: string;
	language?: string;
	description?: string;
}): Promise<Buffer> {
	const { document, pages, formatId, language = "en", description } = opts;
	if (pages.length === 0) {
		throw new Error("No pages to package");
	}

	const uuid = randomUUID();
	const zip = seedEpubSkeleton();
	const oebps = zip.folder("OEBPS")!;

	oebps.folder("styles")!.file("page.css", FIXED_CSS);

	const manifestItems: ManifestItem[] = [
		{ id: "css", href: "styles/page.css", mediaType: "text/css" },
	];
	const navItems: { href: string; title: string }[] = [];

	pages.forEach((page, i) => {
		const num = i + 1;
		const pad = String(num).padStart(4, "0");
		const imgHref = `images/page-${pad}.png`;
		const xhtmlHref = `pages/page-${pad}.xhtml`;

		oebps.folder("images")!.file(`page-${pad}.png`, page.png);
		oebps.folder("pages")!.file(
			`page-${pad}.xhtml`,
			renderFixedPageXhtml({
				imagePath: `../${imgHref}`,
				width: page.widthPx,
				height: page.heightPx,
				pageNum: num,
				language,
			}),
		);

		manifestItems.push({
			id: `img-${pad}`,
			href: imgHref,
			mediaType: "image/png",
			properties: i === 0 ? "cover-image" : undefined,
		});
		manifestItems.push({
			id: `page-${pad}`,
			href: xhtmlHref,
			mediaType: "application/xhtml+xml",
			inSpine: true,
			spineProperties: i === 0 ? undefined : num % 2 === 0 ? "page-spread-left" : "page-spread-right",
		});

		if (i === 0) navItems.push({ href: xhtmlHref, title: document.title });
	});

	// Supplemental TOC entries for long docs
	if (pages.length > 4) {
		navItems.push({
			href: `pages/page-${String(Math.floor(pages.length / 2)).padStart(4, "0")}.xhtml`,
			title: "Middle",
		});
		navItems.push({
			href: `pages/page-${String(pages.length).padStart(4, "0")}.xhtml`,
			title: "End",
		});
	}

	oebps.file("nav.xhtml", buildNav(navItems, language));
	manifestItems.push({
		id: "nav",
		href: "nav.xhtml",
		mediaType: "application/xhtml+xml",
		properties: "nav",
	});

	oebps.file("toc.ncx", buildNcx({ uuid, title: document.title, items: navItems }));
	manifestItems.push({ id: "ncx", href: "toc.ncx", mediaType: "application/x-dtbncx+xml" });

	const fmt = getFormat(formatId);
	const fmtDesc = fmt ? `${fmt.name} fixed-layout EPUB from rPubs` : undefined;

	oebps.file(
		"content.opf",
		buildPackageOpf({
			uuid,
			title: document.title,
			author: document.author || "Unknown",
			language,
			description: description || fmtDesc,
			items: manifestItems,
			fixedLayout: true,
			coverImageId: "img-0001",
		}),
	);

	return finalizeZip(zip);
}
