/**
 * Drips Protocol read-only client.
 *
 * Fetches stream/split state via Drips GraphQL API (primary) with
 * direct eth_call fallback. 5-min cache + in-flight dedup.
 * Pattern: mirrors rwallet/lib/defi-positions.ts.
 */

import { getRpcUrl } from '../../rwallet/mod';

// ── Contract addresses ──

export const DRIPS_CONTRACTS = {
	1: {
		drips: '0xd0Dd053392db676D57317CD4fe96Fc2cCf42D0b4',
		addressDriver: '0x1455d9bD6B98f95dd8FEB2b3D60ed825fcef0610',
		nftDriver: '0xcf9c49B0962EDb01Cdaa5326299ba85D72405258',
	},
	11155111: {
		drips: '0x74A32a38D945b9527524900429b083547DeB9bF4',
		addressDriver: '0x70E1E1437AeFe8024B6780C94490662b45C3B567',
		nftDriver: '0xdC773a04C0D6EFdb80E7dfF961B6a7B063a28B44',
	},
} as Record<number, { drips: string; addressDriver: string; nftDriver: string }>;

// Drips subgraph API endpoints by chain
const DRIPS_API: Record<number, string> = {
	1: 'https://drips-api.onrender.com/graphql',
	11155111: 'https://drips-api-sepolia.onrender.com/graphql',
};

// ── Types ──

export interface DripsStream {
	id: string;
	sender: string;
	receiver: string;
	tokenAddress: string;
	amtPerSec: string; // raw bigint string (extra 9 decimals)
	startTime: number;
	duration: number; // 0 = indefinite
	isPaused: boolean;
}

export interface DripsSplit {
	receiver: string;
	weight: number; // out of 1_000_000
}

export interface DripsAccountState {
	address: string;
	chainId: number;
	incomingStreams: DripsStream[];
	outgoingStreams: DripsStream[];
	splits: DripsSplit[];
	splitsHash: string;
	collectableAmounts: Record<string, string>; // token → amount
	fetchedAt: number;
}

// ── Cache ──

interface CacheEntry {
	state: DripsAccountState;
	ts: number;
}

const TTL = 5 * 60 * 1000;
const cache = new Map<string, CacheEntry>();
const inFlight = new Map<string, Promise<DripsAccountState>>();

// ── Helpers ──

/** Convert Drips amtPerSec (with 9 extra decimals) to monthly USD. */
export function dripsAmtToMonthly(amtPerSec: string, tokenDecimals: number, usdPrice: number): number {
	const raw = BigInt(amtPerSec);
	// Remove 9 extra decimals from Drips encoding
	const perSec = Number(raw) / 1e9;
	// Convert to token units
	const tokenPerSec = perSec / Math.pow(10, tokenDecimals);
	// Monthly = per-sec × 60 × 60 × 24 × 30
	return tokenPerSec * usdPrice * 2_592_000;
}

/** Compute Drips account ID from address (AddressDriver: address as uint256). */
function addressToAccountId(address: string): string {
	// AddressDriver account ID = (driverIndex << 224) | addressAsUint160
	// driverIndex for AddressDriver = 0, so accountId = address as uint256
	return BigInt(address).toString();
}

// ── GraphQL fetch ──

const ACCOUNT_QUERY = `
query DripsAccount($accountId: ID!) {
  userById(accountId: $accountId) {
    splitsEntries {
      receiver { accountId, address }
      weight
    }
    streams {
      outgoing {
        id
        receiver { address }
        config { amtPerSec, start, duration }
        isPaused
        tokenAddress
      }
      incoming {
        id
        sender { address }
        config { amtPerSec, start, duration }
        isPaused
        tokenAddress
      }
    }
  }
}`;

async function fetchGraphQL(chainId: number, address: string): Promise<DripsAccountState | null> {
	const apiUrl = DRIPS_API[chainId];
	if (!apiUrl) return null;

	const accountId = addressToAccountId(address);

	try {
		const res = await fetch(apiUrl, {
			method: 'POST',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify({ query: ACCOUNT_QUERY, variables: { accountId } }),
			signal: AbortSignal.timeout(15000),
		});

		if (!res.ok) {
			console.warn(`[drips-client] GraphQL API error: ${res.status}`);
			return null;
		}

		const json = await res.json() as any;
		const user = json.data?.userById;
		if (!user) return emptyState(chainId, address);

		const incomingStreams: DripsStream[] = (user.streams?.incoming || []).map((s: any) => ({
			id: s.id || '',
			sender: s.sender?.address || '',
			receiver: address,
			tokenAddress: s.tokenAddress || '',
			amtPerSec: s.config?.amtPerSec || '0',
			startTime: Number(s.config?.start || 0),
			duration: Number(s.config?.duration || 0),
			isPaused: s.isPaused ?? false,
		}));

		const outgoingStreams: DripsStream[] = (user.streams?.outgoing || []).map((s: any) => ({
			id: s.id || '',
			sender: address,
			receiver: s.receiver?.address || '',
			tokenAddress: s.tokenAddress || '',
			amtPerSec: s.config?.amtPerSec || '0',
			startTime: Number(s.config?.start || 0),
			duration: Number(s.config?.duration || 0),
			isPaused: s.isPaused ?? false,
		}));

		const splits: DripsSplit[] = (user.splitsEntries || []).map((s: any) => ({
			receiver: s.receiver?.address || s.receiver?.accountId || '',
			weight: Number(s.weight || 0),
		}));

		return {
			address,
			chainId,
			incomingStreams,
			outgoingStreams,
			splits,
			splitsHash: '', // GraphQL doesn't return hash directly
			collectableAmounts: {},
			fetchedAt: Date.now(),
		};
	} catch (e) {
		console.warn('[drips-client] GraphQL fetch failed:', e);
		return null;
	}
}

// ── Fallback: direct eth_call ──

// Drips.splitsHash(uint256 accountId) → bytes32
const SPLITS_HASH_SELECTOR = '0xeca563dd';
// Drips.hashSplits(SplitsReceiver[] memory receivers) — for verification only

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(10000),
		});
		const json = await res.json() as any;
		return json.result || null;
	} catch { return null; }
}

function padUint256(value: string): string {
	return BigInt(value).toString(16).padStart(64, '0');
}

async function fetchViaRPC(chainId: number, address: string): Promise<DripsAccountState | null> {
	const rpcUrl = getRpcUrl(String(chainId));
	if (!rpcUrl) return null;

	const contracts = DRIPS_CONTRACTS[chainId];
	if (!contracts) return null;

	const accountId = addressToAccountId(address);

	// Get splitsHash
	const splitsData = SPLITS_HASH_SELECTOR + padUint256(accountId);
	const splitsHash = await rpcCall(rpcUrl, contracts.drips, splitsData);

	return {
		address,
		chainId,
		incomingStreams: [],
		outgoingStreams: [],
		splits: [], // Can't enumerate splits from hash alone
		splitsHash: splitsHash || '',
		collectableAmounts: {},
		fetchedAt: Date.now(),
	};
}

function emptyState(chainId: number, address: string): DripsAccountState {
	return {
		address,
		chainId,
		incomingStreams: [],
		outgoingStreams: [],
		splits: [],
		splitsHash: '',
		collectableAmounts: {},
		fetchedAt: Date.now(),
	};
}

// ── Public API ──

/**
 * Fetch Drips account state with 5-min cache and in-flight dedup.
 * Tries GraphQL first, falls back to direct eth_call.
 */
export async function getDripsAccountState(chainId: number, address: string): Promise<DripsAccountState> {
	const key = `${chainId}:${address.toLowerCase()}`;

	// Check cache
	const cached = cache.get(key);
	if (cached && Date.now() - cached.ts < TTL) return cached.state;

	// Deduplicate concurrent requests
	const pending = inFlight.get(key);
	if (pending) return pending;

	const promise = (async (): Promise<DripsAccountState> => {
		try {
			// Try GraphQL first
			const graphqlResult = await fetchGraphQL(chainId, address);
			if (graphqlResult) {
				cache.set(key, { state: graphqlResult, ts: Date.now() });
				return graphqlResult;
			}

			// Fallback to RPC
			const rpcResult = await fetchViaRPC(chainId, address);
			if (rpcResult) {
				cache.set(key, { state: rpcResult, ts: Date.now() });
				return rpcResult;
			}

			return emptyState(chainId, address);
		} catch (e) {
			console.warn('[drips-client] Failed to fetch:', e);
			return emptyState(chainId, address);
		} finally {
			inFlight.delete(key);
		}
	})();

	inFlight.set(key, promise);
	return promise;
}
