/**
 * rTime module — timebank commitment pool & weaving dashboard.
 *
 * Visualize community hour pledges as floating orbs in a basket,
 * then weave commitments into tasks on an SVG canvas. Optional
 * Cyclos integration for real timebank balances.
 *
 * All state stored in Automerge documents via SyncServer.
 * Doc layout:
 *   {space}:rtime:commitments  → CommitmentsDoc
 *   {space}:rtime:tasks        → TasksDoc
 */

import { Hono } from "hono";
import * as Automerge from '@automerge/automerge';
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyToken, extractToken } from "../../server/auth";
import { resolveCallerRole } from "../../server/spaces";
import type { SpaceRoleString } from "../../server/spaces";
import { filterArrayByVisibility } from "../../shared/membrane";
import { renderLanding } from "./landing";
import { notify } from '../../server/notification-service';
import type { SyncServer } from '../../server/local-first/sync-server';
import {
	commitmentsSchema, tasksSchema, weavingSchema, externalTimeLogsSchema,
	commitmentsDocId, tasksDocId, weavingDocId, externalTimeLogsDocId,
	SKILL_LABELS,
} from './schemas';
import type {
	CommitmentsDoc, TasksDoc, WeavingDoc, ExternalTimeLogsDoc,
	Commitment, Task, Connection, ExecState, Skill, ExternalTimeLog,
	WeavingOverlay,
} from './schemas';
import { boardDocId, createTaskItem } from '../rtasks/schemas';
import type { BoardDoc, TaskItem } from '../rtasks/schemas';
import {
	intentsSchema, solverResultsSchema,
	skillCurvesSchema, reputationSchema,
} from './schemas-intent';
import { createIntentRoutes } from './intent-routes';

const routes = new Hono();

// ── SyncServer ref (set during onInit) ──
let _syncServer: SyncServer | null = null;

// ── Mount intent routes ──
const intentRoutes = createIntentRoutes(() => _syncServer);
routes.route('/', intentRoutes);

// ── Automerge helpers ──

function ensureCommitmentsDoc(space: string): CommitmentsDoc {
	const docId = commitmentsDocId(space);
	let doc = _syncServer!.getDoc<CommitmentsDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<CommitmentsDoc>(), 'init commitments', (d) => {
			const init = commitmentsSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

function ensureTasksDoc(space: string): TasksDoc {
	const docId = tasksDocId(space);
	let doc = _syncServer!.getDoc<TasksDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<TasksDoc>(), 'init tasks', (d) => {
			const init = tasksSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

function ensureWeavingDoc(space: string): WeavingDoc {
	const docId = weavingDocId(space);
	let doc = _syncServer!.getDoc<WeavingDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<WeavingDoc>(), 'init weaving', (d) => {
			const init = weavingSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

function newId(): string {
	return crypto.randomUUID();
}

// ── External Time Logs helpers ──

function ensureExternalTimeLogsDoc(space: string): ExternalTimeLogsDoc {
	const docId = externalTimeLogsDocId(space);
	let doc = _syncServer!.getDoc<ExternalTimeLogsDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<ExternalTimeLogsDoc>(), 'init external-time-logs', (d) => {
			const init = externalTimeLogsSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

// ── External Time Logs API (backlog-md integration) ──

routes.post("/api/external-time-logs", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = 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 { backlogTaskId, backlogTaskTitle, memberName, hours, skill, note, loggedAt } = body;

	if (!backlogTaskId || !memberName || !hours || !skill) {
		return c.json({ error: "backlogTaskId, memberName, hours, skill required" }, 400);
	}
	if (typeof hours !== 'number' || hours <= 0) {
		return c.json({ error: "hours must be a positive number" }, 400);
	}

	const VALID_SKILLS = ['facilitation', 'design', 'tech', 'outreach', 'logistics'];
	if (!VALID_SKILLS.includes(skill)) {
		return c.json({ error: `skill must be one of: ${VALID_SKILLS.join(', ')}` }, 400);
	}

	const id = newId();
	const now = Date.now();

	ensureExternalTimeLogsDoc(space);
	ensureCommitmentsDoc(space);

	// Create external time log entry
	_syncServer!.changeDoc<ExternalTimeLogsDoc>(externalTimeLogsDocId(space), 'import time log', (d) => {
		d.logs[id] = {
			id,
			backlogTaskId,
			backlogTaskTitle: backlogTaskTitle || backlogTaskId,
			memberName,
			memberId: (claims.did as string) || undefined,
			hours,
			skill: skill as Skill,
			note: note || undefined,
			loggedAt: loggedAt || now,
			importedAt: now,
			status: 'pending',
		} as any;
	});

	// Auto-create a commitment from this time log
	const commitmentId = newId();
	_syncServer!.changeDoc<CommitmentsDoc>(commitmentsDocId(space), 'auto-create commitment from time log', (d) => {
		d.items[commitmentId] = {
			id: commitmentId,
			memberName,
			hours: Math.max(1, Math.min(10, hours)),
			skill: skill as Skill,
			desc: note || `Time logged: ${backlogTaskTitle || backlogTaskId}`,
			createdAt: now,
			status: 'active',
			ownerDid: (claims.did as string) || '',
		} as any;
	});

	// Link commitment to the time log
	_syncServer!.changeDoc<ExternalTimeLogsDoc>(externalTimeLogsDocId(space), 'link commitment', (d) => {
		d.logs[id].commitmentId = commitmentId as any;
		d.logs[id].status = 'commitment_created' as any;
	});

	// Auto-connect to weaving task if a linked task exists on canvas
	ensureWeavingDoc(space);
	const wDoc = _syncServer!.getDoc<WeavingDoc>(weavingDocId(space));
	const board = getWeavingBoard(space);
	if (wDoc && board) {
		// Find placed task whose rTasks title/desc contains backlogTaskId
		const placedIds = Object.keys(wDoc.weavingOverlays);
		const linkedTask = placedIds.find(tid => {
			const item = board.doc.tasks[tid];
			return item && (item.description?.includes(backlogTaskId) || item.title?.includes(backlogTaskId));
		});
		if (linkedTask) {
			const connId = newId();
			_syncServer!.changeDoc<WeavingDoc>(weavingDocId(space), 'auto-connect time log to task', (d) => {
				d.connections[connId] = {
					id: connId,
					fromCommitmentId: commitmentId,
					toTaskId: linkedTask,
					skill,
					hours,
					status: 'committed',
				} as any;
			});
		}
	}

	const doc = _syncServer!.getDoc<ExternalTimeLogsDoc>(externalTimeLogsDocId(space))!;
	return c.json({ log: doc.logs[id], commitmentId }, 201);
});

routes.get("/api/external-time-logs", (c) => {
	const space = c.req.param("space") || "demo";
	ensureExternalTimeLogsDoc(space);
	const doc = _syncServer!.getDoc<ExternalTimeLogsDoc>(externalTimeLogsDocId(space))!;
	let logs = Object.values(doc.logs);

	// Filter by query params
	const backlogTaskId = c.req.query("backlogTaskId");
	const memberName = c.req.query("memberName");
	const status = c.req.query("status");
	if (backlogTaskId) logs = logs.filter(l => l.backlogTaskId === backlogTaskId);
	if (memberName) logs = logs.filter(l => l.memberName === memberName);
	if (status) logs = logs.filter(l => l.status === status);

	return c.json({ logs });
});

routes.post("/api/external-time-logs/:id/settle", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const logId = c.req.param("id");
	ensureExternalTimeLogsDoc(space);

	const doc = _syncServer!.getDoc<ExternalTimeLogsDoc>(externalTimeLogsDocId(space))!;
	const log = doc.logs[logId];
	if (!log) return c.json({ error: "Time log not found" }, 404);
	if (log.status === 'settled') return c.json({ error: "Already settled" }, 400);

	// Update log status to settled
	_syncServer!.changeDoc<ExternalTimeLogsDoc>(externalTimeLogsDocId(space), 'settle time log', (d) => {
		d.logs[logId].status = 'settled' as any;
	});

	// Update commitment status
	if (log.commitmentId) {
		ensureCommitmentsDoc(space);
		const cDoc = _syncServer!.getDoc<CommitmentsDoc>(commitmentsDocId(space))!;
		if (cDoc.items[log.commitmentId]) {
			_syncServer!.changeDoc<CommitmentsDoc>(commitmentsDocId(space), 'settle commitment', (d) => {
				d.items[log.commitmentId!].status = 'settled' as any;
			});
		}
	}

	// Update reputation: self-attestation with neutral rating (3/5)
	const memberId = log.memberId || log.memberName;
	const { reputationDocId } = await import('./schemas-intent');
	const repDocId = reputationDocId(space);
	const { reputationKey } = await import('./reputation');
	const key = reputationKey(memberId, log.skill);

	const repDoc = _syncServer!.getDoc<any>(repDocId);
	if (repDoc) {
		_syncServer!.changeDoc<any>(repDocId, 'update reputation from settled time log', (d: any) => {
			if (!d.entries[key]) {
				d.entries[key] = {
					memberId,
					skill: log.skill,
					score: 50,
					completedHours: 0,
					ratings: [],
				};
			}
			d.entries[key].completedHours = (d.entries[key].completedHours || 0) + log.hours;
			// Self-attestation: neutral 3/5 rating
			d.entries[key].ratings.push({
				from: memberId,
				score: 3,
				timestamp: Date.now(),
			});
		});
	}

	// Update skill curves: add supply hours
	const { skillCurvesDocId } = await import('./schemas-intent');
	const scDocId = skillCurvesDocId(space);
	const scDoc = _syncServer!.getDoc<any>(scDocId);
	if (scDoc) {
		_syncServer!.changeDoc<any>(scDocId, 'update skill curve from settled time log', (d: any) => {
			if (!d.curves[log.skill]) {
				d.curves[log.skill] = {
					skill: log.skill,
					supplyHours: 0,
					demandHours: 0,
					currentPrice: 100,
					history: [],
				};
			}
			d.curves[log.skill].supplyHours = (d.curves[log.skill].supplyHours || 0) + log.hours;
		});
	}

	return c.json({ ok: true, logId, status: 'settled' });
});

routes.post("/api/tasks/:id/link-backlog", 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 taskId = c.req.param("id");
	const body = await c.req.json();
	const { backlogTaskId } = body;
	if (!backlogTaskId) return c.json({ error: "backlogTaskId required" }, 400);

	ensureTasksDoc(space);
	const doc = _syncServer!.getDoc<TasksDoc>(tasksDocId(space))!;
	if (!doc.tasks[taskId]) return c.json({ error: "Task not found" }, 404);

	// Store backlog task ID in the task description as a cross-reference
	_syncServer!.changeDoc<TasksDoc>(tasksDocId(space), 'link backlog task', (d) => {
		const t = d.tasks[taskId];
		const ref = `[backlog:${backlogTaskId}]`;
		if (!t.description.includes(ref)) {
			t.description = t.description ? `${t.description}\n${ref}` : ref;
		}
	});

	const updated = _syncServer!.getDoc<TasksDoc>(tasksDocId(space))!;
	return c.json(updated.tasks[taskId]);
});

// ── Weave API (rTasks integration) ──

/** Helper: find the rTasks board doc for a space's weaving canvas. */
function getWeavingBoard(space: string): { docId: string; doc: BoardDoc } | null {
	const wDoc = ensureWeavingDoc(space);
	const slug = wDoc.boardSlug || space;
	const docId = boardDocId(space, slug);
	const doc = _syncServer!.getDoc<BoardDoc>(docId);
	if (!doc) return null;
	return { docId, doc };
}

routes.get("/api/weave", async (c) => {
	const space = c.req.param("space") || "demo";

	let callerRole: SpaceRoleString = 'viewer';
	const token = extractToken(c.req.raw.headers);
	if (token) {
		try {
			const claims = await verifyToken(token);
			const resolved = await resolveCallerRole(space, claims);
			if (resolved) callerRole = resolved.role;
		} catch {}
	}

	ensureWeavingDoc(space);
	const wDoc = _syncServer!.getDoc<WeavingDoc>(weavingDocId(space))!;
	const board = getWeavingBoard(space);

	const allTasks: Record<string, TaskItem> = board?.doc.tasks || {};
	const placedIds = new Set(Object.keys(wDoc.weavingOverlays));

	const placedTasks = Object.values(allTasks)
		.filter(t => placedIds.has(t.id))
		.map(t => {
			const ov = wDoc.weavingOverlays[t.id];
			return {
				id: t.id, title: t.title, status: t.status, description: t.description,
				priority: t.priority, labels: t.labels, assigneeId: t.assigneeId, dueDate: t.dueDate,
				needs: ov.needs, canvasX: ov.canvasX, canvasY: ov.canvasY,
				notes: ov.notes, links: ov.links, intentFrameId: ov.intentFrameId,
			};
		});

	const unplacedTasks = filterArrayByVisibility(
		Object.values(allTasks).filter(t => !placedIds.has(t.id)),
		callerRole,
	);

	return c.json({
		boardSlug: wDoc.boardSlug || space,
		placedTasks,
		unplacedTasks: unplacedTasks.map(t => ({
			id: t.id, title: t.title, status: t.status, description: t.description,
			priority: t.priority, labels: t.labels, assigneeId: t.assigneeId, dueDate: t.dueDate,
		})),
		connections: Object.values(wDoc.connections),
		execStates: Object.values(wDoc.execStates),
		projectFrames: Object.values(wDoc.projectFrames),
	});
});

routes.post("/api/weave/bind-board", 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 { boardSlug } = await c.req.json();
	if (!boardSlug) return c.json({ error: "boardSlug required" }, 400);

	// Verify board exists
	const docId = boardDocId(space, boardSlug);
	if (!_syncServer!.getDoc<BoardDoc>(docId)) {
		return c.json({ error: "Board not found" }, 404);
	}

	ensureWeavingDoc(space);
	_syncServer!.changeDoc<WeavingDoc>(weavingDocId(space), 'bind board', (d) => {
		d.boardSlug = boardSlug;
	});

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

routes.post("/api/weave/place/:rtasksId", 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 rtasksId = c.req.param("rtasksId");
	const body = await c.req.json();
	const { needs, canvasX, canvasY, notes, links } = body;

	// Verify task exists in rTasks board
	const board = getWeavingBoard(space);
	if (!board || !board.doc.tasks[rtasksId]) {
		return c.json({ error: "Task not found in rTasks board" }, 404);
	}

	ensureWeavingDoc(space);
	_syncServer!.changeDoc<WeavingDoc>(weavingDocId(space), 'place task on canvas', (d) => {
		d.weavingOverlays[rtasksId] = {
			rtasksId,
			needs: needs || {},
			canvasX: canvasX ?? 400,
			canvasY: canvasY ?? 150,
			notes: notes || '',
			links: links || [],
		} as any;
	});

	const wDoc = _syncServer!.getDoc<WeavingDoc>(weavingDocId(space))!;
	return c.json(wDoc.weavingOverlays[rtasksId], 201);
});

routes.delete("/api/weave/place/:rtasksId", 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 rtasksId = c.req.param("rtasksId");

	ensureWeavingDoc(space);
	const wDoc = _syncServer!.getDoc<WeavingDoc>(weavingDocId(space))!;
	if (!wDoc.weavingOverlays[rtasksId]) return c.json({ error: "Not placed" }, 404);

	_syncServer!.changeDoc<WeavingDoc>(weavingDocId(space), 'remove from canvas', (d) => {
		delete d.weavingOverlays[rtasksId];
		// Remove associated connections
		for (const [connId, conn] of Object.entries(d.connections)) {
			if (conn.toTaskId === rtasksId) delete d.connections[connId];
		}
		// Remove exec state
		delete d.execStates[rtasksId];
	});

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

routes.put("/api/weave/overlay/:rtasksId", 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 rtasksId = c.req.param("rtasksId");
	const body = await c.req.json();

	ensureWeavingDoc(space);
	const wDoc = _syncServer!.getDoc<WeavingDoc>(weavingDocId(space))!;
	if (!wDoc.weavingOverlays[rtasksId]) return c.json({ error: "Not placed" }, 404);

	_syncServer!.changeDoc<WeavingDoc>(weavingDocId(space), 'update overlay', (d) => {
		const ov = d.weavingOverlays[rtasksId];
		if (body.needs !== undefined) ov.needs = body.needs;
		if (body.canvasX !== undefined) ov.canvasX = body.canvasX;
		if (body.canvasY !== undefined) ov.canvasY = body.canvasY;
		if (body.notes !== undefined) ov.notes = body.notes;
		if (body.links !== undefined) ov.links = body.links;
		if (body.intentFrameId !== undefined) ov.intentFrameId = body.intentFrameId;
	});

	const updated = _syncServer!.getDoc<WeavingDoc>(weavingDocId(space))!;
	return c.json(updated.weavingOverlays[rtasksId]);
});

routes.post("/api/weave/create-and-place", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = 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 { title, description, needs, canvasX, canvasY, intentFrameId } = body;
	if (!title) return c.json({ error: "title required" }, 400);

	// Find or create the board
	const board = getWeavingBoard(space);
	if (!board) return c.json({ error: "No rTasks board bound" }, 404);

	// Create TaskItem in rTasks
	const taskId = newId();
	const taskItem = createTaskItem(taskId, space, title, {
		description: description || '',
		createdBy: (claims.did as string) || null,
	});

	_syncServer!.changeDoc<BoardDoc>(board.docId, 'create task from weave', (d) => {
		d.tasks[taskId] = taskItem as any;
	});

	// Create WeavingOverlay
	ensureWeavingDoc(space);
	_syncServer!.changeDoc<WeavingDoc>(weavingDocId(space), 'place new task on canvas', (d) => {
		d.weavingOverlays[taskId] = {
			rtasksId: taskId,
			needs: needs || {},
			canvasX: canvasX ?? 400,
			canvasY: canvasY ?? 150,
			notes: '',
			links: [],
			intentFrameId,
		} as any;
	});

	return c.json({ taskId, title, needs: needs || {} }, 201);
});

// ── Exec State API (updated: write to WeavingDoc) ──

routes.put("/api/weave/exec-state/:taskId", 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 taskId = c.req.param("taskId");
	const body = await c.req.json();
	const { steps, launchedAt } = body;
	ensureWeavingDoc(space);

	_syncServer!.changeDoc<WeavingDoc>(weavingDocId(space), 'update exec state', (d) => {
		if (!d.execStates[taskId]) {
			d.execStates[taskId] = { taskId, steps: {}, launchedAt: undefined } as any;
		}
		if (steps) d.execStates[taskId].steps = steps;
		if (launchedAt) d.execStates[taskId].launchedAt = launchedAt;
	});

	const doc = _syncServer!.getDoc<WeavingDoc>(weavingDocId(space))!;
	return c.json(doc.execStates[taskId]);
});

// ── Cyclos proxy config ──
const CYCLOS_URL = process.env.CYCLOS_URL || '';
const CYCLOS_API_KEY = process.env.CYCLOS_API_KEY || '';

function cyclosHeaders(): Record<string, string> {
	const h: Record<string, string> = { 'Content-Type': 'application/json' };
	if (CYCLOS_API_KEY) h['Authorization'] = `Basic ${Buffer.from(CYCLOS_API_KEY).toString('base64')}`;
	return h;
}

// ── Demo seeding ──

const DEMO_COMMITMENTS: Omit<Commitment, 'id' | 'createdAt'>[] = [
	{ memberName: 'Maya Chen', hours: 3, skill: 'facilitation', desc: 'Circle facilitation for group sessions' },
	{ memberName: 'Jordan Rivera', hours: 2, skill: 'design', desc: 'Event poster and social media graphics' },
	{ memberName: 'Sam Okafor', hours: 4, skill: 'tech', desc: 'Website updates and form setup' },
	{ memberName: 'Priya Sharma', hours: 2, skill: 'outreach', desc: 'Community outreach and flyering' },
	{ memberName: 'Alex Kim', hours: 1, skill: 'logistics', desc: 'Venue setup and teardown' },
	{ memberName: 'Taylor Brooks', hours: 3, skill: 'facilitation', desc: 'Harm reduction education session' },
	{ memberName: 'Robin Patel', hours: 2, skill: 'design', desc: 'Printed resource cards' },
	{ memberName: 'Casey Morgan', hours: 2, skill: 'logistics', desc: 'Supply procurement and transport' },
];

const DEMO_SEED_TASKS: { title: string; needs: Record<string, number> }[] = [
	{ title: 'Organize Community Event', needs: { facilitation: 3, design: 2, outreach: 2, logistics: 2 } },
	{ title: 'Run Harm Reduction Workshop', needs: { facilitation: 4, design: 2, tech: 4, logistics: 1 } },
];

function seedDemoIfEmpty(space: string = 'demo') {
	if (!_syncServer) return;
	const existing = _syncServer.getDoc<CommitmentsDoc>(commitmentsDocId(space));
	if (existing?.meta?.seeded) return;

	ensureCommitmentsDoc(space);
	const now = Date.now();

	_syncServer.changeDoc<CommitmentsDoc>(commitmentsDocId(space), 'seed commitments', (d) => {
		for (const c of DEMO_COMMITMENTS) {
			const id = newId();
			d.items[id] = { id, ...c, createdAt: now } as any;
		}
		(d.meta as any).seeded = true;
	});

	// Seed tasks into rTasks board + place on weaving canvas
	const board = getWeavingBoard(space);
	if (board) {
		ensureWeavingDoc(space);
		let canvasX = 300;
		for (const t of DEMO_SEED_TASKS) {
			const id = newId();
			const taskItem = createTaskItem(id, space, t.title, { description: '' });
			_syncServer.changeDoc<BoardDoc>(board.docId, 'seed rtime task', (d) => {
				d.tasks[id] = taskItem as any;
			});
			_syncServer.changeDoc<WeavingDoc>(weavingDocId(space), 'seed weaving overlay', (d) => {
				d.weavingOverlays[id] = {
					rtasksId: id,
					needs: t.needs,
					canvasX,
					canvasY: 150,
					notes: '',
					links: [],
				} as any;
			});
			canvasX += 280;
		}
		console.log(`[rTime] Demo seeded for "${space}": ${DEMO_COMMITMENTS.length} commitments, ${DEMO_SEED_TASKS.length} tasks (rTasks + weaving)`);
	} else {
		// Fallback: seed into legacy TasksDoc
		ensureTasksDoc(space);
		_syncServer.changeDoc<TasksDoc>(tasksDocId(space), 'seed tasks (legacy)', (d) => {
			for (const t of DEMO_SEED_TASKS) {
				const id = newId();
				d.tasks[id] = { id, name: t.title, description: '', needs: t.needs, links: [], notes: '' } as any;
			}
		});
		console.log(`[rTime] Demo seeded for "${space}": ${DEMO_COMMITMENTS.length} commitments, ${DEMO_SEED_TASKS.length} tasks (legacy)`);
	}
}

// ── Commitments API ──

routes.get("/api/commitments", async (c) => {
	const space = c.req.param("space") || "demo";

	// 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(space, claims);
			if (resolved) callerRole = resolved.role;
		} catch {}
	}

	ensureCommitmentsDoc(space);
	const doc = _syncServer!.getDoc<CommitmentsDoc>(commitmentsDocId(space))!;
	const items = filterArrayByVisibility(Object.values(doc.items), callerRole);
	return c.json({ commitments: items });
});

routes.post("/api/commitments", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = 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 { memberName, hours, skill, desc } = body;
	if (!memberName || !hours || !skill) return c.json({ error: "memberName, hours, skill required" }, 400);

	const id = newId();
	const now = Date.now();
	ensureCommitmentsDoc(space);

	_syncServer!.changeDoc<CommitmentsDoc>(commitmentsDocId(space), 'add commitment', (d) => {
		d.items[id] = { id, memberName, hours: Math.max(1, Math.min(10, hours)), skill, desc: desc || '', createdAt: now, ownerDid: (claims.did as string) || '' } as any;
	});

	const doc = _syncServer!.getDoc<CommitmentsDoc>(commitmentsDocId(space))!;
	return c.json(doc.items[id], 201);
});

routes.delete("/api/commitments/:id", 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 id = c.req.param("id");
	ensureCommitmentsDoc(space);

	const doc = _syncServer!.getDoc<CommitmentsDoc>(commitmentsDocId(space))!;
	if (!doc.items[id]) return c.json({ error: "Not found" }, 404);

	_syncServer!.changeDoc<CommitmentsDoc>(commitmentsDocId(space), 'remove commitment', (d) => {
		delete d.items[id];
	});

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

// ── Tasks API (compat shim — reads from WeavingDoc + rTasks board) ──

routes.get("/api/tasks", async (c) => {
	const space = c.req.param("space") || "demo";

	let callerRole: SpaceRoleString = 'viewer';
	const token = extractToken(c.req.raw.headers);
	if (token) {
		try {
			const claims = await verifyToken(token);
			const resolved = await resolveCallerRole(space, claims);
			if (resolved) callerRole = resolved.role;
		} catch {}
	}

	ensureWeavingDoc(space);
	const wDoc = _syncServer!.getDoc<WeavingDoc>(weavingDocId(space))!;
	const board = getWeavingBoard(space);

	// Build legacy-shaped task list from placed overlays + rTasks items
	const tasks: any[] = [];
	for (const [id, ov] of Object.entries(wDoc.weavingOverlays)) {
		const item = board?.doc.tasks[id];
		tasks.push({
			id,
			name: item?.title || id,
			description: item?.description || '',
			needs: ov.needs,
			links: ov.links,
			notes: ov.notes,
			intentFrameId: ov.intentFrameId,
		});
	}

	// Also include legacy TasksDoc tasks if any (migration compat)
	const legacyDoc = _syncServer!.getDoc<TasksDoc>(tasksDocId(space));
	if (legacyDoc) {
		for (const t of Object.values(legacyDoc.tasks)) {
			if (!wDoc.weavingOverlays[t.id]) {
				tasks.push(t);
			}
		}
	}

	return c.json({
		tasks: filterArrayByVisibility(tasks, callerRole),
		connections: Object.values(wDoc.connections),
		execStates: Object.values(wDoc.execStates),
	});
});

// POST /api/tasks — compat shim: creates rTasks item + places on canvas
routes.post("/api/tasks", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = 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 { name, description, needs, intentFrameId } = body;
	if (!name || !needs) return c.json({ error: "name, needs required" }, 400);

	const board = getWeavingBoard(space);
	if (!board) {
		// Fallback to legacy TasksDoc if no board bound
		const id = newId();
		ensureTasksDoc(space);
		_syncServer!.changeDoc<TasksDoc>(tasksDocId(space), 'add task (legacy)', (d) => {
			d.tasks[id] = { id, name, description: description || '', needs, links: [], notes: '' } as any;
		});
		const doc = _syncServer!.getDoc<TasksDoc>(tasksDocId(space))!;
		return c.json(doc.tasks[id], 201);
	}

	// Create in rTasks + WeavingDoc
	const taskId = newId();
	const taskItem = createTaskItem(taskId, space, name, {
		description: description || '',
		createdBy: (claims.did as string) || null,
	});
	_syncServer!.changeDoc<BoardDoc>(board.docId, 'create task from rtime compat', (d) => {
		d.tasks[taskId] = taskItem as any;
	});

	ensureWeavingDoc(space);
	_syncServer!.changeDoc<WeavingDoc>(weavingDocId(space), 'place task (compat)', (d) => {
		d.weavingOverlays[taskId] = {
			rtasksId: taskId,
			needs: needs || {},
			canvasX: 400,
			canvasY: 150,
			notes: '',
			links: [],
			intentFrameId,
		} as any;
	});

	return c.json({ id: taskId, name, description: description || '', needs, links: [], notes: '' }, 201);
});

// PUT /api/tasks/:id — compat shim: updates rTasks title/desc + WeavingDoc overlay
routes.put("/api/tasks/:id", 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 id = c.req.param("id");
	const body = await c.req.json();

	// Update rTasks item (title, description)
	const board = getWeavingBoard(space);
	if (board?.doc.tasks[id]) {
		_syncServer!.changeDoc<BoardDoc>(board.docId, 'update task from rtime', (d) => {
			const t = d.tasks[id];
			if (body.name !== undefined) t.title = body.name;
			if (body.description !== undefined) t.description = body.description;
			t.updatedAt = Date.now();
		});
	}

	// Update WeavingDoc overlay (needs, notes, links)
	ensureWeavingDoc(space);
	const wDoc = _syncServer!.getDoc<WeavingDoc>(weavingDocId(space))!;
	if (wDoc.weavingOverlays[id]) {
		_syncServer!.changeDoc<WeavingDoc>(weavingDocId(space), 'update overlay (compat)', (d) => {
			const ov = d.weavingOverlays[id];
			if (body.needs !== undefined) ov.needs = body.needs;
			if (body.links !== undefined) ov.links = body.links;
			if (body.notes !== undefined) ov.notes = body.notes;
		});
	} else {
		// Legacy fallback
		ensureTasksDoc(space);
		const legacyDoc = _syncServer!.getDoc<TasksDoc>(tasksDocId(space))!;
		if (legacyDoc.tasks[id]) {
			_syncServer!.changeDoc<TasksDoc>(tasksDocId(space), 'update task (legacy)', (d) => {
				const t = d.tasks[id];
				if (body.name !== undefined) t.name = body.name;
				if (body.description !== undefined) t.description = body.description;
				if (body.needs !== undefined) t.needs = body.needs;
				if (body.links !== undefined) t.links = body.links;
				if (body.notes !== undefined) t.notes = body.notes;
			});
		} else {
			return c.json({ error: "Not found" }, 404);
		}
	}

	// Return merged view
	const item = board?.doc ? _syncServer!.getDoc<BoardDoc>(board.docId)?.tasks[id] : null;
	const ov = _syncServer!.getDoc<WeavingDoc>(weavingDocId(space))?.weavingOverlays[id];
	return c.json({
		id,
		name: item?.title || id,
		description: item?.description || '',
		needs: ov?.needs || {},
		links: ov?.links || [],
		notes: ov?.notes || '',
	});
});

// ── Connections API (now writes to WeavingDoc) ──

routes.post("/api/connections", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = 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 { fromCommitmentId, toTaskId, skill, hours } = body;
	if (!fromCommitmentId || !toTaskId || !skill) return c.json({ error: "fromCommitmentId, toTaskId, skill required" }, 400);
	if (typeof hours !== 'number' || hours <= 0) return c.json({ error: "hours must be a positive number" }, 400);

	const id = newId();
	ensureWeavingDoc(space);
	ensureCommitmentsDoc(space);

	// Validate: hours <= commitment's available hours
	const cDoc = _syncServer!.getDoc<CommitmentsDoc>(commitmentsDocId(space));
	const commitment = cDoc?.items?.[fromCommitmentId];
	if (!commitment) return c.json({ error: "Commitment not found" }, 404);

	const wDoc = _syncServer!.getDoc<WeavingDoc>(weavingDocId(space))!;

	// Validate task is placed on canvas
	if (!wDoc.weavingOverlays[toTaskId]) {
		return c.json({ error: "Task not placed on weaving canvas" }, 400);
	}

	const usedHours = Object.values(wDoc.connections)
		.filter(cn => cn.fromCommitmentId === fromCommitmentId)
		.reduce((sum, cn) => sum + (cn.hours || 0), 0);
	if (hours > commitment.hours - usedHours) {
		return c.json({ error: "Requested hours exceed available hours" }, 400);
	}

	_syncServer!.changeDoc<WeavingDoc>(weavingDocId(space), 'add connection', (d) => {
		d.connections[id] = { id, fromCommitmentId, toTaskId, skill, hours, status: 'proposed' } as any;
	});

	// Notify commitment owner
	const board = getWeavingBoard(space);
	const taskItem = board?.doc.tasks[toTaskId];
	if (commitment?.ownerDid && commitment.ownerDid !== claims.did) {
		notify({
			userDid: commitment.ownerDid,
			category: 'module',
			eventType: 'commitment_requested',
			title: `${hours}hr of your ${commitment.hours}hr ${skill} commitment was requested`,
			body: taskItem ? `Task: ${taskItem.title}` : undefined,
			spaceSlug: space,
			moduleId: 'rtime',
			actionUrl: `/rtime`,
			actorDid: claims.did as string | undefined,
			metadata: { resultId: id, fromCommitmentId },
		}).catch(() => {});
	}

	const updatedW = _syncServer!.getDoc<WeavingDoc>(weavingDocId(space))!;
	return c.json(updatedW.connections[id], 201);
});

routes.delete("/api/connections/:id", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const id = c.req.param("id");
	ensureWeavingDoc(space);
	ensureCommitmentsDoc(space);

	const wDoc = _syncServer!.getDoc<WeavingDoc>(weavingDocId(space))!;
	const connection = wDoc.connections[id];
	if (!connection) return c.json({ error: "Not found" }, 404);

	const cDoc = _syncServer!.getDoc<CommitmentsDoc>(commitmentsDocId(space));
	const commitment = cDoc?.items?.[connection.fromCommitmentId];

	_syncServer!.changeDoc<WeavingDoc>(weavingDocId(space), 'remove connection', (d) => {
		delete d.connections[id];
	});

	if (commitment?.ownerDid && commitment.ownerDid !== (claims.did as string)) {
		notify({
			userDid: commitment.ownerDid,
			category: 'module',
			eventType: 'commitment_declined',
			title: `Your ${commitment.hours}hr ${commitment.skill} commitment request was declined`,
			spaceSlug: space,
			moduleId: 'rtime',
			actionUrl: `/rtime`,
			actorDid: claims.did as string | undefined,
		}).catch(() => {});
	}

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

routes.patch("/api/connections/:id", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const id = c.req.param("id");
	const body = await c.req.json();
	const { status } = body;
	if (status !== 'committed' && status !== 'declined') return c.json({ error: "status must be 'committed' or 'declined'" }, 400);

	ensureWeavingDoc(space);
	ensureCommitmentsDoc(space);

	const wDoc = _syncServer!.getDoc<WeavingDoc>(weavingDocId(space))!;
	const connection = wDoc.connections[id];
	if (!connection) return c.json({ error: "Not found" }, 404);

	if (status === 'declined') {
		_syncServer!.changeDoc<WeavingDoc>(weavingDocId(space), 'decline connection', (d) => {
			delete d.connections[id];
		});
		return c.json({ ok: true, deleted: true });
	}

	_syncServer!.changeDoc<WeavingDoc>(weavingDocId(space), 'approve connection', (d) => {
		d.connections[id].status = 'committed' as any;
	});

	const updated = _syncServer!.getDoc<WeavingDoc>(weavingDocId(space))!;
	return c.json(updated.connections[id]);
});

// ── Exec State API (compat — redirects to WeavingDoc) ──

routes.put("/api/tasks/:id/exec-state", 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 taskId = c.req.param("id");
	const body = await c.req.json();
	const { steps, launchedAt } = body;
	ensureWeavingDoc(space);

	_syncServer!.changeDoc<WeavingDoc>(weavingDocId(space), 'update exec state', (d) => {
		if (!d.execStates[taskId]) {
			d.execStates[taskId] = { taskId, steps: {}, launchedAt: undefined } as any;
		}
		if (steps) d.execStates[taskId].steps = steps;
		if (launchedAt) d.execStates[taskId].launchedAt = launchedAt;
	});

	const doc = _syncServer!.getDoc<WeavingDoc>(weavingDocId(space))!;
	return c.json(doc.execStates[taskId]);
});

// ── Cyclos proxy API (graceful no-op when CYCLOS_URL not set) ──

routes.get("/api/cyclos/members", async (c) => {
	if (!CYCLOS_URL) return c.json({ error: "Cyclos not configured", members: [] });
	try {
		const resp = await fetch(`${CYCLOS_URL}/api/users?roles=member&fields=id,display,email`, { headers: cyclosHeaders() });
		if (!resp.ok) throw new Error(`Cyclos ${resp.status}`);
		const users = await resp.json() as any[];
		const members = await Promise.all(users.map(async (u: any) => {
			try {
				const balResp = await fetch(`${CYCLOS_URL}/api/${u.id}/accounts`, { headers: cyclosHeaders() });
				const accounts = balResp.ok ? await balResp.json() as any[] : [];
				const balance = accounts[0]?.status?.balance || '0';
				return { id: u.id, name: u.display, email: u.email, balance: parseFloat(balance) };
			} catch {
				return { id: u.id, name: u.display, email: u.email, balance: 0 };
			}
		}));
		return c.json({ members });
	} catch (err: any) {
		return c.json({ error: 'Failed to fetch from Cyclos', members: [] }, 502);
	}
});

routes.post("/api/cyclos/commitments", async (c) => {
	if (!CYCLOS_URL) return c.json({ error: "Cyclos not configured" }, 501);
	const body = await c.req.json();
	const { fromUserId, amount, description } = body;
	if (!fromUserId || !amount) return c.json({ error: "fromUserId, amount required" }, 400);
	try {
		const resp = await fetch(`${CYCLOS_URL}/api/${fromUserId}/payments`, {
			method: 'POST',
			headers: cyclosHeaders(),
			body: JSON.stringify({ type: 'user.toSystem', amount: String(amount), description: description || 'Commitment', subject: 'system' }),
		});
		if (!resp.ok) throw new Error(await resp.text());
		const result = await resp.json();
		return c.json(result);
	} catch (err: any) {
		return c.json({ error: 'Cyclos commitment failed' }, 502);
	}
});

routes.post("/api/cyclos/transfers", async (c) => {
	if (!CYCLOS_URL) return c.json({ error: "Cyclos not configured" }, 501);
	const body = await c.req.json();
	const { fromUserId, toUserId, amount, description } = body;
	if (!fromUserId || !toUserId || !amount) return c.json({ error: "fromUserId, toUserId, amount required" }, 400);
	try {
		const resp = await fetch(`${CYCLOS_URL}/api/${fromUserId}/payments`, {
			method: 'POST',
			headers: cyclosHeaders(),
			body: JSON.stringify({ type: 'user.toUser', amount: String(amount), description: description || 'Hour transfer', subject: toUserId }),
		});
		if (!resp.ok) throw new Error(await resp.text());
		const result = await resp.json();
		return c.json(result);
	} catch (err: any) {
		return c.json({ error: 'Cyclos transfer failed' }, 502);
	}
});

// ── Page routes ──

// ── Shell tabs for rTime ──
const RTIME_TABS = [
	{ id: "pool", label: "Commitment Pool" },
	{ id: "fulfillment", label: "Fulfillment" },
	{ id: "cyclos", label: "Open in Cyclos" },
] as const;

const RTIME_TAB_IDS = new Set(RTIME_TABS.map(t => t.id));

function renderTimePage(space: string, view: string, activeTab: string, isSubdomain: boolean) {
	return renderShell({
		title: `${space} — rTime | rSpace`,
		moduleId: "rtime",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-timebank-app space="${space}" view="${view}"></folk-timebank-app>`,
		scripts: `<script type="module" src="/modules/rtime/folk-timebank-app.js?v=2"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rtime/rtime.css">`,
		tabs: [...RTIME_TABS],
		activeTab,
		tabBasePath: isSubdomain ? `/rtime` : `/${space}/rtime`,
	});
}

routes.get("/pool", (c) => {
	const space = c.req.param("space") || "demo";
	return c.html(renderTimePage(space, "canvas", "pool", c.get("isSubdomain")));
});

routes.get("/fulfillment", (c) => {
	const space = c.req.param("space") || "demo";
	return c.html(renderTimePage(space, "dashboard", "fulfillment", c.get("isSubdomain")));
});

routes.get("/cyclos", (c) => {
	const cyclosUrl = process.env.CYCLOS_URL || "https://www.cyclos.org";
	return c.redirect(cyclosUrl, 302);
});

routes.get("/", (c) => {
	const space = c.req.param("space") || "demo";
	return c.html(renderTimePage(space, "canvas", "pool", c.get("isSubdomain")));
});

routes.post("/api/tasks/:id/export-to-backlog", 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 taskId = c.req.param("id");

	// Try WeavingDoc + rTasks first, fallback to legacy TasksDoc
	ensureWeavingDoc(space);
	const wDoc = _syncServer!.getDoc<WeavingDoc>(weavingDocId(space))!;
	const ov = wDoc.weavingOverlays[taskId];
	const board = getWeavingBoard(space);
	const item = board?.doc.tasks[taskId];

	if (ov && item) {
		const estimatedHours = Object.values(ov.needs).reduce((sum, h) => sum + h, 0);
		const acceptanceCriteria = Object.entries(ov.needs).map(([skill, hours]) =>
			`${SKILL_LABELS[skill as Skill] || skill}: ${hours}h`
		);
		return c.json({
			title: item.title,
			description: item.description?.replace(/\[backlog:[^\]]+\]/g, '').trim() || '',
			estimatedHours,
			labels: Object.keys(ov.needs),
			notes: ov.notes || '',
			acceptanceCriteria,
			rtime: { taskId, space },
		});
	}

	// Legacy fallback
	ensureTasksDoc(space);
	const doc = _syncServer!.getDoc<TasksDoc>(tasksDocId(space))!;
	const task = doc.tasks[taskId];
	if (!task) return c.json({ error: "Task not found" }, 404);

	const estimatedHours = Object.values(task.needs).reduce((sum, h) => sum + h, 0);
	const acceptanceCriteria = Object.entries(task.needs).map(([skill, hours]) =>
		`${SKILL_LABELS[skill as Skill] || skill}: ${hours}h`
	);
	return c.json({
		title: task.name,
		description: task.description?.replace(/\[backlog:[^\]]+\]/g, '').trim() || '',
		estimatedHours,
		labels: Object.keys(task.needs),
		notes: task.notes || '',
		acceptanceCriteria,
		rtime: { taskId: task.id, space },
	});
});

// Legacy routes — redirect to new tab paths
routes.get("/canvas", (c) => {
	const space = c.req.param("space") || "demo";
	return c.redirect(c.get("isSubdomain") ? `/rtime/pool` : `/${space}/rtime/pool`, 301);
});
routes.get("/collaborate", (c) => {
	const space = c.req.param("space") || "demo";
	return c.redirect(c.get("isSubdomain") ? `/rtime/pool` : `/${space}/rtime/pool`, 301);
});
routes.get("/dashboard", (c) => {
	const space = c.req.param("space") || "demo";
	return c.redirect(c.get("isSubdomain") ? `/rtime/fulfillment` : `/${space}/rtime/fulfillment`, 301);
});

// ── Module export ──

export const timeModule: RSpaceModule = {
	id: "rtime",
	name: "rTime",
	icon: "⏳",
	description: "Timebank commitment pool & weaving dashboard",
	canvasShapes: ["folk-commitment-pool", "folk-task-request"],
	canvasToolIds: ["create_commitment_pool", "create_task_request", "create_weaving_coverage"],
	scoping: { defaultScope: 'space', userConfigurable: false },
	docSchemas: [
		{ pattern: '{space}:rtime:commitments', description: 'Commitment pool', init: commitmentsSchema.init },
		{ pattern: '{space}:rtime:tasks', description: 'Tasks, connections, exec states (legacy)', init: tasksSchema.init },
		{ pattern: '{space}:rtime:weaving', description: 'Weaving overlays, connections, exec states (rTasks integration)', init: weavingSchema.init },
		{ pattern: '{space}:rtime:intents', description: 'Intent pool (offers & needs)', init: intentsSchema.init },
		{ pattern: '{space}:rtime:solver-results', description: 'Solver collaboration recommendations', init: solverResultsSchema.init },
		{ pattern: '{space}:rtime:skill-curves', description: 'Per-skill demand/supply pricing', init: skillCurvesSchema.init },
		{ pattern: '{space}:rtime:reputation', description: 'Per-member per-skill reputation', init: reputationSchema.init },
		{ pattern: '{space}:rtime:external-time-logs', description: 'External time logs from backlog-md', init: externalTimeLogsSchema.init },
	],
	routes,
	landingPage: renderLanding,
	seedTemplate: seedDemoIfEmpty,
	async onInit(ctx) {
		_syncServer = ctx.syncServer;
		seedDemoIfEmpty();
	},
	feeds: [
		{
			id: "commitments",
			name: "Commitments",
			kind: "economic",
			description: "Hour pledges from community members",
			filterable: true,
		},
	],
	// Views (Canvas, Collaborate, Fulfillment) are handled by the component's
	// internal tab-bar — no outputPaths needed to avoid duplicate navigation.
	onboardingActions: [
		{ label: "Pledge Hours", icon: "⏳", description: "Add a commitment to the pool", type: 'create', href: '/rtime' },
	],
};

// ── MI Data Export ──

export interface MICommitmentItem {
	id: string;
	memberName: string;
	hours: number;
	skill: string;
	desc: string;
	status: string;
}

export function getRecentCommitmentsForMI(space: string, limit = 5): MICommitmentItem[] {
	if (!_syncServer) return [];
	const doc = _syncServer.getDoc<CommitmentsDoc>(commitmentsDocId(space));
	if (!doc?.items) return [];

	return Object.values(doc.items)
		.filter(c => (c.status || "active") === "active")
		.sort((a, b) => b.createdAt - a.createdAt)
		.slice(0, limit)
		.map(c => ({
			id: c.id,
			memberName: c.memberName,
			hours: c.hours,
			skill: c.skill,
			desc: (c.desc || "").slice(0, 200),
			status: c.status || "active",
		}));
}
