/**
 * Books module — community PDF library with flipbook reader.
 *
 * Ported from rbooks-online (Next.js) to Hono routes.
 * Routes are relative to mount point (/:space/books in unified, / in standalone).
 */

import { Hono } from "hono";
import { resolve } from "node:path";
import { mkdir, readFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { sql } from "../../shared/db/pool";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule, SpaceLifecycleContext } from "../../shared/module";
import { renderLanding } from "./landing";
import {
	verifyEncryptIDToken,
	extractToken,
} from "@encryptid/sdk/server";

const BOOKS_DIR = process.env.BOOKS_DIR || "/data/books";

// ── Types ──

export interface BookRow {
	id: string;
	slug: string;
	title: string;
	author: string | null;
	description: string | null;
	pdf_path: string;
	pdf_size_bytes: number;
	page_count: number;
	tags: string[];
	license: string;
	cover_color: string;
	contributor_id: string | null;
	contributor_name: string | null;
	status: string;
	featured: boolean;
	view_count: number;
	download_count: number;
	created_at: string;
	updated_at: string;
}

// ── Helpers ──

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

// ── Routes ──

const routes = new Hono();

// ── API: List books ──
routes.get("/api/books", async (c) => {
	const search = c.req.query("search");
	const tag = c.req.query("tag");
	const limit = Math.min(parseInt(c.req.query("limit") || "50"), 100);
	const offset = parseInt(c.req.query("offset") || "0");

	let query = `SELECT id, slug, title, author, description, pdf_size_bytes,
		page_count, tags, cover_color, contributor_name, featured,
		view_count, created_at
		FROM rbooks.books WHERE status = 'published'`;
	const params: (string | number)[] = [];

	if (search) {
		params.push(`%${search}%`);
		query += ` AND (title ILIKE $${params.length} OR author ILIKE $${params.length} OR description ILIKE $${params.length})`;
	}
	if (tag) {
		params.push(tag);
		query += ` AND $${params.length} = ANY(tags)`;
	}

	query += ` ORDER BY featured DESC, created_at DESC`;
	params.push(limit);
	query += ` LIMIT $${params.length}`;
	params.push(offset);
	query += ` OFFSET $${params.length}`;

	const rows = await sql.unsafe(query, params);
	return c.json({ books: rows });
});

// ── API: Upload book ──
routes.post("/api/books", 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 formData = await c.req.formData();
	const file = formData.get("pdf") as File | null;
	const title = (formData.get("title") as string || "").trim();
	const author = (formData.get("author") as string || "").trim() || null;
	const description = (formData.get("description") as string || "").trim() || null;
	const tagsRaw = (formData.get("tags") as string || "").trim();
	const license = (formData.get("license") as string || "CC BY-SA 4.0").trim();

	if (!file || file.type !== "application/pdf") {
		return c.json({ error: "PDF file required" }, 400);
	}
	if (!title) {
		return c.json({ error: "Title required" }, 400);
	}

	const tags = tagsRaw ? tagsRaw.split(",").map((t) => t.trim()).filter(Boolean) : [];
	const shortId = randomUUID().slice(0, 8);
	let slug = slugify(title);

	// Check slug collision
	const existing = await sql.unsafe(
		`SELECT 1 FROM rbooks.books WHERE slug = $1`, [slug]
	);
	if (existing.length > 0) {
		slug = `${slug}-${shortId}`;
	}

	// Save PDF to disk
	await mkdir(BOOKS_DIR, { recursive: true });
	const filename = `${slug}.pdf`;
	const filepath = resolve(BOOKS_DIR, filename);
	const buffer = Buffer.from(await file.arrayBuffer());
	await Bun.write(filepath, buffer);

	// Insert into DB
	const rows = await sql.unsafe(
		`INSERT INTO rbooks.books (slug, title, author, description, pdf_path, pdf_size_bytes, tags, license, contributor_id, contributor_name)
		 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
		 RETURNING id, slug, title, author, description, tags, created_at`,
		[slug, title, author, description, filename, buffer.length, tags, license, claims.sub, claims.username || null]
	);

	return c.json(rows[0], 201);
});

// ── API: Get book details ──
routes.get("/api/books/:id", async (c) => {
	const id = c.req.param("id");

	const rows = await sql.unsafe(
		`SELECT * FROM rbooks.books WHERE (slug = $1 OR id::text = $1) AND status = 'published'`,
		[id]
	);

	if (rows.length === 0) return c.json({ error: "Book not found" }, 404);

	// Increment view count
	await sql.unsafe(
		`UPDATE rbooks.books SET view_count = view_count + 1 WHERE id = $1`,
		[rows[0].id]
	);

	return c.json(rows[0]);
});

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

	const rows = await sql.unsafe(
		`SELECT id, slug, title, pdf_path FROM rbooks.books WHERE (slug = $1 OR id::text = $1) AND status = 'published'`,
		[id]
	);

	if (rows.length === 0) return c.json({ error: "Book not found" }, 404);

	const book = rows[0];
	const filepath = resolve(BOOKS_DIR, book.pdf_path);
	const file = Bun.file(filepath);

	if (!(await file.exists())) {
		return c.json({ error: "PDF file not found" }, 404);
	}

	// Increment download count
	await sql.unsafe(
		`UPDATE rbooks.books SET download_count = download_count + 1 WHERE id = $1`,
		[book.id]
	);

	return new Response(file, {
		headers: {
			"Content-Type": "application/pdf",
			"Content-Disposition": `inline; filename="${book.slug}.pdf"`,
			"Content-Length": String(file.size),
		},
	});
});

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

// ── Page: Book reader ──
routes.get("/read/:id", async (c) => {
	const spaceSlug = c.req.param("space") || "personal";
	const id = c.req.param("id");

	const rows = await sql.unsafe(
		`SELECT * FROM rbooks.books WHERE (slug = $1 OR id::text = $1) AND status = 'published'`,
		[id]
	);

	if (rows.length === 0) {
		const html = renderShell({
			title: "Book not found | rSpace",
			moduleId: "rbooks",
			spaceSlug,
			body: `<div style="padding:3rem;text-align:center;color:#94a3b8;"><h2>Book not found</h2><p><a href="/${spaceSlug}/rbooks" style="color:#60a5fa;">Back to library</a></p></div>`,
			modules: getModuleInfoList(),
		});
		return c.html(html, 404);
	}

	const book = rows[0];

	// Increment view count
	await sql.unsafe(
		`UPDATE rbooks.books SET view_count = view_count + 1 WHERE id = $1`,
		[book.id]
	);

	// Build the PDF URL relative to this module's mount point
	const pdfUrl = `/${spaceSlug}/rbooks/api/books/${book.slug}/pdf`;

	const html = renderShell({
		title: `${book.title} | rSpace`,
		moduleId: "rbooks",
		spaceSlug,
		body: `
			<folk-book-reader
				id="reader"
				pdf-url="${pdfUrl}"
				book-id="${book.slug}"
				title="${escapeAttr(book.title)}"
				author="${escapeAttr(book.author || '')}"
			></folk-book-reader>
		`,
		modules: getModuleInfoList(),
		theme: "dark",
		head: `<link rel="stylesheet" href="/modules/rbooks/books.css">`,
		scripts: `
			<script type="module">
				import { FolkBookReader } from '/modules/rbooks/folk-book-reader.js';
			</script>
		`,
	});

	return c.html(html);
});

// ── Initialize DB schema ──
async function initDB(): Promise<void> {
	try {
		const schemaPath = resolve(import.meta.dir, "db/schema.sql");
		const schemaSql = await readFile(schemaPath, "utf-8");
		await sql.unsafe(`SET search_path TO rbooks, public`);
		await sql.unsafe(schemaSql);
		await sql.unsafe(`SET search_path TO public`);
		console.log("[Books] Database schema initialized");
	} catch (e) {
		console.error("[Books] Schema init failed:", e);
	}
}

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

// ── Module export ──

export const booksModule: RSpaceModule = {
	id: "rbooks",
	name: "rBooks",
	icon: "📚",
	description: "Community PDF library with flipbook reader",
	scoping: { defaultScope: 'global', userConfigurable: true },
	routes,
	standaloneDomain: "rbooks.online",
	landingPage: renderLanding,
	feeds: [
		{
			id: "reading-list",
			name: "Reading List",
			kind: "data",
			description: "Books in the community library — titles, authors, tags",
			filterable: true,
		},
		{
			id: "annotations",
			name: "Annotations",
			kind: "data",
			description: "Reader highlights, bookmarks, and notes on books",
		},
	],
	acceptsFeeds: ["data", "resource"],
	outputPaths: [
		{ path: "books", name: "Books", icon: "📚", description: "Community PDF library" },
		{ path: "collections", name: "Collections", icon: "📑", description: "Curated book collections" },
	],

	async onSpaceCreate(ctx: SpaceLifecycleContext) {
		// Books are global, not space-scoped (for now). No-op.
	},
};

// Run schema init on import
initDB();
