/**
 * Tasks module — kanban workspace boards.
 *
 * Multi-tenant collaborative workspace with drag-and-drop kanban,
 * configurable statuses, and activity logging.
 *
 * All persistence uses Automerge documents via SyncServer —
 * no PostgreSQL dependency.
 */

import { Hono } from "hono";
import * as Automerge from '@automerge/automerge';
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule, SpaceLifecycleContext } from "../../shared/module";
import { verifyToken, extractToken } from "../../server/auth";
import { resolveCallerRole } from "../../server/spaces";
import type { SpaceRoleString } from "../../server/spaces";
import { filterByVisibility } from "../../shared/membrane";
import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server';
import { boardSchema, boardDocId, createTaskItem, clickupConnectionDocId, clickupConnectionSchema } from './schemas';
import type { BoardDoc, TaskItem, BoardMeta, ClickUpConnectionDoc } from './schemas';
import { ClickUpClient } from './lib/clickup-client';
import { importClickUpList, pushBoardToClickUp, handleClickUpWebhook, initClickUpSync } from './lib/clickup-sync';
import { buildStatusMaps } from './lib/clickup-mapping';

// Email checklist routes exported separately — see checklist-routes.ts

const routes = new Hono();

// ── Local-first helpers ──
let _syncServer: SyncServer | null = null;

/**
 * Lazily create the board Automerge doc if it doesn't exist yet.
 * Returns the current (immutable) doc snapshot.
 */
function ensureDoc(space: string, boardId?: string): BoardDoc {
	const id = boardDocId(space, boardId ?? space);
	let doc = _syncServer!.getDoc<BoardDoc>(id);
	if (!doc) {
		doc = Automerge.change(Automerge.init<BoardDoc>(), 'init board', (d) => {
			const init = boardSchema.init();
			d.meta = init.meta;
			d.meta.spaceSlug = space;
			d.board = init.board;
			d.board.id = boardId ?? space;
			d.board.slug = boardId ?? space;
			d.board.name = space;
			d.tasks = {};
		});
		_syncServer!.setDoc(id, doc);
	}
	return doc;
}

/**
 * Get all board doc IDs for a given space.
 */
function getBoardDocIds(space: string): string[] {
	return _syncServer!.getDocIds().filter((id) => id.startsWith(`${space}:tasks:boards:`));
}

/**
 * Seed demo data if no boards exist for the given space.
 */
function seedDemoIfEmpty(space: string = 'rspace-dev') {
	if (!_syncServer) return;
	// Check if this space already has tasks boards (or was already seeded)
	const spaceWorkDocs = _syncServer!.getDocIds().filter((id) => id.startsWith(`${space}:tasks:boards:`));
	const _seedCheckDoc = _syncServer!.getDoc<BoardDoc>(boardDocId(space, space));
	if ((_seedCheckDoc?.meta as any)?.seeded || spaceWorkDocs.length > 0) return;

	const docId = boardDocId(space, space);

	const doc = Automerge.change(Automerge.init<BoardDoc>(), 'seed demo board', (d) => {
		const now = Date.now();
		d.meta = { module: 'tasks', collection: 'boards', version: 1, spaceSlug: space, createdAt: now };
		d.board = {
			id: space,
			name: 'rSpace Development',
			slug: space,
			description: 'Building the cosmolocal r* ecosystem',
			icon: null,
			ownerDid: 'did:demo:seed',
			statuses: ['TODO', 'IN_PROGRESS', 'REVIEW', 'DONE'],
			labels: [],
			createdAt: now,
			updatedAt: now,
		};
		d.tasks = {};

		const seedTasks: Array<{ title: string; status: string; priority: string; labels: string[]; sort: number }> = [
			{ title: "Add dark mode toggle to settings page", status: "TODO", priority: "MEDIUM", labels: ["feature"], sort: 0 },
			{ title: "Write API documentation for rPubs endpoints", status: "TODO", priority: "LOW", labels: ["docs"], sort: 1 },
			{ title: "Investigate slow PDF generation on large documents", status: "TODO", priority: "HIGH", labels: ["bug"], sort: 2 },
			{ title: "Implement file search and filtering in rFiles", status: "IN_PROGRESS", priority: "HIGH", labels: ["feature"], sort: 0 },
			{ title: "Set up SMTP relay for transactional notifications", status: "IN_PROGRESS", priority: "MEDIUM", labels: ["chore"], sort: 1 },
			{ title: "Add PDF export to rNotes notebooks", status: "REVIEW", priority: "MEDIUM", labels: ["feature"], sort: 0 },
			{ title: "Fix conviction score decay calculation in rVote", status: "REVIEW", priority: "HIGH", labels: ["bug"], sort: 1 },
			{ title: "Deploy EncryptID passkey authentication", status: "DONE", priority: "URGENT", labels: ["feature"], sort: 0 },
			{ title: "Set up Cloudflare tunnel for all r* domains", status: "DONE", priority: "HIGH", labels: ["chore"], sort: 1 },
			{ title: "Create cosmolocal provider directory with 6 printers", status: "DONE", priority: "MEDIUM", labels: ["feature"], sort: 2 },
			{ title: "Migrate email from Resend to self-hosted Mailcow", status: "DONE", priority: "MEDIUM", labels: ["chore"], sort: 3 },
		];

		for (const t of seedTasks) {
			const taskId = crypto.randomUUID();
			d.tasks[taskId] = createTaskItem(taskId, space, t.title, {
				status: t.status,
				priority: t.priority,
				labels: t.labels,
				sortOrder: t.sort,
				createdBy: 'did:demo:seed',
			});
		}
	});

	_syncServer!.setDoc(docId, doc);
	_syncServer!.changeDoc<BoardDoc>(docId, 'mark seeded', (d) => {
		if (d.meta) (d.meta as any).seeded = true;
	});
	console.log(`[Tasks] Demo data seeded for "${space}": 1 board, 11 tasks`);
}

/**
 * Seed a "BCRG Outcomes" board with 11 tasks matching the BCRG flow outcomes.
 * Called for demo space on startup and when new spaces are created via seedTemplate.
 */
function seedBCRGTasksIfEmpty(space: string = 'demo') {
	if (!_syncServer) return;
	const boardId = `${space}-bcrg`;
	const docId = boardDocId(space, boardId);
	const existing = _syncServer.getDoc<BoardDoc>(docId);
	if (existing) return; // already seeded

	const now = Date.now();
	const doc = Automerge.change(Automerge.init<BoardDoc>(), 'seed BCRG outcomes board', (d) => {
		d.meta = { module: 'tasks', collection: 'boards', version: 1, spaceSlug: space, createdAt: now };
		d.board = {
			id: boardId,
			name: 'BCRG Outcomes',
			slug: boardId,
			description: 'Tasks tracking BCRG community flow outcomes',
			icon: null,
			ownerDid: 'did:demo:seed',
			statuses: ['TODO', 'IN_PROGRESS', 'REVIEW', 'DONE'],
			labels: ['rflows', 'bcrg'],
			createdAt: now,
			updatedAt: now,
		};
		d.tasks = {};

		const bcrgTasks: Array<{ id: string; title: string; status: string; priority: string; description: string; sort: number }> = [
			// 4 DONE (completed outcomes)
			{ id: 'alice-comms', title: 'Comms Strategy', status: 'DONE', priority: 'MEDIUM', description: 'ref:rflows:outcome:alice-comms — Community communications and outreach', sort: 0 },
			{ id: 'carol-ops', title: 'Operations', status: 'DONE', priority: 'HIGH', description: 'ref:rflows:outcome:carol-ops — Day-to-day operational management', sort: 1 },
			{ id: 'dave-design', title: 'Design System', status: 'DONE', priority: 'HIGH', description: 'ref:rflows:outcome:dave-design — Shared UI/UX design system', sort: 2 },
			{ id: 'eve-legal', title: 'Legal Framework', status: 'DONE', priority: 'MEDIUM', description: 'ref:rflows:outcome:eve-legal — Legal structure and agreements', sort: 3 },
			// 5 IN_PROGRESS (partially-funded outcomes)
			{ id: 'alice-events', title: 'Event Series', status: 'IN_PROGRESS', priority: 'MEDIUM', description: 'ref:rflows:outcome:alice-events — Quarterly community gatherings', sort: 0 },
			{ id: 'bob-research', title: 'Field Research', status: 'IN_PROGRESS', priority: 'HIGH', description: 'ref:rflows:outcome:bob-research — Participatory action research', sort: 1 },
			{ id: 'carol-infra', title: 'Infrastructure', status: 'IN_PROGRESS', priority: 'HIGH', description: 'ref:rflows:outcome:carol-infra — Shared infrastructure and hosting', sort: 2 },
			{ id: 'dave-prototypes', title: 'Prototypes', status: 'IN_PROGRESS', priority: 'MEDIUM', description: 'ref:rflows:outcome:dave-prototypes — Rapid prototyping of new tools', sort: 3 },
			{ id: 'eve-compliance', title: 'Compliance', status: 'IN_PROGRESS', priority: 'HIGH', description: 'ref:rflows:outcome:eve-compliance — Regulatory compliance and reporting', sort: 4 },
			// 2 TODO (not-started outcomes)
			{ id: 'bob-writing', title: 'Publications', status: 'TODO', priority: 'LOW', description: 'ref:rflows:outcome:bob-writing — Research papers and policy briefs', sort: 0 },
			{ id: 'eve-governance', title: 'Governance Model', status: 'TODO', priority: 'MEDIUM', description: 'ref:rflows:outcome:eve-governance — Governance framework and voting mechanisms', sort: 1 },
		];

		for (const t of bcrgTasks) {
			const taskId = crypto.randomUUID();
			d.tasks[taskId] = createTaskItem(taskId, space, t.title, {
				status: t.status,
				priority: t.priority,
				description: t.description,
				labels: ['rflows', 'bcrg'],
				sortOrder: t.sort,
				createdBy: 'did:demo:seed',
			});
		}
	});

	_syncServer.setDoc(docId, doc);
	console.log(`[Tasks] BCRG outcomes board seeded for "${space}": 11 tasks`);
}

// ── API: Spaces (Boards) ──

// GET /api/spaces — list workspaces (boards)
routes.get("/api/spaces", async (c) => {
	const allIds = _syncServer!.getDocIds().filter((id) => id.includes(':tasks:boards:'));
	const rows = allIds.map((docId) => {
		const doc = _syncServer!.getDoc<BoardDoc>(docId);
		if (!doc) return null;
		const taskCount = Object.keys(doc.tasks).length;
		return {
			id: doc.board.id,
			name: doc.board.name,
			slug: doc.board.slug,
			description: doc.board.description,
			icon: doc.board.icon,
			owner_did: doc.board.ownerDid,
			statuses: doc.board.statuses,
			created_at: new Date(doc.board.createdAt).toISOString(),
			updated_at: new Date(doc.board.updatedAt).toISOString(),
			member_count: 0,
			task_count: taskCount,
		};
	}).filter(Boolean);

	// Sort by created_at DESC
	rows.sort((a, b) => (b!.created_at > a!.created_at ? 1 : -1));
	return c.json(rows);
});

// POST /api/spaces — create workspace (board)
routes.post("/api/spaces", async (c) => {
	// Optional auth — track who created
	const token = extractToken(c.req.raw.headers);
	let ownerDid: string | null = null;
	if (token) {
		try { const claims = await verifyToken(token); ownerDid = claims.sub; } catch {}
	}

	const body = await c.req.json();
	const { name, description, icon } = body;
	if (!name?.trim()) return c.json({ error: "Name required" }, 400);

	const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
	const docId = boardDocId(slug, slug);

	// Check if board already exists
	const existing = _syncServer!.getDoc<BoardDoc>(docId);
	if (existing) return c.json({ error: "Space with this slug already exists" }, 409);

	const now = Date.now();
	const doc = Automerge.change(Automerge.init<BoardDoc>(), 'create board', (d) => {
		d.meta = { module: 'tasks', collection: 'boards', version: 1, spaceSlug: slug, createdAt: now };
		d.board = {
			id: slug,
			name: name.trim(),
			slug,
			description: description || '',
			icon: icon || null,
			ownerDid,
			statuses: ['TODO', 'IN_PROGRESS', 'DONE'],
			labels: [],
			createdAt: now,
			updatedAt: now,
		};
		d.tasks = {};
	});
	_syncServer!.setDoc(docId, doc);

	return c.json({
		id: slug,
		name: name.trim(),
		slug,
		description: description || null,
		icon: icon || null,
		owner_did: ownerDid,
		statuses: ['TODO', 'IN_PROGRESS', 'DONE'],
		created_at: new Date(now).toISOString(),
		updated_at: new Date(now).toISOString(),
	}, 201);
});

// GET /api/spaces/:slug — workspace detail
routes.get("/api/spaces/:slug", async (c) => {
	const slug = c.req.param("slug");
	const docId = boardDocId(slug, slug);
	const doc = _syncServer!.getDoc<BoardDoc>(docId);
	if (!doc) return c.json({ error: "Space not found" }, 404);

	return c.json({
		id: doc.board.id,
		name: doc.board.name,
		slug: doc.board.slug,
		description: doc.board.description,
		icon: doc.board.icon,
		owner_did: doc.board.ownerDid,
		statuses: doc.board.statuses,
		labels: doc.board.labels,
		created_at: new Date(doc.board.createdAt).toISOString(),
		updated_at: new Date(doc.board.updatedAt).toISOString(),
		...(doc.board.clickup ? { clickup: { listId: doc.board.clickup.listId, listName: doc.board.clickup.listName, syncEnabled: doc.board.clickup.syncEnabled } } : {}),
	});
});

// PATCH /api/spaces/:slug — update board meta (statuses, labels, name)
routes.patch("/api/spaces/:slug", async (c) => {
	// Optional auth
	const token = extractToken(c.req.raw.headers);
	if (token) { try { await verifyToken(token); } catch {} }

	const slug = c.req.param("slug");
	const docId = boardDocId(slug, slug);
	const doc = _syncServer!.getDoc<BoardDoc>(docId);
	if (!doc) return c.json({ error: "Space not found" }, 404);

	const body = await c.req.json();
	const { name, description, statuses, labels } = body;

	_syncServer!.changeDoc<BoardDoc>(docId, `Update board meta ${slug}`, (d) => {
		if (name !== undefined) d.board.name = name;
		if (description !== undefined) d.board.description = description;
		if (statuses !== undefined && Array.isArray(statuses)) d.board.statuses = statuses;
		if (labels !== undefined && Array.isArray(labels)) d.board.labels = labels;
		d.board.updatedAt = Date.now();
	});

	const updated = _syncServer!.getDoc<BoardDoc>(docId)!;
	return c.json({
		id: updated.board.id,
		name: updated.board.name,
		slug: updated.board.slug,
		description: updated.board.description,
		statuses: updated.board.statuses,
		labels: updated.board.labels,
		updated_at: new Date(updated.board.updatedAt).toISOString(),
	});
});

// ── API: Tasks ──

// GET /api/spaces/:slug/tasks — list tasks in workspace
routes.get("/api/spaces/:slug/tasks", async (c) => {
	const slug = c.req.param("slug");
	const doc = ensureDoc(slug);

	// Resolve caller role for membrane filtering
	let callerRole: SpaceRoleString = 'viewer';
	const token = extractToken(c.req.raw.headers);
	if (token) {
		try {
			const claims = await verifyToken(token);
			const resolved = await resolveCallerRole(slug, claims);
			if (resolved) callerRole = resolved.role;
		} catch {}
	}
	const visibleTasks = filterByVisibility(doc.tasks, callerRole);

	const tasks = Object.values(visibleTasks).map((t) => ({
		id: t.id,
		space_id: t.spaceId,
		title: t.title,
		description: t.description,
		status: t.status,
		priority: t.priority,
		labels: t.labels,
		assignee_id: t.assigneeId,
		assignee_name: null,
		created_by: t.createdBy,
		sort_order: t.sortOrder,
		due_date: t.dueDate ? new Date(t.dueDate).toISOString() : null,
		created_at: new Date(t.createdAt).toISOString(),
		updated_at: new Date(t.updatedAt).toISOString(),
		...(t.clickup ? { clickup: { taskId: t.clickup.taskId, url: t.clickup.url, syncStatus: t.clickup.syncStatus } } : {}),
	}));

	// Sort by status, then sort_order, then created_at DESC
	const statusOrder: Record<string, number> = {};
	doc.board.statuses.forEach((s, i) => { statusOrder[s] = i; });
	tasks.sort((a, b) => {
		const sa = statusOrder[a.status] ?? 999;
		const sb = statusOrder[b.status] ?? 999;
		if (sa !== sb) return sa - sb;
		if (a.sort_order !== b.sort_order) return a.sort_order - b.sort_order;
		return b.created_at > a.created_at ? 1 : -1;
	});

	return c.json(tasks);
});

// POST /api/spaces/:slug/tasks — create task
routes.post("/api/spaces/:slug/tasks", async (c) => {
	// Optional auth — track who created
	const token = extractToken(c.req.raw.headers);
	let createdBy: string | null = null;
	if (token) {
		try { const claims = await verifyToken(token); createdBy = claims.sub; } catch {}
	}

	const slug = c.req.param("slug");
	const body = await c.req.json();
	const { title, description, status, priority, labels, due_date, assignee_id, visibility } = body;
	if (!title?.trim()) return c.json({ error: "Title required" }, 400);

	const doc = ensureDoc(slug);
	const taskStatus = status || doc.board.statuses[0] || "TODO";
	const taskId = crypto.randomUUID();
	const now = Date.now();

	const docId = boardDocId(slug, slug);
	_syncServer!.changeDoc<BoardDoc>(docId, `Create task ${taskId}`, (d) => {
		const task = createTaskItem(taskId, slug, title.trim(), {
			description: description || '',
			status: taskStatus,
			priority: priority || 'MEDIUM',
			labels: labels || [],
			dueDate: due_date ? new Date(due_date).getTime() : null,
			createdBy,
		});
		if (assignee_id) task.assigneeId = assignee_id;
		if (visibility) task.visibility = visibility;
		d.tasks[taskId] = task;
	});

	// Notify space members about the new task
	import('../rinbox/agent-notify').then(({ sendSpaceNotification }) => {
		sendSpaceNotification(slug, `New Task: ${title.trim()}`,
			`<h3>${title.trim()}</h3>${description ? `<p>${description}</p>` : ''}<p><strong>Priority:</strong> ${priority || 'MEDIUM'}</p><p><a href="https://${slug}.rspace.online/rtasks">View in rTasks</a></p>`
		).catch(() => {});
	}).catch(() => {});

	return c.json({
		id: taskId,
		space_id: slug,
		title: title.trim(),
		description: description || null,
		status: taskStatus,
		priority: priority || "MEDIUM",
		labels: labels || [],
		assignee_id: assignee_id || null,
		created_by: createdBy,
		sort_order: 0,
		due_date: due_date ? new Date(due_date).toISOString() : null,
		created_at: new Date(now).toISOString(),
		updated_at: new Date(now).toISOString(),
	}, 201);
});

// PATCH /api/tasks/bulk-sort-order — rebalance sort orders for multiple tasks
// Registered BEFORE /api/tasks/:id to avoid param capture
routes.patch("/api/tasks/bulk-sort-order", async (c) => {
	const body = await c.req.json();
	const { updates } = body;
	if (!Array.isArray(updates) || updates.length === 0) {
		return c.json({ error: "updates array required" }, 400);
	}

	// Group updates by board doc
	const allBoardIds = _syncServer!.getDocIds().filter((docId) => docId.includes(':tasks:boards:'));
	let applied = 0;

	for (const { id, sort_order } of updates) {
		if (!id || sort_order === undefined) continue;
		for (const docId of allBoardIds) {
			const doc = _syncServer!.getDoc<BoardDoc>(docId);
			if (doc && doc.tasks[id]) {
				_syncServer!.changeDoc<BoardDoc>(docId, `Rebalance sort order ${id}`, (d) => {
					if (d.tasks[id]) {
						d.tasks[id].sortOrder = sort_order;
						d.tasks[id].updatedAt = Date.now();
					}
				});
				applied++;
				break;
			}
		}
	}

	return c.json({ ok: true, applied });
});

// PATCH /api/tasks/:id — update task (status change, assignment, etc.)
routes.patch("/api/tasks/:id", async (c) => {
	// Optional auth — track who updated
	const token = extractToken(c.req.raw.headers);
	let updatedBy: string | null = null;
	if (token) {
		try { const claims = await verifyToken(token); updatedBy = claims.sub; } catch {}
	}

	const id = c.req.param("id");
	const body = await c.req.json();
	const { title, description, status, priority, labels, sort_order, assignee_id, due_date, visibility } = body;

	// Check that at least one field is being updated
	if (title === undefined && description === undefined && status === undefined &&
		priority === undefined && labels === undefined && sort_order === undefined &&
		assignee_id === undefined && due_date === undefined && visibility === undefined) {
		return c.json({ error: "No fields to update" }, 400);
	}

	// Find which board doc contains this task
	const allBoardIds = _syncServer!.getDocIds().filter((docId) => docId.includes(':tasks:boards:'));
	let targetDocId: string | null = null;
	for (const docId of allBoardIds) {
		const doc = _syncServer!.getDoc<BoardDoc>(docId);
		if (doc && doc.tasks[id]) {
			targetDocId = docId;
			break;
		}
	}
	if (!targetDocId) return c.json({ error: "Task not found" }, 404);

	_syncServer!.changeDoc<BoardDoc>(targetDocId, `Update task ${id}`, (d) => {
		const task = d.tasks[id];
		if (!task) return;
		if (title !== undefined) task.title = title;
		if (description !== undefined) task.description = description;
		if (status !== undefined) task.status = status;
		if (priority !== undefined) task.priority = priority;
		if (labels !== undefined) task.labels = labels;
		if (sort_order !== undefined) task.sortOrder = sort_order;
		if (assignee_id !== undefined) task.assigneeId = assignee_id || null;
		if (due_date !== undefined) task.dueDate = due_date ? new Date(due_date).getTime() : null;
		if (visibility !== undefined) task.visibility = visibility || undefined;
		task.updatedAt = Date.now();
	});

	// Return the updated task
	const updatedDoc = _syncServer!.getDoc<BoardDoc>(targetDocId)!;
	const task = updatedDoc.tasks[id];
	return c.json({
		id: task.id,
		space_id: task.spaceId,
		title: task.title,
		description: task.description,
		status: task.status,
		priority: task.priority,
		labels: task.labels,
		assignee_id: task.assigneeId,
		created_by: task.createdBy,
		sort_order: task.sortOrder,
		due_date: task.dueDate ? new Date(task.dueDate).toISOString() : null,
		created_at: new Date(task.createdAt).toISOString(),
		updated_at: new Date(task.updatedAt).toISOString(),
	});
});

// DELETE /api/tasks/:id
routes.delete("/api/tasks/:id", async (c) => {
	const id = c.req.param("id");

	// Find which board doc contains this task
	const allBoardIds = _syncServer!.getDocIds().filter((docId) => docId.includes(':tasks:boards:'));
	let targetDocId: string | null = null;
	for (const docId of allBoardIds) {
		const doc = _syncServer!.getDoc<BoardDoc>(docId);
		if (doc && doc.tasks[id]) {
			targetDocId = docId;
			break;
		}
	}
	if (!targetDocId) return c.json({ error: "Task not found" }, 404);

	_syncServer!.changeDoc<BoardDoc>(targetDocId, `Delete task ${id}`, (d) => {
		delete d.tasks[id];
	});

	return c.json({ ok: true });
});

// ── API: Activity ──

// GET /api/spaces/:slug/activity — recent activity
// With Automerge, activity is tracked via document change history.
// Return an empty array for now; real activity can be derived from
// Automerge.getHistory() or a dedicated activity doc in the future.
routes.get("/api/spaces/:slug/activity", async (c) => {
	return c.json([]);
});

// ── ClickUp integration helpers ──

function getClickUpConnection(space: string): ClickUpConnectionDoc | null {
	if (!_syncServer) return null;
	return _syncServer.getDoc<ClickUpConnectionDoc>(clickupConnectionDocId(space)) ?? null;
}

function getAccessToken(space: string): string | null {
	const conn = getClickUpConnection(space);
	return conn?.clickup?.accessToken || null;
}

// ── API: ClickUp Integration ──

// GET /api/clickup/status — connection status
routes.get("/api/clickup/status", async (c) => {
	const space = c.req.param("space") || "demo";
	const conn = getClickUpConnection(space);
	if (!conn?.clickup) return c.json({ connected: false });

	// Count synced boards
	const boardDocIds = getBoardDocIds(space);
	let syncedBoards = 0;
	let pendingTasks = 0;
	for (const docId of boardDocIds) {
		const doc = _syncServer!.getDoc<BoardDoc>(docId);
		if (doc?.board?.clickup?.syncEnabled) {
			syncedBoards++;
			for (const task of Object.values(doc.tasks)) {
				if (task.clickup && task.clickup.syncStatus !== 'synced') pendingTasks++;
			}
		}
	}

	return c.json({
		connected: true,
		teamId: conn.clickup.teamId,
		teamName: conn.clickup.teamName,
		connectedAt: conn.clickup.connectedAt,
		syncedBoards,
		pendingTasks,
	});
});

// POST /api/clickup/connect-token — connect via personal API token
routes.post("/api/clickup/connect-token", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const body = await c.req.json();
	const apiToken = body.token;
	if (!apiToken) return c.json({ error: "ClickUp API token required" }, 400);

	// Verify token by fetching teams
	const client = new ClickUpClient(apiToken);
	let teams: any[];
	try {
		teams = await client.getTeams();
	} catch (err) {
		return c.json({ error: "Invalid ClickUp API token" }, 400);
	}

	const team = teams[0];
	if (!team) return c.json({ error: "No ClickUp workspaces found" }, 400);

	// Generate webhook secret
	const secretBuf = new Uint8Array(32);
	crypto.getRandomValues(secretBuf);
	const webhookSecret = Array.from(secretBuf).map(b => b.toString(16).padStart(2, '0')).join('');

	// Store connection
	const docId = clickupConnectionDocId(space);
	let connDoc = _syncServer!.getDoc<ClickUpConnectionDoc>(docId);
	if (!connDoc) {
		connDoc = Automerge.change(Automerge.init<ClickUpConnectionDoc>(), 'init clickup connection', (d) => {
			d.meta = { module: 'tasks', collection: 'clickup-connection', version: 1, spaceSlug: space, createdAt: Date.now() };
		});
		_syncServer!.setDoc(docId, connDoc);
	}

	_syncServer!.changeDoc<ClickUpConnectionDoc>(docId, 'Connect ClickUp via API token', (d) => {
		d.clickup = {
			accessToken: apiToken,
			teamId: team.id,
			teamName: team.name,
			connectedAt: Date.now(),
			webhookSecret,
		};
	});

	return c.json({ ok: true, teamId: team.id, teamName: team.name });
});

// POST /api/clickup/disconnect — disconnect + cleanup webhook
routes.post("/api/clickup/disconnect", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const conn = getClickUpConnection(space);

	if (conn?.clickup) {
		// Cleanup webhooks on synced boards
		const client = new ClickUpClient(conn.clickup.accessToken);
		const boardDocIds = getBoardDocIds(space);
		for (const docId of boardDocIds) {
			const doc = _syncServer!.getDoc<BoardDoc>(docId);
			if (doc?.board?.clickup?.webhookId) {
				try { await client.deleteWebhook(doc.board.clickup.webhookId); } catch {}
				_syncServer!.changeDoc<BoardDoc>(docId, 'Remove ClickUp sync', (d) => {
					delete d.board.clickup;
				});
			}
		}

		// Remove connection
		_syncServer!.changeDoc<ClickUpConnectionDoc>(clickupConnectionDocId(space), 'Disconnect ClickUp', (d) => {
			delete d.clickup;
		});
	}

	return c.json({ ok: true });
});

// GET /api/clickup/workspaces — list ClickUp teams
routes.get("/api/clickup/workspaces", async (c) => {
	const space = c.req.param("space") || "demo";
	const accessToken = getAccessToken(space);
	if (!accessToken) return c.json({ error: "Not connected to ClickUp" }, 400);

	const client = new ClickUpClient(accessToken);
	const teams = await client.getTeams();
	return c.json(teams.map((t: any) => ({ id: t.id, name: t.name, members: t.members?.length || 0 })));
});

// GET /api/clickup/spaces/:teamId — list ClickUp spaces
routes.get("/api/clickup/spaces/:teamId", async (c) => {
	const space = c.req.param("space") || "demo";
	const accessToken = getAccessToken(space);
	if (!accessToken) return c.json({ error: "Not connected to ClickUp" }, 400);

	const teamId = c.req.param("teamId");
	const client = new ClickUpClient(accessToken);
	const spaces = await client.getSpaces(teamId);
	return c.json(spaces.map((s: any) => ({ id: s.id, name: s.name })));
});

// GET /api/clickup/lists/:spaceId — list all lists in a space (including folders)
routes.get("/api/clickup/lists/:spaceId", async (c) => {
	const space = c.req.param("space") || "demo";
	const accessToken = getAccessToken(space);
	if (!accessToken) return c.json({ error: "Not connected to ClickUp" }, 400);

	const spaceId = c.req.param("spaceId");
	const client = new ClickUpClient(accessToken);

	// Get folderless lists + lists inside folders
	const [folderlessLists, folders] = await Promise.all([
		client.getFolderlessLists(spaceId),
		client.getFolders(spaceId),
	]);

	const lists: any[] = folderlessLists.map((l: any) => ({
		id: l.id, name: l.name, taskCount: l.task_count || 0, folder: null,
	}));

	for (const folder of folders) {
		const folderLists = folder.lists || [];
		for (const l of folderLists) {
			lists.push({ id: l.id, name: l.name, taskCount: l.task_count || 0, folder: folder.name });
		}
	}

	return c.json(lists);
});

// POST /api/clickup/import — import a ClickUp list → rTasks board
routes.post("/api/clickup/import", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const accessToken = getAccessToken(space);
	if (!accessToken) return c.json({ error: "Not connected to ClickUp" }, 400);

	const body = await c.req.json();
	const { listId, boardSlug, enableSync } = body;
	if (!listId) return c.json({ error: "listId required" }, 400);

	const slug = boardSlug || `clickup-${listId}`;

	try {
		const result = await importClickUpList(_syncServer!, space, slug, listId, accessToken, {
			enableSync: enableSync ?? false,
			createNew: !boardSlug,
		});

		// Register webhook if sync enabled
		if (enableSync) {
			const conn = getClickUpConnection(space);
			if (conn?.clickup) {
				const client = new ClickUpClient(accessToken);
				const host = c.req.header('host') || 'rspace.online';
				const protocol = c.req.header('x-forwarded-proto') || 'https';
				const endpoint = c.get("isSubdomain") ? `${protocol}://${host}/rtasks/api/clickup/webhook` : `${protocol}://${host}/${space}/rtasks/api/clickup/webhook`;
				try {
					const wh = await client.createWebhook(
						conn.clickup.teamId,
						endpoint,
						['taskCreated', 'taskUpdated', 'taskStatusUpdated', 'taskDeleted'],
						conn.clickup.webhookSecret,
					);
					// Store webhook ID on board
					_syncServer!.changeDoc<BoardDoc>(boardDocId(space, slug), 'Store webhook ID', (d) => {
						if (d.board.clickup) d.board.clickup.webhookId = wh.id;
					});
				} catch (err) {
					console.error('[ClickUp] Failed to create webhook:', err);
				}
			}
		}

		return c.json(result, 201);
	} catch (err: any) {
		return c.json({ error: err.message || 'Import failed' }, 500);
	}
});

// POST /api/clickup/push-board/:slug — export rTasks board → ClickUp list
routes.post("/api/clickup/push-board/:slug", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const accessToken = getAccessToken(space);
	if (!accessToken) return c.json({ error: "Not connected to ClickUp" }, 400);

	const slug = c.req.param("slug");
	const body = await c.req.json();
	const { listId } = body;
	if (!listId) return c.json({ error: "listId required" }, 400);

	try {
		const result = await pushBoardToClickUp(_syncServer!, space, slug, listId, accessToken);
		return c.json(result);
	} catch (err: any) {
		return c.json({ error: err.message || 'Push failed' }, 500);
	}
});

// POST /api/clickup/sync-board/:slug — toggle two-way sync on/off
routes.post("/api/clickup/sync-board/:slug", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const slug = c.req.param("slug");
	const body = await c.req.json();
	const { enabled } = body;

	const docId = boardDocId(space, slug);
	const doc = _syncServer!.getDoc<BoardDoc>(docId);
	if (!doc) return c.json({ error: "Board not found" }, 404);
	if (!doc.board.clickup) return c.json({ error: "Board not connected to ClickUp" }, 400);

	_syncServer!.changeDoc<BoardDoc>(docId, `Toggle ClickUp sync ${enabled ? 'on' : 'off'}`, (d) => {
		if (d.board.clickup) d.board.clickup.syncEnabled = !!enabled;
	});

	return c.json({ ok: true, syncEnabled: !!enabled });
});

// POST /api/clickup/webhook — receive ClickUp webhook events (public, no auth)
routes.post("/api/clickup/webhook", async (c) => {
	const space = c.req.param("space") || "demo";
	const body = await c.req.json();
	const signature = c.req.header('x-signature') || null;

	const result = await handleClickUpWebhook(_syncServer!, space, body, signature);
	return c.json(result, result.ok ? 200 : 400);
});

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

export const tasksModule: RSpaceModule = {
	id: "rtasks",
	name: "rTasks",
	icon: "📋",
	publicWrite: true,
	description: "Kanban workspace boards for collaborative task management",
	scoping: { defaultScope: 'space', userConfigurable: false },
	canvasToolIds: ["create_resource_coverage"],
	docSchemas: [
		{ pattern: '{space}:tasks:boards:{boardId}', description: 'Kanban board with tasks', init: boardSchema.init },
		{ pattern: '{space}:tasks:clickup-connection', description: 'ClickUp integration credentials', init: clickupConnectionSchema.init },
	],
	settingsSchema: [
		{ key: 'clickupApiToken', label: 'ClickUp API Token', type: 'password', description: 'Personal API token from ClickUp Settings > Apps (pk_...)' },
	],
	routes,
	standaloneDomain: "rtasks.online",
	landingPage: renderLanding,
	seedTemplate(space: string) {
		seedDemoIfEmpty(space);
		seedBCRGTasksIfEmpty(space);
	},
	async onInit(ctx) {
		_syncServer = ctx.syncServer;
		seedDemoIfEmpty();
		seedBCRGTasksIfEmpty('demo');
		initClickUpSync(ctx.syncServer);
	},
	async onSpaceCreate(ctx: SpaceLifecycleContext) {
		if (!_syncServer) return;
		const docId = boardDocId(ctx.spaceSlug, ctx.spaceSlug);
		const doc = Automerge.init<BoardDoc>();
		const initialized = Automerge.change(doc, 'Init board', (d) => {
			d.meta = { module: 'tasks', collection: 'boards', version: 1, spaceSlug: ctx.spaceSlug, createdAt: Date.now() };
			d.board = { id: ctx.spaceSlug, name: ctx.spaceSlug, slug: ctx.spaceSlug, description: '', icon: null, ownerDid: ctx.ownerDID, statuses: ['TODO', 'IN_PROGRESS', 'DONE'], labels: [], createdAt: Date.now(), updatedAt: Date.now() };
			d.tasks = {};
		});
		_syncServer.setDoc(docId, initialized);
	},
	feeds: [
		{
			id: "task-activity",
			name: "Task Activity",
			kind: "data",
			description: "Task creation, status changes, and assignment updates",
			filterable: true,
		},
		{
			id: "board-summary",
			name: "Board Summary",
			kind: "data",
			description: "Kanban board state — counts by status column",
		},
	],
	acceptsFeeds: ["governance", "data"],
	outputPaths: [
		{ path: "projects", name: "Projects", icon: "📋", description: "Kanban project boards" },
		{ path: "tasks", name: "Tasks", icon: "✅", description: "Task cards across all boards" },
	],
	onboardingActions: [
		{ label: "Create a Taskboard", icon: "📋", description: "Start a new kanban project board", type: 'create', href: '/{space}/rtasks' },
	],
};

// ── MI Integration ──

export interface MITaskItem {
	id: string;
	title: string;
	status: string;
	priority: string | null;
	description: string;
	createdAt: number;
}

/**
 * Read recent/open tasks directly from Automerge for the MI system prompt.
 */
export function getRecentTasksForMI(space: string, limit = 5): MITaskItem[] {
	if (!_syncServer) return [];
	const allTasks: MITaskItem[] = [];

	for (const docId of _syncServer.getDocIds()) {
		if (!docId.startsWith(`${space}:tasks:boards:`)) continue;
		const doc = _syncServer.getDoc<BoardDoc>(docId);
		if (!doc?.tasks) continue;

		for (const task of Object.values(doc.tasks)) {
			allTasks.push({
				id: task.id,
				title: task.title,
				status: task.status,
				priority: task.priority,
				description: (task.description || "").slice(0, 200),
				createdAt: task.createdAt,
			});
		}
	}

	// Prioritize non-DONE tasks, then sort by creation date
	return allTasks
		.filter((t) => t.status !== "DONE")
		.sort((a, b) => b.createdAt - a.createdAt)
		.slice(0, limit);
}
