/**
 * Notes module — notebooks, rich-text notes, voice transcription.
 *
 * Port of rnotes-online (Next.js + Prisma → Hono + postgres.js).
 * Supports multiple note types: text, code, bookmark, audio, image, file.
 */

import { Hono } from "hono";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { sql } from "../../shared/db/pool";
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";

const routes = new Hono();

// ── DB initialization ──
const SCHEMA_SQL = readFileSync(resolve(import.meta.dir, "db/schema.sql"), "utf-8");

async function initDB() {
	try {
		await sql.unsafe(SCHEMA_SQL);
		console.log("[Notes] DB schema initialized");
	} catch (e) {
		console.error("[Notes] DB init error:", e);
	}
}

async function seedDemoIfEmpty() {
	try {
		const count = await sql.unsafe("SELECT count(*)::int as cnt FROM rnotes.notebooks");
		if (parseInt(count[0].cnt) > 0) return;

		// Notebook 1: Project Ideas
		const nb1 = await sql.unsafe(
			`INSERT INTO rnotes.notebooks (title, description, cover_color, is_public)
			 VALUES ('Project Ideas', 'Brainstorms and design notes for the r* ecosystem', '#6366f1', true) RETURNING id`
		);
		// Notebook 2: Meeting Notes
		const nb2 = await sql.unsafe(
			`INSERT INTO rnotes.notebooks (title, description, cover_color, is_public)
			 VALUES ('Meeting Notes', 'Weekly standups, design reviews, and retrospectives', '#f59e0b', true) RETURNING id`
		);
		// Notebook 3: How-To Guides
		const nb3 = await sql.unsafe(
			`INSERT INTO rnotes.notebooks (title, description, cover_color, is_public)
			 VALUES ('How-To Guides', 'Tutorials and onboarding guides for contributors', '#10b981', true) RETURNING id`
		);

		// Create tags
		const tagIds: Record<string, string> = {};
		for (const name of ["design", "architecture", "cosmolocal", "governance", "onboarding", "review", "standup"]) {
			const row = await sql.unsafe(
				`INSERT INTO rnotes.tags (name) VALUES ($1) ON CONFLICT (name) DO UPDATE SET name = $1 RETURNING id`,
				[name]
			);
			tagIds[name] = row[0].id;
		}

		// Seed notes
		const notes = [
			{
				nbId: nb1[0].id, title: "Cosmolocal Manufacturing Network",
				content: "## Vision\n\nDesign global, manufacture local. Every creative work should be producible by the nearest capable provider.\n\n## Key Components\n\n- **Artifact Spec**: Standardized envelope describing what to produce\n- **Provider Registry**: Directory of local makers with capabilities + pricing\n- **rCart**: Marketplace connecting creators to providers\n- **Revenue Splits**: 50% provider, 35% creator, 15% community\n\n## Open Questions\n\n- How do we handle quality assurance across distributed providers?\n- Should providers be able to set custom margins?\n- What's the minimum viable set of capabilities for launch?",
				tags: ["cosmolocal", "architecture"], pinned: true,
			},
			{
				nbId: nb1[0].id, title: "Revenue Sharing Model",
				content: "## Current Split\n\n| Recipient | Share | Rationale |\n|-----------|-------|-----------|\n| Provider | 50% | Covers materials, labor, shipping |\n| Creator | 35% | Design and creative work |\n| Community | 15% | Platform maintenance, commons fund |\n\n## Enoughness Thresholds\n\nOnce a funnel reaches its sufficient threshold, surplus flows to the next highest-need funnel. This prevents accumulation and keeps resources flowing.\n\n## Implementation\n\nrFunds Flow Service handles deposits from rCart. Each order total is routed through the configured flow → funnel → overflow splits.",
				tags: ["cosmolocal", "governance"],
			},
			{
				nbId: nb1[0].id, title: "FUN Model: Forget, Update, New",
				content: "## Replacing CRUD\n\nNothing is permanently destroyed in rSpace.\n\n- **Forget** replaces Delete — soft-delete with `forgotten: true`. Shapes stay in document, hidden from canvas. Memory panel lets you browse + Remember.\n- **Update** stays the same — public `sync.updateShape()` for programmatic updates\n- **New** replaces Create — language shift: toolbar says \"New X\", events are `new-shape`\n\n## Why?\n\nData sovereignty means users should always be able to recover their work. The Memory panel makes forgotten shapes discoverable, like a digital archive.",
				tags: ["design", "architecture"],
			},
			{
				nbId: nb2[0].id, title: "Weekly Standup — Feb 15, 2026",
				content: "## Attendees\n\nAlice, Bob, Carol\n\n## Updates\n\n**Alice**: Finished EncryptID guardian recovery flow. 2-of-3 guardian approval working. Next: device linking via QR code.\n\n**Bob**: Provider registry now has 6 printers globally. Working on proximity search with earthdistance extension.\n\n**Carol**: rFunds river visualization deployed. Enoughness layer showing golden glow on sufficient funnels.\n\n## Action Items\n\n- [ ] Alice: Document guardian recovery API endpoints\n- [ ] Bob: Add turnaround time estimates to provider matching\n- [ ] Carol: Add demo mode to river view with mock data",
				tags: ["standup"],
			},
			{
				nbId: nb2[0].id, title: "Design Review — rBooks Flipbook Reader",
				content: "## What We Reviewed\n\nThe react-pageflip integration for PDF reading in rBooks.\n\n## Feedback\n\n1. **Page turn animation** — smooth, feels good on desktop. On mobile, swipe gesture needs larger hit area.\n2. **PDF rendering** — react-pdf handles most PDFs well. Large files (>50MB) cause browser memory issues.\n3. **Read Locally mode** — IndexedDB storage works. Need to show storage usage somewhere.\n\n## Decisions\n\n- Ship current version, iterate on mobile\n- Add a 50MB soft warning on upload\n- Explore PDF.js worker for background rendering",
				tags: ["review", "design"],
			},
			{
				nbId: nb3[0].id, title: "Getting Started with rSpace Development",
				content: "## Prerequisites\n\n- Bun runtime (v1.3+)\n- Docker + Docker Compose\n- Git access to Gitea\n\n## Local Setup\n\n```bash\ngit clone ssh://git@gitea.jeffemmett.com:223/jeffemmett/rspace-online.git\ncd rspace-online\nbun install\nbun run dev\n```\n\n## Module Structure\n\nEach module lives in `modules/{name}/` and exports an `RSpaceModule` interface:\n\n```typescript\nexport interface RSpaceModule {\n  id: string;\n  name: string;\n  icon: string;\n  description: string;\n  routes: Hono;\n}\n```\n\n## Adding a New Module\n\n1. Create `modules/{name}/mod.ts`\n2. Create `modules/{name}/components/` for web components\n3. Add build step in `vite.config.ts`\n4. Register in `server/index.ts`",
				tags: ["onboarding"],
			},
			{
				nbId: nb3[0].id, title: "How to Add a Cosmolocal Provider",
				content: "## Overview\n\nProviders are local print shops, makerspaces, or studios that can fulfill rCart orders.\n\n## Steps\n\n1. Visit `providers.mycofi.earth`\n2. Sign in with your rStack passkey\n3. Click \"Register Provider\"\n4. Fill in:\n   - Name, location (address + coordinates)\n   - Capabilities (laser-print, risograph, screen-print, etc.)\n   - Substrates (paper types, fabric, vinyl)\n   - Turnaround time and pricing\n5. Submit for review\n\n## Matching Algorithm\n\nWhen an order comes in, rCart matches based on:\n- Required capabilities vs. provider capabilities\n- Geographic distance (earthdistance extension)\n- Turnaround time\n- Price",
				tags: ["cosmolocal", "onboarding"],
			},
		];

		for (const n of notes) {
			const row = await sql.unsafe(
				`INSERT INTO rnotes.notes (notebook_id, title, content, content_plain, type, is_pinned)
				 VALUES ($1, $2, $3, $4, 'NOTE', $5) RETURNING id`,
				[n.nbId, n.title, n.content, n.content.replace(/<[^>]*>/g, " ").replace(/[#*|`\-\[\]]/g, " ").replace(/\s+/g, " ").trim(), n.pinned || false]
			);
			for (const tagName of n.tags) {
				if (tagIds[tagName]) {
					await sql.unsafe(
						"INSERT INTO rnotes.note_tags (note_id, tag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING",
						[row[0].id, tagIds[tagName]]
					);
				}
			}
		}

		console.log("[Notes] Demo data seeded: 3 notebooks, 7 notes, 7 tags");
	} catch (e) {
		console.error("[Notes] Seed error:", e);
	}
}

initDB().then(seedDemoIfEmpty);

// ── Helper: get or create user ──
async function getOrCreateUser(did: string, username?: string) {
	const rows = await sql.unsafe(
		`INSERT INTO rnotes.users (did, username) VALUES ($1, $2)
		 ON CONFLICT (did) DO UPDATE SET username = COALESCE($2, rnotes.users.username)
		 RETURNING *`,
		[did, username || null]
	);
	return rows[0];
}

// ── Notebooks API ──

// GET /api/notebooks — list notebooks
routes.get("/api/notebooks", async (c) => {
	const rows = await sql.unsafe(
		`SELECT n.*, count(note.id) as note_count
		 FROM rnotes.notebooks n
		 LEFT JOIN rnotes.notes note ON note.notebook_id = n.id
		 GROUP BY n.id
		 ORDER BY n.updated_at DESC LIMIT 50`
	);
	return c.json({ notebooks: rows });
});

// POST /api/notebooks — create notebook
routes.post("/api/notebooks", 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 { title, description, cover_color } = body;

	const user = await getOrCreateUser(claims.sub, claims.username);
	const rows = await sql.unsafe(
		`INSERT INTO rnotes.notebooks (title, description, cover_color, owner_id)
		 VALUES ($1, $2, $3, $4) RETURNING *`,
		[title || "Untitled Notebook", description || null, cover_color || "#3b82f6", user.id]
	);
	return c.json(rows[0], 201);
});

// GET /api/notebooks/:id — notebook detail with notes
routes.get("/api/notebooks/:id", async (c) => {
	const id = c.req.param("id");
	const nb = await sql.unsafe("SELECT * FROM rnotes.notebooks WHERE id = $1", [id]);
	if (nb.length === 0) return c.json({ error: "Notebook not found" }, 404);

	const notes = await sql.unsafe(
		`SELECT n.*, array_agg(t.name) FILTER (WHERE t.name IS NOT NULL) as tags
		 FROM rnotes.notes n
		 LEFT JOIN rnotes.note_tags nt ON nt.note_id = n.id
		 LEFT JOIN rnotes.tags t ON t.id = nt.tag_id
		 WHERE n.notebook_id = $1
		 GROUP BY n.id
		 ORDER BY n.is_pinned DESC, n.sort_order ASC, n.updated_at DESC`,
		[id]
	);
	return c.json({ ...nb[0], notes });
});

// PUT /api/notebooks/:id — update notebook
routes.put("/api/notebooks/:id", 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 { title, description, cover_color, is_public } = body;

	const fields: string[] = [];
	const params: any[] = [];
	let idx = 1;

	if (title !== undefined) { fields.push(`title = $${idx}`); params.push(title); idx++; }
	if (description !== undefined) { fields.push(`description = $${idx}`); params.push(description); idx++; }
	if (cover_color !== undefined) { fields.push(`cover_color = $${idx}`); params.push(cover_color); idx++; }
	if (is_public !== undefined) { fields.push(`is_public = $${idx}`); params.push(is_public); idx++; }

	if (fields.length === 0) return c.json({ error: "No fields to update" }, 400);
	fields.push("updated_at = NOW()");
	params.push(id);

	const rows = await sql.unsafe(
		`UPDATE rnotes.notebooks SET ${fields.join(", ")} WHERE id = $${idx} RETURNING *`,
		params
	);
	if (rows.length === 0) return c.json({ error: "Notebook not found" }, 404);
	return c.json(rows[0]);
});

// DELETE /api/notebooks/:id
routes.delete("/api/notebooks/:id", async (c) => {
	const result = await sql.unsafe(
		"DELETE FROM rnotes.notebooks WHERE id = $1 RETURNING id",
		[c.req.param("id")]
	);
	if (result.length === 0) return c.json({ error: "Notebook not found" }, 404);
	return c.json({ ok: true });
});

// ── Notes API ──

// GET /api/notes — list all notes (query: notebook_id, type, q)
routes.get("/api/notes", async (c) => {
	const { notebook_id, type, q, limit = "50", offset = "0" } = c.req.query();
	const conditions: string[] = [];
	const params: any[] = [];
	let idx = 1;

	if (notebook_id) { conditions.push(`n.notebook_id = $${idx}`); params.push(notebook_id); idx++; }
	if (type) { conditions.push(`n.type = $${idx}`); params.push(type); idx++; }
	if (q) {
		conditions.push(`(n.title ILIKE $${idx} OR n.content_plain ILIKE $${idx})`);
		params.push(`%${q}%`);
		idx++;
	}

	const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";

	const rows = await sql.unsafe(
		`SELECT n.*, array_agg(t.name) FILTER (WHERE t.name IS NOT NULL) as tags
		 FROM rnotes.notes n
		 LEFT JOIN rnotes.note_tags nt ON nt.note_id = n.id
		 LEFT JOIN rnotes.tags t ON t.id = nt.tag_id
		 ${where}
		 GROUP BY n.id
		 ORDER BY n.is_pinned DESC, n.updated_at DESC
		 LIMIT ${Math.min(parseInt(limit), 100)} OFFSET ${parseInt(offset) || 0}`,
		params
	);
	return c.json({ notes: rows });
});

// POST /api/notes — create note
routes.post("/api/notes", 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 { notebook_id, title, content, type, url, language, file_url, mime_type, file_size, duration, tags } = body;

	if (!title?.trim()) return c.json({ error: "Title is required" }, 400);

	// Strip HTML for plain text search
	const contentPlain = content ? content.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim() : null;

	const rows = await sql.unsafe(
		`INSERT INTO rnotes.notes (notebook_id, title, content, content_plain, type, url, language, file_url, mime_type, file_size, duration)
		 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *`,
		[notebook_id || null, title.trim(), content || "", contentPlain, type || "NOTE",
		 url || null, language || null, file_url || null, mime_type || null, file_size || null, duration || null]
	);

	// Handle tags
	if (tags && Array.isArray(tags)) {
		for (const tagName of tags) {
			const name = tagName.trim().toLowerCase();
			if (!name) continue;
			const tag = await sql.unsafe(
				"INSERT INTO rnotes.tags (name) VALUES ($1) ON CONFLICT (name) DO UPDATE SET name = $1 RETURNING id",
				[name]
			);
			await sql.unsafe(
				"INSERT INTO rnotes.note_tags (note_id, tag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING",
				[rows[0].id, tag[0].id]
			);
		}
	}

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

// GET /api/notes/:id — note detail
routes.get("/api/notes/:id", async (c) => {
	const id = c.req.param("id");
	const rows = await sql.unsafe(
		`SELECT n.*, array_agg(t.name) FILTER (WHERE t.name IS NOT NULL) as tags
		 FROM rnotes.notes n
		 LEFT JOIN rnotes.note_tags nt ON nt.note_id = n.id
		 LEFT JOIN rnotes.tags t ON t.id = nt.tag_id
		 WHERE n.id = $1
		 GROUP BY n.id`,
		[id]
	);
	if (rows.length === 0) return c.json({ error: "Note not found" }, 404);
	return c.json(rows[0]);
});

// PUT /api/notes/:id — update note
routes.put("/api/notes/:id", async (c) => {
	const id = c.req.param("id");
	const body = await c.req.json();
	const { title, content, type, url, language, is_pinned, sort_order } = body;

	const fields: string[] = [];
	const params: any[] = [];
	let idx = 1;

	if (title !== undefined) { fields.push(`title = $${idx}`); params.push(title); idx++; }
	if (content !== undefined) {
		fields.push(`content = $${idx}`); params.push(content); idx++;
		const plain = content.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
		fields.push(`content_plain = $${idx}`); params.push(plain); idx++;
	}
	if (type !== undefined) { fields.push(`type = $${idx}`); params.push(type); idx++; }
	if (url !== undefined) { fields.push(`url = $${idx}`); params.push(url); idx++; }
	if (language !== undefined) { fields.push(`language = $${idx}`); params.push(language); idx++; }
	if (is_pinned !== undefined) { fields.push(`is_pinned = $${idx}`); params.push(is_pinned); idx++; }
	if (sort_order !== undefined) { fields.push(`sort_order = $${idx}`); params.push(sort_order); idx++; }

	if (fields.length === 0) return c.json({ error: "No fields to update" }, 400);
	fields.push("updated_at = NOW()");
	params.push(id);

	const rows = await sql.unsafe(
		`UPDATE rnotes.notes SET ${fields.join(", ")} WHERE id = $${idx} RETURNING *`,
		params
	);
	if (rows.length === 0) return c.json({ error: "Note not found" }, 404);
	return c.json(rows[0]);
});

// DELETE /api/notes/:id
routes.delete("/api/notes/:id", async (c) => {
	const result = await sql.unsafe("DELETE FROM rnotes.notes WHERE id = $1 RETURNING id", [c.req.param("id")]);
	if (result.length === 0) return c.json({ error: "Note not found" }, 404);
	return c.json({ ok: true });
});

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

export const notesModule: RSpaceModule = {
	id: "rnotes",
	name: "rNotes",
	icon: "📝",
	description: "Notebooks with rich-text notes, voice transcription, and collaboration",
	scoping: { defaultScope: 'global', userConfigurable: true },
	routes,
	landingPage: renderLanding,
	standaloneDomain: "rnotes.online",
	feeds: [
		{
			id: "notes-by-tag",
			name: "Notes by Tag",
			kind: "data",
			description: "Stream of notes filtered by tag (design, architecture, etc.)",
			emits: ["folk-markdown"],
			filterable: true,
		},
		{
			id: "recent-notes",
			name: "Recent Notes",
			kind: "data",
			description: "Latest notes across all notebooks",
			emits: ["folk-markdown"],
		},
	],
	acceptsFeeds: ["data", "resource"],
	outputPaths: [
		{ path: "notebooks", name: "Notebooks", icon: "📓", description: "Rich-text collaborative notebooks" },
		{ path: "transcripts", name: "Transcripts", icon: "🎙️", description: "Voice transcription records" },
		{ path: "articles", name: "Articles", icon: "📰", description: "Published articles and posts" },
	],
};
