/**
 * Recurring Payment Executor
 *
 * Handles automated ERC-20 subscription payments via two mechanisms:
 *
 * 1. **Approval-based pulls (ERC-20 transferFrom)**:
 *    The payer approves a server-controlled relayer address to spend tokens.
 *    The server calls transferFrom when payments are due. Works with any wallet.
 *
 * 2. **EIP-4337 UserOp execution (Safe smart accounts)**:
 *    For Safe wallets with the server's session key as an authorized signer,
 *    the server constructs and submits UserOps via the Pimlico bundler.
 *
 * The relayer keypair is derived deterministically from HKDF so it's
 * consistent across restarts without storing a private key in the DB.
 */

import { createWalletClient, createPublicClient, http, type Chain } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base, baseSepolia, mainnet } from 'viem/chains';

// ============================================================================
// TYPES
// ============================================================================

export interface SubscriptionApproval {
	paymentRequestId: string;
	payerAddress: string;
	recipientAddress: string;
	tokenAddress: string;
	amount: string;         // amount per period in token units (e.g. "10" USDC)
	decimals: number;
	chainId: number;
	approvedAt: number;
}

// ============================================================================
// CHAIN CONFIG
// ============================================================================

const CHAINS: Record<number, Chain> = {
	8453: base,
	84532: baseSepolia,
	1: mainnet,
};

const RPC_URLS: Record<number, string> = {
	8453: 'https://mainnet.base.org',
	84532: 'https://sepolia.base.org',
	1: 'https://eth.llamarpc.com',
};

// ERC-20 function selectors
const TRANSFER_FROM_SELECTOR = '0x23b872dd'; // transferFrom(address,address,uint256)
const ALLOWANCE_SELECTOR = '0xdd62ed3e';     // allowance(address,address)

// ============================================================================
// RELAYER
// ============================================================================

let _relayerKey: `0x${string}` | null = null;
let _relayerAddress: string | null = null;

/**
 * Derive the relayer keypair from the server's RELAYER_SEED env var.
 * Falls back to a deterministic derivation from PIMLICO_API_KEY if no seed is set.
 */
async function getRelayerKey(): Promise<`0x${string}`> {
	if (_relayerKey) return _relayerKey;

	const seed = process.env.RELAYER_SEED || process.env.PIMLICO_API_KEY;
	if (!seed) throw new Error('RELAYER_SEED or PIMLICO_API_KEY required for recurring payments');

	// Derive a deterministic private key via HKDF-SHA256
	const encoder = new TextEncoder();
	const keyMaterial = await crypto.subtle.importKey(
		'raw', encoder.encode(seed), 'HKDF', false, ['deriveBits']
	);
	const bits = await crypto.subtle.deriveBits(
		{
			name: 'HKDF',
			hash: 'SHA-256',
			salt: encoder.encode('rcart-relayer-key-v1'),
			info: encoder.encode('secp256k1-relayer'),
		},
		keyMaterial,
		256,
	);

	const keyBytes = new Uint8Array(bits);
	_relayerKey = ('0x' + Array.from(keyBytes).map(b => b.toString(16).padStart(2, '0')).join('')) as `0x${string}`;
	_relayerAddress = privateKeyToAccount(_relayerKey).address;

	console.log(`[rcart] Relayer address: ${_relayerAddress}`);
	return _relayerKey;
}

/**
 * Get the relayer's Ethereum address.
 */
export async function getRelayerAddress(): Promise<string> {
	await getRelayerKey();
	return _relayerAddress!;
}

// ============================================================================
// ALLOWANCE CHECK
// ============================================================================

/**
 * Check the current ERC-20 allowance the payer has granted to the relayer.
 */
export async function checkAllowance(
	tokenAddress: string,
	payerAddress: string,
	chainId: number,
): Promise<bigint> {
	const chain = CHAINS[chainId];
	if (!chain) throw new Error(`Unsupported chain: ${chainId}`);

	const relayerAddr = await getRelayerAddress();

	const client = createPublicClient({
		chain,
		transport: http(RPC_URLS[chainId]),
	});

	// Encode allowance(payer, relayer) call
	const owner = payerAddress.slice(2).toLowerCase().padStart(64, '0');
	const spender = relayerAddr.slice(2).toLowerCase().padStart(64, '0');
	const data = `${ALLOWANCE_SELECTOR}${owner}${spender}` as `0x${string}`;

	const result = await client.call({
		to: tokenAddress as `0x${string}`,
		data,
	});

	if (!result.data || result.data === '0x') return 0n;
	return BigInt(result.data);
}

// ============================================================================
// EXECUTE TRANSFER
// ============================================================================

/**
 * Parse a decimal amount string into a BigInt with the given decimals.
 */
function parseTokenAmount(amount: string, decimals: number): bigint {
	const parts = amount.split('.');
	const whole = parts[0] || '0';
	const frac = (parts[1] || '').slice(0, decimals).padEnd(decimals, '0');
	return BigInt(whole) * BigInt(10 ** decimals) + BigInt(frac);
}

/**
 * Execute a transferFrom call: pull tokens from payer to recipient.
 * Requires that the payer has approved the relayer for sufficient allowance.
 */
export async function executeTransferFrom(
	tokenAddress: string,
	payerAddress: string,
	recipientAddress: string,
	amount: string,
	decimals: number,
	chainId: number,
): Promise<string> {
	const chain = CHAINS[chainId];
	if (!chain) throw new Error(`Unsupported chain: ${chainId}`);

	const relayerKey = await getRelayerKey();
	const account = privateKeyToAccount(relayerKey);

	// Check allowance first
	const allowance = await checkAllowance(tokenAddress, payerAddress, chainId);
	const rawAmount = parseTokenAmount(amount, decimals);
	if (allowance < rawAmount) {
		throw new Error(`Insufficient allowance: have ${allowance}, need ${rawAmount}`);
	}

	const client = createWalletClient({
		account,
		chain,
		transport: http(RPC_URLS[chainId]),
	});

	// Encode transferFrom(from, to, amount)
	const from = payerAddress.slice(2).toLowerCase().padStart(64, '0');
	const to = recipientAddress.slice(2).toLowerCase().padStart(64, '0');
	const amountHex = rawAmount.toString(16).padStart(64, '0');
	const data = `${TRANSFER_FROM_SELECTOR}${from}${to}${amountHex}` as `0x${string}`;

	const txHash = await client.sendTransaction({
		account,
		to: tokenAddress as `0x${string}`,
		data,
		chain,
	});

	return txHash;
}

// ============================================================================
// APPROVAL TX BUILDER (client-side helper)
// ============================================================================

/**
 * Build the calldata for an ERC-20 approve(relayer, amount) transaction.
 * Returns the data the client needs to sign and submit.
 */
export async function buildApprovalCalldata(
	amount: string,
	decimals: number,
	maxPayments: number,
): Promise<{ spender: string; calldata: string; totalAllowance: string }> {
	const relayerAddr = await getRelayerAddress();
	const perPayment = parseTokenAmount(amount, decimals);
	// Approve enough for all scheduled payments (or a generous amount for unlimited)
	const totalPayments = maxPayments > 0 ? BigInt(maxPayments) : 120n; // ~10 years monthly
	const totalAllowance = perPayment * totalPayments;

	const spender = relayerAddr.slice(2).toLowerCase().padStart(64, '0');
	const allowanceHex = totalAllowance.toString(16).padStart(64, '0');
	// approve(address spender, uint256 amount) — selector: 0x095ea7b3
	const calldata = `0x095ea7b3${spender}${allowanceHex}`;

	return {
		spender: relayerAddr,
		calldata,
		totalAllowance: totalAllowance.toString(),
	};
}
