/**
 * Listen to real-time reserve changes via WebSocket.
 *
 * Subscribes to all reserve accounts for a given market using
 * programNotifications with dataSize + market memcmp filters.
 *
 * Returns raw Reserve (not KaminoReserve) because constructing a
 * KaminoReserve requires oracle price data and an RPC connection
 * that are not available from the WS notification alone. The consumer
 * should hydrate into KaminoReserve using their market context.
 */
import { type Address, type Base58EncodedBytes, type Commitment, type Slot, getBase58Decoder } from '@solana/kit';

import { Reserve } from '../@codegen/klend/accounts';
import { PROGRAM_ID } from '../@codegen/klend/programId';

import {
  type AccountSubscriptionManager,
  type ProgramFilter,
  type SubscribeProgramAccountsOptions,
} from './accountSubscriptionManager';
import { toBuffer } from './utils';

/** Anchor accounts are prefixed with an 8-byte discriminator */
const ANCHOR_DISCRIMINATOR_BYTES = 8;
const RESERVE_ACCOUNT_SIZE = BigInt(Reserve.layout.span + ANCHOR_DISCRIMINATOR_BYTES);
const RESERVE_DISCRIMINATOR_BASE58 = getBase58Decoder().decode(Reserve.discriminator) as Base58EncodedBytes;

export interface ReserveChangeEvent {
  reserve: Reserve;
  address: Address;
  slot: Slot;
}

export type ReserveChangeCallback = (event: ReserveChangeEvent) => void;

export interface ReserveListenerParams {
  manager: AccountSubscriptionManager;
  marketAddress: Address;
  onChange: ReserveChangeCallback;
  onError?: (error: unknown) => void;
  programId?: Address;
  commitment?: Commitment;
  /** Throttle notifications to at most once per N ms. Reserves update every
   *  slot (~400ms) — throttling prevents excessive re-renders. */
  throttleMs?: number;
}

export function buildReserveFilters(marketAddress: Address): ProgramFilter[] {
  return [
    { dataSize: RESERVE_ACCOUNT_SIZE },
    { memcmp: { offset: 32n, bytes: marketAddress.toString() as Base58EncodedBytes, encoding: 'base58' } },
    // Match Reserve's 8-byte Anchor discriminator at offset 0 so uninitialized
    // klend-owned accounts with the same 8624-byte layout (e.g. pre-allocated
    // placeholders awaiting init_reserve) don't trigger spurious WS decode errors.
    { memcmp: { offset: 0n, bytes: RESERVE_DISCRIMINATOR_BASE58, encoding: 'base58' } },
  ];
}

/**
 * Subscribe to all reserve account changes for a given market.
 * Returns an unsubscribe function.
 */
export function listenToReserveChanges(params: ReserveListenerParams): () => void {
  const programId = params.programId ?? PROGRAM_ID;
  const filters = buildReserveFilters(params.marketAddress);

  const options: SubscribeProgramAccountsOptions = {
    // Reserves update every slot (~400ms) — use 'confirmed' to reduce noise
    commitment: params.commitment ?? 'confirmed',
    throttleMs: params.throttleMs,
  };

  return params.manager.subscribeProgramAccounts(
    programId,
    filters,
    (addr: Address, buffer: Uint8Array, slot: Slot) => {
      try {
        // Returns raw Reserve (not KaminoReserve) — see file header for rationale
        const reserve = Reserve.decode(toBuffer(buffer));
        params.onChange({ reserve, address: addr, slot });
      } catch (e) {
        params.onError?.(e);
      }
    },
    options
  );
}
