/**
 * rExchange module — P2P crypto/fiat exchange within communities.
 *
 * Community members post buy/sell intents for CRDT tokens (cUSDC, $MYCO, fUSDC)
 * against fiat currencies. Solver matches intents, escrow handles settlement.
 *
 * All state stored in Automerge documents via SyncServer.
 * Doc layout:
 *   {space}:rexchange:intents    → ExchangeIntentsDoc
 *   {space}:rexchange:trades     → ExchangeTradesDoc
 *   {space}:rexchange:reputation → ExchangeReputationDoc
 */

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 type { SyncServer } from '../../server/local-first/sync-server';
import { renderLanding } from './landing';
import {
	exchangeIntentsSchema, exchangeTradesSchema, exchangeReputationSchema, exchangePoolsSchema,
	exchangeIntentsDocId, exchangeTradesDocId, exchangeReputationDocId, exchangePoolsDocId,
} from './schemas';
import type {
	ExchangeIntentsDoc, ExchangeTradesDoc, ExchangeReputationDoc, ExchangePoolsDoc,
	ExchangeIntent, ExchangeTrade, ExchangeReputationRecord, LiquidityPosition,
} from './schemas';
import { createExchangeRoutes, startSolverCron } from './exchange-routes';

const routes = new Hono();

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

// ── Mount exchange routes ──
const exchangeRoutes = createExchangeRoutes(() => _syncServer);
routes.route('/', exchangeRoutes);

// ── Automerge helpers ──

function ensureIntentsDoc(space: string): ExchangeIntentsDoc {
	const docId = exchangeIntentsDocId(space);
	let doc = _syncServer!.getDoc<ExchangeIntentsDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<ExchangeIntentsDoc>(), 'init exchange intents', (d) => {
			const init = exchangeIntentsSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

function ensureTradesDoc(space: string): ExchangeTradesDoc {
	const docId = exchangeTradesDocId(space);
	let doc = _syncServer!.getDoc<ExchangeTradesDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<ExchangeTradesDoc>(), 'init exchange trades', (d) => {
			const init = exchangeTradesSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

function ensureReputationDoc(space: string): ExchangeReputationDoc {
	const docId = exchangeReputationDocId(space);
	let doc = _syncServer!.getDoc<ExchangeReputationDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<ExchangeReputationDoc>(), 'init exchange reputation', (d) => {
			const init = exchangeReputationSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

function ensurePoolsDoc(space: string): ExchangePoolsDoc {
	const docId = exchangePoolsDocId(space);
	let doc = _syncServer!.getDoc<ExchangePoolsDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<ExchangePoolsDoc>(), 'init exchange pools', (d) => {
			const init = exchangePoolsSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

// ── Demo seeding ──

const DEMO_DIDS = {
	alice:  'did:key:alice-contributor-rspace-2026',
	bob:    'did:key:bob-auditor-rspace-2026',
	carol:  'did:key:carol-designer-rspace-2026',
	maya:   'did:key:maya-facilitator-rspace-2026',
	jordan: 'did:key:jordan-designer-rspace-2026',
};

const DEMO_INTENTS: Omit<ExchangeIntent, 'id' | 'createdAt'>[] = [
	// Buys (want crypto, have fiat)
	{
		creatorDid: DEMO_DIDS.alice, creatorName: 'Alice',
		side: 'buy', tokenId: 'cusdc', fiatCurrency: 'EUR',
		tokenAmountMin: 50_000_000, tokenAmountMax: 200_000_000,
		rateType: 'market_plus_bps', rateMarketBps: 50,
		paymentMethods: ['SEPA', 'Revolut'], isStandingOrder: false,
		autoAccept: false, allowInstitutionalFallback: true, status: 'active',
	},
	{
		creatorDid: DEMO_DIDS.maya, creatorName: 'Maya',
		side: 'buy', tokenId: 'cusdc', fiatCurrency: 'USD',
		tokenAmountMin: 100_000_000, tokenAmountMax: 500_000_000,
		rateType: 'fixed', rateFixed: 1.00,
		paymentMethods: ['Revolut', 'Cash'], isStandingOrder: true,
		autoAccept: true, allowInstitutionalFallback: false, status: 'active',
	},
	{
		creatorDid: DEMO_DIDS.jordan, creatorName: 'Jordan',
		side: 'buy', tokenId: 'myco', fiatCurrency: 'BRL',
		tokenAmountMin: 1_000_000_000, tokenAmountMax: 5_000_000_000,
		rateType: 'market_plus_bps', rateMarketBps: 100,
		paymentMethods: ['PIX'], isStandingOrder: false,
		autoAccept: false, allowInstitutionalFallback: true, status: 'active',
	},
	{
		creatorDid: DEMO_DIDS.carol, creatorName: 'Carol',
		side: 'buy', tokenId: 'cusdc', fiatCurrency: 'GBP',
		tokenAmountMin: 25_000_000, tokenAmountMax: 100_000_000,
		rateType: 'fixed', rateFixed: 0.79,
		paymentMethods: ['SEPA', 'Revolut'], isStandingOrder: false,
		autoAccept: false, allowInstitutionalFallback: false, status: 'active',
	},

	// Sells (have crypto, want fiat)
	{
		creatorDid: DEMO_DIDS.bob, creatorName: 'Bob',
		side: 'sell', tokenId: 'cusdc', fiatCurrency: 'EUR',
		tokenAmountMin: 100_000_000, tokenAmountMax: 300_000_000,
		rateType: 'market_plus_bps', rateMarketBps: 30,
		paymentMethods: ['SEPA'], isStandingOrder: true,
		autoAccept: true, allowInstitutionalFallback: false, status: 'active',
	},
	{
		creatorDid: DEMO_DIDS.carol, creatorName: 'Carol',
		side: 'sell', tokenId: 'cusdc', fiatCurrency: 'USD',
		tokenAmountMin: 50_000_000, tokenAmountMax: 250_000_000,
		rateType: 'fixed', rateFixed: 1.01,
		paymentMethods: ['Revolut', 'Cash'], isStandingOrder: false,
		autoAccept: false, allowInstitutionalFallback: false, status: 'active',
	},
	{
		creatorDid: DEMO_DIDS.alice, creatorName: 'Alice',
		side: 'sell', tokenId: 'myco', fiatCurrency: 'EUR',
		tokenAmountMin: 500_000_000, tokenAmountMax: 2_000_000_000,
		rateType: 'market_plus_bps', rateMarketBps: 75,
		paymentMethods: ['SEPA', 'Revolut'], isStandingOrder: false,
		autoAccept: false, allowInstitutionalFallback: true, status: 'active',
	},
	{
		creatorDid: DEMO_DIDS.maya, creatorName: 'Maya',
		side: 'sell', tokenId: 'cusdc', fiatCurrency: 'GBP',
		tokenAmountMin: 30_000_000, tokenAmountMax: 150_000_000,
		rateType: 'fixed', rateFixed: 0.80,
		paymentMethods: ['SEPA', 'Revolut'], isStandingOrder: true,
		autoAccept: true, allowInstitutionalFallback: false, status: 'active',
	},
];

const DEMO_REPUTATION: ExchangeReputationRecord[] = [
	{ did: DEMO_DIDS.alice, tradesCompleted: 12, tradesCancelled: 1, disputesRaised: 0, disputesLost: 0, totalVolumeBase: 2_500_000_000, avgConfirmTimeMs: 1800_000, score: 88, badges: ['verified_seller'] },
	{ did: DEMO_DIDS.bob, tradesCompleted: 27, tradesCancelled: 0, disputesRaised: 1, disputesLost: 0, totalVolumeBase: 8_000_000_000, avgConfirmTimeMs: 900_000, score: 95, badges: ['verified_seller'] },
	{ did: DEMO_DIDS.carol, tradesCompleted: 6, tradesCancelled: 2, disputesRaised: 1, disputesLost: 1, totalVolumeBase: 1_200_000_000, avgConfirmTimeMs: 3600_000, score: 65, badges: [] },
	{ did: DEMO_DIDS.maya, tradesCompleted: 45, tradesCancelled: 1, disputesRaised: 0, disputesLost: 0, totalVolumeBase: 15_000_000_000, avgConfirmTimeMs: 600_000, score: 97, badges: ['verified_seller', 'liquidity_provider', 'top_trader'] },
	{ did: DEMO_DIDS.jordan, tradesCompleted: 3, tradesCancelled: 0, disputesRaised: 0, disputesLost: 0, totalVolumeBase: 500_000_000, avgConfirmTimeMs: 2400_000, score: 72, badges: [] },
];

const DEMO_TRADES: Omit<ExchangeTrade, 'id' | 'createdAt'>[] = [
	{
		buyIntentId: '', sellIntentId: '',
		buyerDid: DEMO_DIDS.alice, buyerName: 'Alice',
		sellerDid: DEMO_DIDS.bob, sellerName: 'Bob',
		tokenId: 'cusdc', tokenAmount: 150_000_000, fiatCurrency: 'EUR',
		fiatAmount: 138.75, agreedRate: 0.925, paymentMethod: 'SEPA',
		status: 'completed', acceptances: { [DEMO_DIDS.alice]: true, [DEMO_DIDS.bob]: true },
		chatMessages: [
			{ id: 'msg-1', senderDid: DEMO_DIDS.alice, senderName: 'Alice', text: 'SEPA sent, ref: ALICE-BOB-001', timestamp: Date.now() - 86400_000 * 3 },
			{ id: 'msg-2', senderDid: DEMO_DIDS.bob, senderName: 'Bob', text: 'Received, releasing escrow', timestamp: Date.now() - 86400_000 * 3 + 1800_000 },
		],
		completedAt: Date.now() - 86400_000 * 3,
	},
	{
		buyIntentId: '', sellIntentId: '',
		buyerDid: DEMO_DIDS.jordan, buyerName: 'Jordan',
		sellerDid: DEMO_DIDS.carol, sellerName: 'Carol',
		tokenId: 'cusdc', tokenAmount: 75_000_000, fiatCurrency: 'USD',
		fiatAmount: 75.50, agreedRate: 1.007, paymentMethod: 'Revolut',
		status: 'escrow_locked', acceptances: { [DEMO_DIDS.jordan]: true, [DEMO_DIDS.carol]: true },
		chatMessages: [],
		fiatConfirmDeadline: Date.now() + 86400_000,
	},
];

const DEMO_LP_POSITIONS: Omit<LiquidityPosition, 'id' | 'createdAt' | 'updatedAt' | 'buyIntentId' | 'sellIntentId'>[] = [
	{
		creatorDid: DEMO_DIDS.maya, creatorName: 'Maya',
		tokenId: 'cusdc', fiatCurrency: 'USD',
		tokenCommitted: 500_000_000, tokenRemaining: 488_000_000,
		fiatCommitted: 490, fiatRemaining: 477.50,
		spreadBps: 50, paymentMethods: ['Revolut', 'Cash'],
		feesEarnedToken: 62_500, feesEarnedFiat: 12.50,
		tradesMatched: 8, status: 'active',
	},
	{
		creatorDid: DEMO_DIDS.bob, creatorName: 'Bob',
		tokenId: 'cusdc', fiatCurrency: 'EUR',
		tokenCommitted: 300_000_000, tokenRemaining: 285_000_000,
		fiatCommitted: 294, fiatRemaining: 285.80,
		spreadBps: 30, paymentMethods: ['SEPA'],
		feesEarnedToken: 45_000, feesEarnedFiat: 8.20,
		tradesMatched: 15, status: 'active',
	},
];

function seedDemoIfEmpty(space: string = 'demo') {
	if (!_syncServer) return;
	const existing = _syncServer.getDoc<ExchangeIntentsDoc>(exchangeIntentsDocId(space));
	if (existing && Object.keys(existing.intents).length > 0) return;

	const now = Date.now();

	// Seed intents
	ensureIntentsDoc(space);
	_syncServer.changeDoc<ExchangeIntentsDoc>(exchangeIntentsDocId(space), 'seed exchange intents', (d) => {
		for (const intent of DEMO_INTENTS) {
			const id = crypto.randomUUID();
			d.intents[id] = { id, ...intent, createdAt: now } as any;
		}
	});

	// Seed trades
	ensureTradesDoc(space);
	_syncServer.changeDoc<ExchangeTradesDoc>(exchangeTradesDocId(space), 'seed exchange trades', (d) => {
		for (const trade of DEMO_TRADES) {
			const id = crypto.randomUUID();
			d.trades[id] = { id, ...trade, createdAt: now - 86400_000 * 5 } as any;
		}
	});

	// Seed reputation
	ensureReputationDoc(space);
	_syncServer.changeDoc<ExchangeReputationDoc>(exchangeReputationDocId(space), 'seed exchange reputation', (d) => {
		for (const rec of DEMO_REPUTATION) {
			d.records[rec.did] = rec as any;
		}
	});

	// Seed LP positions
	ensurePoolsDoc(space);
	_syncServer.changeDoc<ExchangePoolsDoc>(exchangePoolsDocId(space), 'seed LP positions', (d) => {
		for (const lp of DEMO_LP_POSITIONS) {
			const id = crypto.randomUUID();
			const buyIntentId = crypto.randomUUID();
			const sellIntentId = crypto.randomUUID();
			d.positions[id] = {
				id, ...lp,
				buyIntentId, sellIntentId,
				createdAt: now - 86400_000 * 7,
				updatedAt: now - 3600_000,
			} as any;
		}
	});

	console.log(`[rExchange] Demo data seeded for "${space}": ${DEMO_INTENTS.length} intents, ${DEMO_TRADES.length} trades, ${DEMO_REPUTATION.length} reputation records, ${DEMO_LP_POSITIONS.length} LP positions`);
}

// ── Server-rendered order book page ──

function renderOrderBook(space: string): string {
	const intentsDoc = _syncServer?.getDoc<ExchangeIntentsDoc>(exchangeIntentsDocId(space));
	const tradesDoc = _syncServer?.getDoc<ExchangeTradesDoc>(exchangeTradesDocId(space));
	const repDoc = _syncServer?.getDoc<ExchangeReputationDoc>(exchangeReputationDocId(space));
	const poolsDoc = _syncServer?.getDoc<ExchangePoolsDoc>(exchangePoolsDocId(space));

	const intents = intentsDoc ? Object.values(intentsDoc.intents) : [];
	const trades = tradesDoc ? Object.values(tradesDoc.trades) : [];

	const buyIntents = intents.filter(i => i.status === 'active' && i.side === 'buy');
	const sellIntents = intents.filter(i => i.status === 'active' && i.side === 'sell');
	const activeTrades = trades.filter(t => !['completed', 'cancelled', 'timed_out', 'resolved'].includes(t.status));
	const completedTrades = trades.filter(t => t.status === 'completed');
	const activePositions = poolsDoc ? Object.values(poolsDoc.positions).filter(p => p.status === 'active') : [];

	function fmtAmount(base: number) {
		return (base / 1_000_000).toFixed(2);
	}

	function rateStr(i: { rateType: string; rateFixed?: number; rateMarketBps?: number; fiatCurrency: string }) {
		if (i.rateType === 'fixed') return `${i.rateFixed} ${i.fiatCurrency}`;
		return `mkt+${i.rateMarketBps}bps`;
	}

	function statusBadge(s: string) {
		const colors: Record<string, string> = {
			proposed: '#f59e0b', accepted: '#3b82f6', escrow_locked: '#8b5cf6',
			fiat_sent: '#f97316', fiat_confirmed: '#10b981', completed: '#22c55e',
			disputed: '#ef4444', resolved: '#6b7280', cancelled: '#6b7280', timed_out: '#6b7280',
		};
		const c = colors[s] || '#6b7280';
		return `<span style="background:${c}22;color:${c};padding:2px 8px;border-radius:9999px;font-size:0.7rem;font-weight:600">${s.replace(/_/g, ' ')}</span>`;
	}

	function repBadge(did: string) {
		const rec = repDoc?.records[did];
		if (!rec) return '<span style="color:#64748b;font-size:0.7rem">new</span>';
		const c = rec.score >= 80 ? '#22c55e' : rec.score >= 60 ? '#f59e0b' : '#ef4444';
		const badges = rec.badges.length ? ` ${rec.badges.map(b => b === 'verified_seller' ? '✓' : b === 'liquidity_provider' ? '💧' : b === 'top_trader' ? '🏆' : '').join('')}` : '';
		return `<span style="color:${c};font-size:0.7rem;font-weight:600">${rec.score}${badges}</span>`;
	}

	function intentRow(i: typeof intents[0]) {
		const sideColor = i.side === 'buy' ? '#10b981' : '#f59e0b';
		const icon = i.tokenId === 'cusdc' ? '💵' : i.tokenId === 'myco' ? '🌱' : '🎮';
		return `<tr style="border-bottom:1px solid #1e293b">
			<td style="padding:8px"><span style="color:${sideColor};font-weight:600;text-transform:uppercase">${i.side}</span></td>
			<td style="padding:8px">${icon} ${i.tokenId}</td>
			<td style="padding:8px">${fmtAmount(i.tokenAmountMin)}–${fmtAmount(i.tokenAmountMax)}</td>
			<td style="padding:8px">${rateStr(i)}</td>
			<td style="padding:8px">${i.paymentMethods.join(', ')}</td>
			<td style="padding:8px">${i.creatorName} ${repBadge(i.creatorDid)}</td>
			<td style="padding:8px">${i.isStandingOrder ? '<span style="color:#3b82f6;font-size:0.7rem">LP</span>' : ''} ${i.autoAccept ? '<span style="color:#22c55e;font-size:0.7rem">auto</span>' : ''}</td>
		</tr>`;
	}

	function tradeRow(t: typeof trades[0]) {
		const icon = t.tokenId === 'cusdc' ? '💵' : t.tokenId === 'myco' ? '🌱' : '🎮';
		return `<tr style="border-bottom:1px solid #1e293b">
			<td style="padding:8px">${icon} ${fmtAmount(t.tokenAmount)} ${t.tokenId}</td>
			<td style="padding:8px">${t.fiatAmount.toFixed(2)} ${t.fiatCurrency}</td>
			<td style="padding:8px">${t.buyerName} ${repBadge(t.buyerDid)}</td>
			<td style="padding:8px">${t.sellerName} ${repBadge(t.sellerDid)}</td>
			<td style="padding:8px">${t.paymentMethod}</td>
			<td style="padding:8px">${statusBadge(t.status)}</td>
		</tr>`;
	}

	function poolRow(p: LiquidityPosition) {
		const icon = p.tokenId === 'cusdc' ? '💵' : p.tokenId === 'myco' ? '🌱' : '🎮';
		const tokenDepth = fmtAmount(p.tokenRemaining);
		const fiatDepth = p.fiatRemaining.toFixed(2);
		const fiatSymbol = p.fiatCurrency === 'USD' ? '$' : p.fiatCurrency === 'EUR' ? '€' : p.fiatCurrency === 'GBP' ? '£' : '';
		const earned = p.fiatCurrency === 'USD' ? `$${p.feesEarnedFiat.toFixed(2)}`
			: p.fiatCurrency === 'EUR' ? `€${p.feesEarnedFiat.toFixed(2)}`
			: `${p.feesEarnedFiat.toFixed(2)} ${p.fiatCurrency}`;
		return `<tr style="border-bottom:1px solid #1e293b">
			<td style="padding:8px">${p.creatorName} ${repBadge(p.creatorDid)}</td>
			<td style="padding:8px">${icon} ${p.tokenId}/${p.fiatCurrency}</td>
			<td style="padding:8px">${tokenDepth} / ${fiatSymbol}${fiatDepth}</td>
			<td style="padding:8px">${p.spreadBps}bps</td>
			<td style="padding:8px;color:#22c55e">${earned}</td>
			<td style="padding:8px">${p.tradesMatched}</td>
		</tr>`;
	}

	return `
<div style="max-width:960px;margin:0 auto;padding:24px 16px;color:#e2e8f0;font-family:system-ui,sans-serif">
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:24px">
    <div>
      <h1 style="font-size:1.5rem;font-weight:700;margin:0;background:linear-gradient(to right,#f59e0b,#10b981);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">
        💱 rExchange
      </h1>
      <p style="color:#64748b;font-size:0.85rem;margin:4px 0 0">P2P order book for ${space}</p>
    </div>
    <div style="display:flex;gap:12px;font-size:0.8rem">
      <span style="color:#10b981">${buyIntents.length} buys</span>
      <span style="color:#f59e0b">${sellIntents.length} sells</span>
      <span style="color:#8b5cf6">${activePositions.length} pools</span>
      <span style="color:#3b82f6">${activeTrades.length} active</span>
      <span style="color:#22c55e">${completedTrades.length} settled</span>
    </div>
  </div>

  <!-- Active Order Book -->
  <div style="background:#0f172a;border:1px solid #1e293b;border-radius:12px;padding:16px;margin-bottom:24px">
    <h2 style="font-size:1rem;font-weight:600;margin:0 0 12px;color:#94a3b8">Order Book</h2>
    ${intents.filter(i => i.status === 'active').length === 0
      ? '<p style="color:#475569;text-align:center;padding:24px">No active intents</p>'
      : `<table style="width:100%;border-collapse:collapse;font-size:0.85rem">
      <thead><tr style="border-bottom:2px solid #1e293b;color:#64748b;font-size:0.75rem;text-transform:uppercase">
        <th style="padding:8px;text-align:left">Side</th>
        <th style="padding:8px;text-align:left">Token</th>
        <th style="padding:8px;text-align:left">Amount</th>
        <th style="padding:8px;text-align:left">Rate</th>
        <th style="padding:8px;text-align:left">Payment</th>
        <th style="padding:8px;text-align:left">Trader</th>
        <th style="padding:8px;text-align:left">Flags</th>
      </tr></thead>
      <tbody>${buyIntents.map(intentRow).join('')}${sellIntents.map(intentRow).join('')}</tbody>
    </table>`}
  </div>

  <!-- Liquidity Pools -->
  ${activePositions.length > 0 ? `
  <div style="background:#0f172a;border:1px solid #1e293b;border-radius:12px;padding:16px;margin-bottom:24px">
    <h2 style="font-size:1rem;font-weight:600;margin:0 0 12px;color:#94a3b8;display:flex;align-items:center;gap:8px">
      <span style="background:linear-gradient(to right,#10b981,#f59e0b);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">💧 Liquidity Pools</span>
    </h2>
    <table style="width:100%;border-collapse:collapse;font-size:0.85rem">
      <thead><tr style="border-bottom:2px solid #1e293b;color:#64748b;font-size:0.75rem;text-transform:uppercase">
        <th style="padding:8px;text-align:left">Provider</th>
        <th style="padding:8px;text-align:left">Pair</th>
        <th style="padding:8px;text-align:left">Depth</th>
        <th style="padding:8px;text-align:left">Spread</th>
        <th style="padding:8px;text-align:left">Earned</th>
        <th style="padding:8px;text-align:left">Trades</th>
      </tr></thead>
      <tbody>${activePositions.map(poolRow).join('')}</tbody>
    </table>
  </div>` : ''}

  <!-- Active Trades -->
  ${activeTrades.length > 0 ? `
  <div style="background:#0f172a;border:1px solid #1e293b;border-radius:12px;padding:16px;margin-bottom:24px">
    <h2 style="font-size:1rem;font-weight:600;margin:0 0 12px;color:#94a3b8">Active Trades</h2>
    <table style="width:100%;border-collapse:collapse;font-size:0.85rem">
      <thead><tr style="border-bottom:2px solid #1e293b;color:#64748b;font-size:0.75rem;text-transform:uppercase">
        <th style="padding:8px;text-align:left">Amount</th>
        <th style="padding:8px;text-align:left">Fiat</th>
        <th style="padding:8px;text-align:left">Buyer</th>
        <th style="padding:8px;text-align:left">Seller</th>
        <th style="padding:8px;text-align:left">Method</th>
        <th style="padding:8px;text-align:left">Status</th>
      </tr></thead>
      <tbody>${activeTrades.map(tradeRow).join('')}</tbody>
    </table>
  </div>` : ''}

  <!-- Recent Completed -->
  ${completedTrades.length > 0 ? `
  <div style="background:#0f172a;border:1px solid #1e293b;border-radius:12px;padding:16px">
    <h2 style="font-size:1rem;font-weight:600;margin:0 0 12px;color:#94a3b8">Recent Trades</h2>
    <table style="width:100%;border-collapse:collapse;font-size:0.85rem">
      <thead><tr style="border-bottom:2px solid #1e293b;color:#64748b;font-size:0.75rem;text-transform:uppercase">
        <th style="padding:8px;text-align:left">Amount</th>
        <th style="padding:8px;text-align:left">Fiat</th>
        <th style="padding:8px;text-align:left">Buyer</th>
        <th style="padding:8px;text-align:left">Seller</th>
        <th style="padding:8px;text-align:left">Method</th>
        <th style="padding:8px;text-align:left">Status</th>
      </tr></thead>
      <tbody>${completedTrades.map(tradeRow).join('')}</tbody>
    </table>
  </div>` : ''}
</div>

<script>
  // Auto-refresh every 30s
  setTimeout(() => location.reload(), 30000);
</script>`;
}

// ── Page routes ──

routes.get('/', (c) => {
	const space = c.req.param('space') || 'demo';
	return c.html(renderShell({
		title: `${space} — rExchange | rSpace`,
		moduleId: 'rexchange',
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: 'dark',
		body: renderOrderBook(space),
	}));
});

// ── Module export ──

export function getRecentIntentsForMI(space: string, limit = 5): { id: string; side: string; tokenId: string; status: string; createdAt: number }[] {
	if (!_syncServer) return [];
	const docId = exchangeIntentsDocId(space);
	const doc = _syncServer.getDoc<ExchangeIntentsDoc>(docId);
	if (!doc) return [];
	return Object.values(doc.intents)
		.sort((a, b) => b.createdAt - a.createdAt)
		.slice(0, limit)
		.map((i) => ({ id: i.id, side: i.side, tokenId: i.tokenId, status: i.status, createdAt: i.createdAt }));
}

export const exchangeModule: RSpaceModule = {
	id: 'rexchange',
	name: 'rExchange',
	icon: '💱',
	description: 'P2P crypto/fiat exchange with escrow & reputation',
	canvasShapes: ['folk-exchange-node'],
	canvasToolIds: ['create_exchange_node'],
	scoping: { defaultScope: 'space', userConfigurable: false },
	docSchemas: [
		{ pattern: '{space}:rexchange:intents', description: 'Buy/sell intent order book', init: exchangeIntentsSchema.init },
		{ pattern: '{space}:rexchange:trades', description: 'Active and historical trades', init: exchangeTradesSchema.init },
		{ pattern: '{space}:rexchange:reputation', description: 'Per-member exchange reputation', init: exchangeReputationSchema.init },
		{ pattern: '{space}:rexchange:pools', description: 'Liquidity pool positions', init: exchangePoolsSchema.init },
	],
	routes,
	landingPage: renderLanding,
	seedTemplate: seedDemoIfEmpty,
	async onInit(ctx) {
		_syncServer = ctx.syncServer;
		seedDemoIfEmpty();
		startSolverCron(() => _syncServer);
	},
	feeds: [
		{
			id: 'exchange-trades',
			name: 'Exchange Trades',
			kind: 'economic',
			description: 'P2P exchange trade completions',
			filterable: true,
		},
	],
	outputPaths: [
		{ path: 'canvas', name: 'Order Book', icon: '📊', description: 'Visual order book with buy/sell orbs' },
		{ path: 'collaborate', name: 'My Trades', icon: '🤝', description: 'Active trades and chat' },
	],
	onboardingActions: [
		{ label: 'Post Intent', icon: '💱', description: 'Post a buy or sell intent', type: 'create', href: '/rexchange' },
		{ label: 'Add Liquidity', icon: '💧', description: 'Provide liquidity to earn the spread', type: 'create', href: '/rexchange' },
	],
};
