/**
 * Pimlico ERC-4337 bundler client — wraps Pimlico's JSON-RPC v2 API.
 *
 * Provides gas estimation, paymaster sponsorship, UserOperation submission,
 * and receipt polling for Account Abstraction transactions.
 *
 * Docs: https://docs.pimlico.io/
 */

const PIMLICO_BASE = 'https://api.pimlico.io/v2';

export interface UserOperation {
	sender: string;
	nonce: string;
	initCode: string;
	callData: string;
	callGasLimit?: string;
	verificationGasLimit?: string;
	preVerificationGas?: string;
	maxFeePerGas?: string;
	maxPriorityFeePerGas?: string;
	paymasterAndData?: string;
	signature?: string;
}

export interface GasEstimate {
	callGasLimit: string;
	verificationGasLimit: string;
	preVerificationGas: string;
	maxFeePerGas: string;
	maxPriorityFeePerGas: string;
}

export interface PaymasterResult {
	paymasterAndData: string;
	callGasLimit: string;
	verificationGasLimit: string;
	preVerificationGas: string;
}

export interface UserOperationReceipt {
	userOpHash: string;
	sender: string;
	nonce: string;
	actualGasCost: string;
	actualGasUsed: string;
	success: boolean;
	receipt: {
		transactionHash: string;
		blockNumber: string;
		status: string;
	};
}

export class PimlicoClient {
	private apiKey: string;
	private chainId: number;

	constructor(opts: { apiKey: string; chainId: number }) {
		this.apiKey = opts.apiKey;
		this.chainId = opts.chainId;
	}

	private get endpoint(): string {
		return `${PIMLICO_BASE}/${this.chainId}/rpc?apikey=${this.apiKey}`;
	}

	private async rpc(method: string, params: any[]): Promise<any> {
		const res = await fetch(this.endpoint, {
			method: 'POST',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
			signal: AbortSignal.timeout(15000),
		});

		if (!res.ok) {
			const text = await res.text();
			throw new Error(`Pimlico RPC error (${res.status}): ${text}`);
		}

		const data = await res.json() as any;
		if (data.error) {
			throw new Error(`Pimlico ${method} error: ${data.error.message || JSON.stringify(data.error)}`);
		}
		return data.result;
	}

	async estimateGas(userOp: UserOperation, entryPoint: string): Promise<GasEstimate> {
		const result = await this.rpc('eth_estimateUserOperationGas', [userOp, entryPoint]);
		// Pimlico also returns fee data via pimlico_getUserOperationGasPrice
		const fees = await this.rpc('pimlico_getUserOperationGasPrice', []);
		return {
			callGasLimit: result.callGasLimit,
			verificationGasLimit: result.verificationGasLimit,
			preVerificationGas: result.preVerificationGas,
			maxFeePerGas: fees.fast.maxFeePerGas,
			maxPriorityFeePerGas: fees.fast.maxPriorityFeePerGas,
		};
	}

	async getPaymasterData(
		userOp: UserOperation,
		entryPoint: string,
		opts?: { sponsorshipPolicyId?: string },
	): Promise<PaymasterResult> {
		const params: any[] = [userOp, entryPoint, { type: 'payg' }];
		if (opts?.sponsorshipPolicyId) {
			params[2] = { sponsorshipPolicyId: opts.sponsorshipPolicyId };
		}
		return await this.rpc('pm_sponsorUserOperation', params);
	}

	async sendUserOperation(userOp: UserOperation, entryPoint: string): Promise<string> {
		return await this.rpc('eth_sendUserOperation', [userOp, entryPoint]);
	}

	async getUserOperationReceipt(userOpHash: string): Promise<UserOperationReceipt | null> {
		return await this.rpc('eth_getUserOperationReceipt', [userOpHash]);
	}

	/**
	 * Full prepare flow: estimate gas → get paymaster sponsorship → return complete UserOp.
	 */
	async prepareUserOperation(
		userOp: UserOperation,
		entryPoint: string,
		opts?: { sponsorshipPolicyId?: string },
	): Promise<UserOperation> {
		// 1. Estimate gas limits + fee data
		const gas = await this.estimateGas(userOp, entryPoint);

		const prepared: UserOperation = {
			...userOp,
			callGasLimit: gas.callGasLimit,
			verificationGasLimit: gas.verificationGasLimit,
			preVerificationGas: gas.preVerificationGas,
			maxFeePerGas: gas.maxFeePerGas,
			maxPriorityFeePerGas: gas.maxPriorityFeePerGas,
		};

		// 2. Get paymaster sponsorship data
		const pm = await this.getPaymasterData(prepared, entryPoint, opts);
		prepared.paymasterAndData = pm.paymasterAndData;
		// Paymaster may override gas limits
		if (pm.callGasLimit) prepared.callGasLimit = pm.callGasLimit;
		if (pm.verificationGasLimit) prepared.verificationGasLimit = pm.verificationGasLimit;
		if (pm.preVerificationGas) prepared.preVerificationGas = pm.preVerificationGas;

		return prepared;
	}
}
