//#region extensions/crypto/src/services/heartbeat-monitor.ts
var HeartbeatMonitor = class HeartbeatMonitor {
	config;
	timer = null;
	running = false;
	lastPositions = /* @__PURE__ */ new Map();
	lastPortfolioValueUsd = 0;
	lastCheckMs = 0;
	alerts = [];
	callbacks = [];
	checkCount = 0;
	tickInProgress = false;
	static MAX_ALERTS = 500;
	static ALERT_KEY_SEP = ":";
	constructor(config = {}) {
		this.config = {
			intervalMs: config.intervalMs ?? 3e5,
			priceDropAlertPercent: config.priceDropAlertPercent ?? 10,
			priceGainAlertPercent: config.priceGainAlertPercent ?? 20,
			portfolioDropAlertUsd: config.portfolioDropAlertUsd ?? 100,
			enabled: config.enabled ?? true
		};
	}
	/**
	* Register a callback that fires when an alert is generated.
	*/
	onAlert(cb) {
		this.callbacks.push(cb);
	}
	/**
	* Start the periodic heartbeat.
	*/
	start() {
		if (!this.config.enabled || this.running) return;
		this.running = true;
		this.timer = setInterval(() => {
			this.tick().catch(() => {});
		}, this.config.intervalMs);
		if (this.timer && typeof this.timer === "object" && "unref" in this.timer) this.timer.unref();
	}
	/**
	* Stop the periodic heartbeat.
	*/
	stop() {
		this.running = false;
		if (this.timer) {
			clearInterval(this.timer);
			this.timer = null;
		}
	}
	/**
	* Run a single check cycle. Called automatically by the timer,
	* but can also be called manually for testing.
	*/
	async tick() {
		if (this.tickInProgress) return [];
		this.tickInProgress = true;
		this.checkCount++;
		this.lastCheckMs = Date.now();
		const newAlerts = [];
		try {
			try {
				const { getPortfolioService } = await import("./portfolio-snapshot.mjs");
				const { getWalletState } = await import("./walletconnect-service.mjs");
				const wallet = getWalletState();
				if (!wallet.connected || !wallet.address) return newAlerts;
				const snapshot = await getPortfolioService().getSnapshot(wallet.address);
				for (const token of snapshot.tokens) {
					const key = `${token.symbol}${HeartbeatMonitor.ALERT_KEY_SEP}${token.chain}`;
					const prev = this.lastPositions.get(key);
					if (prev && prev.priceUsd > 0 && token.priceUsd > 0) {
						const changePct = (token.priceUsd - prev.priceUsd) / prev.priceUsd * 100;
						if (changePct <= -this.config.priceDropAlertPercent) {
							const alert = {
								type: "price_drop",
								severity: changePct <= -25 ? "critical" : "warning",
								symbol: token.symbol,
								chain: token.chain,
								message: `${token.symbol} dropped ${Math.abs(changePct).toFixed(1)}% ($${prev.priceUsd.toFixed(4)} → $${token.priceUsd.toFixed(4)}) on ${token.chain}. Position value: $${token.valueUsd.toFixed(2)}`,
								data: {
									previousPrice: prev.priceUsd,
									currentPrice: token.priceUsd,
									changePct: Math.round(changePct * 100) / 100,
									valueUsd: token.valueUsd,
									balance: token.balanceHuman
								},
								timestamp: Date.now()
							};
							newAlerts.push(alert);
						} else if (changePct >= this.config.priceGainAlertPercent) {
							const alert = {
								type: "price_gain",
								severity: "info",
								symbol: token.symbol,
								chain: token.chain,
								message: `${token.symbol} gained ${changePct.toFixed(1)}% ($${prev.priceUsd.toFixed(4)} → $${token.priceUsd.toFixed(4)}) on ${token.chain}. Position value: $${token.valueUsd.toFixed(2)}`,
								data: {
									previousPrice: prev.priceUsd,
									currentPrice: token.priceUsd,
									changePct: Math.round(changePct * 100) / 100,
									valueUsd: token.valueUsd,
									balance: token.balanceHuman
								},
								timestamp: Date.now()
							};
							newAlerts.push(alert);
						}
					}
					if (!prev && this.checkCount > 1) newAlerts.push({
						type: "new_token",
						severity: "info",
						symbol: token.symbol,
						chain: token.chain,
						message: `New token detected: ${token.symbol} on ${token.chain} (${token.balanceHuman.toFixed(4)} @ $${token.priceUsd.toFixed(4)}, value: $${token.valueUsd.toFixed(2)})`,
						data: {
							address: token.address,
							balance: token.balanceHuman,
							priceUsd: token.priceUsd,
							valueUsd: token.valueUsd
						},
						timestamp: Date.now()
					});
					this.lastPositions.set(key, {
						symbol: token.symbol,
						address: token.address,
						chain: token.chain,
						priceUsd: token.priceUsd,
						valueUsd: token.valueUsd,
						balanceHuman: token.balanceHuman,
						timestamp: Date.now()
					});
				}
				if (this.checkCount > 1) {
					const currentKeys = new Set(snapshot.tokens.map((t) => `${t.symbol}${HeartbeatMonitor.ALERT_KEY_SEP}${t.chain}`));
					for (const [key, prev] of this.lastPositions) if (!currentKeys.has(key) && prev.valueUsd > 1) {
						newAlerts.push({
							type: "position_gone",
							severity: "warning",
							symbol: prev.symbol,
							chain: prev.chain,
							message: `Position disappeared: ${prev.symbol} on ${prev.chain} (was ${prev.balanceHuman.toFixed(4)} @ $${prev.priceUsd.toFixed(4)}, value was $${prev.valueUsd.toFixed(2)})`,
							data: {
								lastBalance: prev.balanceHuman,
								lastPrice: prev.priceUsd,
								lastValue: prev.valueUsd
							},
							timestamp: Date.now()
						});
						this.lastPositions.delete(key);
					}
				}
				if (this.lastPortfolioValueUsd > 0) {
					const dropUsd = this.lastPortfolioValueUsd - snapshot.totalValueUsd;
					if (dropUsd >= this.config.portfolioDropAlertUsd) {
						const dropPct = dropUsd / this.lastPortfolioValueUsd * 100;
						newAlerts.push({
							type: "portfolio_drop",
							severity: dropPct > 20 ? "critical" : "warning",
							symbol: "PORTFOLIO",
							chain: "all",
							message: `Portfolio dropped $${dropUsd.toFixed(2)} (${dropPct.toFixed(1)}%) since last check ($${this.lastPortfolioValueUsd.toFixed(2)} → $${snapshot.totalValueUsd.toFixed(2)})`,
							data: {
								previousValueUsd: this.lastPortfolioValueUsd,
								currentValueUsd: snapshot.totalValueUsd,
								dropUsd,
								dropPct: Math.round(dropPct * 100) / 100
							},
							timestamp: Date.now()
						});
					}
				}
				this.lastPortfolioValueUsd = snapshot.totalValueUsd;
				try {
					const { getLendingService } = await import("./lending-service.mjs");
					const { requirePublicClient } = await import("./walletconnect-service.mjs");
					const lendingSvc = getLendingService();
					const publicClient = requirePublicClient();
					const accountData = await lendingSvc.getUserAccountData(wallet.address, publicClient);
					if (accountData.healthFactor !== Infinity && accountData.totalDebtUsd > 0) {
						if (accountData.healthFactor < 1.1) newAlerts.push({
							type: "health_factor_critical",
							severity: "critical",
							symbol: "AAVE_POSITION",
							chain: "base",
							message: `DANGER: Aave health factor is ${accountData.healthFactor.toFixed(4)} — immediate liquidation risk! Collateral: $${accountData.totalCollateralUsd.toFixed(2)}, Debt: $${accountData.totalDebtUsd.toFixed(2)}. Add collateral or repay debt immediately.`,
							data: {
								healthFactor: accountData.healthFactor,
								totalCollateralUsd: accountData.totalCollateralUsd,
								totalDebtUsd: accountData.totalDebtUsd,
								availableBorrowsUsd: accountData.availableBorrowsUsd
							},
							timestamp: Date.now()
						});
						else if (accountData.healthFactor < 1.5) newAlerts.push({
							type: "health_factor_warning",
							severity: "warning",
							symbol: "AAVE_POSITION",
							chain: "base",
							message: `WARNING: Aave health factor is ${accountData.healthFactor.toFixed(4)} — approaching liquidation zone. Collateral: $${accountData.totalCollateralUsd.toFixed(2)}, Debt: $${accountData.totalDebtUsd.toFixed(2)}. Consider adding collateral or repaying debt.`,
							data: {
								healthFactor: accountData.healthFactor,
								totalCollateralUsd: accountData.totalCollateralUsd,
								totalDebtUsd: accountData.totalDebtUsd,
								availableBorrowsUsd: accountData.availableBorrowsUsd
							},
							timestamp: Date.now()
						});
					}
				} catch {}
			} catch {}
			for (const alert of newAlerts) {
				this.alerts.push(alert);
				for (const cb of this.callbacks) try {
					await cb(alert);
				} catch {}
			}
			if (this.alerts.length > HeartbeatMonitor.MAX_ALERTS) this.alerts = this.alerts.slice(-HeartbeatMonitor.MAX_ALERTS);
			return newAlerts;
		} finally {
			this.tickInProgress = false;
		}
	}
	/**
	* Get recent alerts.
	*/
	getAlerts(limit = 50) {
		return this.alerts.slice(-limit);
	}
	/**
	* Get current tracked positions.
	*/
	getPositions() {
		return Array.from(this.lastPositions.values());
	}
	/**
	* Get monitor status for diagnostics.
	*/
	getStatus() {
		return {
			running: this.running,
			enabled: this.config.enabled,
			checkCount: this.checkCount,
			lastCheckMs: this.lastCheckMs,
			trackedPositions: this.lastPositions.size,
			totalAlerts: this.alerts.length,
			intervalMs: this.config.intervalMs,
			config: this.config
		};
	}
	/**
	* Manually set position data (useful for testing or seeding from ledger).
	*/
	seedPosition(pos) {
		const key = `${pos.symbol}${HeartbeatMonitor.ALERT_KEY_SEP}${pos.chain}`;
		this.lastPositions.set(key, pos);
	}
	/**
	* Manually set the last portfolio value (useful for testing).
	*/
	seedPortfolioValue(valueUsd) {
		this.lastPortfolioValueUsd = valueUsd;
	}
};
let _instance = null;
function getHeartbeatMonitor(config) {
	if (!_instance) _instance = new HeartbeatMonitor(config);
	return _instance;
}
function resetHeartbeatMonitor() {
	if (_instance) _instance.stop();
	_instance = null;
}
//#endregion
export { getHeartbeatMonitor, resetHeartbeatMonitor };

//# sourceMappingURL=heartbeat-monitor.mjs.map