/**
 * Safe transaction builder for yield operations — builds calldata for
 * approve+deposit (via MultiSend) and single-tx withdrawals.
 */

import type { SupportedYieldChain, YieldProtocol, StablecoinSymbol } from "./yield-protocols";
import {
	AAVE_V3_POOL, MORPHO_VAULTS, MULTISEND_CALL_ONLY,
	STABLECOIN_ADDRESSES, STABLECOIN_DECIMALS,
	encodeApprove, encodeAaveSupply, encodeAaveWithdraw,
	encodeMorphoDeposit, encodeMorphoRedeem,
	encodeMaxDeposit, encodeBalanceOf,
	SELECTORS, padUint256,
	AAVE_ATOKENS,
} from "./yield-protocols";
import { getRpcUrl } from "../mod";

export interface SafeTxData {
	to: string;
	value: string;
	data: string;
	operation: number; // 0 = Call, 1 = DelegateCall
	description: string;
	steps: string[];
}

// ── MultiSend encoding ──

interface MultiSendTx {
	operation: number; // 0 = Call
	to: string;
	value: bigint;
	data: string; // hex with 0x prefix
}

function encodeMultiSend(txs: MultiSendTx[]): string {
	let packed = "";
	for (const tx of txs) {
		const dataBytes = tx.data.startsWith("0x") ? tx.data.slice(2) : tx.data;
		const dataLength = dataBytes.length / 2;
		packed +=
			padUint256(BigInt(tx.operation)).slice(62, 64) + // operation: 1 byte (last byte of uint8)
			tx.to.slice(2).toLowerCase().padStart(40, "0") + // to: 20 bytes
			padUint256(tx.value) + // value: 32 bytes
			padUint256(BigInt(dataLength)) + // dataLength: 32 bytes
			dataBytes; // data: variable
	}

	// Wrap in multiSend(bytes) — ABI encode: selector + offset(32) + length(32) + data + padding
	const packedBytes = packed;
	const packedLength = packedBytes.length / 2;
	const offset = padUint256(32n); // bytes offset
	const length = padUint256(BigInt(packedLength));
	// Pad to 32-byte boundary
	const padLen = (32 - (packedLength % 32)) % 32;
	const padding = "0".repeat(padLen * 2);

	return `${SELECTORS.multiSend}${offset}${length}${packedBytes}${padding}`;
}

// ── RPC helper ──

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

// ── Deposit builder ──

export async function buildDepositTransaction(
	chainId: SupportedYieldChain,
	safeAddress: string,
	protocol: YieldProtocol,
	asset: StablecoinSymbol,
	amount: bigint,
	vaultAddress?: string, // required for Morpho (which vault)
): Promise<SafeTxData> {
	const assetAddress = STABLECOIN_ADDRESSES[chainId]?.[asset];
	if (!assetAddress || assetAddress === "0x0000000000000000000000000000000000000000") {
		throw new Error(`${asset} not available on chain ${chainId}`);
	}

	const decimals = STABLECOIN_DECIMALS[asset];
	const amountStr = `${Number(amount) / 10 ** decimals} ${asset}`;

	if (protocol === "aave-v3") {
		const pool = AAVE_V3_POOL[chainId];
		const approveTx: MultiSendTx = {
			operation: 0,
			to: assetAddress,
			value: 0n,
			data: encodeApprove(pool, amount),
		};
		const supplyTx: MultiSendTx = {
			operation: 0,
			to: pool,
			value: 0n,
			data: encodeAaveSupply(assetAddress, amount, safeAddress),
		};

		return {
			to: MULTISEND_CALL_ONLY[chainId],
			value: "0",
			data: encodeMultiSend([approveTx, supplyTx]),
			operation: 1, // DelegateCall for MultiSend
			description: `Deposit ${amountStr} to Aave V3`,
			steps: [
				`Approve Aave V3 Pool to spend ${amountStr}`,
				`Supply ${amountStr} to Aave V3 Pool`,
			],
		};
	}

	// Morpho Blue vault
	const vault = vaultAddress || MORPHO_VAULTS[chainId]?.find((v) => v.asset === asset)?.address;
	if (!vault) throw new Error(`No Morpho vault for ${asset} on chain ${chainId}`);

	// Check vault capacity
	const rpcUrl = getRpcUrl(chainId);
	if (rpcUrl) {
		const maxHex = await rpcCall(rpcUrl, vault, encodeMaxDeposit(safeAddress));
		if (maxHex) {
			const maxDeposit = BigInt(maxHex);
			if (maxDeposit < amount) {
				throw new Error(`Morpho vault capacity exceeded: max ${Number(maxDeposit) / 10 ** decimals} ${asset}`);
			}
		}
	}

	const approveTx: MultiSendTx = {
		operation: 0,
		to: assetAddress,
		value: 0n,
		data: encodeApprove(vault, amount),
	};
	const depositTx: MultiSendTx = {
		operation: 0,
		to: vault,
		value: 0n,
		data: encodeMorphoDeposit(amount, safeAddress),
	};

	const vaultName = MORPHO_VAULTS[chainId]?.find((v) => v.address.toLowerCase() === vault.toLowerCase())?.name || "Morpho Vault";

	return {
		to: MULTISEND_CALL_ONLY[chainId],
		value: "0",
		data: encodeMultiSend([approveTx, depositTx]),
		operation: 1, // DelegateCall for MultiSend
		description: `Deposit ${amountStr} to ${vaultName}`,
		steps: [
			`Approve ${vaultName} to spend ${amountStr}`,
			`Deposit ${amountStr} into ${vaultName}`,
		],
	};
}

// ── Withdraw builder ──

export async function buildWithdrawTransaction(
	chainId: SupportedYieldChain,
	safeAddress: string,
	protocol: YieldProtocol,
	asset: StablecoinSymbol,
	amount: bigint | "max",
	vaultAddress?: string,
): Promise<SafeTxData> {
	const assetAddress = STABLECOIN_ADDRESSES[chainId]?.[asset];
	if (!assetAddress || assetAddress === "0x0000000000000000000000000000000000000000") {
		throw new Error(`${asset} not available on chain ${chainId}`);
	}

	const decimals = STABLECOIN_DECIMALS[asset];

	if (protocol === "aave-v3") {
		// For "max", use type(uint256).max which tells Aave to withdraw everything
		const withdrawAmount = amount === "max"
			? (2n ** 256n - 1n)
			: amount;

		const amountStr = amount === "max" ? `all ${asset}` : `${Number(amount) / 10 ** decimals} ${asset}`;

		return {
			to: AAVE_V3_POOL[chainId],
			value: "0",
			data: encodeAaveWithdraw(assetAddress, withdrawAmount, safeAddress),
			operation: 0, // Direct call
			description: `Withdraw ${amountStr} from Aave V3`,
			steps: [`Withdraw ${amountStr} from Aave V3 Pool`],
		};
	}

	// Morpho vault
	const vault = vaultAddress || MORPHO_VAULTS[chainId]?.find((v) => v.asset === asset)?.address;
	if (!vault) throw new Error(`No Morpho vault for ${asset} on chain ${chainId}`);

	const vaultName = MORPHO_VAULTS[chainId]?.find((v) => v.address.toLowerCase() === vault.toLowerCase())?.name || "Morpho Vault";

	// For "max", query current share balance
	let shares: bigint;
	if (amount === "max") {
		const rpcUrl = getRpcUrl(chainId);
		if (!rpcUrl) throw new Error("No RPC URL for chain");
		const balHex = await rpcCall(rpcUrl, vault, encodeBalanceOf(safeAddress));
		if (!balHex) throw new Error("Could not query vault balance");
		shares = BigInt(balHex);
		if (shares === 0n) throw new Error("No shares to withdraw");
	} else {
		// For specific amounts, we'd need previewWithdraw — approximate with shares = amount (ERC-4626 1:1-ish for stables)
		// This is approximate; the actual share amount may differ slightly
		shares = amount;
	}

	const amountStr = amount === "max" ? `all ${asset}` : `${Number(amount) / 10 ** decimals} ${asset}`;

	return {
		to: vault,
		value: "0",
		data: encodeMorphoRedeem(shares, safeAddress, safeAddress),
		operation: 0,
		description: `Withdraw ${amountStr} from ${vaultName}`,
		steps: [`Redeem shares from ${vaultName} for ${amountStr}`],
	};
}
