/**
 * Wallet module — multichain Safe wallet visualization.
 *
 * Client-side only (no DB). Queries Safe Global API for balances,
 * transfers, and multisig data across multiple chains.
 */

import { Hono } from "hono";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing";
import { walletApplets } from "./applets";
import { verifyToken, extractToken } from "../../server/auth";
import { resolveCallerRole } from "../../server/spaces";
import type { SpaceRoleString } from "../../server/spaces";
import { filterByVisibility } from "../../shared/membrane";
import { enrichWithPrices } from "./lib/price-feed";
import { getDefiPositions } from "./lib/defi-positions";
import type { DefiPosition } from "./lib/defi-positions";

const routes = new Hono();

// ── Address validation (prevents SSRF via path traversal) ──
const VALID_ETH_ADDR = /^0x[0-9a-fA-F]{40}$/;
function validateAddress(c: any): string | null {
	const address = c.req.param("address");
	if (!VALID_ETH_ADDR.test(address)) {
		return null;
	}
	return address;
}

// ── Proxy Safe Global API (avoid CORS issues from browser) ──
routes.get("/api/safe/:chainId/:address/balances", async (c) => {
	const chainId = c.req.param("chainId");
	const address = validateAddress(c);
	if (!address) return c.json({ error: "Invalid Ethereum address" }, 400);
	const chainPrefix = getSafePrefix(chainId);
	if (!chainPrefix) return c.json({ error: "Unsupported chain" }, 400);

	const res = await fetch(`${safeApiBase(chainPrefix)}/safes/${address}/balances/?trusted=true&exclude_spam=true`);
	if (!res.ok) return c.json({ error: "Safe API error" }, res.status as any);
	const raw = await res.json() as any[];
	const nativeToken = NATIVE_TOKENS[chainId] || { name: "ETH", symbol: "ETH", decimals: 18 };
	// Normalize: fill in native token info and ensure fiatBalance fields exist
	const data = raw.map((item: any) => ({
		tokenAddress: item.tokenAddress,
		token: item.token || nativeToken,
		balance: item.balance || "0",
		fiatBalance: item.fiatBalance || "0",
		fiatConversion: item.fiatConversion || "0",
	}));
	const enriched = (await enrichWithPrices(data, chainId, { filterSpam: true }))
		.filter(b => BigInt(b.balance || "0") > 0n);
	c.header("Cache-Control", "public, max-age=30");
	return c.json(enriched);
});

// ── Fetch with exponential backoff (retry on 429) ──
async function fetchWithBackoff(url: string, maxRetries = 5): Promise<Response> {
	let delay = 2000;
	for (let attempt = 0; attempt <= maxRetries; attempt++) {
		const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
		if (res.status !== 429 || attempt === maxRetries) return res;
		await new Promise((r) => setTimeout(r, delay));
		delay = Math.min(delay * 2, 32000);
	}
	throw new Error("Max retries exceeded");
}

routes.get("/api/safe/:chainId/:address/transfers", async (c) => {
	const chainId = c.req.param("chainId");
	const address = validateAddress(c);
	if (!address) return c.json({ error: "Invalid Ethereum address" }, 400);
	const chainPrefix = getSafePrefix(chainId);
	if (!chainPrefix) return c.json({ error: "Unsupported chain" }, 400);

	const rawLimit = parseInt(c.req.query("limit") || "100", 10);
	const maxResults = isNaN(rawLimit) || rawLimit < 1 ? 100 : Math.min(rawLimit, 3000);
	const pageSize = Math.min(maxResults, 100);

	const allResults: any[] = [];
	let nextUrl: string | null = `${safeApiBase(chainPrefix)}/safes/${address}/all-transactions/?limit=${pageSize}&executed=true`;

	while (nextUrl && allResults.length < maxResults) {
		const res = await fetchWithBackoff(nextUrl);
		if (!res.ok) return c.json({ error: "Safe API error" }, res.status as any);
		const data = await res.json() as { results?: any[]; next?: string | null };
		if (data.results) allResults.push(...data.results);
		nextUrl = data.next || null;
		if (nextUrl && allResults.length < maxResults) {
			await new Promise((r) => setTimeout(r, 400)); // polite delay
		}
	}

	c.header("Cache-Control", "public, max-age=60");
	return c.json({ count: allResults.length, results: allResults.slice(0, maxResults) });
});

routes.get("/api/safe/:chainId/:address/info", async (c) => {
	const chainId = c.req.param("chainId");
	const address = validateAddress(c);
	if (!address) return c.json({ error: "Invalid Ethereum address" }, 400);
	const chainPrefix = getSafePrefix(chainId);
	if (!chainPrefix) return c.json({ error: "Unsupported chain" }, 400);

	const res = await fetch(`${safeApiBase(chainPrefix)}/safes/${address}/`);
	if (!res.ok) return c.json({ error: "Safe API error" }, res.status as any);
	const data = await res.json();
	c.header("Cache-Control", "public, max-age=300");
	return c.json(data);
});

// Detect which chains have a Safe for this address
routes.get("/api/safe/detect/:address", async (c) => {
	const address = validateAddress(c);
	if (!address) return c.json({ error: "Invalid Ethereum address" }, 400);
	const includeTestnets = c.req.query("testnets") === "true";
	const chains = getChains(includeTestnets);
	const results: Array<{ chainId: string; name: string; prefix: string }> = [];

	await Promise.allSettled(
		chains.map(async ([chainId, info]) => {
			try {
				const res = await fetch(`${safeApiBase(info.prefix)}/safes/${address}/`, {
					signal: AbortSignal.timeout(5000),
				});
				if (res.ok) results.push({ chainId, name: info.name, prefix: info.prefix });
			} catch {}
		})
	);

	// Always include testnet chains when toggled on (skip detection)
	if (includeTestnets) {
		for (const [chainId, info] of Object.entries(CHAIN_MAP)) {
			if (TESTNET_CHAIN_IDS.has(chainId) && !results.some((r) => r.chainId === chainId)) {
				results.push({ chainId, name: info.name, prefix: info.prefix });
			}
		}
	}

	return c.json({ address, chains: results.sort((a, b) => a.name.localeCompare(b.name)) });
});

// ── Chain mapping (prefix = Safe TX Service shortcode) ──
const CHAIN_MAP: Record<string, { name: string; prefix: string }> = {
	"1": { name: "Ethereum", prefix: "eth" },
	"10": { name: "Optimism", prefix: "oeth" },
	"100": { name: "Gnosis", prefix: "gno" },
	"137": { name: "Polygon", prefix: "pol" },
	"8453": { name: "Base", prefix: "base" },
	"42161": { name: "Arbitrum", prefix: "arb1" },
	"42220": { name: "Celo", prefix: "celo" },
	"43114": { name: "Avalanche", prefix: "avax" },
	"56": { name: "BSC", prefix: "bnb" },
	"324": { name: "zkSync", prefix: "zksync" },
	"59144": { name: "Linea", prefix: "linea" },
	"59141": { name: "Linea Sepolia", prefix: "lineasep" },
	"11155111": { name: "Sepolia", prefix: "sep" },
	"84532": { name: "Base Sepolia", prefix: "basesep" },
	"421614": { name: "Arbitrum Sepolia", prefix: "arbsep" },
	"11155420": { name: "Optimism Sepolia", prefix: "optsep" },
	"80002": { name: "Polygon Amoy", prefix: "polyamoy" },
	"43113": { name: "Avalanche Fuji", prefix: "avaxfuji" },
	"97": { name: "BSC Testnet", prefix: "bsctest" },
};

const TESTNET_CHAIN_IDS = new Set(["11155111", "84532", "421614", "11155420", "80002", "43113", "97", "59141"]);

function getChains(includeTestnets: boolean): [string, { name: string; prefix: string }][] {
	return Object.entries(CHAIN_MAP).filter(([id]) => includeTestnets || !TESTNET_CHAIN_IDS.has(id));
}

function getSafePrefix(chainId: string): string | null {
	return CHAIN_MAP[chainId]?.prefix || null;
}

function safeApiBase(prefix: string): string {
	return `https://api.safe.global/tx-service/${prefix}/api/v1`;
}

// ── Public RPC endpoints for EOA balance lookups ──
const DEFAULT_RPC_URLS: Record<string, string> = {
	"1": "https://eth.llamarpc.com",
	"10": "https://mainnet.optimism.io",
	"100": "https://rpc.gnosischain.com",
	"137": "https://polygon-rpc.com",
	"8453": "https://mainnet.base.org",
	"42161": "https://arb1.arbitrum.io/rpc",
	"42220": "https://forno.celo.org",
	"43114": "https://api.avax.network/ext/bc/C/rpc",
	"56": "https://bsc-dataseed.binance.org",
	"324": "https://mainnet.era.zksync.io",
	"59144": "https://rpc.linea.build",
	"59141": "https://rpc.sepolia.linea.build",
	"11155111": "https://rpc.sepolia.org",
	"84532": "https://sepolia.base.org",
	"421614": "https://sepolia-rollup.arbitrum.io/rpc",
	"11155420": "https://sepolia.optimism.io",
	"80002": "https://rpc-amoy.polygon.technology",
	"43113": "https://api.avax-test.network/ext/bc/C/rpc",
	"97": "https://data-seed-prebsc-1-s1.binance.org:8545",
};

// Chain ID → env var name fragment + Alchemy subdomain (for auto-construct)
const CHAIN_ENV_NAMES: Record<string, { envName: string; alchemySlug?: string }> = {
	"1":     { envName: "ETHEREUM",  alchemySlug: "eth-mainnet" },
	"10":    { envName: "OPTIMISM",  alchemySlug: "opt-mainnet" },
	"137":   { envName: "POLYGON",   alchemySlug: "polygon-mainnet" },
	"8453":  { envName: "BASE",      alchemySlug: "base-mainnet" },
	"42161": { envName: "ARBITRUM",  alchemySlug: "arb-mainnet" },
	"100":   { envName: "GNOSIS" },
	"42220": { envName: "CELO" },
	"43114": { envName: "AVALANCHE" },
	"56":    { envName: "BSC" },
	"324":   { envName: "ZKSYNC" },
	"59144": { envName: "LINEA",     alchemySlug: "linea-mainnet" },
	"59141": { envName: "LINEA_SEPOLIA" },
	"11155111": { envName: "SEPOLIA",         alchemySlug: "eth-sepolia" },
	"84532":    { envName: "BASE_SEPOLIA",    alchemySlug: "base-sepolia" },
	"421614":   { envName: "ARB_SEPOLIA",     alchemySlug: "arb-sepolia" },
	"11155420": { envName: "OPT_SEPOLIA",     alchemySlug: "opt-sepolia" },
	"80002":    { envName: "POLYGON_AMOY",    alchemySlug: "polygon-amoy" },
	"43113":    { envName: "AVAX_FUJI" },
	"97":       { envName: "BSC_TESTNET" },
};

/**
 * Resolve the RPC URL for a given chain, with env var overrides and Alchemy auto-construct.
 * Priority: RPC_{CHAIN} env > Alchemy auto-construct > default public RPC.
 */
export function getRpcUrl(chainId: string): string | undefined {
	// 1. Explicit env var override (e.g. RPC_BASE, RPC_ETHEREUM)
	const chainEnv = CHAIN_ENV_NAMES[chainId];
	if (chainEnv) {
		const envOverride = process.env[`RPC_${chainEnv.envName}`];
		if (envOverride) return envOverride;
	}

	// 2. Alchemy auto-construct if API key is set
	const alchemyKey = process.env.ALCHEMY_API_KEY;
	if (alchemyKey && chainEnv?.alchemySlug) {
		return `https://${chainEnv.alchemySlug}.g.alchemy.com/v2/${alchemyKey}`;
	}

	// 3. Default public RPC
	return DEFAULT_RPC_URLS[chainId];
}

const NATIVE_TOKENS: Record<string, { name: string; symbol: string; decimals: number }> = {
	"1": { name: "Ether", symbol: "ETH", decimals: 18 },
	"10": { name: "Ether", symbol: "ETH", decimals: 18 },
	"100": { name: "xDAI", symbol: "xDAI", decimals: 18 },
	"137": { name: "MATIC", symbol: "MATIC", decimals: 18 },
	"8453": { name: "Ether", symbol: "ETH", decimals: 18 },
	"42161": { name: "Ether", symbol: "ETH", decimals: 18 },
	"42220": { name: "CELO", symbol: "CELO", decimals: 18 },
	"43114": { name: "AVAX", symbol: "AVAX", decimals: 18 },
	"56": { name: "BNB", symbol: "BNB", decimals: 18 },
	"324": { name: "Ether", symbol: "ETH", decimals: 18 },
	"59144": { name: "Ether", symbol: "ETH", decimals: 18 },
	"59141": { name: "Linea Sepolia ETH", symbol: "ETH", decimals: 18 },
	"11155111": { name: "Sepolia ETH", symbol: "ETH", decimals: 18 },
	"84532": { name: "Base Sepolia ETH", symbol: "ETH", decimals: 18 },
	"421614": { name: "Arb Sepolia ETH", symbol: "ETH", decimals: 18 },
	"11155420": { name: "OP Sepolia ETH", symbol: "ETH", decimals: 18 },
	"80002": { name: "Amoy POL", symbol: "POL", decimals: 18 },
	"43113": { name: "Fuji AVAX", symbol: "AVAX", decimals: 18 },
	"97": { name: "Test BNB", symbol: "tBNB", decimals: 18 },
};

async function rpcCall(rpcUrl: string, method: string, params: any[]): Promise<any> {
	const res = await fetch(rpcUrl, {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
		signal: AbortSignal.timeout(5000),
	});
	const data = await res.json();
	return data.result;
}

// ── Safe Proposal / Confirm / Execute (EncryptID-authenticated) ──

// Helper: extract and verify JWT from request
async function verifyWalletAuth(c: any): Promise<{ sub: string; did?: string; username?: string; eid?: any } | null> {
	const token = extractToken(c.req.raw.headers);
	if (!token) return null;
	try {
		const claims = await verifyToken(token);
		return claims as any;
	} catch {
		return null;
	}
}

// POST /api/safe/:chainId/:address/propose — Create a Safe transaction proposal
routes.post("/api/safe/:chainId/:address/propose", async (c) => {
	const claims = await verifyWalletAuth(c);
	if (!claims) return c.json({ error: "Authentication required" }, 401);

	// Require ELEVATED auth level (3+)
	if (!claims.eid || claims.eid.authLevel < 3) {
		return c.json({ error: "Elevated authentication required for proposals" }, 403);
	}
	if (!claims.eid.capabilities?.wallet) {
		return c.json({ error: "Wallet capability required" }, 403);
	}

	const chainId = c.req.param("chainId");
	const address = validateAddress(c);
	if (!address) return c.json({ error: "Invalid Safe address" }, 400);
	const chainPrefix = getSafePrefix(chainId);
	if (!chainPrefix) return c.json({ error: "Unsupported chain" }, 400);

	const body = await c.req.json();
	const { to, value, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, nonce, signature, sender } = body;

	if (!to || !signature || !sender) {
		return c.json({ error: "Missing required fields: to, signature, sender" }, 400);
	}
	if (!VALID_ETH_ADDR.test(sender)) {
		return c.json({ error: "Invalid sender address" }, 400);
	}

	// Submit proposal to Safe Transaction Service
	const res = await fetch(
		`${safeApiBase(chainPrefix)}/safes/${address}/multisig-transactions/`,
		{
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify({
				to,
				value: value || "0",
				data: data || "0x",
				operation: operation || 0,
				safeTxGas: safeTxGas || "0",
				baseGas: baseGas || "0",
				gasPrice: gasPrice || "0",
				gasToken: gasToken || "0x0000000000000000000000000000000000000000",
				refundReceiver: refundReceiver || "0x0000000000000000000000000000000000000000",
				nonce,
				signature,
				sender,
			}),
		},
	);

	if (!res.ok) {
		console.warn('rwallet: Safe propose error', res.status, await res.text());
		return c.json({ error: "Safe Transaction Service rejected the request" }, res.status as any);
	}

	return c.json(await res.json(), 201);
});

// POST /api/safe/:chainId/:address/confirm — Confirm a pending transaction
routes.post("/api/safe/:chainId/:address/confirm", async (c) => {
	const claims = await verifyWalletAuth(c);
	if (!claims) return c.json({ error: "Authentication required" }, 401);

	if (!claims.eid || claims.eid.authLevel < 3) {
		return c.json({ error: "Elevated authentication required" }, 403);
	}
	if (!claims.eid.capabilities?.wallet) {
		return c.json({ error: "Wallet capability required" }, 403);
	}

	const chainId = c.req.param("chainId");
	const chainPrefix = getSafePrefix(chainId);
	if (!chainPrefix) return c.json({ error: "Unsupported chain" }, 400);

	const { safeTxHash, signature } = await c.req.json();
	if (!safeTxHash || !signature) {
		return c.json({ error: "Missing required fields: safeTxHash, signature" }, 400);
	}

	const res = await fetch(
		`${safeApiBase(chainPrefix)}/multisig-transactions/${safeTxHash}/confirmations/`,
		{
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify({ signature }),
		},
	);

	if (!res.ok) {
		console.warn('rwallet: Safe confirm error', res.status, await res.text());
		return c.json({ error: "Confirmation failed" }, res.status as any);
	}

	return c.json(await res.json());
});

// POST /api/safe/:chainId/:address/execute — Execute a ready transaction (CRITICAL auth)
routes.post("/api/safe/:chainId/:address/execute", async (c) => {
	const claims = await verifyWalletAuth(c);
	if (!claims) return c.json({ error: "Authentication required" }, 401);

	// Require CRITICAL auth level (4) for execution
	if (!claims.eid || claims.eid.authLevel < 4) {
		return c.json({ error: "Critical authentication required for execution" }, 403);
	}
	if (!claims.eid.capabilities?.wallet) {
		return c.json({ error: "Wallet capability required" }, 403);
	}

	// Check auth freshness (must be within 60 seconds)
	const now = Math.floor(Date.now() / 1000);
	if (claims.eid.authTime && (now - claims.eid.authTime) > 60) {
		return c.json({ error: "Authentication too old for execution — re-authenticate" }, 403);
	}

	const chainId = c.req.param("chainId");
	const address = validateAddress(c);
	if (!address) return c.json({ error: "Invalid Safe address" }, 400);
	const chainPrefix = getSafePrefix(chainId);
	if (!chainPrefix) return c.json({ error: "Unsupported chain" }, 400);

	// Get pending transactions and find the one to execute
	const { safeTxHash } = await c.req.json();
	if (!safeTxHash) {
		return c.json({ error: "Missing required field: safeTxHash" }, 400);
	}

	// Fetch the transaction details from Safe Transaction Service
	const txRes = await fetch(
		`${safeApiBase(chainPrefix)}/multisig-transactions/${safeTxHash}/`,
	);

	if (!txRes.ok) {
		return c.json({ error: "Transaction not found" }, 404);
	}

	const txData = await txRes.json() as { confirmations?: any[]; confirmationsRequired?: number; isExecuted?: boolean };

	if (txData.isExecuted) {
		return c.json({ error: "Transaction already executed" }, 400);
	}

	const confirmationsNeeded = txData.confirmationsRequired || 1;
	const confirmationsReceived = txData.confirmations?.length || 0;

	if (confirmationsReceived < confirmationsNeeded) {
		return c.json({
			error: "Not enough confirmations",
			required: confirmationsNeeded,
			received: confirmationsReceived,
		}, 400);
	}

	// Transaction is ready — return execution data for client-side submission
	// (Actual on-chain execution must happen client-side with the user's signer)
	return c.json({
		ready: true,
		safeTxHash,
		confirmations: confirmationsReceived,
		required: confirmationsNeeded,
		transaction: txData,
	});
});

// ── Popular ERC-20 tokens to scan for EOA wallets ──
const POPULAR_TOKENS: Record<string, Array<{ address: string; name: string; symbol: string; decimals: number }>> = {
	"1": [
		{ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", name: "USD Coin", symbol: "USDC", decimals: 6 },
		{ address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", name: "Tether USD", symbol: "USDT", decimals: 6 },
		{ address: "0x6B175474E89094C44Da98b954EedeAC495271d0F", name: "Dai Stablecoin", symbol: "DAI", decimals: 18 },
		{ address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", name: "Wrapped Ether", symbol: "WETH", decimals: 18 },
	],
	"8453": [
		{ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", name: "USD Coin", symbol: "USDC", decimals: 6 },
		{ address: "0x4200000000000000000000000000000000000006", name: "Wrapped Ether", symbol: "WETH", decimals: 18 },
		{ address: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", name: "Dai Stablecoin", symbol: "DAI", decimals: 18 },
	],
	"10": [
		{ address: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", name: "USD Coin", symbol: "USDC", decimals: 6 },
		{ address: "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58", name: "Tether USD", symbol: "USDT", decimals: 6 },
		{ address: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", name: "Dai Stablecoin", symbol: "DAI", decimals: 18 },
		{ address: "0x4200000000000000000000000000000000000006", name: "Wrapped Ether", symbol: "WETH", decimals: 18 },
	],
	"137": [
		{ address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", name: "USD Coin", symbol: "USDC", decimals: 6 },
		{ address: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", name: "Tether USD", symbol: "USDT", decimals: 6 },
		{ address: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063", name: "Dai Stablecoin", symbol: "DAI", decimals: 18 },
		{ address: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", name: "Wrapped Ether", symbol: "WETH", decimals: 18 },
	],
	"42161": [
		{ address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", name: "USD Coin", symbol: "USDC", decimals: 6 },
		{ address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", name: "Tether USD", symbol: "USDT", decimals: 6 },
		{ address: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", name: "Dai Stablecoin", symbol: "DAI", decimals: 18 },
		{ address: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", name: "Wrapped Ether", symbol: "WETH", decimals: 18 },
	],
	"100": [
		{ address: "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83", name: "USD Coin", symbol: "USDC", decimals: 6 },
		{ address: "0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1", name: "Wrapped Ether", symbol: "WETH", decimals: 18 },
	],
	"11155111": [
		{ address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", name: "USD Coin", symbol: "USDC", decimals: 6 },
		{ address: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14", name: "Wrapped Ether", symbol: "WETH", decimals: 18 },
		{ address: "0x779877A7B0D9E8603169DdbD7836e478b4624789", name: "Chainlink Token", symbol: "LINK", decimals: 18 },
	],
	"84532": [
		{ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", name: "USD Coin", symbol: "USDC", decimals: 6 },
	],
	"421614": [
		{ address: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d", name: "USD Coin", symbol: "USDC", decimals: 6 },
		{ address: "0x980B62Da83eFf3D4576C647993b0c1D7faf17c73", name: "Wrapped Ether", symbol: "WETH", decimals: 18 },
	],
	"11155420": [
		{ address: "0x5fd84259d66Cd46123540766Be93DFE6D43130D7", name: "USD Coin", symbol: "USDC", decimals: 6 },
		{ address: "0x4200000000000000000000000000000000000006", name: "Wrapped Ether", symbol: "WETH", decimals: 18 },
	],
	"80002": [
		{ address: "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582", name: "USD Coin", symbol: "USDC", decimals: 6 },
	],
	"59144": [
		{ address: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff", name: "USD Coin", symbol: "USDC", decimals: 6 },
		{ address: "0xe5D7C2a44FfDDf6b295A15996c1F68c7BbB2d4b0", name: "Wrapped Ether", symbol: "WETH", decimals: 18 },
		{ address: "0xA219439258ca9da29E9Cc4cE5596924745e12B93", name: "Tether USD", symbol: "USDT", decimals: 6 },
	],
};

// ERC-20 balanceOf(address) — selector 0x70a08231
async function getErc20Balance(rpcUrl: string, tokenAddress: string, walletAddress: string): Promise<string> {
	const paddedAddr = walletAddress.slice(2).toLowerCase().padStart(64, "0");
	const data = `0x70a08231${paddedAddr}`;
	const result = await rpcCall(rpcUrl, "eth_call", [{ to: tokenAddress, data }, "latest"]);
	if (!result || result === "0x" || result === "0x0") return "0";
	return BigInt(result).toString();
}

// ── EOA (any wallet) balance endpoints ──

// Detect which chains have a non-zero native balance for any address
routes.get("/api/eoa/detect/:address", async (c) => {
	const address = validateAddress(c);
	if (!address) return c.json({ error: "Invalid Ethereum address" }, 400);
	const includeTestnets = c.req.query("testnets") === "true";
	const results: Array<{ chainId: string; name: string; prefix: string; balance: string }> = [];

	await Promise.allSettled(
		getChains(includeTestnets).map(async ([chainId, info]) => {
			const rpcUrl = getRpcUrl(chainId);
			if (!rpcUrl) return;
			try {
				const balHex = await rpcCall(rpcUrl, "eth_getBalance", [address, "latest"]);
				if (balHex && balHex !== "0x0" && balHex !== "0x") {
					results.push({ chainId, name: info.name, prefix: info.prefix, balance: balHex });
				}
			} catch {}
		})
	);

	// Always include testnet chains when toggled on (skip detection)
	if (includeTestnets) {
		for (const [chainId, info] of Object.entries(CHAIN_MAP)) {
			if (TESTNET_CHAIN_IDS.has(chainId) && !results.some((r) => r.chainId === chainId)) {
				results.push({ chainId, name: info.name, prefix: info.prefix, balance: "0x0" });
			}
		}
	}

	return c.json({ address, chains: results.sort((a, b) => a.name.localeCompare(b.name)) });
});

// Get native + ERC-20 token balances for an EOA on a specific chain
routes.get("/api/eoa/:chainId/:address/balances", async (c) => {
	const chainId = c.req.param("chainId");
	const address = validateAddress(c);
	if (!address) return c.json({ error: "Invalid Ethereum address" }, 400);
	const rpcUrl = getRpcUrl(chainId);
	if (!rpcUrl) return c.json({ error: "Unsupported chain" }, 400);

	const nativeToken = NATIVE_TOKENS[chainId] || { name: "ETH", symbol: "ETH", decimals: 18 };
	const balances: BalanceItem[] = [];

	// Fetch native + ERC-20 balances in parallel
	const tokens = POPULAR_TOKENS[chainId] || [];
	const promises: Promise<void>[] = [];

	// Native balance
	promises.push((async () => {
		try {
			const balHex = await rpcCall(rpcUrl, "eth_getBalance", [address, "latest"]);
			const balWei = BigInt(balHex || "0x0");
			if (balWei > 0n) {
				balances.push({
					tokenAddress: null,
					token: nativeToken,
					balance: balWei.toString(),
					fiatBalance: "0",
					fiatConversion: "0",
				});
			}
		} catch {}
	})());

	// ERC-20 balances
	for (const tok of tokens) {
		promises.push((async () => {
			try {
				const bal = await getErc20Balance(rpcUrl, tok.address, address);
				if (bal !== "0") {
					balances.push({
						tokenAddress: tok.address,
						token: { name: tok.name, symbol: tok.symbol, decimals: tok.decimals },
						balance: bal,
						fiatBalance: "0",
						fiatConversion: "0",
					});
				}
			} catch {}
		})());
	}

	await Promise.allSettled(promises);

	const enriched = await enrichWithPrices(balances, chainId, { filterSpam: true });
	c.header("Cache-Control", "public, max-age=30");
	return c.json(enriched);
});

// ── All-chains balance endpoints (fan out to every chain in parallel) ──

// Get all balances across all chains for an EOA
routes.get("/api/eoa/:address/all-balances", async (c) => {
	const address = validateAddress(c);
	if (!address) return c.json({ error: "Invalid Ethereum address" }, 400);
	const includeTestnets = c.req.query("testnets") !== "false"; // default: include
	const chains = getChains(includeTestnets);

	const results: Array<{ chainId: string; chainName: string; balances: BalanceItem[] }> = [];

	await Promise.allSettled(
		chains.map(async ([chainId, info]) => {
			const rpcUrl = getRpcUrl(chainId);
			if (!rpcUrl) return;
			const nativeToken = NATIVE_TOKENS[chainId] || { name: "ETH", symbol: "ETH", decimals: 18 };
			const chainBalances: BalanceItem[] = [];
			const tokenPromises: Promise<void>[] = [];

			// Native balance
			tokenPromises.push((async () => {
				try {
					const balHex = await rpcCall(rpcUrl, "eth_getBalance", [address, "latest"]);
					const balWei = BigInt(balHex || "0x0");
					if (balWei > 0n) {
						chainBalances.push({
							tokenAddress: null,
							token: nativeToken,
							balance: balWei.toString(),
							fiatBalance: "0",
							fiatConversion: "0",
						});
					}
				} catch {}
			})());

			// ERC-20 balances
			for (const tok of (POPULAR_TOKENS[chainId] || [])) {
				tokenPromises.push((async () => {
					try {
						const bal = await getErc20Balance(rpcUrl, tok.address, address);
						if (bal !== "0") {
							chainBalances.push({
								tokenAddress: tok.address,
								token: { name: tok.name, symbol: tok.symbol, decimals: tok.decimals },
								balance: bal,
								fiatBalance: "0",
								fiatConversion: "0",
							});
						}
					} catch {}
				})());
			}

			await Promise.allSettled(tokenPromises);
			if (chainBalances.length > 0) {
				const enriched = await enrichWithPrices(chainBalances, chainId, { filterSpam: true });
				results.push({ chainId, chainName: info.name, balances: enriched });
			}
		})
	);

	results.sort((a, b) => a.chainName.localeCompare(b.chainName));
	c.header("Cache-Control", "public, max-age=30");
	return c.json({ address, chains: results });
});

// Get all balances across all chains for a Safe
routes.get("/api/safe/:address/all-balances", async (c) => {
	const address = validateAddress(c);
	if (!address) return c.json({ error: "Invalid Ethereum address" }, 400);
	const includeTestnets = c.req.query("testnets") !== "false"; // default: include
	const chains = getChains(includeTestnets);

	const results: Array<{ chainId: string; chainName: string; balances: BalanceItem[] }> = [];

	await Promise.allSettled(
		chains.map(async ([chainId, info]) => {
			try {
				const res = await fetch(
					`${safeApiBase(info.prefix)}/safes/${address}/balances/?trusted=true&exclude_spam=true`,
					{ signal: AbortSignal.timeout(8000) },
				);
				if (!res.ok) return;
				const raw = await res.json() as any[];
				const nativeToken = NATIVE_TOKENS[chainId] || { name: "ETH", symbol: "ETH", decimals: 18 };
				const chainBalances: BalanceItem[] = raw.map((item: any) => ({
					tokenAddress: item.tokenAddress,
					token: item.token || nativeToken,
					balance: item.balance || "0",
					fiatBalance: item.fiatBalance || "0",
					fiatConversion: item.fiatConversion || "0",
				})).filter((b: BalanceItem) => BigInt(b.balance || "0") > 0n);

				if (chainBalances.length > 0) {
					const enriched = await enrichWithPrices(chainBalances, chainId, { filterSpam: true });
					results.push({ chainId, chainName: info.name, balances: enriched });
				} else {
					// Safe exists on this chain (API returned 200) but has zero balance.
					// Check if there are any historical transactions (>$0 activity).
					try {
						const txRes = await fetch(
							`${safeApiBase(info.prefix)}/safes/${address}/all-transactions/?limit=1&executed=true`,
							{ signal: AbortSignal.timeout(5000) },
						);
						if (txRes.ok) {
							const txData = await txRes.json() as { count?: number; results?: any[] };
							if (txData.results && txData.results.length > 0) {
								results.push({ chainId, chainName: info.name, balances: [] });
							}
						}
					} catch {}
				}
			} catch {}
		})
	);

	results.sort((a, b) => a.chainName.localeCompare(b.chainName));
	c.header("Cache-Control", "public, max-age=30");
	return c.json({ address, chains: results });
});

interface BalanceItem {
	tokenAddress: string | null;
	token: { name: string; symbol: string; decimals: number };
	balance: string;
	fiatBalance: string;
	fiatConversion: string;
}

// ── DeFi protocol positions via Zerion ──
routes.get("/api/defi/:address/positions", async (c) => {
	const address = validateAddress(c);
	if (!address) return c.json({ error: "Invalid Ethereum address" }, 400);

	const positions = await getDefiPositions(address);
	const totalUSD = positions.reduce((sum, p) => sum + p.totalValueUSD, 0);

	c.header("Cache-Control", "public, max-age=300");
	return c.json({ positions, totalUSD });
});

// ── Safe owner addition proposal (add EncryptID EOA as signer) ──
routes.post("/api/safe/:chainId/:address/add-owner-proposal", async (c) => {
	const claims = await verifyWalletAuth(c);
	if (!claims) return c.json({ error: "Authentication required" }, 401);
	if (!claims.eid || claims.eid.authLevel < 3) {
		return c.json({ error: "Elevated authentication required" }, 403);
	}
	if (!claims.eid.capabilities?.wallet) {
		return c.json({ error: "Wallet capability required" }, 403);
	}

	const chainId = c.req.param("chainId");
	const address = validateAddress(c);
	if (!address) return c.json({ error: "Invalid Safe address" }, 400);
	const chainPrefix = getSafePrefix(chainId);
	if (!chainPrefix) return c.json({ error: "Unsupported chain" }, 400);

	const { newOwner, threshold, signature, sender } = await c.req.json();
	if (!newOwner || !signature || !sender) {
		return c.json({ error: "Missing required fields: newOwner, signature, sender" }, 400);
	}
	if (!VALID_ETH_ADDR.test(newOwner)) {
		return c.json({ error: "Invalid newOwner address" }, 400);
	}
	if (!VALID_ETH_ADDR.test(sender)) {
		return c.json({ error: "Invalid sender address" }, 400);
	}

	// H-4: Verify sender is the authenticated user's wallet address
	// The JWT contains claims.eid.walletAddress (if set) from the user profile
	const userWallet = claims.eid?.walletAddress;
	if (!userWallet || sender.toLowerCase() !== userWallet.toLowerCase()) {
		return c.json({ error: "Sender address does not match your authenticated wallet" }, 403);
	}

	// Get Safe info (need nonce + owner count for threshold validation)
	const infoRes = await fetch(`${safeApiBase(chainPrefix)}/safes/${address}/`);
	if (!infoRes.ok) return c.json({ error: "Safe not found" }, 404);
	const safeInfo = await infoRes.json() as { nonce?: number; owners?: string[]; threshold?: number };

	// M-2: Validate threshold is a positive integer within safe bounds
	const currentOwnerCount = safeInfo.owners?.length || 1;
	const newOwnerCount = currentOwnerCount + 1; // adding an owner
	const th = Number(threshold) || safeInfo.threshold || 1;
	if (!Number.isInteger(th) || th < 1 || th > newOwnerCount) {
		return c.json({ error: `Threshold must be between 1 and ${newOwnerCount} (current owners + new)` }, 400);
	}

	// Encode addOwnerWithThreshold(address owner, uint256 _threshold)
	// Function selector: 0x0d582f13
	const paddedOwner = newOwner.slice(2).toLowerCase().padStart(64, "0");
	const paddedThreshold = th.toString(16).padStart(64, "0");
	const data = `0x0d582f13${paddedOwner}${paddedThreshold}`;

	// Submit proposal to Safe Transaction Service
	const res = await fetch(
		`${safeApiBase(chainPrefix)}/safes/${address}/multisig-transactions/`,
		{
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify({
				to: address,
				value: "0",
				data,
				operation: 0,
				safeTxGas: "0",
				baseGas: "0",
				gasPrice: "0",
				gasToken: "0x0000000000000000000000000000000000000000",
				refundReceiver: "0x0000000000000000000000000000000000000000",
				nonce: safeInfo.nonce,
				signature,
				sender,
			}),
		},
	);

	if (!res.ok) {
		console.warn('rwallet: Safe add-owner error', res.status, await res.text());
		return c.json({ error: "Safe Transaction Service rejected the request" }, res.status as any);
	}

	return c.json(await res.json(), 201);
});

// ── CRDT Token API ──
import { getTokenDoc, listTokenDocs, getAllBalances, getBalance, mintTokens, burnTokens, burnTokensEscrow, getAllTransfers } from "../../server/token-service";
import { tokenDocId } from "../../server/token-schemas";
import type { TokenLedgerDoc } from "../../server/token-schemas";

// List all CRDT tokens with metadata
routes.get("/api/crdt-tokens", (c) => {
	const docIds = listTokenDocs();
	const tokens = docIds.map((docId) => {
		const tokenId = docId.replace('global:tokens:ledgers:', '');
		const doc = getTokenDoc(tokenId);
		if (!doc) return null;
		return {
			id: doc.token.id,
			name: doc.token.name,
			symbol: doc.token.symbol,
			decimals: doc.token.decimals,
			description: doc.token.description,
			totalSupply: doc.token.totalSupply,
			icon: doc.token.icon,
			color: doc.token.color,
			createdAt: doc.token.createdAt,
		};
	}).filter(Boolean);
	return c.json({ tokens });
});

// Get authenticated user's CRDT token balances
routes.get("/api/crdt-tokens/my-balances", async (c) => {
	const claims = await verifyWalletAuth(c);
	if (!claims) return c.json({ error: "Authentication required" }, 401);

	const did = (claims as any).did || claims.sub;
	const docIds = listTokenDocs();
	const balances = docIds.map((docId) => {
		const tokenId = docId.replace('global:tokens:ledgers:', '');
		const doc = getTokenDoc(tokenId);
		if (!doc) return null;
		const balance = getBalance(doc, did);
		if (balance <= 0) return null;
		return {
			tokenId: doc.token.id,
			name: doc.token.name,
			symbol: doc.token.symbol,
			decimals: doc.token.decimals,
			icon: doc.token.icon,
			color: doc.token.color,
			balance,
		};
	}).filter(Boolean);
	return c.json({ balances });
});

// Get all holder balances for a specific token
routes.get("/api/crdt-tokens/:tokenId/balances", (c) => {
	const tokenId = c.req.param("tokenId");
	const doc = getTokenDoc(tokenId);
	if (!doc || !doc.token.name) return c.json({ error: "Token not found" }, 404);

	const holders = getAllBalances(doc);
	return c.json({
		token: {
			id: doc.token.id,
			name: doc.token.name,
			symbol: doc.token.symbol,
			decimals: doc.token.decimals,
		},
		holders: Object.values(holders),
	});
});

// Mint tokens (authenticated)
routes.post("/api/crdt-tokens/:tokenId/mint", async (c) => {
	const claims = await verifyWalletAuth(c);
	if (!claims) return c.json({ error: "Authentication required" }, 401);

	const tokenId = c.req.param("tokenId");
	const doc = getTokenDoc(tokenId);
	if (!doc || !doc.token.name) return c.json({ error: "Token not found" }, 404);

	const body = await c.req.json();
	const { toDid, toLabel, amount, memo } = body;
	if (!toDid || !amount || typeof amount !== 'number' || amount <= 0) {
		return c.json({ error: "Invalid mint parameters: toDid, amount (positive number) required" }, 400);
	}

	const success = mintTokens(tokenId, toDid, toLabel || '', amount, memo || '', claims.sub);
	if (!success) return c.json({ error: "Mint failed" }, 500);
	return c.json({ ok: true, minted: amount, to: toDid });
});

// Get all CRDT token transfers (optionally filtered by DID)
routes.get("/api/crdt-tokens/transfers", (c) => {
	const did = c.req.query("did");
	const entries = getAllTransfers(did || undefined);
	return c.json({ entries });
});

// ── $MYCO Bonding Curve (cUSDC ↔ $MYCO swaps) ──
import { calculatePrice, calculateBuyReturn, calculateSellReturn, getCurveConfig } from "../../server/bonding-curve";

/** Get current $MYCO supply from the CRDT ledger. */
function getMycoSupply(): number {
	const doc = getTokenDoc("myco");
	if (!doc || !doc.token.name) return 0;
	return doc.token.totalSupply || 0;
}

// Quote endpoint — no auth required
routes.get("/api/crdt-tokens/myco/quote", (c) => {
	const action = c.req.query("action");
	const amountStr = c.req.query("amount");
	if (!action || !amountStr) {
		return c.json({ error: "action (buy|sell) and amount are required" }, 400);
	}
	const amount = parseFloat(amountStr);
	if (isNaN(amount) || amount <= 0) {
		return c.json({ error: "amount must be a positive number" }, 400);
	}

	const supply = getMycoSupply();
	const amountBaseUnits = Math.round(amount * 1_000_000);

	if (action === "buy") {
		const result = calculateBuyReturn(amountBaseUnits, supply);
		return c.json({
			action: "buy",
			input: { amount, token: "cUSDC" },
			output: { amount: result.tokensOut / 1_000_000, token: "$MYCO", baseUnits: result.tokensOut },
			pricePerToken: result.averagePrice / 1_000_000,
			priceImpact: result.priceImpact,
			currentSupply: supply / 1_000_000,
			config: getCurveConfig(),
		});
	} else if (action === "sell") {
		if (amountBaseUnits > supply) {
			return c.json({ error: "Cannot sell more than current supply" }, 400);
		}
		const result = calculateSellReturn(amountBaseUnits, supply);
		return c.json({
			action: "sell",
			input: { amount, token: "$MYCO" },
			output: { amount: result.cUSDCOut / 1_000_000, token: "cUSDC", baseUnits: result.cUSDCOut },
			fee: { amount: result.fee / 1_000_000, bps: 50 },
			pricePerToken: result.averagePrice / 1_000_000,
			priceImpact: result.priceImpact,
			currentSupply: supply / 1_000_000,
			config: getCurveConfig(),
		});
	}
	return c.json({ error: 'action must be "buy" or "sell"' }, 400);
});

// Buy $MYCO with cUSDC (authenticated)
routes.post("/api/crdt-tokens/myco/buy", async (c) => {
	const claims = await verifyWalletAuth(c);
	if (!claims) return c.json({ error: "Authentication required" }, 401);

	const body = await c.req.json<{ cUSDCAmount?: number }>();
	const { cUSDCAmount } = body;
	if (!cUSDCAmount || cUSDCAmount <= 0) {
		return c.json({ error: "cUSDCAmount (positive number in display units) required" }, 400);
	}

	const did = (claims as any).did || claims.sub;
	const amountBaseUnits = Math.round(cUSDCAmount * 1_000_000);
	const supply = getMycoSupply();

	// Check user has enough cUSDC
	const cusdcDoc = getTokenDoc("cusdc");
	if (!cusdcDoc) return c.json({ error: "cUSDC token not found" }, 500);
	const cusdcBalance = getBalance(cusdcDoc, did);
	if (cusdcBalance < amountBaseUnits) {
		return c.json({ error: `Insufficient cUSDC: have ${cusdcBalance / 1_000_000}, need ${cUSDCAmount}` }, 400);
	}

	// Calculate swap
	const result = calculateBuyReturn(amountBaseUnits, supply);
	if (result.tokensOut <= 0) {
		return c.json({ error: "Amount too small to produce tokens" }, 400);
	}

	// Atomic-ish: burn cUSDC, mint $MYCO with shared swap ID
	const swapId = `swap-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
	const burnOk = burnTokens("cusdc", did, "", amountBaseUnits, `Bonding curve buy: ${swapId}`, "bonding-curve");
	if (!burnOk) return c.json({ error: "Failed to burn cUSDC" }, 500);

	const mintOk = mintTokens("myco", did, "", result.tokensOut, `Bonding curve buy: ${swapId}`, "bonding-curve");
	if (!mintOk) {
		// Compensate: re-mint the burned cUSDC
		mintTokens("cusdc", did, "", amountBaseUnits, `Refund: failed swap ${swapId}`, "bonding-curve");
		return c.json({ error: "Failed to mint $MYCO, cUSDC refunded" }, 500);
	}

	return c.json({
		ok: true,
		swapId,
		burned: { token: "cUSDC", amount: amountBaseUnits / 1_000_000 },
		received: { token: "$MYCO", amount: result.tokensOut / 1_000_000 },
		pricePerToken: result.averagePrice / 1_000_000,
		priceImpact: result.priceImpact,
	});
});

// Sell $MYCO for cUSDC (authenticated)
routes.post("/api/crdt-tokens/myco/sell", async (c) => {
	const claims = await verifyWalletAuth(c);
	if (!claims) return c.json({ error: "Authentication required" }, 401);

	const body = await c.req.json<{ mycoAmount?: number }>();
	const { mycoAmount } = body;
	if (!mycoAmount || mycoAmount <= 0) {
		return c.json({ error: "mycoAmount (positive number in display units) required" }, 400);
	}

	const did = (claims as any).did || claims.sub;
	const amountBaseUnits = Math.round(mycoAmount * 1_000_000);
	const supply = getMycoSupply();

	// Check user has enough $MYCO
	const mycoDoc = getTokenDoc("myco");
	if (!mycoDoc) return c.json({ error: "$MYCO token not found" }, 500);
	const mycoBalance = getBalance(mycoDoc, did);
	if (mycoBalance < amountBaseUnits) {
		return c.json({ error: `Insufficient $MYCO: have ${mycoBalance / 1_000_000}, need ${mycoAmount}` }, 400);
	}

	// Calculate swap
	const result = calculateSellReturn(amountBaseUnits, supply);
	if (result.cUSDCOut <= 0) {
		return c.json({ error: "Amount too small to produce cUSDC" }, 400);
	}

	// Atomic-ish: burn $MYCO, mint cUSDC with shared swap ID
	const swapId = `swap-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
	const burnOk = burnTokens("myco", did, "", amountBaseUnits, `Bonding curve sell: ${swapId}`, "bonding-curve");
	if (!burnOk) return c.json({ error: "Failed to burn $MYCO" }, 500);

	const mintOk = mintTokens("cusdc", did, "", result.cUSDCOut, `Bonding curve sell: ${swapId}`, "bonding-curve");
	if (!mintOk) {
		// Compensate: re-mint the burned $MYCO
		mintTokens("myco", did, "", amountBaseUnits, `Refund: failed swap ${swapId}`, "bonding-curve");
		return c.json({ error: "Failed to mint cUSDC, $MYCO refunded" }, 500);
	}

	return c.json({
		ok: true,
		swapId,
		burned: { token: "$MYCO", amount: amountBaseUnits / 1_000_000 },
		received: { token: "cUSDC", amount: result.cUSDCOut / 1_000_000 },
		fee: { token: "cUSDC", amount: result.fee / 1_000_000, bps: 50 },
		pricePerToken: result.averagePrice / 1_000_000,
		priceImpact: result.priceImpact,
	});
});

// Settlement state — for bonding-curve-service cron to read CRDT state for on-chain sync
routes.get("/api/crdt-tokens/myco/settlement-state", (c) => {
	const mycoDoc = getTokenDoc("myco");
	const cusdcDoc = getTokenDoc("cusdc");

	const mycoSupply = mycoDoc?.token.totalSupply || 0;
	const currentPrice = calculatePrice(mycoSupply);

	// Calculate reserve: sum of all cUSDC burned via bonding curve buys
	let reserveBalance = 0;
	if (cusdcDoc) {
		for (const entry of Object.values(cusdcDoc.entries)) {
			if (entry.type === 'burn' && entry.memo.startsWith('Bonding curve buy:')) {
				reserveBalance += entry.amount;
			} else if (entry.type === 'mint' && entry.memo.startsWith('Bonding curve sell:')) {
				reserveBalance -= entry.amount;
			}
		}
	}

	return c.json({
		mycoSupply: mycoSupply / 1_000_000,
		mycoSupplyBaseUnits: mycoSupply,
		reserveBalance: reserveBalance / 1_000_000,
		reserveBalanceBaseUnits: reserveBalance,
		currentPrice: currentPrice / 1_000_000,
		currentPriceBaseUnits: currentPrice,
		marketCap: (mycoSupply * currentPrice) / (1_000_000 * 1_000_000),
		config: getCurveConfig(),
		timestamp: Date.now(),
	});
});

// ── Yield API routes ──
import { getYieldRates } from "./lib/yield-rates";
import { getYieldPositions } from "./lib/yield-positions";
import { buildDepositTransaction, buildWithdrawTransaction } from "./lib/yield-tx-builder";
import { computeStrategy } from "./lib/yield-strategy";
import type { SupportedYieldChain, YieldProtocol, StablecoinSymbol } from "./lib/yield-protocols";
import { STABLECOIN_DECIMALS } from "./lib/yield-protocols";

const VALID_YIELD_CHAINS = new Set(["1", "8453"]);
const VALID_PROTOCOLS = new Set(["aave-v3", "morpho-blue"]);
const VALID_ASSETS = new Set(["USDC", "USDT", "DAI"]);

// GET /api/yield/rates — public, cached
routes.get("/api/yield/rates", async (c) => {
	const chainId = c.req.query("chainId");
	const protocol = c.req.query("protocol");
	const asset = c.req.query("asset");

	try {
		const rates = await getYieldRates({ chainId: chainId || undefined, protocol: protocol || undefined, asset: asset || undefined });
		c.header("Cache-Control", "public, max-age=300");
		return c.json({ rates });
	} catch (err) {
		console.warn("rwallet: yield rates error", err);
		return c.json({ error: "Failed to fetch yield rates" }, 500);
	}
});

// GET /api/yield/:address/positions — public, cached
routes.get("/api/yield/:address/positions", async (c) => {
	const address = validateAddress(c);
	if (!address) return c.json({ error: "Invalid Ethereum address" }, 400);

	try {
		const positions = await getYieldPositions(address);
		c.header("Cache-Control", "public, max-age=60");
		return c.json({ positions });
	} catch (err) {
		console.warn("rwallet: yield positions error", err);
		return c.json({ error: "Failed to fetch yield positions" }, 500);
	}
});

// POST /api/yield/:chainId/:address/build-deposit — auth level 3+
routes.post("/api/yield/:chainId/:address/build-deposit", async (c) => {
	const claims = await verifyWalletAuth(c);
	if (!claims) return c.json({ error: "Authentication required" }, 401);
	if (!claims.eid || claims.eid.authLevel < 3) {
		return c.json({ error: "Elevated authentication required" }, 403);
	}

	const chainId = c.req.param("chainId");
	if (!VALID_YIELD_CHAINS.has(chainId)) return c.json({ error: "Unsupported chain for yield" }, 400);

	const address = validateAddress(c);
	if (!address) return c.json({ error: "Invalid Safe address" }, 400);

	const body = await c.req.json();
	const { protocol, asset, amount, vaultAddress } = body;

	if (!VALID_PROTOCOLS.has(protocol)) return c.json({ error: "Invalid protocol" }, 400);
	if (!VALID_ASSETS.has(asset)) return c.json({ error: "Invalid asset" }, 400);
	if (!amount || typeof amount !== "string" || !/^\d+$/.test(amount)) {
		return c.json({ error: "Amount must be a numeric string (raw units)" }, 400);
	}

	try {
		const tx = await buildDepositTransaction(
			chainId as SupportedYieldChain,
			address,
			protocol as YieldProtocol,
			asset as StablecoinSymbol,
			BigInt(amount),
			vaultAddress,
		);
		return c.json({ transaction: tx });
	} catch (err: any) {
		return c.json({ error: err.message || "Failed to build deposit transaction" }, 400);
	}
});

// POST /api/yield/:chainId/:address/build-withdraw — auth level 3+
routes.post("/api/yield/:chainId/:address/build-withdraw", async (c) => {
	const claims = await verifyWalletAuth(c);
	if (!claims) return c.json({ error: "Authentication required" }, 401);
	if (!claims.eid || claims.eid.authLevel < 3) {
		return c.json({ error: "Elevated authentication required" }, 403);
	}

	const chainId = c.req.param("chainId");
	if (!VALID_YIELD_CHAINS.has(chainId)) return c.json({ error: "Unsupported chain for yield" }, 400);

	const address = validateAddress(c);
	if (!address) return c.json({ error: "Invalid Safe address" }, 400);

	const body = await c.req.json();
	const { protocol, asset, amount, vaultAddress } = body;

	if (!VALID_PROTOCOLS.has(protocol)) return c.json({ error: "Invalid protocol" }, 400);
	if (!VALID_ASSETS.has(asset)) return c.json({ error: "Invalid asset" }, 400);

	const withdrawAmount = amount === "max" ? "max" as const : BigInt(amount || "0");
	if (withdrawAmount !== "max" && withdrawAmount === 0n) {
		return c.json({ error: "Amount required" }, 400);
	}

	try {
		const tx = await buildWithdrawTransaction(
			chainId as SupportedYieldChain,
			address,
			protocol as YieldProtocol,
			asset as StablecoinSymbol,
			withdrawAmount,
			vaultAddress,
		);
		return c.json({ transaction: tx });
	} catch (err: any) {
		return c.json({ error: err.message || "Failed to build withdraw transaction" }, 400);
	}
});

// GET /api/yield/:address/strategy — auth level 3+
routes.get("/api/yield/:address/strategy", async (c) => {
	const claims = await verifyWalletAuth(c);
	if (!claims) return c.json({ error: "Authentication required" }, 401);
	if (!claims.eid || claims.eid.authLevel < 3) {
		return c.json({ error: "Elevated authentication required" }, 403);
	}

	const address = validateAddress(c);
	if (!address) return c.json({ error: "Invalid Ethereum address" }, 400);

	try {
		const strategy = await computeStrategy(address);
		return c.json({ strategy });
	} catch (err) {
		console.warn("rwallet: yield strategy error", err);
		return c.json({ error: "Failed to compute strategy" }, 500);
	}
});

// ── Page route ──
// ── Page routes: subnav tab links ──

function renderWallet(spaceSlug: string, initialView?: string) {
	const viewAttr = initialView ? ` initial-view="${initialView}"` : "";
	return renderShell({
		title: `${spaceSlug} — Wallet | rSpace`,
		moduleId: "rwallet",
		spaceSlug,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-wallet-viewer space="${spaceSlug}"${viewAttr}></folk-wallet-viewer>`,
		scripts: `<script type="module" src="/modules/rwallet/folk-wallet-viewer.js?v=21"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rwallet/wallet.css">`,
	});
}

routes.get("/budget", (c) => c.html(renderWallet(c.req.param("space") || "demo", "budget")));
routes.get("/tokens", (c) => c.html(renderWallet(c.req.param("space") || "demo", "balances")));
routes.get("/flows", (c) => c.html(renderWallet(c.req.param("space") || "demo", "flows")));

// Legacy aliases
routes.get("/wallets", (c) => c.html(renderWallet(c.req.param("space") || "demo", "budget")));
routes.get("/transactions", (c) => c.html(renderWallet(c.req.param("space") || "demo", "budget")));

routes.get("/", (c) => c.html(renderWallet(c.req.param("space") || "demo", "budget")));

// ── MI export ──

export function getCrdtTokensForMI(_space: string, limit = 5): { tokenId: string; name: string; symbol: string; totalSupply: number }[] {
	const docIds = listTokenDocs();
	return docIds.slice(0, limit).flatMap((docId) => {
		const tokenId = docId.replace('global:tokens:ledgers:', '');
		const doc = getTokenDoc(tokenId);
		if (!doc || !doc.token.name) return [];
		return [{
			tokenId: doc.token.id,
			name: doc.token.name,
			symbol: doc.token.symbol,
			totalSupply: doc.token.totalSupply,
		}];
	});
}

export const walletModule: RSpaceModule = {
	id: "rwallet",
	name: "rWallet",
	icon: "💰",
	description: "Multichain Safe wallet visualization and treasury management",
	applets: walletApplets,
	canvasShapes: ["folk-token-mint", "folk-token-ledger", "folk-transaction-builder"],
	scoping: { defaultScope: 'global', userConfigurable: false },
	routes,
	standaloneDomain: "rwallet.online",
	landingPage: renderLanding,
	feeds: [
		{
			id: "balances",
			name: "Token Balances",
			kind: "economic",
			description: "Multichain Safe token balances with USD valuations",
		},
		{
			id: "transfers",
			name: "Transfer History",
			kind: "economic",
			description: "Incoming and outgoing token transfers across chains",
		},
	],
	acceptsFeeds: ["economic", "governance"],
	outputPaths: [
		{ path: "budget", name: "Budget", icon: "📊", description: "Budget visualization — balance river timeline" },
		{ path: "tokens", name: "Token Balances", icon: "🪙", description: "Token balances across chains" },
		{ path: "flows", name: "Flows", icon: "🔀", description: "Sankey flow diagram with transaction scrubber" },
	],
};
