/**
 * Flows module — budget flows, river visualization, and treasury management.
 *
 * Proxies flow-service API calls and serves the FlowRiver visualization.
 */

import { Hono } from "hono";
import * as Automerge from "@automerge/automerge";
import { renderShell } from "../../server/shell";
import type { RSpaceModule } from "../../shared/module";
import { getModuleInfoList } from "../../shared/module";
import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing";
import { flowsApplets } from "./applets";
import { getTransakEnv, getTransakWebhookSecret } from "../../shared/transak";
import type { SyncServer } from '../../server/local-first/sync-server';
import { flowsSchema, flowsDocId, type FlowsDoc, type SpaceFlow, type CanvasFlow, type DripsSync } from './schemas';
import { demoNodes } from './lib/presets';
import { OpenfortProvider } from './lib/openfort';
import { boardDocId, createTaskItem } from '../rtasks/schemas';
import type { BoardDoc } from '../rtasks/schemas';
import type { OutcomeNodeData, MortgagePosition, ReinvestmentPosition } from './lib/types';
import { getAvailableProviders, getProvider, getDefaultProvider } from './lib/onramp-registry';
import type { OnrampProviderId } from './lib/onramp-provider';
import { PimlicoClient } from './lib/pimlico';
import { getDripsAccountState } from './lib/drips-client';
import { mapDripsToFlowNodes } from './lib/drips-mapper';

let _syncServer: SyncServer | null = null;
let _openfort: OpenfortProvider | null = null;
let _pimlico: PimlicoClient | null = null;
const _completedOutcomes = new Set<string>(); // space:outcomeId — dedup for watcher

const FLOW_SERVICE_URL = process.env.FLOW_SERVICE_URL || "http://payment-flow:3010";

function ensureDoc(space: string): FlowsDoc {
	const docId = flowsDocId(space);
	let doc = _syncServer!.getDoc<FlowsDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<FlowsDoc>(), 'init', (d) => {
			const init = flowsSchema.init();
			d.meta = init.meta;
			d.meta.spaceSlug = space;
			d.spaceFlows = {};
			d.canvasFlows = {} as any;
			d.activeFlowId = '';
		});
		_syncServer!.setDoc(docId, doc);
	}
	// Migrate v1 → v2: add canvasFlows and activeFlowId
	if (!doc.canvasFlows || doc.meta.version < 2) {
		_syncServer!.changeDoc<FlowsDoc>(docId, 'migrate to v2', (d) => {
			if (!d.canvasFlows) d.canvasFlows = {} as any;
			if (!d.activeFlowId) d.activeFlowId = '' as any;
			d.meta.version = 2;
		});
		doc = _syncServer!.getDoc<FlowsDoc>(docId)!;
	}
	// Migrate v2 → v3: add mortgagePositions and reinvestmentPositions
	if (doc.meta.version < 3) {
		_syncServer!.changeDoc<FlowsDoc>(docId, 'migrate to v3', (d) => {
			if (!d.mortgagePositions) d.mortgagePositions = {} as any;
			if (!d.reinvestmentPositions) d.reinvestmentPositions = {} as any;
			d.meta.version = 3;
		});
		doc = _syncServer!.getDoc<FlowsDoc>(docId)!;
	}
	// Migrate v3 → v4: add budget fields
	if (doc.meta.version < 4) {
		_syncServer!.changeDoc<FlowsDoc>(docId, 'migrate to v4', (d) => {
			if (!d.budgetSegments) d.budgetSegments = {} as any;
			if (!d.budgetAllocations) d.budgetAllocations = {} as any;
			if (!d.budgetTotalAmount) d.budgetTotalAmount = 0 as any;
			d.meta.version = 4;
		});
		doc = _syncServer!.getDoc<FlowsDoc>(docId)!;
	}
	// Migrate v4 → v5: add linkedTaskIds to outcome nodes
	if (doc.meta.version < 5) {
		_syncServer!.changeDoc<FlowsDoc>(docId, 'migrate to v5', (d) => {
			if (d.canvasFlows) {
				for (const flow of Object.values(d.canvasFlows)) {
					if (!flow.nodes) continue;
					for (const node of flow.nodes as any[]) {
						if (node.type === 'outcome' && !node.data.linkedTaskIds) {
							node.data.linkedTaskIds = [] as any;
						}
					}
				}
			}
			d.meta.version = 5 as any;
		});
		doc = _syncServer!.getDoc<FlowsDoc>(docId)!;
	}
	// Migrate v5 → v6: add dripsSyncs to canvas flows
	if (doc.meta.version < 6) {
		_syncServer!.changeDoc<FlowsDoc>(docId, 'migrate to v6', (d) => {
			if (d.canvasFlows) {
				for (const flow of Object.values(d.canvasFlows)) {
					if (!(flow as any).dripsSyncs) (flow as any).dripsSyncs = {} as any;
				}
			}
			d.meta.version = 6 as any;
		});
		doc = _syncServer!.getDoc<FlowsDoc>(docId)!;
	}
	return doc;
}

/**
 * Create a DONE task in rTasks when an rFlows outcome is completed.
 * Deduplicates by checking for existing `ref:rflows:outcome:{id}` in the board doc.
 */
function createTaskForOutcome(space: string, outcomeId: string, label: string) {
	if (!_syncServer) return;
	const boardId = `${space}-bcrg`;
	const docId = boardDocId(space, boardId);

	// Ensure the board doc exists
	let doc = _syncServer.getDoc<BoardDoc>(docId);
	if (!doc) return; // BCRG board not seeded yet

	// Check for duplicate — look for ref:rflows:outcome:{outcomeId}
	const refTag = `ref:rflows:outcome:${outcomeId}`;
	for (const t of Object.values(doc.tasks)) {
		if (t.description?.includes(refTag)) return; // already exists
	}

	const taskId = crypto.randomUUID();
	_syncServer.changeDoc<BoardDoc>(docId, `Auto-create task for outcome ${outcomeId}`, (d) => {
		d.tasks[taskId] = createTaskItem(taskId, space, label, {
			status: 'DONE',
			priority: 'MEDIUM',
			description: `${refTag} — Auto-created from rFlows outcome completion`,
			labels: ['rflows', 'bcrg'],
			createdBy: 'did:system:rflows-watcher',
		});
	});

	console.log(`[rflows] Auto-created DONE task for outcome "${outcomeId}" in space "${space}"`);
}

const routes = new Hono();

// ─── Flow Service API proxy ─────────────────────────────
// These proxy to the payment-flow backend so the frontend
// can call them from the same origin.

routes.get("/api/flows", async (c) => {
	const owner = c.req.header("X-Owner-Address") || "";
	const space = c.req.query("space") || "";

	// If space filter provided, get flow IDs from Automerge doc
	if (space) {
		const doc = ensureDoc(space);
		const flowIds = Object.values(doc.spaceFlows).map((sf) => sf.flowId);
		if (flowIds.length === 0) return c.json([]);

		const flows = await Promise.all(
			flowIds.map(async (fid) => {
				try {
					const res = await fetch(`${FLOW_SERVICE_URL}/api/flows/${fid}`);
					if (res.ok) return await res.json();
				} catch {}
				return null;
			}),
		);
		return c.json(flows.filter(Boolean));
	}

	const res = await fetch(`${FLOW_SERVICE_URL}/api/flows?owner=${encodeURIComponent(owner)}`);
	return c.json(await res.json(), res.status as any);
});

routes.get("/api/flows/:flowId", async (c) => {
	const res = await fetch(`${FLOW_SERVICE_URL}/api/flows/${c.req.param("flowId")}`);
	return c.json(await res.json(), res.status as any);
});

routes.post("/api/flows", async (c) => {
	// Auth-gated: require EncryptID token
	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 body = await c.req.text();
	const res = await fetch(`${FLOW_SERVICE_URL}/api/flows`, {
		method: "POST",
		headers: {
			"Content-Type": "application/json",
			"X-Owner-Address": claims.sub,
		},
		body,
	});
	return c.json(await res.json(), res.status as any);
});

routes.post("/api/flows/:flowId/deposit", async (c) => {
	const body = await c.req.text();
	const res = await fetch(`${FLOW_SERVICE_URL}/api/flows/${c.req.param("flowId")}/deposit`, {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body,
	});
	return c.json(await res.json(), res.status as any);
});

routes.post("/api/flows/:flowId/withdraw", async (c) => {
	const body = await c.req.text();
	const res = await fetch(`${FLOW_SERVICE_URL}/api/flows/${c.req.param("flowId")}/withdraw`, {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body,
	});
	return c.json(await res.json(), res.status as any);
});

routes.post("/api/flows/:flowId/activate", async (c) => {
	const res = await fetch(`${FLOW_SERVICE_URL}/api/flows/${c.req.param("flowId")}/activate`, { method: "POST" });
	return c.json(await res.json(), res.status as any);
});

routes.post("/api/flows/:flowId/pause", async (c) => {
	const res = await fetch(`${FLOW_SERVICE_URL}/api/flows/${c.req.param("flowId")}/pause`, { method: "POST" });
	return c.json(await res.json(), res.status as any);
});

routes.post("/api/flows/:flowId/funnels", async (c) => {
	const body = await c.req.text();
	const res = await fetch(`${FLOW_SERVICE_URL}/api/flows/${c.req.param("flowId")}/funnels`, {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body,
	});
	return c.json(await res.json(), res.status as any);
});

routes.post("/api/flows/:flowId/outcomes", async (c) => {
	const body = await c.req.text();
	const res = await fetch(`${FLOW_SERVICE_URL}/api/flows/${c.req.param("flowId")}/outcomes`, {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body,
	});
	return c.json(await res.json(), res.status as any);
});

routes.get("/api/flows/:flowId/transactions", async (c) => {
	const res = await fetch(`${FLOW_SERVICE_URL}/api/flows/${c.req.param("flowId")}/transactions`);
	return c.json(await res.json(), res.status as any);
});

// ─── User on-ramp (email → wallet → widget) ─────────────

routes.post("/api/flows/user-onramp", async (c) => {
	try {
		const { email, fiatAmount, fiatCurrency, returnUrl, provider: reqProvider } = await c.req.json();
		if (!email || !fiatAmount || !fiatCurrency) {
			return c.json({ error: "email, fiatAmount, and fiatCurrency are required" }, 400);
		}

		if (!_openfort) return c.json({ error: "Openfort not configured" }, 503);

		// Resolve on-ramp provider: use requested, else first available
		const onramp = reqProvider
			? getProvider(reqProvider as OnrampProviderId)
			: getDefaultProvider();
		if (!onramp) return c.json({ error: "No on-ramp provider available" }, 503);

		// 1. Find or create Openfort smart wallet for this user (one wallet per email)
		const wallet = await _openfort.findOrCreateWallet(`user-${email}`, {
			type: 'user-onramp',
			email,
		});

		const sessionId = crypto.randomUUID();

		// 2. Create on-ramp session via provider
		const { widgetUrl, provider } = await onramp.createSession({
			walletAddress: wallet.address,
			email,
			fiatAmount,
			fiatCurrency,
			sessionId,
			returnUrl,
			hostname: new URL(c.req.url).hostname,
		});

		console.log(`[rflows] On-ramp session created: provider=${provider} session=${sessionId} wallet=${wallet.address}`);

		// Non-fatal side-effect: create fund claim → sends email via EncryptID
		const encryptidServiceKey = process.env.ENCRYPTID_SERVICE_KEY;
		if (encryptidServiceKey) {
			const encryptidUrl = process.env.ENCRYPTID_URL || 'https://auth.rspace.online';
			try {
				await fetch(`${encryptidUrl}/api/internal/fund-claims`, {
					method: 'POST',
					headers: {
						'Content-Type': 'application/json',
						'X-Service-Key': encryptidServiceKey,
					},
					body: JSON.stringify({
						email,
						walletAddress: wallet.address,
						openfortPlayerId: wallet.playerId,
						fiatAmount: String(fiatAmount),
						fiatCurrency,
						sessionId,
						provider,
					}),
				});
				console.log(`[rflows] Fund claim created for ${email}`);
			} catch (err) {
				console.error('[rflows] Failed to create fund claim:', err);
			}
		}

		return c.json({
			success: true,
			sessionId,
			widgetUrl,
			walletAddress: wallet.address,
			provider,
			isNewUser: true,
		});
	} catch (err) {
		console.error("[rflows] user-onramp failed:", err);
		let message: string;
		if (err instanceof Error) message = err.message;
		else if (err && typeof err === 'object') message = JSON.stringify(err);
		else message = String(err);
		return c.json({ error: message }, 500);
	}
});

// ─── On-ramp config ──────────────────────────────────────

routes.get("/api/onramp/config", (c) => {
	const available = getAvailableProviders();
	return c.json({
		provider: available[0]?.id || null,
		available,
	});
});

// Legacy endpoint — keep for backwards compat
routes.get("/api/transak/config", (c) => {
	return c.json({
		provider: "transak",
		environment: getTransakEnv(),
	});
});

routes.post("/api/transak/webhook", async (c) => {
	let body: any;
	try { body = await c.req.json(); } catch { return c.json({ error: "Invalid JSON" }, 400); }

	// HMAC verification — if webhook secret is set, validate signature
	const webhookSecret = getTransakWebhookSecret();
	if (webhookSecret) {
		const signature = c.req.header("x-transak-signature") || "";
		const { createHmac } = await import("crypto");
		// Re-serialize for HMAC (Transak signs the raw JSON body)
		const expected = createHmac("sha256", webhookSecret).update(JSON.stringify(body)).digest("hex");
		if (signature !== expected) {
			console.error("[Transak] Invalid webhook signature");
			return c.json({ error: "Invalid signature" }, 401);
		}
	}

	const { webhookData } = body;

	// Ack non-completion events (Transak sends multiple status updates)
	if (!webhookData || webhookData.status !== "COMPLETED") {
		return c.json({ ok: true });
	}

	const { partnerOrderId, cryptoAmount, cryptocurrency, network } = webhookData;
	if (!partnerOrderId || cryptocurrency !== "USDC" || !network?.toLowerCase().includes("base")) {
		return c.json({ error: "Invalid webhook data" }, 400);
	}

	// partnerOrderId format: "flowId:funnelId" or "flowId" (uses env default)
	const [flowId, funnelId] = partnerOrderId.split(":");
	if (!flowId) return c.json({ error: "Missing flowId in partnerOrderId" }, 400);
	const resolvedFunnelId = funnelId || process.env.FUNNEL_ID || "";
	if (!resolvedFunnelId) return c.json({ error: "Missing funnelId" }, 400);

	// Convert crypto amount to USDC units (6 decimals)
	const amountUnits = Math.round(parseFloat(cryptoAmount) * 1e6).toString();

	const depositUrl = `${FLOW_SERVICE_URL}/api/flows/${flowId}/deposit`;
	const res = await fetch(depositUrl, {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify({
			amount: amountUnits,
			source: "card",
			funnelId: resolvedFunnelId,
		}),
	});

	if (!res.ok) {
		console.error(`[Transak] Deposit failed: ${await res.text()}`);
		return c.json({ error: "Deposit failed" }, 500);
	}

	console.log(`[Transak] Deposit OK: flow=${flowId} amount=${amountUnits} USDC`);
	return c.json({ ok: true });
});

// ─── ERC-4337 UserOperation routes (Pimlico bundler) ─────

const ENTRY_POINT = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789'; // v0.6

routes.post("/api/flows/submit-userop", 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); }

	if (!_pimlico) return c.json({ error: "Pimlico bundler not configured" }, 503);

	const userOp = await c.req.json();
	try {
		const prepared = await _pimlico.prepareUserOperation(userOp, ENTRY_POINT);
		return c.json({ success: true, userOp: prepared, entryPoint: ENTRY_POINT });
	} catch (err) {
		console.error("[pimlico] prepare failed:", err);
		const msg = err instanceof Error ? err.message : String(err);
		return c.json({ error: msg }, 500);
	}
});

routes.post("/api/flows/send-userop", 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); }

	if (!_pimlico) return c.json({ error: "Pimlico bundler not configured" }, 503);

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

	try {
		const hash = await _pimlico.sendUserOperation(userOp, ENTRY_POINT);
		return c.json({ success: true, userOpHash: hash });
	} catch (err) {
		console.error("[pimlico] send failed:", err);
		const msg = err instanceof Error ? err.message : String(err);
		return c.json({ error: msg }, 500);
	}
});

routes.get("/api/flows/userop/:hash", 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); }

	if (!_pimlico) return c.json({ error: "Pimlico bundler not configured" }, 503);

	const hash = c.req.param("hash");
	try {
		const receipt = await _pimlico.getUserOperationReceipt(hash);
		return c.json({ receipt });
	} catch (err) {
		console.error("[pimlico] receipt failed:", err);
		const msg = err instanceof Error ? err.message : String(err);
		return c.json({ error: msg }, 500);
	}
});

// ─── Coinbase webhook ────────────────────────────────────

routes.post("/api/coinbase/webhook", async (c) => {
	let body: any;
	try { body = await c.req.json(); } catch { return c.json({ error: "Invalid JSON" }, 400); }

	// HMAC verification
	const webhookSecret = process.env.COINBASE_WEBHOOK_SECRET;
	if (webhookSecret) {
		const signature = c.req.header("x-cc-webhook-signature") || "";
		const { createHmac } = await import("crypto");
		const expected = createHmac("sha256", webhookSecret).update(JSON.stringify(body)).digest("hex");
		if (signature !== expected) {
			console.error("[Coinbase] Invalid webhook signature");
			return c.json({ error: "Invalid signature" }, 401);
		}
	}

	const { event } = body;
	if (!event || event.type !== "charge:confirmed") return c.json({ ok: true });

	const metadata = event.data?.metadata || {};
	const { flowId, funnelId } = metadata;
	const pricing = event.data?.pricing?.local;
	if (!flowId || !pricing) return c.json({ error: "Missing flowId or pricing" }, 400);

	const resolvedFunnelId = funnelId || process.env.FUNNEL_ID || "";
	if (!resolvedFunnelId) return c.json({ error: "Missing funnelId" }, 400);

	const amountUnits = Math.round(parseFloat(pricing.amount) * 1e6).toString();

	const depositUrl = `${FLOW_SERVICE_URL}/api/flows/${flowId}/deposit`;
	const res = await fetch(depositUrl, {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify({ amount: amountUnits, source: "card", funnelId: resolvedFunnelId }),
	});

	if (!res.ok) {
		console.error(`[Coinbase] Deposit failed: ${await res.text()}`);
		return c.json({ error: "Deposit failed" }, 500);
	}

	console.log(`[Coinbase] Deposit OK: flow=${flowId} amount=${amountUnits} USDC`);
	return c.json({ ok: true });
});

// ─── Ramp Network webhook ────────────────────────────────

routes.post("/api/ramp/webhook", async (c) => {
	let body: any;
	try { body = await c.req.json(); } catch { return c.json({ error: "Invalid JSON" }, 400); }

	// HMAC verification
	const webhookSecret = process.env.RAMP_WEBHOOK_SECRET;
	if (webhookSecret) {
		const signature = c.req.header("x-body-signature") || "";
		const { createHmac } = await import("crypto");
		const expected = createHmac("sha256", webhookSecret).update(JSON.stringify(body)).digest("hex");
		if (signature !== expected) {
			console.error("[Ramp] Invalid webhook signature");
			return c.json({ error: "Invalid signature" }, 401);
		}
	}

	if (body.type !== "RELEASED" || body.asset?.symbol !== "USDC") return c.json({ ok: true });

	const purchaseViewToken = body.purchaseViewToken || "";
	// Ramp uses receiverAddress metadata or custom purchase field for flowId
	const flowId = body.metadata?.flowId || body.flowId;
	const funnelId = body.metadata?.funnelId || body.funnelId;
	if (!flowId) return c.json({ error: "Missing flowId" }, 400);

	const resolvedFunnelId = funnelId || process.env.FUNNEL_ID || "";
	if (!resolvedFunnelId) return c.json({ error: "Missing funnelId" }, 400);

	const amountUnits = Math.round(parseFloat(body.cryptoAmount || "0") * 1e6).toString();

	const depositUrl = `${FLOW_SERVICE_URL}/api/flows/${flowId}/deposit`;
	const res = await fetch(depositUrl, {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify({ amount: amountUnits, source: "card", funnelId: resolvedFunnelId }),
	});

	if (!res.ok) {
		console.error(`[Ramp] Deposit failed: ${await res.text()}`);
		return c.json({ error: "Deposit failed" }, 500);
	}

	console.log(`[Ramp] Deposit OK: flow=${flowId} amount=${amountUnits} USDC`);
	return c.json({ ok: true });
});

// ─── Space-flow association endpoints ────────────────────

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

	const docId = flowsDocId(space);
	ensureDoc(space);
	_syncServer!.changeDoc<FlowsDoc>(docId, 'add space flow', (d) => {
		const key = `${space}:${flowId}`;
		if (!d.spaceFlows[key]) {
			d.spaceFlows[key] = { id: key, spaceSlug: space, flowId, addedBy: claims.sub, createdAt: Date.now() };
		}
	});
	return c.json({ ok: true });
});

routes.delete("/api/space-flows/:flowId", 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 flowId = c.req.param("flowId");
	const space = c.req.query("space") || "";
	if (!space) return c.json({ error: "space query param required" }, 400);

	const docId = flowsDocId(space);
	const doc = _syncServer!.getDoc<FlowsDoc>(docId);
	if (doc) {
		const key = `${space}:${flowId}`;
		if (doc.spaceFlows[key]) {
			_syncServer!.changeDoc<FlowsDoc>(docId, 'remove space flow', (d) => {
				delete d.spaceFlows[key];
			});
		}
	}
	return c.json({ ok: true });
});

// ─── Mortgage API routes ─────────────────────────────────

// Aave v3 Pool on Base
const AAVE_V3_POOL_BASE = '0xA238Dd80C259a72e81d7e4664a9801593F98d1c5';
const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const BASE_RPC = 'https://mainnet.base.org';

routes.get("/api/mortgage/rates", async (c) => {
	try {
		// getReserveData(address) selector = 0x35ea6a75
		const calldata = '0x35ea6a75000000000000000000000000' + USDC_BASE.slice(2).toLowerCase();
		const res = await fetch(BASE_RPC, {
			method: 'POST',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify({
				jsonrpc: '2.0', id: 1, method: 'eth_call',
				params: [{ to: AAVE_V3_POOL_BASE, data: calldata }, 'latest'],
			}),
		});
		const json = await res.json() as any;
		if (json.error) throw new Error(json.error.message);

		// currentLiquidityRate is the 2nd word (index 1) in the result tuple — 27 decimals (ray)
		const resultHex = json.result as string;
		// Each word is 32 bytes = 64 hex chars. Skip 0x prefix, word at index 2 (currentLiquidityRate)
		const liquidityRateHex = '0x' + resultHex.slice(2 + 64 * 2, 2 + 64 * 3);
		const liquidityRate = Number(BigInt(liquidityRateHex)) / 1e27;
		// Convert ray rate to APY: ((1 + rate/SECONDS_PER_YEAR)^SECONDS_PER_YEAR - 1) * 100
		const SECONDS_PER_YEAR = 31536000;
		const apy = (Math.pow(1 + liquidityRate / SECONDS_PER_YEAR, SECONDS_PER_YEAR) - 1) * 100;

		return c.json({
			rates: [
				{ protocol: 'Aave v3', chain: 'Base', asset: 'USDC', apy: Math.round(apy * 100) / 100, updatedAt: Date.now() },
			],
		});
	} catch (err) {
		console.error('[mortgage] Rate fetch failed:', err);
		return c.json({
			rates: [
				{ protocol: 'Aave v3', chain: 'Base', asset: 'USDC', apy: null, updatedAt: Date.now(), error: 'Unavailable' },
			],
		});
	}
});

routes.get("/api/mortgage/positions", async (c) => {
	const space = c.req.query("space") || "demo";
	const doc = ensureDoc(space);
	return c.json(Object.values(doc.mortgagePositions || {}));
});

routes.post("/api/mortgage/positions", 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 body = await c.req.json() as Partial<MortgagePosition>;
	if (!body.principal || !body.termMonths || !body.interestRate) {
		return c.json({ error: "principal, termMonths, and interestRate required" }, 400);
	}

	const space = (body as any).space || "demo";
	const docId = flowsDocId(space);
	ensureDoc(space);

	const id = crypto.randomUUID();
	const monthlyRate = body.interestRate / 100 / 12;
	const monthlyPayment = monthlyRate > 0
		? body.principal * (monthlyRate * Math.pow(1 + monthlyRate, body.termMonths)) / (Math.pow(1 + monthlyRate, body.termMonths) - 1)
		: body.principal / body.termMonths;

	const position: MortgagePosition = {
		id,
		borrower: body.borrower || claims.sub,
		borrowerDid: (claims as any).did || claims.sub,
		principal: body.principal,
		interestRate: body.interestRate,
		termMonths: body.termMonths,
		monthlyPayment: Math.round(monthlyPayment * 100) / 100,
		startDate: Date.now(),
		trustScore: body.trustScore || 0,
		status: 'active',
		collateralType: body.collateralType || 'trust-backed',
	};

	_syncServer!.changeDoc<FlowsDoc>(docId, 'create mortgage position', (d) => {
		d.mortgagePositions[id] = position as any;
	});

	return c.json(position, 201);
});

// ─── Budget API routes ───────────────────────────────────

routes.get("/api/budgets", async (c) => {
	const space = c.req.query("space") || "demo";
	const doc = ensureDoc(space);

	const segments = Object.entries(doc.budgetSegments || {}).map(([id, s]) => ({
		id, name: s.name, color: s.color, createdBy: s.createdBy,
	}));

	const allocations = Object.values(doc.budgetAllocations || {});

	// Compute collective averages per segment
	const collective: { segmentId: string; avgPercentage: number; participantCount: number }[] = [];
	if (segments.length > 0 && allocations.length > 0) {
		for (const seg of segments) {
			const vals = allocations.map((a) => a.allocations[seg.id] || 0);
			const avg = vals.reduce((s, v) => s + v, 0) / vals.length;
			collective.push({ segmentId: seg.id, avgPercentage: Math.round(avg * 100) / 100, participantCount: vals.filter((v) => v > 0).length });
		}
	}

	return c.json({
		segments,
		allocations,
		collective,
		totalAmount: doc.budgetTotalAmount || 0,
		participantCount: allocations.length,
	});
});

routes.post("/api/budgets/allocate", 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, allocations } = await c.req.json() as { space?: string; allocations?: Record<string, number> };
	if (!allocations) return c.json({ error: "allocations required" }, 400);

	const spaceSlug = space || "demo";
	const docId = flowsDocId(spaceSlug);
	ensureDoc(spaceSlug);

	const did = (claims as any).did || claims.sub;
	_syncServer!.changeDoc<FlowsDoc>(docId, 'save budget allocation', (d) => {
		d.budgetAllocations[did] = {
			participantDid: did,
			allocations: allocations as any,
			updatedAt: Date.now(),
		} as any;
	});

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

routes.get("/api/budgets/segments", async (c) => {
	const space = c.req.query("space") || "demo";
	const doc = ensureDoc(space);
	const segments = Object.entries(doc.budgetSegments || {}).map(([id, s]) => ({
		id, name: s.name, color: s.color, createdBy: s.createdBy,
	}));
	return c.json(segments);
});

routes.post("/api/budgets/segments", 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, action, segmentId, name, color } = await c.req.json() as {
		space?: string; action: 'add' | 'remove'; segmentId?: string; name?: string; color?: string;
	};

	const spaceSlug = space || "demo";
	const docId = flowsDocId(spaceSlug);
	ensureDoc(spaceSlug);

	const did = (claims as any).did || claims.sub;

	if (action === 'add') {
		if (!name) return c.json({ error: "name required" }, 400);
		const id = segmentId || crypto.randomUUID();
		_syncServer!.changeDoc<FlowsDoc>(docId, 'add budget segment', (d) => {
			d.budgetSegments[id] = { name: name as any, color: (color || '#6366f1') as any, createdBy: did as any };
		});
		return c.json({ ok: true, id });
	}

	if (action === 'remove') {
		if (!segmentId) return c.json({ error: "segmentId required" }, 400);
		_syncServer!.changeDoc<FlowsDoc>(docId, 'remove budget segment', (d) => {
			delete d.budgetSegments[segmentId];
			// Remove this segment from all allocations
			for (const alloc of Object.values(d.budgetAllocations)) {
				delete alloc.allocations[segmentId];
			}
		});
		return c.json({ ok: true });
	}

	return c.json({ error: "action must be 'add' or 'remove'" }, 400);
});

// ─── Outcome-Tasks integration API ───────────────────────

/** Helper: find outcome node in FlowsDoc */
function findOutcomeNode(doc: FlowsDoc, outcomeId: string): { flow: CanvasFlow; nodeIdx: number; data: OutcomeNodeData } | null {
	for (const flow of Object.values(doc.canvasFlows)) {
		if (!flow.nodes) continue;
		for (let i = 0; i < flow.nodes.length; i++) {
			const node = flow.nodes[i];
			if (node.id === outcomeId && node.type === 'outcome') {
				return { flow, nodeIdx: i, data: node.data as OutcomeNodeData };
			}
		}
	}
	return null;
}

routes.get("/api/flows/outcome-tasks", async (c) => {
	const space = c.req.query("space") || "";
	const outcomeId = c.req.query("outcomeId") || "";
	if (!space || !outcomeId) return c.json({ error: "space and outcomeId required" }, 400);
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);

	const doc = ensureDoc(space);
	const found = findOutcomeNode(doc, outcomeId);
	if (!found) return c.json({ error: "Outcome not found" }, 404);

	const linkedTaskIds = found.data.linkedTaskIds || [];
	const tasks: any[] = [];

	for (const ref of linkedTaskIds) {
		const [boardId, taskId] = ref.split(":");
		if (!boardId || !taskId) continue;
		const boardDoc = _syncServer.getDoc<BoardDoc>(boardDocId(space, boardId));
		if (!boardDoc?.tasks?.[taskId]) continue;
		const t = boardDoc.tasks[taskId];
		tasks.push({
			ref,
			boardId,
			taskId,
			title: t.title,
			status: t.status,
			priority: t.priority,
			assigneeId: t.assigneeId,
			labels: t.labels,
		});
	}

	// Also return available boards for the task picker
	const boards: { id: string; name: string; taskCount: number }[] = [];
	for (const id of _syncServer.getDocIds()) {
		if (!id.startsWith(`${space}:tasks:boards:`)) continue;
		const bd = _syncServer.getDoc<BoardDoc>(id);
		if (!bd?.board) continue;
		boards.push({ id: bd.board.id, name: bd.board.name, taskCount: Object.keys(bd.tasks || {}).length });
	}

	return c.json({ tasks, boards, linkedBoardId: found.data.linkedBoardId || null });
});

routes.post("/api/flows/outcome-tasks/link", 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); }
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);

	const { space, outcomeId, boardId, taskId } = await c.req.json();
	if (!space || !outcomeId || !boardId || !taskId) return c.json({ error: "space, outcomeId, boardId, taskId required" }, 400);

	const doc = ensureDoc(space);
	const found = findOutcomeNode(doc, outcomeId);
	if (!found) return c.json({ error: "Outcome not found" }, 404);

	// Verify task exists
	const boardDoc = _syncServer.getDoc<BoardDoc>(boardDocId(space, boardId));
	if (!boardDoc?.tasks?.[taskId]) return c.json({ error: "Task not found" }, 404);

	const ref = `${boardId}:${taskId}`;
	const docId = flowsDocId(space);

	_syncServer.changeDoc<FlowsDoc>(docId, `link task ${ref} to outcome ${outcomeId}`, (d) => {
		for (const flow of Object.values(d.canvasFlows)) {
			if (!flow.nodes) continue;
			for (const node of flow.nodes) {
				if (node.id === outcomeId && node.type === 'outcome') {
					const data = node.data as any;
					if (!data.linkedTaskIds) data.linkedTaskIds = [];
					if (!data.linkedTaskIds.includes(ref)) data.linkedTaskIds.push(ref);
				}
			}
		}
	});

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

routes.post("/api/flows/outcome-tasks/unlink", 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); }
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);

	const { space, outcomeId, ref } = await c.req.json();
	if (!space || !outcomeId || !ref) return c.json({ error: "space, outcomeId, ref required" }, 400);

	const docId = flowsDocId(space);
	_syncServer.changeDoc<FlowsDoc>(docId, `unlink task ${ref} from outcome ${outcomeId}`, (d) => {
		for (const flow of Object.values(d.canvasFlows)) {
			if (!flow.nodes) continue;
			for (const node of flow.nodes) {
				if (node.id === outcomeId && node.type === 'outcome') {
					const data = node.data as any;
					if (data.linkedTaskIds) {
						const idx = data.linkedTaskIds.indexOf(ref);
						if (idx >= 0) data.linkedTaskIds.splice(idx, 1);
					}
				}
			}
		}
	});

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

routes.post("/api/flows/outcome-tasks/create", 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); }
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);

	const { space, outcomeId, title, boardId: reqBoardId } = await c.req.json();
	if (!space || !outcomeId) return c.json({ error: "space, outcomeId required" }, 400);

	const doc = ensureDoc(space);
	const found = findOutcomeNode(doc, outcomeId);
	if (!found) return c.json({ error: "Outcome not found" }, 404);

	// Use requested board or linked board or default BCRG board
	const boardId = reqBoardId || found.data.linkedBoardId || `${space}-bcrg`;
	const bDocId = boardDocId(space, boardId);
	const boardDoc = _syncServer.getDoc<BoardDoc>(bDocId);
	if (!boardDoc) return c.json({ error: "Board not found" }, 404);

	const taskId = crypto.randomUUID();
	const taskTitle = title || `Task: ${found.data.label}`;
	const refTag = `ref:rflows:outcome:${outcomeId}`;
	const did = (claims as any).did || claims.sub;

	_syncServer.changeDoc<BoardDoc>(bDocId, `Create task for outcome ${outcomeId}`, (d) => {
		d.tasks[taskId] = createTaskItem(taskId, space, taskTitle, {
			status: 'TODO',
			priority: 'MEDIUM',
			description: `${refTag} — Created from rFlows outcome "${found!.data.label}"`,
			labels: ['rflows'],
			createdBy: did,
		});
	});

	// Auto-link the new task to the outcome
	const ref = `${boardId}:${taskId}`;
	const fDocId = flowsDocId(space);
	_syncServer.changeDoc<FlowsDoc>(fDocId, `auto-link created task to outcome ${outcomeId}`, (d) => {
		for (const flow of Object.values(d.canvasFlows)) {
			if (!flow.nodes) continue;
			for (const node of flow.nodes) {
				if (node.id === outcomeId && node.type === 'outcome') {
					const data = node.data as any;
					if (!data.linkedTaskIds) data.linkedTaskIds = [];
					if (!data.linkedTaskIds.includes(ref)) data.linkedTaskIds.push(ref);
				}
			}
		}
	});

	console.log(`[rflows] Created task "${taskTitle}" in board ${boardId}, linked to outcome ${outcomeId}`);
	return c.json({ ok: true, ref, taskId, boardId });
});

// List unlinked tasks from a board (for picker)
routes.get("/api/flows/board-tasks", async (c) => {
	const space = c.req.query("space") || "";
	const boardId = c.req.query("boardId") || "";
	const outcomeId = c.req.query("outcomeId") || "";
	if (!space || !boardId) return c.json({ error: "space and boardId required" }, 400);
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);

	const boardDoc = _syncServer.getDoc<BoardDoc>(boardDocId(space, boardId));
	if (!boardDoc) return c.json({ tasks: [] });

	// Get already-linked task IDs for this outcome
	const linkedRefs = new Set<string>();
	if (outcomeId) {
		const doc = ensureDoc(space);
		const found = findOutcomeNode(doc, outcomeId);
		if (found?.data.linkedTaskIds) {
			for (const ref of found.data.linkedTaskIds) linkedRefs.add(ref);
		}
	}

	const tasks = Object.values(boardDoc.tasks)
		.filter((t) => !linkedRefs.has(`${boardId}:${t.id}`))
		.map((t) => ({
			id: t.id,
			title: t.title,
			status: t.status,
			priority: t.priority,
			labels: t.labels,
		}));

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

// ─── Page routes ────────────────────────────────────────

const flowsScripts = `
	<script src="https://cdn.jsdelivr.net/npm/lz-string@1.5.0/libs/lz-string.min.js"></script>
	<script type="module" src="/modules/rflows/folk-flows-app.js?v=5"></script>
	<script type="module" src="/modules/rflows/folk-flow-river.js?v=4"></script>`;

const mortgageScripts = `
	<script src="https://cdn.jsdelivr.net/npm/lz-string@1.5.0/libs/lz-string.min.js"></script>
	<script type="module" src="/modules/rflows/folk-mortgage-simulator.js?v=1"></script>`;

const flowsStyles = `<link rel="stylesheet" href="/modules/rflows/flows.css">`;

// Landing page (also serves demo via centralized /demo → space="demo" rewrite)
routes.get("/", (c) => {
	const spaceSlug = c.req.param("space") || "demo";
	return c.html(renderShell({
		title: `${spaceSlug} — Flows | rSpace`,
		moduleId: "rflows",
		spaceSlug,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-flows-app space="${spaceSlug}"${spaceSlug === "demo" ? ' mode="demo"' : ''}></folk-flows-app>`,
		scripts: flowsScripts,
		styles: flowsStyles,
	}));
});

// Mortgage sub-tab — full distributed mortgage simulator
routes.get("/mortgage", (c) => {
	const spaceSlug = c.req.param("space") || "demo";
	return c.html(renderShell({
		title: `${spaceSlug} — rMortgage | rFlows | rSpace`,
		moduleId: "rflows",
		spaceSlug,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-mortgage-simulator space="${spaceSlug}"></folk-mortgage-simulator>`,
		scripts: mortgageScripts,
		styles: flowsStyles,
	}));
});

// Budgets sub-tab
routes.get("/budgets", (c) => {
	const spaceSlug = c.req.param("space") || "demo";
	return c.html(renderShell({
		title: `${spaceSlug} — rBudgets | rFlows | rSpace`,
		moduleId: "rflows",
		spaceSlug,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-flows-app space="${spaceSlug}" view="budgets"${spaceSlug === "demo" ? ' mode="demo"' : ''}></folk-flows-app>`,
		scripts: flowsScripts,
		styles: flowsStyles,
	}));
});

// Flow detail — specific flow from API
routes.get("/flow/:flowId", (c) => {
	const spaceSlug = c.req.param("space") || "demo";
	const flowId = c.req.param("flowId");
	return c.html(renderShell({
		title: `Flow — rFlows | rSpace`,
		moduleId: "rflows",
		spaceSlug,
		modules: getModuleInfoList(),
		theme: "dark",
		styles: flowsStyles,
		body: `<folk-flows-app space="${spaceSlug}" flow-id="${flowId}"></folk-flows-app>`,
		scripts: flowsScripts,
	}));
});

// ── Seed template data ──

function seedTemplateFlows(space: string) {
	if (!_syncServer) return;
	const doc = ensureDoc(space);

	// Seed SpaceFlow association if empty
	if (Object.keys(doc.spaceFlows).length === 0) {
		const docId = flowsDocId(space);
		const now = Date.now();
		const flowId = crypto.randomUUID();

		_syncServer.changeDoc<FlowsDoc>(docId, 'seed template flow', (d) => {
			d.spaceFlows[flowId] = {
				id: flowId, spaceSlug: space, flowId: 'demo',
				addedBy: 'did:demo:seed', createdAt: now,
			};
		});
	}

	// Seed a canvas flow with demoNodes if none exist
	if (Object.keys(doc.canvasFlows || {}).length === 0) {
		const docId = flowsDocId(space);
		const now = Date.now();
		const canvasFlowId = crypto.randomUUID();

		const seedFlow: CanvasFlow = {
			id: canvasFlowId,
			name: 'BCRG Community Flow',
			nodes: demoNodes.map((n) => ({ ...n, data: { ...n.data } })),
			createdAt: now,
			updatedAt: now,
			createdBy: 'did:demo:seed',
		};

		_syncServer.changeDoc<FlowsDoc>(docId, 'seed canvas flow', (d) => {
			d.canvasFlows[canvasFlowId] = seedFlow as any;
			d.activeFlowId = canvasFlowId as any;
		});

		console.log(`[Flows] Template seeded for "${space}": 1 canvas flow + association`);
	}

	// Seed mortgage demo positions if empty
	if (Object.keys(doc.mortgagePositions || {}).length === 0) {
		const docId = flowsDocId(space);
		const now = Date.now();
		const demoMortgages: MortgagePosition[] = [
			{
				id: crypto.randomUUID(), borrower: 'alice.eth', borrowerDid: 'did:key:alice123',
				principal: 250000, interestRate: 4.2, termMonths: 360, monthlyPayment: 1222.95,
				startDate: now - 86400000 * 120, trustScore: 92, status: 'active', collateralType: 'trust-backed',
			},
			{
				id: crypto.randomUUID(), borrower: 'bob.base', borrowerDid: 'did:key:bob456',
				principal: 180000, interestRate: 3.8, termMonths: 240, monthlyPayment: 1079.19,
				startDate: now - 86400000 * 60, trustScore: 87, status: 'active', collateralType: 'hybrid',
			},
			{
				id: crypto.randomUUID(), borrower: 'carol.eth', borrowerDid: 'did:key:carol789',
				principal: 75000, interestRate: 5.1, termMonths: 120, monthlyPayment: 799.72,
				startDate: now - 86400000 * 200, trustScore: 78, status: 'active', collateralType: 'trust-backed',
			},
			{
				id: crypto.randomUUID(), borrower: 'dave.base', borrowerDid: 'did:key:dave012',
				principal: 320000, interestRate: 3.5, termMonths: 360, monthlyPayment: 1436.94,
				startDate: now - 86400000 * 30, trustScore: 95, status: 'pending', collateralType: 'asset-backed',
			},
		];

		const demoReinvestments: ReinvestmentPosition[] = [
			{ protocol: 'Aave v3', chain: 'Base', asset: 'USDC', deposited: 500000, currentValue: 512340, apy: 4.87, lastUpdated: now },
			{ protocol: 'Morpho Blue', chain: 'Ethereum', asset: 'USDC', deposited: 200000, currentValue: 203120, apy: 3.12, lastUpdated: now },
		];

		_syncServer!.changeDoc<FlowsDoc>(docId, 'seed mortgage demo', (d) => {
			for (const m of demoMortgages) d.mortgagePositions[m.id] = m as any;
			for (const r of demoReinvestments) {
				const rid = `${r.protocol}:${r.chain}:${r.asset}`;
				d.reinvestmentPositions[rid] = r as any;
			}
		});
		console.log(`[Flows] Mortgage demo seeded for "${space}"`);
	}

	// Seed budget demo data if empty
	if (Object.keys(doc.budgetSegments || {}).length === 0) {
		const docId = flowsDocId(space);
		const demoSegments: Record<string, { name: string; color: string; createdBy: string | null }> = {
			'eng': { name: 'Engineering', color: '#3b82f6', createdBy: null },
			'ops': { name: 'Operations', color: '#10b981', createdBy: null },
			'mkt': { name: 'Marketing', color: '#f59e0b', createdBy: null },
			'com': { name: 'Community', color: '#8b5cf6', createdBy: null },
			'res': { name: 'Research', color: '#ef4444', createdBy: null },
		};

		const demoAllocations: Record<string, { participantDid: string; allocations: Record<string, number>; updatedAt: number }> = {
			'did:key:alice123': { participantDid: 'did:key:alice123', allocations: { eng: 35, ops: 15, mkt: 20, com: 15, res: 15 }, updatedAt: Date.now() },
			'did:key:bob456': { participantDid: 'did:key:bob456', allocations: { eng: 25, ops: 20, mkt: 10, com: 30, res: 15 }, updatedAt: Date.now() },
			'did:key:carol789': { participantDid: 'did:key:carol789', allocations: { eng: 30, ops: 10, mkt: 25, com: 10, res: 25 }, updatedAt: Date.now() },
			'did:key:dave012': { participantDid: 'did:key:dave012', allocations: { eng: 40, ops: 15, mkt: 15, com: 20, res: 10 }, updatedAt: Date.now() },
		};

		_syncServer!.changeDoc<FlowsDoc>(docId, 'seed budget demo', (d) => {
			for (const [id, seg] of Object.entries(demoSegments)) d.budgetSegments[id] = seg as any;
			for (const [did, alloc] of Object.entries(demoAllocations)) d.budgetAllocations[did] = alloc as any;
			d.budgetTotalAmount = 500000 as any;
		});
		console.log(`[Flows] Budget demo seeded for "${space}"`);
	}
}

// ── Drips Protocol routes ──

const VALID_ETH_ADDR = /^0x[0-9a-fA-F]{40}$/;

routes.get("/api/flows/drips/:chainId/:address", async (c) => {
	const chainId = Number(c.req.param("chainId"));
	const address = c.req.param("address");
	if (!VALID_ETH_ADDR.test(address)) return c.json({ error: "Invalid address" }, 400);
	if (![1, 11155111].includes(chainId)) return c.json({ error: "Unsupported chain" }, 400);

	const state = await getDripsAccountState(chainId, address);
	return c.json(state);
});

routes.get("/api/flows/drips/:chainId/:address/preview", async (c) => {
	const chainId = Number(c.req.param("chainId"));
	const address = c.req.param("address");
	if (!VALID_ETH_ADDR.test(address)) return c.json({ error: "Invalid address" }, 400);
	if (![1, 11155111].includes(chainId)) return c.json({ error: "Unsupported chain" }, 400);

	const state = await getDripsAccountState(chainId, address);
	const nodes = mapDripsToFlowNodes(state);
	return c.json({ nodes, state });
});

routes.post("/api/flows/drips/import", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Unauthorized" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const body = await c.req.json() as {
		space: string; flowId: string; chainId: number; address: string;
	};
	const { space, flowId, chainId, address } = body;
	if (!space || !flowId || !chainId || !address) return c.json({ error: "Missing fields" }, 400);
	if (!VALID_ETH_ADDR.test(address)) return c.json({ error: "Invalid address" }, 400);

	const state = await getDripsAccountState(chainId, address);
	const newNodes = mapDripsToFlowNodes(state, { originX: 400, originY: 100 });

	const docId = flowsDocId(space);
	ensureDoc(space);

	_syncServer!.changeDoc<FlowsDoc>(docId, 'import drips nodes', (d) => {
		const flow = d.canvasFlows[flowId];
		if (!flow) return;

		// Deduplicate by drips- prefixed IDs
		const existingIds = new Set(flow.nodes.map((n: any) => n.id));
		for (const node of newNodes) {
			if (existingIds.has(node.id)) {
				// Update existing node data
				const idx = flow.nodes.findIndex((n: any) => n.id === node.id);
				if (idx >= 0) flow.nodes[idx].data = node.data as any;
			} else {
				flow.nodes.push(node as any);
			}
		}

		// Record sync entry
		if (!flow.dripsSyncs) flow.dripsSyncs = {} as any;
		const syncKey = `${chainId}:${address.toLowerCase()}`;
		(flow.dripsSyncs as any)[syncKey] = {
			chainId,
			address: address.toLowerCase(),
			lastSyncedAt: Date.now(),
			streamIds: state.incomingStreams.concat(state.outgoingStreams).map(s => s.id),
			splitsHash: state.splitsHash,
			importedNodeIds: newNodes.map(n => n.id),
		} as DripsSync;

		flow.updatedAt = Date.now() as any;
	});

	return c.json({ ok: true, importedCount: newNodes.length, nodeIds: newNodes.map(n => n.id) });
});

export function getRecentFlowsForMI(space: string, limit = 5): { id: string; name: string; nodeCount: number; createdAt: number }[] {
	if (!_syncServer) return [];
	const docId = flowsDocId(space);
	const doc = _syncServer.getDoc<FlowsDoc>(docId);
	if (!doc) return [];
	return Object.values(doc.canvasFlows)
		.sort((a, b) => b.createdAt - a.createdAt)
		.slice(0, limit)
		.map((f) => ({ id: f.id, name: f.name, nodeCount: f.nodes?.length || 0, createdAt: f.createdAt }));
}

export const flowsModule: RSpaceModule = {
	id: "rflows",
	name: "rFlows",
	icon: "🌊",
	description: "Budget flows, river visualization, and treasury management",
	publicWrite: true,
	applets: flowsApplets,
	scoping: { defaultScope: 'space', userConfigurable: false },
	docSchemas: [{ pattern: '{space}:flows:data', description: 'Space flow associations', init: flowsSchema.init }],
	routes,
	landingPage: renderLanding,
	seedTemplate: seedTemplateFlows,
	async onInit(ctx) {
		_syncServer = ctx.syncServer;

		if (process.env.OPENFORT_API_KEY && process.env.OPENFORT_PUBLISHABLE_KEY) {
			_openfort = new OpenfortProvider({
				apiKey: process.env.OPENFORT_API_KEY,
				publishableKey: process.env.OPENFORT_PUBLISHABLE_KEY,
				chainId: 8453, // Base mainnet — hardcoded to avoid testnet misconfiguration
			});
			console.log('[rflows] Openfort provider initialized');
		}

		if (process.env.PIMLICO_API_KEY) {
			_pimlico = new PimlicoClient({
				apiKey: process.env.PIMLICO_API_KEY,
				chainId: 8453, // Base mainnet
			});
			console.log('[rflows] Pimlico bundler initialized');
		}

		// Log available on-ramp providers
		const onrampProviders = getAvailableProviders();
		console.log(`[rflows] On-ramp providers: ${onrampProviders.map((p) => p.id).join(', ') || 'none'}`)

		// Watch for completed outcomes in flow docs → auto-create DONE tasks
		_syncServer.registerWatcher(':flows:data', (docId, doc) => {
			try {
				const flowsDoc = doc as FlowsDoc;
				if (!flowsDoc.canvasFlows) return;
				// Extract space slug from docId (format: {space}:flows:data)
				const space = docId.split(':flows:data')[0];
				if (!space) return;

				for (const flow of Object.values(flowsDoc.canvasFlows)) {
					if (!flow.nodes) continue;
					for (const node of flow.nodes) {
						if (node.type !== 'outcome') continue;
						const data = node.data as OutcomeNodeData;
						if (data.status !== 'completed') continue;

						const key = `${space}:${node.id}`;
						if (_completedOutcomes.has(key)) continue;
						_completedOutcomes.add(key);

						createTaskForOutcome(space, node.id, data.label);
					}
				}
			} catch {}
		});

		// Reverse sync: rTasks status changes → update linked outcome status
		let _syncInProgress = false;
		_syncServer.registerWatcher(':tasks:boards:', (docId, doc) => {
			if (_syncInProgress) return;
			try {
				const boardDoc = doc as BoardDoc;
				if (!boardDoc?.tasks || !boardDoc?.board) return;

				// Extract space from docId: {space}:tasks:boards:{boardId}
				const parts = docId.split(':tasks:boards:');
				const space = parts[0];
				const boardId = parts[1];
				if (!space || !boardId) return;

				// Find flow docs for this space
				const fDocId = flowsDocId(space);
				const flowsDoc = _syncServer!.getDoc<FlowsDoc>(fDocId);
				if (!flowsDoc?.canvasFlows) return;

				// Check each outcome's linked tasks
				for (const flow of Object.values(flowsDoc.canvasFlows)) {
					if (!flow.nodes) continue;
					for (const node of flow.nodes) {
						if (node.type !== 'outcome') continue;
						const data = node.data as OutcomeNodeData;
						if (!data.linkedTaskIds || data.linkedTaskIds.length === 0) continue;

						// Filter linked tasks that belong to this board
						const boardRefs = data.linkedTaskIds.filter((r) => r.startsWith(`${boardId}:`));
						if (boardRefs.length === 0) continue;

						// Check statuses of ALL linked tasks (across all boards)
						const allStatuses: string[] = [];
						for (const ref of data.linkedTaskIds) {
							const [bid, tid] = ref.split(':');
							const bd = bid === boardId ? boardDoc : _syncServer!.getDoc<BoardDoc>(boardDocId(space, bid));
							if (bd?.tasks?.[tid]) allStatuses.push(bd.tasks[tid].status);
						}

						if (allStatuses.length === 0) continue;

						const allDone = allStatuses.every((s) => s === 'DONE');
						const anyInProgress = allStatuses.some((s) => s === 'IN_PROGRESS');

						let newStatus: OutcomeNodeData['status'] | null = null;
						if (allDone && data.status !== 'completed') {
							newStatus = 'completed';
						} else if (anyInProgress && data.status === 'not-started') {
							newStatus = 'in-progress';
						}

						if (newStatus) {
							_syncInProgress = true;
							_syncServer!.changeDoc<FlowsDoc>(fDocId, `sync outcome ${node.id} → ${newStatus}`, (d) => {
								for (const f of Object.values(d.canvasFlows)) {
									if (!f.nodes) continue;
									for (const n of f.nodes) {
										if (n.id === node.id && n.type === 'outcome') {
											(n.data as any).status = newStatus;
										}
									}
								}
							});
							_syncInProgress = false;
							console.log(`[rflows] Reverse sync: outcome "${node.id}" → ${newStatus}`);
						}
					}
				}
			} catch {}
		});

		// Pre-populate _completedOutcomes from existing docs to avoid duplicates on restart
		for (const id of _syncServer.getDocIds()) {
			if (!id.includes(':flows:data')) continue;
			const doc = _syncServer.getDoc<FlowsDoc>(id);
			if (!doc?.canvasFlows) continue;
			const space = id.split(':flows:data')[0];
			for (const flow of Object.values(doc.canvasFlows)) {
				if (!flow.nodes) continue;
				for (const node of flow.nodes) {
					if (node.type === 'outcome' && (node.data as OutcomeNodeData).status === 'completed') {
						_completedOutcomes.add(`${space}:${node.id}`);
					}
				}
			}
		}
	},
	standaloneDomain: "rflows.online",
	feeds: [
		{
			id: "treasury-flows",
			name: "Treasury Flows",
			kind: "economic",
			description: "Budget flow states, deposits, withdrawals, and funnel allocations",
			filterable: true,
		},
		{
			id: "transactions",
			name: "Transaction Stream",
			kind: "economic",
			description: "Real-time deposit and withdrawal events",
		},
	],
	acceptsFeeds: ["governance", "data"],
	outputPaths: [
		{ path: "budgets", name: "rBudgets", icon: "💰", description: "Collective budget allocation with pie charts" },
	],
	subPageInfos: [
		{
			path: "budgets",
			title: "rBudgets",
			icon: "💰",
			tagline: "rFlows Tool",
			description: "Collective budget allocation where participants distribute funds across departments using pie charts. Aggregated view shows the group consensus.",
			features: [
				{ icon: "🥧", title: "Collective Pie Chart", text: "See the aggregated budget breakdown from all participants as a dynamic pie chart." },
				{ icon: "🎚️", title: "Personal Sliders", text: "Adjust your own allocation across departments — sliders auto-normalize to 100%." },
				{ icon: "👥", title: "Participant Tracking", text: "View how many people contributed and the consensus distribution." },
			],
		},
		{
			path: "mortgage",
			title: "rMortgage",
			icon: "🏠",
			tagline: "rFlows Tool",
			description: "Social trust-based mortgage lending with DeFi yield reinvestment. Track mortgage positions backed by community trust scores, and earn yield on idle pool capital via Aave and Morpho.",
			features: [
				{ icon: "🤝", title: "Trust-Backed Lending", text: "Mortgage positions backed by community trust scores instead of traditional credit." },
				{ icon: "📊", title: "DeFi Reinvestment", text: "Idle pool capital reinvested into Aave v3 and Morpho Blue for passive yield." },
				{ icon: "🧮", title: "Projection Calculator", text: "Model deposits and durations to forecast compound yield on pool capital." },
			],
		},
	],
	onboardingActions: [
		{ label: "Create a Budget", icon: "🌊", description: "Set up a community funding flow", type: 'create', href: '/{space}/rflows' },
	],
};
