/**
 * <folk-wallet-viewer> — multichain Safe wallet visualization.
 *
 * Enter a Safe address to see balances across chains, transfer history,
 * and flow visualizations. Authenticated users can link external wallets
 * via EIP-6963 + SIWE.
 */

import { transformToTimelineData, transformToSankeyData, transformToMultichainData } from "../lib/data-transform";
import type { TimelineEntry, SankeyData, MultichainData } from "../lib/data-transform";
import { loadD3, renderTimeline, renderFlowChart, renderSankey } from "../lib/wallet-viz";
import { DEMO_TIMELINE_DATA, DEMO_SANKEY_DATA, DEMO_MULTICHAIN_DATA } from "../lib/wallet-demo-data";
import { TourEngine } from "../../../shared/tour-engine";

interface ChainInfo {
	chainId: string;
	name: string;
	prefix: string;
	color: string;
}

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

interface LinkedWallet {
	id: string;
	address: string;
	type: "eoa" | "safe";
	label: string;
	providerName?: string;
	providerRdns?: string;
	safeInfo?: {
		threshold: number;
		ownerCount: number;
		isEncryptIdOwner: boolean;
	};
}

interface DiscoveredProvider {
	uuid: string;
	name: string;
	icon: string;
	rdns: string;
}

const CHAIN_COLORS: Record<string, string> = {
	"1": "#627eea",
	"10": "#ff0420",
	"100": "#04795b",
	"137": "#8247e5",
	"8453": "#0052ff",
	"42161": "#28a0f0",
	"42220": "#35d07f",
	"43114": "#e84142",
	"56": "#f3ba2f",
	"324": "#8c8dfc",
	"11155111": "#f59e0b",
	"84532": "#f59e0b",
};

const CHAIN_NAMES: Record<string, string> = {
	"1": "Ethereum", "10": "Optimism", "100": "Gnosis", "137": "Polygon",
	"8453": "Base", "42161": "Arbitrum", "42220": "Celo", "43114": "Avalanche",
	"56": "BSC", "324": "zkSync",
};

const EXAMPLE_WALLETS = [
	{ name: "TEC Commons Fund", address: "0x29567BdBcC92aCF37AC6B56B69180857bB69f7D1", type: "Safe" },
	{ name: "Gitcoin Treasury", address: "0xde21F729137C5Af1b01d73aF1dC21eFfa2B8a0d6", type: "Safe" },
	{ name: "Vitalik.eth", address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", type: "EOA" },
];

type ViewTab = "balances" | "timeline" | "flow" | "sankey";

class FolkWalletViewer extends HTMLElement {
	private shadow: ShadowRoot;
	private address = "";
	private detectedChains: ChainInfo[] = [];
	private selectedChain: string | null = null;
	private balances: BalanceItem[] = [];
	private loading = false;
	private error = "";
	private isDemo = false;
	private walletType: "safe" | "eoa" | "" = "";
	private includeTestnets = false;

	// Linked wallets state
	private isAuthenticated = false;
	private passKeyEOA = "";
	private linkedWallets: LinkedWallet[] = [];
	private showProviderPicker = false;
	private discoveredProviders: DiscoveredProvider[] = [];
	private linkingInProgress = false;
	private linkError = "";

	// Visualization state
	private activeView: ViewTab = "balances";
	private transfers: Map<string, any> | null = null;
	private transfersLoading = false;
	private d3Ready = false;
	private vizData: {
		timeline?: TimelineEntry[];
		sankey?: SankeyData;
		multichain?: MultichainData;
	} = {};

	// Guided tour
	private _tour!: TourEngine;
	private static readonly TOUR_STEPS = [
		{ target: '#address-input', title: "Enter Address", message: "Paste any wallet or Safe multisig address to load balances across all supported chains.", advanceOnClick: false },
		{ target: '#testnet-toggle', title: "Testnet Toggle", message: "Include testnet chains in the scan. Useful for checking Safe deployments on Sepolia or Goerli.", advanceOnClick: true },
		{ target: '.chain-btn', title: "Chain Selector", message: "Click a chain to load balances for that specific network. Active chains are highlighted.", advanceOnClick: false },
		{ target: '.view-tab', title: "Dashboard Views", message: "Switch between balance view, transfer timeline, and flow visualisations for deeper analysis.", advanceOnClick: false },
	];

	constructor() {
		super();
		this.shadow = this.attachShadow({ mode: "open" });
		this._tour = new TourEngine(
			this.shadow,
			FolkWalletViewer.TOUR_STEPS,
			"rwallet_tour_done",
			() => this.shadow.host as HTMLElement,
		);
	}

	connectedCallback() {
		const space = this.getAttribute("space") || "";
		if (space === "demo") {
			this.loadDemoData();
		} else {
			const params = new URLSearchParams(window.location.search);
			this.address = params.get("address") || "";
			this.checkAuthState();
			this.render();
			if (this.address) this.detectChains();
		}
		if (!localStorage.getItem("rwallet_tour_done")) {
			setTimeout(() => this._tour.start(), 1200);
		}
	}

	private checkAuthState() {
		try {
			const session = localStorage.getItem("encryptid_session");
			if (session) {
				const parsed = JSON.parse(session);
				if (parsed.claims?.exp > Math.floor(Date.now() / 1000)) {
					this.isAuthenticated = true;
					this.passKeyEOA = parsed.claims?.eid?.walletAddress || "";
					this.loadLinkedWallets();
				}
			}
		} catch {}
	}

	private getAuthToken(): string | null {
		try {
			const session = localStorage.getItem("encryptid_session");
			if (!session) return null;
			const parsed = JSON.parse(session);
			return parsed.accessToken || null;
		} catch {
			return null;
		}
	}

	private async loadLinkedWallets() {
		const token = this.getAuthToken();
		if (!token) return;

		try {
			const res = await fetch("/encryptid/api/wallet-link/list", {
				headers: { "Authorization": `Bearer ${token}` },
			});
			if (!res.ok) return;

			const data = await res.json();
			// Server decrypts at rest and returns entry data directly
			this.linkedWallets = (data.wallets || []).map((w: any) => ({
				id: w.id,
				address: w.address || "",
				type: w.type || "eoa",
				label: w.label || "",
				providerName: w.providerName,
				providerRdns: w.providerRdns,
				safeInfo: w.safeInfo,
			}));
			this.render();
		} catch {}
	}

	private loadDemoData() {
		this.isDemo = true;
		this.address = "0x29567BdBcC92aCF37AC6B56B69180857bB69f7D1";
		this.detectedChains = [
			{ chainId: "1", name: "Ethereum", prefix: "eth", color: "#627eea" },
			{ chainId: "10", name: "Optimism", prefix: "oeth", color: "#ff0420" },
			{ chainId: "100", name: "Gnosis", prefix: "gno", color: "#04795b" },
			{ chainId: "137", name: "Polygon", prefix: "pol", color: "#8247e5" },
			{ chainId: "8453", name: "Base", prefix: "base", color: "#0052ff" },
			{ chainId: "42161", name: "Arbitrum", prefix: "arb1", color: "#28a0f0" },
			{ chainId: "43114", name: "Avalanche", prefix: "avax", color: "#e84142" },
		];
		this.selectedChain = "100";
		this.balances = [
			{ tokenAddress: null, token: { name: "xDAI", symbol: "xDAI", decimals: 18 }, balance: "45230000000000000000000", fiatBalance: "45230", fiatConversion: "1" },
			{ tokenAddress: "0x5dF8339c5E282ee48c0c7cE252A7842F74e378b2", token: { name: "Token Engineering Commons", symbol: "TEC", decimals: 18 }, balance: "1250000000000000000000000", fiatBalance: "12500", fiatConversion: "0.01" },
			{ tokenAddress: "0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1", token: { name: "Wrapped Ether", symbol: "WETH", decimals: 18 }, balance: "8500000000000000000", fiatBalance: "28050", fiatConversion: "3300" },
			{ tokenAddress: "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83", token: { name: "USD Coin", symbol: "USDC", decimals: 6 }, balance: "15750000000", fiatBalance: "15750", fiatConversion: "1" },
			{ tokenAddress: "0x4f4F9b8D5B4d0Dc10506e5551B0513B61fD59e75", token: { name: "Giveth", symbol: "GIV", decimals: 18 }, balance: "500000000000000000000000", fiatBalance: "2500", fiatConversion: "0.005" },
		];
		// Pre-load demo viz data
		this.vizData = {
			timeline: DEMO_TIMELINE_DATA,
			sankey: DEMO_SANKEY_DATA,
			multichain: DEMO_MULTICHAIN_DATA,
		};
		this.render();
	}

	private getApiBase(): string {
		const path = window.location.pathname;
		const match = path.match(/^(\/[^/]+)?\/rwallet/);
		return match ? match[0] : "";
	}

	private async detectChains() {
		if (this.isDemo) return;
		if (!this.address || !/^0x[a-fA-F0-9]{40}$/.test(this.address)) {
			this.error = "Please enter a valid Ethereum address (0x...)";
			this.render();
			return;
		}

		this.loading = true;
		this.error = "";
		this.detectedChains = [];
		this.balances = [];
		this.walletType = "";
		this.activeView = "balances";
		this.transfers = null;
		this.vizData = {};
		this.render();

		try {
			const base = this.getApiBase();
			const tn = this.includeTestnets ? "?testnets=true" : "";

			// Try Safe detection first
			const res = await fetch(`${base}/api/safe/detect/${this.address}${tn}`);
			const data = await res.json();

			this.detectedChains = (data.chains || []).map((c: any) => ({
				...c,
				color: CHAIN_COLORS[c.chainId] || "#888",
			}));

			if (this.detectedChains.length > 0) {
				this.walletType = "safe";
				this.selectedChain = this.detectedChains[0].chainId;
				await this.loadBalances();
			} else {
				// Fall back to EOA detection (any wallet)
				const eoaRes = await fetch(`${base}/api/eoa/detect/${this.address}${tn}`);
				const eoaData = await eoaRes.json();

				this.detectedChains = (eoaData.chains || []).map((c: any) => ({
					...c,
					color: CHAIN_COLORS[c.chainId] || "#888",
				}));

				if (this.detectedChains.length > 0) {
					this.walletType = "eoa";
					this.selectedChain = this.detectedChains[0].chainId;
					await this.loadBalances();
				} else {
					this.error = "No balances found for this address on any supported chain.";
				}
			}
		} catch (e) {
			this.error = "Failed to detect wallet. Check the address and try again.";
		}

		this.loading = false;
		this.render();
	}

	private async loadBalances() {
		if (this.isDemo) return;
		if (!this.selectedChain) return;
		try {
			const base = this.getApiBase();
			const apiPath = this.walletType === "safe"
				? `${base}/api/safe/${this.selectedChain}/${this.address}/balances`
				: `${base}/api/eoa/${this.selectedChain}/${this.address}/balances`;
			const res = await fetch(apiPath);
			if (res.ok) {
				this.balances = await res.json();
			}
		} catch {
			this.balances = [];
		}
	}

	// ── Transfer Loading for Visualizations ──

	private async loadTransfers() {
		if (this.isDemo || this.transfers || this.transfersLoading) return;
		if (this.walletType !== "safe") return; // Transfer API only for Safes

		this.transfersLoading = true;
		this.render();

		try {
			const base = this.getApiBase();
			const chainDataMap = new Map<string, any>();

			// Fan out requests to all detected chains
			await Promise.allSettled(
				this.detectedChains.map(async (ch) => {
					try {
						const res = await fetch(`${base}/api/safe/${ch.chainId}/${this.address}/transfers?limit=200`);
						if (!res.ok) return;
						const data = await res.json();
						// Parse results into incoming/outgoing
						const incoming: any[] = [];
						const outgoing: any[] = [];
						const results = data.results || [];
						for (const tx of results) {
							if (tx.txType === "MULTISIG_TRANSACTION") {
								outgoing.push(tx);
							}
							// Incoming transfers from transfers array inside txs
							if (tx.transfers) {
								for (const t of tx.transfers) {
									if (t.to?.toLowerCase() === this.address.toLowerCase()) {
										incoming.push(t);
									}
								}
							}
						}
						chainDataMap.set(ch.chainId, { incoming, outgoing, chainId: ch.chainId });
					} catch {}
				}),
			);

			this.transfers = chainDataMap;
			this.computeVizData();
		} catch {
			this.transfers = new Map();
		}

		this.transfersLoading = false;
		this.render();
		if (this.activeView !== "balances") {
			requestAnimationFrame(() => this.drawActiveVisualization());
		}
	}

	private computeVizData() {
		if (!this.transfers) return;

		this.vizData.timeline = transformToTimelineData(this.transfers, this.address, CHAIN_NAMES);

		// Sankey for selected chain
		if (this.selectedChain && this.transfers.has(this.selectedChain)) {
			this.vizData.sankey = transformToSankeyData(
				this.transfers.get(this.selectedChain),
				this.address,
				this.selectedChain,
			);
		}

		this.vizData.multichain = transformToMultichainData(this.transfers, this.address, CHAIN_NAMES);
	}

	private async drawActiveVisualization() {
		const container = this.shadow.querySelector("#viz-container") as HTMLElement;
		if (!container) return;

		// Lazy-load D3
		if (!this.d3Ready) {
			container.innerHTML = '<div class="loading"><span class="spinner"></span> Loading visualization library...</div>';
			try {
				await loadD3();
				this.d3Ready = true;
			} catch {
				container.innerHTML = '<div class="error">Failed to load D3 visualization library.</div>';
				return;
			}
		}

		// Build chain color map for flow chart
		const chainColorMap: Record<string, string> = {};
		for (const ch of this.detectedChains) {
			chainColorMap[ch.name.toLowerCase()] = ch.color;
		}

		switch (this.activeView) {
			case "timeline":
				if (this.vizData.timeline && this.vizData.timeline.length > 0) {
					renderTimeline(container, this.vizData.timeline, { chainColors: chainColorMap });
				} else {
					container.innerHTML = '<div class="empty">No timeline data available. Transfer data may still be loading.</div>';
				}
				break;

			case "flow":
				if (this.vizData.multichain) {
					const mc = this.vizData.multichain;
					renderFlowChart(container, mc.flowData["all"] || [], mc.chainStats["all"], {
						chainColors: chainColorMap,
						safeAddress: this.address,
					});
				} else {
					container.innerHTML = '<div class="empty">No flow data available.</div>';
				}
				break;

			case "sankey":
				if (this.vizData.sankey && this.vizData.sankey.links.length > 0) {
					renderSankey(container, this.vizData.sankey);
				} else {
					container.innerHTML = '<div class="empty">No Sankey data available for the selected chain.</div>';
				}
				break;
		}
	}

	private formatBalance(balance: string, decimals: number): string {
		const val = Number(balance) / Math.pow(10, decimals);
		if (val >= 1000000) return `${(val / 1000000).toFixed(2)}M`;
		if (val >= 1000) return `${(val / 1000).toFixed(2)}K`;
		if (val >= 1) return val.toFixed(2);
		if (val >= 0.0001) return val.toFixed(4);
		return val.toExponential(2);
	}

	private formatUSD(val: string): string {
		const n = parseFloat(val);
		if (isNaN(n) || n === 0) return "$0";
		if (n >= 1000000) return `$${(n / 1000000).toFixed(2)}M`;
		if (n >= 1000) return `$${(n / 1000).toFixed(1)}K`;
		return `$${n.toFixed(2)}`;
	}

	private shortenAddress(addr: string): string {
		return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
	}

	private handleSubmit(e: Event) {
		e.preventDefault();
		const input = this.shadow.querySelector("#address-input") as HTMLInputElement;
		if (input) {
			this.address = input.value.trim();
			const url = new URL(window.location.href);
			url.searchParams.set("address", this.address);
			window.history.replaceState({}, "", url.toString());
			this.detectChains();
		}
	}

	private async handleChainSelect(chainId: string) {
		this.selectedChain = chainId;
		if (this.isDemo) {
			this.render();
			return;
		}
		this.loading = true;
		this.render();
		await this.loadBalances();
		// Recompute sankey for new chain
		if (this.transfers && this.transfers.has(chainId)) {
			this.vizData.sankey = transformToSankeyData(
				this.transfers.get(chainId),
				this.address,
				chainId,
			);
		}
		this.loading = false;
		this.render();
		if (this.activeView !== "balances") {
			requestAnimationFrame(() => this.drawActiveVisualization());
		}
	}

	private handleViewTabClick(view: ViewTab) {
		if (this.activeView === view) return;
		this.activeView = view;

		if (view !== "balances" && !this.transfers && !this.isDemo) {
			this.loadTransfers();
		}

		this.render();

		if (view !== "balances") {
			requestAnimationFrame(() => this.drawActiveVisualization());
		}
	}

	private hasData(): boolean {
		return this.detectedChains.length > 0;
	}

	// ── EIP-6963 Provider Discovery ──

	private sanitizeIconUri(uri: string): string {
		if (!uri) return "";
		// Allow https: URLs and safe data: image types (no SVG — can contain scripts)
		if (/^https:\/\//i.test(uri)) return uri;
		if (/^data:image\/(png|jpeg|gif|webp);base64,/i.test(uri)) return uri;
		return ""; // Block javascript:, data:image/svg+xml, etc.
	}

	private startProviderDiscovery() {
		this.discoveredProviders = [];
		this.showProviderPicker = true;
		this.linkError = "";
		this.render();

		const handler = (e: Event) => {
			const detail = (e as CustomEvent).detail;
			if (!detail?.info?.uuid || !detail?.provider) return;
			const exists = this.discoveredProviders.some(p => p.uuid === detail.info.uuid);
			if (!exists) {
				this.discoveredProviders.push({
					uuid: detail.info.uuid,
					name: detail.info.name,
					icon: this.sanitizeIconUri(detail.info.icon || ""),
					rdns: detail.info.rdns,
				});
				this.render();
			}
		};

		window.addEventListener("eip6963:announceProvider", handler);
		window.dispatchEvent(new Event("eip6963:requestProvider"));

		// Store handler for cleanup
		(this as any)._eip6963Handler = handler;

		// If no providers found after 2s, show message
		setTimeout(() => {
			if (this.discoveredProviders.length === 0 && this.showProviderPicker) {
				this.linkError = "No browser wallets detected. Install MetaMask or another EIP-6963 compatible wallet.";
				this.render();
			}
		}, 2000);
	}

	private stopProviderDiscovery() {
		const handler = (this as any)._eip6963Handler;
		if (handler) {
			window.removeEventListener("eip6963:announceProvider", handler);
			delete (this as any)._eip6963Handler;
		}
		this.showProviderPicker = false;
		this.discoveredProviders = [];
		this.linkError = "";
	}

	private async handleProviderSelect(uuid: string) {
		const provider = this.discoveredProviders.find(p => p.uuid === uuid);
		if (!provider) return;

		this.linkingInProgress = true;
		this.linkError = "";
		this.render();

		try {
			// Get the actual EIP-1193 provider reference
			let eip1193Provider: any = null;
			const getProvider = new Promise<any>((resolve) => {
				const handler = (e: Event) => {
					const detail = (e as CustomEvent).detail;
					if (detail?.info?.uuid === uuid) {
						window.removeEventListener("eip6963:announceProvider", handler);
						resolve(detail.provider);
					}
				};
				window.addEventListener("eip6963:announceProvider", handler);
				window.dispatchEvent(new Event("eip6963:requestProvider"));
				setTimeout(() => resolve(null), 3000);
			});
			eip1193Provider = await getProvider;
			if (!eip1193Provider) throw new Error("Could not get wallet provider");

			// 1. Request accounts
			const accounts = await eip1193Provider.request({ method: "eth_requestAccounts" });
			if (!accounts || accounts.length === 0) throw new Error("No accounts returned");
			const walletAddress = accounts[0] as string;

			// 2. Get nonce from server
			const token = this.getAuthToken();
			if (!token) throw new Error("Not authenticated");

			const nonceRes = await fetch("/encryptid/api/wallet-link/nonce", {
				method: "POST",
				headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
			});
			if (!nonceRes.ok) throw new Error("Failed to get nonce");
			const { nonce } = await nonceRes.json();

			// 3. Build SIWE message
			const domain = window.location.host;
			const origin = window.location.origin;
			const issuedAt = new Date().toISOString();
			const siweMessage = [
				`${domain} wants you to sign in with your Ethereum account:`,
				walletAddress,
				"",
				"Link this wallet to your EncryptID identity",
				"",
				`URI: ${origin}`,
				`Version: 1`,
				`Chain ID: 1`,
				`Nonce: ${nonce}`,
				`Issued At: ${issuedAt}`,
			].join("\n");

			// 4. Sign with external wallet
			const signature = await eip1193Provider.request({
				method: "personal_sign",
				params: [siweMessage, walletAddress],
			});

			// 5. Hash the address for dedup (salted with user ID from session)
			const addressHash = await this.hashAddress(walletAddress);

			// 6. Build entry data — server encrypts at rest with AES-256-GCM
			const entry = {
				address: walletAddress,
				type: "eoa",
				label: provider.name,
				providerRdns: provider.rdns,
				providerName: provider.name,
			};

			// 7. Verify with server (server handles encryption at rest)
			const verifyRes = await fetch("/encryptid/api/wallet-link/verify", {
				method: "POST",
				headers: {
					"Authorization": `Bearer ${token}`,
					"Content-Type": "application/json",
				},
				body: JSON.stringify({
					message: siweMessage,
					signature,
					addressHash,
					walletType: "eoa",
					entry,
				}),
			});

			if (!verifyRes.ok) {
				const err = await verifyRes.json();
				throw new Error(err.error || "Verification failed");
			}

			const result = await verifyRes.json();

			// 8. Add to local state
			const newWallet: LinkedWallet = {
				id: result.id,
				address: walletAddress,
				type: "eoa",
				label: provider.name,
				providerName: provider.name,
				providerRdns: provider.rdns,
			};
			this.linkedWallets.push(newWallet);

			this.stopProviderDiscovery();
		} catch (err: any) {
			this.linkError = err?.message || "Failed to link wallet";
		}

		this.linkingInProgress = false;
		this.render();
	}

	private async handleUnlinkWallet(id: string) {
		const token = this.getAuthToken();
		if (!token) return;

		try {
			const res = await fetch(`/encryptid/api/wallet-link/${id}`, {
				method: "DELETE",
				headers: { "Authorization": `Bearer ${token}` },
			});
			if (res.ok) {
				this.linkedWallets = this.linkedWallets.filter(w => w.id !== id);
				this.render();
			}
		} catch {}
	}

	private async handleViewLinkedWallet(address: string) {
		this.address = address;
		const input = this.shadow.querySelector("#address-input") as HTMLInputElement;
		if (input) input.value = address;
		const url = new URL(window.location.href);
		url.searchParams.set("address", address);
		window.history.replaceState({}, "", url.toString());
		await this.detectChains();
	}

	private async hashAddress(address: string): Promise<string> {
		// Salt with user ID to prevent cross-user address correlation
		const session = localStorage.getItem("encryptid_session");
		const userId = session ? (JSON.parse(session).claims?.sub || "") : "";
		const normalized = userId + ":" + address.toLowerCase();
		const encoded = new TextEncoder().encode(normalized);
		const hash = await crypto.subtle.digest("SHA-256", encoded);
		const bytes = new Uint8Array(hash);
		return btoa(String.fromCharCode(...bytes))
			.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
	}

	// ── Render Methods ──

	private renderStyles(): string {
		return `
		<style>
			:host { display: block; font-family: system-ui, -apple-system, sans-serif; color: var(--rs-text-primary); }
			* { box-sizing: border-box; }

			/* ── Hero ── */
			.hero {
				text-align: center; padding: 32px 16px 24px;
				border-bottom: 1px solid var(--rs-border-subtle); margin-bottom: 24px;
			}
			.hero-title {
				font-size: 28px; font-weight: 700; margin: 0 0 6px;
				background: linear-gradient(135deg, var(--rs-accent), #4ade80);
				-webkit-background-clip: text; -webkit-text-fill-color: transparent;
				background-clip: text;
			}
			.hero-subtitle { font-size: 14px; color: var(--rs-text-secondary); margin: 0 0 20px; }

			/* ── Address bar ── */
			.address-bar { display: flex; gap: 8px; margin-bottom: 12px; max-width: 640px; margin-left: auto; margin-right: auto; }
			.address-bar input {
				flex: 1; padding: 12px 16px; border-radius: 10px;
				border: 1px solid var(--rs-input-border); background: var(--rs-bg-surface); color: var(--rs-text-primary);
				font-family: monospace; font-size: 14px;
			}
			.address-bar input:focus { border-color: var(--rs-accent); outline: none; box-shadow: 0 0 0 2px rgba(20,184,166,0.15); }
			.address-bar button {
				padding: 12px 24px; border-radius: 10px; border: none;
				background: linear-gradient(135deg, var(--rs-accent), var(--rs-accent-hover)); color: #000;
				font-weight: 600; cursor: pointer; font-size: 14px; transition: opacity 0.2s;
			}
			.address-bar button:hover { opacity: 0.9; }

			/* ── Controls row ── */
			.controls-row {
				display: flex; align-items: center; justify-content: space-between;
				max-width: 640px; margin: 0 auto 24px; flex-wrap: wrap; gap: 8px;
			}
			.testnet-toggle {
				display: flex; align-items: center; gap: 8px;
				cursor: pointer; user-select: none; font-size: 13px; color: var(--rs-text-secondary);
			}
			.testnet-toggle:hover { color: var(--rs-text-primary); }
			.toggle-track {
				width: 34px; height: 18px; border-radius: 9px; background: var(--rs-border);
				position: relative; transition: background 0.2s;
			}
			.testnet-toggle.active .toggle-track { background: #f59e0b; }
			.toggle-thumb {
				width: 14px; height: 14px; border-radius: 50%; background: var(--rs-text-muted);
				position: absolute; top: 2px; left: 2px; transition: all 0.2s;
			}
			.testnet-toggle.active .toggle-thumb { left: 18px; background: #fff; }
			.wallet-badge {
				display: inline-flex; align-items: center; gap: 6px;
				padding: 4px 12px; border-radius: 6px; font-size: 12px; font-weight: 600;
				text-transform: uppercase; letter-spacing: 0.5px;
			}
			.wallet-badge.safe { background: rgba(74,222,128,0.12); color: #4ade80; border: 1px solid rgba(74,222,128,0.25); }
			.wallet-badge.eoa { background: rgba(20,184,166,0.12); color: var(--rs-accent); border: 1px solid rgba(20,184,166,0.25); }
			.wallet-badge.encryptid { background: rgba(168,85,247,0.12); color: #a855f7; border: 1px solid rgba(168,85,247,0.25); }

			/* ── My Wallets section ── */
			.my-wallets {
				max-width: 640px; margin: 0 auto 24px;
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle); border-radius: 12px;
				padding: 16px;
			}
			.my-wallets-header {
				display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;
			}
			.my-wallets-title { font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--rs-text-secondary); }
			.link-wallet-btn {
				padding: 6px 14px; border-radius: 8px; border: 1px dashed var(--rs-border);
				background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 12px;
				transition: all 0.2s;
			}
			.link-wallet-btn:hover { border-color: var(--rs-accent); color: var(--rs-accent); background: rgba(20,184,166,0.05); }
			.wallet-list { display: flex; flex-direction: column; gap: 6px; }
			.wallet-item {
				display: flex; align-items: center; gap: 10px; padding: 10px 12px;
				border-radius: 8px; background: var(--rs-bg-hover); cursor: pointer;
				transition: all 0.15s;
			}
			.wallet-item:hover { background: var(--rs-bg-surface); box-shadow: 0 0 0 1px var(--rs-border); }
			.wallet-item-info { flex: 1; min-width: 0; }
			.wallet-item-label { font-size: 13px; font-weight: 500; color: var(--rs-text-primary); }
			.wallet-item-addr { font-size: 11px; color: var(--rs-text-muted); font-family: monospace; }
			.wallet-item-actions { display: flex; gap: 4px; }
			.wallet-item-actions button {
				padding: 4px 8px; border-radius: 4px; border: none; cursor: pointer; font-size: 11px;
				background: transparent; color: var(--rs-text-muted); transition: all 0.15s;
			}
			.wallet-item-actions button:hover { color: var(--rs-text-primary); background: var(--rs-bg-surface); }
			.wallet-item-actions .unlink-btn:hover { color: var(--rs-error); }

			/* ── Provider picker ── */
			.provider-picker {
				margin-top: 12px; padding: 12px; border-radius: 8px;
				background: var(--rs-bg-hover); border: 1px solid var(--rs-border-subtle);
			}
			.provider-picker-title { font-size: 12px; color: var(--rs-text-secondary); margin-bottom: 8px; }
			.provider-list { display: flex; flex-wrap: wrap; gap: 8px; }
			.provider-item {
				display: flex; align-items: center; gap: 8px; padding: 8px 14px;
				border-radius: 8px; border: 1px solid var(--rs-border); background: var(--rs-bg-surface);
				cursor: pointer; transition: all 0.2s; font-size: 13px; color: var(--rs-text-primary);
			}
			.provider-item:hover { border-color: var(--rs-accent); background: rgba(20,184,166,0.05); }
			.provider-item img { width: 20px; height: 20px; border-radius: 4px; }
			.provider-cancel {
				margin-top: 8px; padding: 4px 10px; border-radius: 6px; border: none;
				background: transparent; color: var(--rs-text-muted); cursor: pointer; font-size: 12px;
			}
			.provider-cancel:hover { color: var(--rs-text-primary); }

			/* ── Supported chains ── */
			.supported-chains {
				display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
				margin-bottom: 28px; max-width: 640px; margin-left: auto; margin-right: auto;
			}
			.supported-chain {
				display: flex; align-items: center; gap: 5px;
				padding: 4px 10px; border-radius: 6px; background: var(--rs-bg-surface);
				border: 1px solid var(--rs-border-subtle); font-size: 12px; color: var(--rs-text-secondary);
			}
			.supported-chain .dot { width: 6px; height: 6px; border-radius: 50%; }

			/* ── Feature cards ── */
			.features {
				display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
				gap: 16px; margin-bottom: 28px;
			}
			.feature-card {
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle); border-radius: 12px;
				padding: 20px; text-align: center; transition: border-color 0.2s;
			}
			.feature-card:hover { border-color: var(--rs-border-strong); }
			.feature-icon { font-size: 28px; margin-bottom: 10px; }
			.feature-card h3 { font-size: 14px; font-weight: 600; margin: 0 0 6px; color: var(--rs-text-primary); }
			.feature-card p { font-size: 12px; color: var(--rs-text-secondary); margin: 0; line-height: 1.5; }

			/* ── Example wallets ── */
			.examples {
				max-width: 640px; margin: 0 auto 20px;
			}
			.examples-label { font-size: 11px; color: var(--rs-text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
			.example-list { display: flex; flex-direction: column; gap: 6px; }
			.example-item {
				display: flex; align-items: center; gap: 10px; padding: 10px 14px;
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle); border-radius: 10px;
				cursor: pointer; transition: all 0.2s;
			}
			.example-item:hover { border-color: var(--rs-accent); background: var(--rs-bg-hover); }
			.example-name { font-size: 13px; font-weight: 500; color: var(--rs-text-primary); }
			.example-addr { font-size: 11px; color: var(--rs-text-muted); font-family: monospace; }
			.example-type {
				margin-left: auto; font-size: 10px; font-weight: 600;
				padding: 2px 8px; border-radius: 4px; text-transform: uppercase;
			}
			.example-type.safe { background: rgba(74,222,128,0.1); color: #4ade80; }
			.example-type.eoa { background: rgba(20,184,166,0.1); color: var(--rs-accent); }

			/* ── Dashboard: chains ── */
			.chains { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 20px; }
			.chain-btn {
				padding: 6px 14px; border-radius: 8px; border: 2px solid var(--rs-border);
				background: var(--rs-bg-surface); color: var(--rs-text-secondary); cursor: pointer; font-size: 13px;
				display: flex; align-items: center; gap: 6px; transition: all 0.2s;
			}
			.chain-btn:hover { border-color: var(--rs-border-strong); }
			.chain-btn.active { border-color: var(--chain-color); background: var(--rs-bg-hover); }
			.chain-dot { width: 8px; height: 8px; border-radius: 50%; }

			/* ── Dashboard: stats ── */
			.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px; }
			.stat-card {
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 10px;
				padding: 14px; text-align: center;
			}
			.stat-label { font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase; margin-bottom: 6px; }
			.stat-value { font-size: 20px; font-weight: 700; color: var(--rs-accent); }

			/* ── View Tabs ── */
			.view-tabs {
				display: flex; gap: 4px; margin-bottom: 20px;
				border-bottom: 2px solid var(--rs-border-subtle); padding-bottom: 0;
			}
			.view-tab {
				padding: 10px 18px; border: none; background: transparent;
				color: var(--rs-text-secondary); cursor: pointer; font-size: 13px; font-weight: 500;
				border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all 0.2s;
			}
			.view-tab:hover { color: var(--rs-text-primary); background: var(--rs-bg-hover); }
			.view-tab.active {
				color: var(--rs-accent); border-bottom-color: var(--rs-accent);
			}

			/* ── Viz container ── */
			.viz-container { min-height: 200px; }

			/* ── Dashboard: balance table ── */
			.balance-table { width: 100%; border-collapse: collapse; }
			.balance-table th {
				text-align: left; padding: 10px 8px; border-bottom: 2px solid var(--rs-border);
				color: var(--rs-text-secondary); font-size: 11px; text-transform: uppercase;
			}
			.balance-table td { padding: 10px 8px; border-bottom: 1px solid var(--rs-border-subtle); }
			.balance-table tr:hover td { background: var(--rs-bg-hover); }
			.token-symbol { font-weight: 600; color: var(--rs-text-primary); }
			.token-name { font-size: 12px; color: var(--rs-text-secondary); }
			.amount-cell { text-align: right; font-family: monospace; }
			.fiat { color: var(--rs-success); }

			/* ── States ── */
			.empty { text-align: center; color: var(--rs-text-muted); padding: 40px; }
			.loading { text-align: center; color: var(--rs-text-secondary); padding: 40px; }
			.loading .spinner {
				display: inline-block; width: 20px; height: 20px;
				border: 2px solid var(--rs-border); border-top-color: var(--rs-accent); border-radius: 50%;
				animation: spin 0.8s linear infinite; margin-right: 8px; vertical-align: middle;
			}
			@keyframes spin { to { transform: rotate(360deg); } }
			.error {
				text-align: center; color: var(--rs-error); padding: 16px;
				background: rgba(239,83,80,0.08); border: 1px solid rgba(239,83,80,0.2);
				border-radius: 10px; margin-bottom: 16px;
			}
			.link-error {
				color: var(--rs-error); font-size: 12px; margin-top: 8px;
				padding: 6px 10px; background: rgba(239,83,80,0.08); border-radius: 6px;
			}

			@media (max-width: 768px) {
				.hero-title { font-size: 22px; }
				.balance-table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
				.address-bar { flex-wrap: wrap; }
				.address-bar input { min-width: 0; }
				.chains { flex-wrap: wrap; }
				.features { grid-template-columns: 1fr 1fr; }
				.view-tabs { overflow-x: auto; }
			}
			@media (max-width: 480px) {
				.features { grid-template-columns: 1fr; }
			}
		</style>`;
	}

	private renderHero(): string {
		if (this.hasData()) return "";
		return `
		<div class="hero">
			<div class="hero-title">rWallet</div>
			<div class="hero-subtitle">Multichain treasury visualization — Safe multisigs and EOA wallets</div>
		</div>`;
	}

	private renderMyWallets(): string {
		if (!this.isAuthenticated) return "";

		const hasWallets = this.passKeyEOA || this.linkedWallets.length > 0;

		return `
		<div class="my-wallets">
			<div class="my-wallets-header">
				<div class="my-wallets-title">My Wallets</div>
				<button class="link-wallet-btn" id="link-wallet-btn">+ Link Wallet</button>
			</div>
			<div class="wallet-list">
				${this.passKeyEOA ? `
					<div class="wallet-item" data-view-address="${this.esc(this.passKeyEOA)}" data-collab-id="wallet:${this.esc(this.passKeyEOA)}">
						<div class="wallet-item-info">
							<div class="wallet-item-label">
								<span class="wallet-badge encryptid">EncryptID</span>
								Passkey EOA
							</div>
							<div class="wallet-item-addr">${this.shortenAddress(this.passKeyEOA)}</div>
						</div>
					</div>
				` : ""}
				${this.linkedWallets.map(w => `
					<div class="wallet-item" data-view-address="${this.esc(w.address)}" data-collab-id="wallet:${this.esc(w.address)}">
						<div class="wallet-item-info">
							<div class="wallet-item-label">
								<span class="wallet-badge ${w.type}">${this.esc(w.providerName || w.type.toUpperCase())}</span>
								${this.esc(w.label)}
								${w.safeInfo && !w.safeInfo.isEncryptIdOwner ? '<span style="font-size:10px;color:var(--rs-text-muted)">(not co-signer)</span>' : ""}
							</div>
							<div class="wallet-item-addr">${this.shortenAddress(w.address)}</div>
						</div>
						<div class="wallet-item-actions">
							<button class="unlink-btn" data-unlink="${this.esc(w.id)}" title="Unlink wallet">&#10005;</button>
						</div>
					</div>
				`).join("")}
				${!hasWallets ? '<div style="font-size:12px;color:var(--rs-text-muted);padding:8px;">No wallets linked yet. Click "Link Wallet" to connect a browser wallet.</div>' : ""}
			</div>
			${this.renderProviderPicker()}
		</div>`;
	}

	private renderProviderPicker(): string {
		if (!this.showProviderPicker) return "";

		return `
		<div class="provider-picker">
			<div class="provider-picker-title">
				${this.linkingInProgress ? '<span class="spinner" style="width:14px;height:14px;border-width:2px;display:inline-block;vertical-align:middle;margin-right:6px"></span> Connecting...' : 'Select a wallet to link:'}
			</div>
			${!this.linkingInProgress ? `
				<div class="provider-list">
					${this.discoveredProviders.map(p => `
						<div class="provider-item" data-provider-uuid="${this.esc(p.uuid)}">
							${p.icon ? `<img src="${this.esc(p.icon)}" alt="" onerror="this.style.display='none'">` : ""}
							${this.esc(p.name)}
						</div>
					`).join("")}
					${this.discoveredProviders.length === 0 ? '<div style="font-size:12px;color:var(--rs-text-muted);padding:4px;">Searching for wallets...</div>' : ""}
				</div>
				<button class="provider-cancel" id="provider-cancel">Cancel</button>
			` : ""}
			${this.linkError ? `<div class="link-error">${this.esc(this.linkError)}</div>` : ""}
		</div>`;
	}

	private renderSupportedChains(): string {
		if (this.hasData() || this.loading) return "";
		return `
		<div class="supported-chains">
			${Object.entries(CHAIN_NAMES).map(([id, name]) => `
				<div class="supported-chain">
					<div class="dot" style="background: ${CHAIN_COLORS[id]}"></div>
					${name}
				</div>
			`).join("")}
		</div>`;
	}

	private renderFeatures(): string {
		if (this.hasData() || this.loading) return "";
		return `
		<div class="features">
			<div class="feature-card">
				<div class="feature-icon">&#9939;</div>
				<h3>Safe Multisig</h3>
				<p>Visualize Gnosis Safe balances, signers, and thresholds across all chains.</p>
			</div>
			<div class="feature-card">
				<div class="feature-icon">&#128279;</div>
				<h3>Any Wallet (EOA)</h3>
				<p>Paste any 0x address — works for regular wallets too, not just Safes.</p>
			</div>
			<div class="feature-card">
				<div class="feature-icon">&#127760;</div>
				<h3>10+ Chains</h3>
				<p>Ethereum, Base, Polygon, Gnosis, Arbitrum, Optimism, and more in one view.</p>
			</div>
			<div class="feature-card">
				<div class="feature-icon">&#128274;</div>
				<h3>No Custody Risk</h3>
				<p>Read-only. rWallet never holds keys or moves funds — just visualizes.</p>
			</div>
		</div>`;
	}

	private renderExamples(): string {
		if (this.hasData() || this.loading) return "";
		return `
		<div class="examples">
			<div class="examples-label">Try an example</div>
			<div class="example-list">
				${EXAMPLE_WALLETS.map((w) => `
					<div class="example-item" data-address="${w.address}">
						<div>
							<div class="example-name">${w.name}</div>
							<div class="example-addr">${w.address.slice(0, 6)}...${w.address.slice(-4)}</div>
						</div>
						<div class="example-type ${w.type.toLowerCase()}">${w.type}</div>
					</div>
				`).join("")}
			</div>
		</div>`;
	}

	private renderViewTabs(): string {
		if (!this.hasData()) return "";
		const tabs: { id: ViewTab; label: string }[] = [
			{ id: "balances", label: "Balances" },
			{ id: "timeline", label: "Timeline" },
			{ id: "flow", label: "Flow Map" },
			{ id: "sankey", label: "Sankey" },
		];
		// Only show viz tabs for Safe wallets (or demo)
		const showViz = this.walletType === "safe" || this.isDemo;
		const visibleTabs = showViz ? tabs : [tabs[0]];

		return `
		<div class="view-tabs">
			${visibleTabs.map(t => `
				<button class="view-tab ${this.activeView === t.id ? "active" : ""}" data-view="${t.id}">${t.label}</button>
			`).join("")}
		</div>`;
	}

	private renderBalanceTable(): string {
		return this.balances.length > 0 ? `
			<table class="balance-table">
				<thead>
					<tr><th>Token</th><th class="amount-cell">Balance</th><th class="amount-cell">USD Value</th></tr>
				</thead>
				<tbody>
					${this.balances
						.filter((b) => parseFloat(b.fiatBalance || "0") > 0.01 || BigInt(b.balance || "0") > 0n)
						.sort((a, b) => {
							const fiatDiff = parseFloat(b.fiatBalance || "0") - parseFloat(a.fiatBalance || "0");
							if (fiatDiff !== 0) return fiatDiff;
							return Number(BigInt(b.balance || "0") - BigInt(a.balance || "0"));
						})
						.map((b) => `
							<tr>
								<td>
									<span class="token-symbol">${this.esc(b.token?.symbol || "ETH")}</span>
									<span class="token-name">${this.esc(b.token?.name || "Ether")}</span>
								</td>
								<td class="amount-cell">${this.formatBalance(b.balance, b.token?.decimals || 18)}</td>
								<td class="amount-cell fiat">${this.formatUSD(b.fiatBalance)}</td>
							</tr>
						`).join("")}
				</tbody>
			</table>
		` : '<div class="empty">No token balances found on this chain.</div>';
	}

	private renderDashboard(): string {
		if (!this.hasData()) return "";
		const totalUSD = this.balances.reduce((sum, b) => sum + parseFloat(b.fiatBalance || "0"), 0);

		return `
		<div class="chains">
			${this.detectedChains.map((ch) => `
				<div class="chain-btn ${this.selectedChain === ch.chainId ? "active" : ""}"
					data-chain="${ch.chainId}" style="--chain-color: ${ch.color}">
					<div class="chain-dot" style="background: ${ch.color}"></div>
					${ch.name}
				</div>
			`).join("")}
		</div>

		<div class="stats">
			<div class="stat-card">
				<div class="stat-label">Total Value</div>
				<div class="stat-value">${this.formatUSD(String(totalUSD))}</div>
			</div>
			<div class="stat-card">
				<div class="stat-label">Tokens</div>
				<div class="stat-value">${this.balances.filter((b) => parseFloat(b.fiatBalance || "0") > 0 || BigInt(b.balance || "0") > 0n).length}</div>
			</div>
			<div class="stat-card">
				<div class="stat-label">Chains</div>
				<div class="stat-value">${this.detectedChains.length}</div>
			</div>
			<div class="stat-card">
				<div class="stat-label">Address</div>
				<div class="stat-value" style="font-size:13px;font-family:monospace">${this.shortenAddress(this.address)}</div>
			</div>
		</div>

		${this.renderViewTabs()}

		${this.activeView === "balances"
			? this.renderBalanceTable()
			: `<div class="viz-container" id="viz-container">
				${this.transfersLoading ? '<div class="loading"><span class="spinner"></span> Loading transfer data...</div>' : ""}
			</div>`
		}`;
	}

	private render() {
		this.shadow.innerHTML = `
		${this.renderStyles()}

		${this.renderHero()}

		${this.renderMyWallets()}

		<form class="address-bar" id="address-form">
			<input id="address-input" type="text" placeholder="Enter any wallet or Safe address (0x...)"
				value="${this.address}" spellcheck="false">
			<button type="submit">Load</button>
		</form>

		<div class="controls-row">
			<div class="testnet-toggle ${this.includeTestnets ? "active" : ""}" id="testnet-toggle">
				<div class="toggle-track"><div class="toggle-thumb"></div></div>
				<span>Include testnets</span>
			</div>
			<button class="view-tab" id="btn-tour" style="margin-left:auto;font-size:0.78rem;padding:4px 10px">Tour</button>
			${this.walletType ? `
				<div class="wallet-badge ${this.walletType}">
					${this.walletType === "safe" ? "&#9939; Safe Multisig" : "&#128100; EOA Wallet"}
				</div>
			` : ""}
		</div>

		${this.renderSupportedChains()}

		${this.error ? `<div class="error">${this.esc(this.error)}</div>` : ""}
		${this.loading ? '<div class="loading"><span class="spinner"></span> Detecting wallet across chains...</div>' : ""}

		${this.renderFeatures()}
		${this.renderExamples()}
		${this.renderDashboard()}
		`;

		// Event listeners
		const form = this.shadow.querySelector("#address-form");
		form?.addEventListener("submit", (e) => this.handleSubmit(e));

		this.shadow.querySelector("#testnet-toggle")?.addEventListener("click", () => {
			this.includeTestnets = !this.includeTestnets;
			if (this.address) this.detectChains();
			else this.render();
		});

		this.shadow.querySelectorAll(".chain-btn").forEach((btn) => {
			btn.addEventListener("click", () => {
				const chainId = (btn as HTMLElement).dataset.chain!;
				this.handleChainSelect(chainId);
			});
		});

		this.shadow.querySelectorAll(".example-item").forEach((item) => {
			item.addEventListener("click", () => {
				const addr = (item as HTMLElement).dataset.address!;
				const input = this.shadow.querySelector("#address-input") as HTMLInputElement;
				if (input) input.value = addr;
				this.address = addr;
				const url = new URL(window.location.href);
				url.searchParams.set("address", addr);
				window.history.replaceState({}, "", url.toString());
				this.detectChains();
			});
		});

		// View tab listeners
		this.shadow.querySelectorAll(".view-tab").forEach((tab) => {
			tab.addEventListener("click", () => {
				const view = (tab as HTMLElement).dataset.view as ViewTab;
				this.handleViewTabClick(view);
			});
		});

		// Linked wallet event listeners
		this.shadow.querySelector("#link-wallet-btn")?.addEventListener("click", () => {
			this.startProviderDiscovery();
		});

		this.shadow.querySelector("#provider-cancel")?.addEventListener("click", () => {
			this.stopProviderDiscovery();
			this.render();
		});

		this.shadow.querySelectorAll(".provider-item").forEach((item) => {
			item.addEventListener("click", () => {
				const uuid = (item as HTMLElement).dataset.providerUuid!;
				this.handleProviderSelect(uuid);
			});
		});

		this.shadow.querySelectorAll("[data-view-address]").forEach((item) => {
			item.addEventListener("click", (e) => {
				// Don't navigate if clicking the unlink button
				if ((e.target as HTMLElement).closest(".unlink-btn")) return;
				const addr = (item as HTMLElement).dataset.viewAddress!;
				this.handleViewLinkedWallet(addr);
			});
		});

		this.shadow.querySelectorAll("[data-unlink]").forEach((btn) => {
			btn.addEventListener("click", (e) => {
				e.stopPropagation();
				const id = (btn as HTMLElement).dataset.unlink!;
				if (confirm("Unlink this wallet?")) {
					this.handleUnlinkWallet(id);
				}
			});
		});

		this.shadow.querySelector("#btn-tour")?.addEventListener("click", () => this.startTour());

		// Draw visualization if active
		if (this.activeView !== "balances" && this.hasData()) {
			requestAnimationFrame(() => this.drawActiveVisualization());
		}

		this._tour.renderOverlay();
	}

	startTour() {
		this._tour.start();
	}

	private esc(s: string): string {
		const d = document.createElement("div");
		d.textContent = s || "";
		return d.innerHTML;
	}
}

customElements.define("folk-wallet-viewer", FolkWalletViewer);
