// file: kb_lib/src/dex/raydium_stable_swap.rs

//! Raydium Stable Swap decoder.
//!
//! Raydium Stable Swap uses a legacy one-byte instruction discriminator layout.
//! The decoder below follows the Pinax/Substreams layout and keeps Anchor-like
//! eight-byte discriminants as upstream discovery evidence only.

/// Raydium Stable Swap `initialize` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_INITIALIZE_DISCRIMINATOR_HEX: &str = "00";
/// Raydium Stable Swap `init_model_data` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_INIT_MODEL_DATA_DISCRIMINATOR_HEX: &str = "01";
/// Raydium Stable Swap `update_model_data` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_UPDATE_MODEL_DATA_DISCRIMINATOR_HEX: &str = "02";
/// Raydium Stable Swap `deposit` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_DEPOSIT_DISCRIMINATOR_HEX: &str = "03";
/// Raydium Stable Swap `withdraw` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_WITHDRAW_DISCRIMINATOR_HEX: &str = "04";
/// Raydium Stable Swap `monitor_step` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_MONITOR_STEP_DISCRIMINATOR_HEX: &str = "05";
/// Raydium Stable Swap `set_params` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_SET_PARAMS_DISCRIMINATOR_HEX: &str = "06";
/// Raydium Stable Swap `withdraw_pnl` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_WITHDRAW_PNL_DISCRIMINATOR_HEX: &str = "07";
/// Raydium Stable Swap `withdraw_srm` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_WITHDRAW_SRM_DISCRIMINATOR_HEX: &str = "08";
/// Raydium Stable Swap `swap_base_in` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_SWAP_BASE_IN_DISCRIMINATOR_HEX: &str = "09";
/// Raydium Stable Swap `pre_initialize` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_PRE_INITIALIZE_DISCRIMINATOR_HEX: &str = "0a";
/// Raydium Stable Swap `swap_base_out` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_SWAP_BASE_OUT_DISCRIMINATOR_HEX: &str = "0b";
/// Raydium Stable Swap `simulate_info` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_SIMULATE_INFO_DISCRIMINATOR_HEX: &str = "0c";
/// Raydium Stable Swap `admin_cancel_orders` one-byte discriminator observed locally.
const RAYDIUM_STABLE_SWAP_ADMIN_CANCEL_ORDERS_DISCRIMINATOR_HEX: &str = "0d";
/// Raydium Stable Swap `SwapEvent` discriminator reused by Raydium CPMM logs.
const RAYDIUM_STABLE_SWAP_SWAP_EVENT_DISCRIMINATOR_HEX: &str = "40c6cde8260871e2";

/// Raydium Stable Swap decoded event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum RaydiumStableSwapDecodedEvent {
    /// Known Stable Swap instruction decoded from the local one-byte layout.
    Instruction(std::boxed::Box<RaydiumStableSwapInstructionDecoded>),
    /// Program-data swap event retained as decoded-only audit evidence.
    SwapEvent(std::boxed::Box<RaydiumStableSwapSwapEventDecoded>),
}

/// Decoded Raydium Stable Swap instruction.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RaydiumStableSwapInstructionDecoded {
    /// Parent transaction id.
    pub transaction_id: i64,
    /// Parent instruction id.
    pub instruction_id: i64,
    /// Transaction signature.
    pub signature: std::string::String,
    /// Program id.
    pub program_id: std::string::String,
    /// Local decoded event kind.
    pub event_kind: std::string::String,
    /// Upstream/local instruction name.
    pub instruction_name: std::string::String,
    /// One-byte instruction discriminator in hexadecimal form.
    pub discriminator_hex: std::string::String,
    /// Optional pool/AMM account.
    pub pool_account: std::option::Option<std::string::String>,
    /// Optional market account.
    pub market_account: std::option::Option<std::string::String>,
    /// Optional LP mint account.
    pub lp_mint: std::option::Option<std::string::String>,
    /// Optional normalized base mint.
    pub token_a_mint: std::option::Option<std::string::String>,
    /// Optional normalized quote mint.
    pub token_b_mint: std::option::Option<std::string::String>,
    /// Optional normalized base vault.
    pub base_vault: std::option::Option<std::string::String>,
    /// Optional normalized quote vault.
    pub quote_vault: std::option::Option<std::string::String>,
    /// Decoded and enrichment-ready payload.
    pub payload_json: serde_json::Value,
}

/// Decoded Raydium Stable Swap program-data swap event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RaydiumStableSwapSwapEventDecoded {
    /// Event discriminator in hexadecimal form.
    pub event_discriminator_hex: std::string::String,
    /// Raydium stable-swap event-local dex flag.
    pub dex: u8,
    /// Raw input amount emitted by the event.
    pub amount_in_raw: std::string::String,
    /// Raw output amount emitted by the event.
    pub amount_out_raw: std::string::String,
    /// Whether the event can materialize as a trade.
    pub trade_candidate: bool,
    /// Whether the event can materialize as a candle.
    pub candle_candidate: bool,
    /// Reason why trade materialization is skipped.
    pub skip_trade_reason: std::string::String,
    /// Reason why candle materialization is skipped.
    pub skip_candle_reason: std::string::String,
}

/// Raydium Stable Swap decoder.
#[derive(Debug, Clone, Default)]
pub struct RaydiumStableSwapDecoder;

impl RaydiumStableSwapDecoder {
    /// Creates a new Raydium Stable Swap decoder.
    pub fn new() -> Self {
        return Self;
    }

    /// Decodes all Raydium Stable Swap instructions in one projected transaction.
    pub fn decode_transaction(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::RaydiumStableSwapDecodedEvent>, crate::Error> {
        let transaction_id = match transaction.id {
            Some(transaction_id) => transaction_id,
            None => {
                return Err(crate::Error::InvalidState(format!(
                    "chain transaction '{}' has no internal id",
                    transaction.signature
                )));
            },
        };
        let transaction_json_value = parse_json_value(transaction.transaction_json.as_str());
        let meta_json_value = parse_optional_json_value(transaction.meta_json.as_deref());
        let account_keys = extract_transaction_account_keys(
            transaction_json_value.as_ref(),
            meta_json_value.as_ref(),
        );
        let token_mints_by_account = extract_token_mints_by_account(
            transaction_json_value.as_ref(),
            meta_json_value.as_ref(),
            account_keys.as_slice(),
        );
        let token_balance_records = extract_token_balance_records(
            transaction_json_value.as_ref(),
            meta_json_value.as_ref(),
            account_keys.as_slice(),
        );
        let mut decoded_events = std::vec::Vec::new();
        for instruction in instructions {
            let program_id = match instruction.program_id.as_ref() {
                Some(program_id) => program_id,
                None => continue,
            };
            if program_id.as_str() != crate::RAYDIUM_STABLE_SWAP_AMM_PROGRAM_ID {
                continue;
            }
            let instruction_id = match instruction.id {
                Some(instruction_id) => instruction_id,
                None => continue,
            };
            let decoded = decode_raydium_stable_swap_instruction_with_context(
                transaction_id,
                instruction_id,
                transaction.signature.as_str(),
                program_id.as_str(),
                instruction.accounts_json.as_str(),
                instruction.data_json.as_deref(),
                &token_mints_by_account,
                token_balance_records.as_slice(),
            );
            for event in decoded {
                decoded_events.push(event);
            }
        }
        return Ok(decoded_events);
    }
}

impl RaydiumStableSwapDecodedEvent {
    /// Returns the local decoded event kind.
    pub fn event_kind(&self) -> &str {
        match self {
            Self::Instruction(event) => return event.event_kind.as_str(),
            Self::SwapEvent(_) => return "raydium_stable_swap.swap_event",
        }
    }

    /// Returns the optional pool account.
    pub fn pool_account(&self) -> std::option::Option<&str> {
        match self {
            Self::Instruction(event) => return event.pool_account.as_deref(),
            Self::SwapEvent(_) => return None,
        }
    }

    /// Returns the optional market account.
    pub fn market_account(&self) -> std::option::Option<&str> {
        match self {
            Self::Instruction(event) => return event.market_account.as_deref(),
            Self::SwapEvent(_) => return None,
        }
    }

    /// Returns the optional normalized base mint.
    pub fn base_mint(&self) -> std::option::Option<&str> {
        match self {
            Self::Instruction(event) => return event.token_a_mint.as_deref(),
            Self::SwapEvent(_) => return None,
        }
    }

    /// Returns the optional normalized quote mint.
    pub fn quote_mint(&self) -> std::option::Option<&str> {
        match self {
            Self::Instruction(event) => return event.token_b_mint.as_deref(),
            Self::SwapEvent(_) => return None,
        }
    }

    /// Returns the optional LP mint.
    pub fn lp_mint(&self) -> std::option::Option<&str> {
        match self {
            Self::Instruction(event) => return event.lp_mint.as_deref(),
            Self::SwapEvent(_) => return None,
        }
    }

    /// Serializes the decoded payload as JSON text.
    pub fn to_payload_json(&self) -> std::option::Option<std::string::String> {
        match self {
            Self::Instruction(event) => {
                let result = serde_json::to_string(&event.payload_json);
                match result {
                    Ok(payload) => return Some(payload),
                    Err(_) => return None,
                }
            },
            Self::SwapEvent(event) => {
                let result = serde_json::to_string(event);
                match result {
                    Ok(payload) => return Some(payload),
                    Err(_) => return None,
                }
            },
        }
    }

    /// Returns the parent instruction id when available.
    pub fn instruction_id(&self) -> std::option::Option<i64> {
        match self {
            Self::Instruction(event) => return Some(event.instruction_id),
            Self::SwapEvent(_) => return None,
        }
    }
}

/// Classifies one Raydium Stable Swap instruction data payload.
pub fn classify_raydium_stable_swap_instruction_data(
    data_json: &str,
) -> std::option::Option<&'static str> {
    let data = match decode_data_json_base58(data_json) {
        Some(data) => data,
        None => return None,
    };
    let discriminator = match data.first() {
        Some(discriminator) => *discriminator,
        None => return None,
    };
    return stable_instruction_name(discriminator);
}

/// Decodes Raydium Stable Swap `Program data:` event payloads.
pub fn decode_raydium_stable_swap_program_data_event(
    data_base64: &str,
) -> std::option::Option<crate::RaydiumStableSwapDecodedEvent> {
    use base64::Engine as _;
    let decoded_result = base64::engine::general_purpose::STANDARD.decode(data_base64.as_bytes());
    let data = match decoded_result {
        Ok(data) => data,
        Err(_) => return None,
    };
    if data.len() < 25 {
        return None;
    }
    let discriminator_hex = bytes_to_hex(&data[0..8]);
    if discriminator_hex != RAYDIUM_STABLE_SWAP_SWAP_EVENT_DISCRIMINATOR_HEX {
        return None;
    }
    let dex = data[8];
    let amount_in = match read_u64_le(data.as_slice(), 9) {
        Some(amount_in) => amount_in,
        None => return None,
    };
    let amount_out = match read_u64_le(data.as_slice(), 17) {
        Some(amount_out) => amount_out,
        None => return None,
    };
    return Some(crate::RaydiumStableSwapDecodedEvent::SwapEvent(std::boxed::Box::new(
        RaydiumStableSwapSwapEventDecoded {
            event_discriminator_hex: discriminator_hex,
            dex,
            amount_in_raw: amount_in.to_string(),
            amount_out_raw: amount_out.to_string(),
            trade_candidate: false,
            candle_candidate: false,
            skip_trade_reason: "raydium_stable_swap_swap_event_decoded_only".to_string(),
            skip_candle_reason: "raydium_stable_swap_swap_event_decoded_only".to_string(),
        },
    )));
}

#[allow(clippy::too_many_arguments)]
fn decode_raydium_stable_swap_instruction_with_context(
    transaction_id: i64,
    instruction_id: i64,
    signature: &str,
    program_id: &str,
    accounts_json: &str,
    data_json: std::option::Option<&str>,
    token_mints_by_account: &std::collections::BTreeMap<std::string::String, std::string::String>,
    token_balance_records: &[TokenBalanceRecord],
) -> std::vec::Vec<crate::RaydiumStableSwapDecodedEvent> {
    let data_json = match data_json {
        Some(data_json) => data_json,
        None => return std::vec::Vec::new(),
    };
    let accounts = match parse_accounts_json(accounts_json) {
        Some(accounts) => accounts,
        None => return std::vec::Vec::new(),
    };
    let data = match decode_data_json_base58(data_json) {
        Some(data) => data,
        None => return std::vec::Vec::new(),
    };
    let discriminator = match data.first() {
        Some(discriminator) => *discriminator,
        None => return std::vec::Vec::new(),
    };
    let instruction_name = match stable_instruction_name(discriminator) {
        Some(instruction_name) => instruction_name,
        None => return std::vec::Vec::new(),
    };
    let decoded = build_stable_instruction_event(
        transaction_id,
        instruction_id,
        signature,
        program_id,
        instruction_name,
        discriminator,
        accounts.as_slice(),
        data.as_slice(),
        token_mints_by_account,
        token_balance_records,
    );
    let decoded = match decoded {
        Some(decoded) => decoded,
        None => return std::vec::Vec::new(),
    };
    return vec![crate::RaydiumStableSwapDecodedEvent::Instruction(std::boxed::Box::new(decoded))];
}

#[allow(clippy::too_many_arguments)]
fn build_stable_instruction_event(
    transaction_id: i64,
    instruction_id: i64,
    signature: &str,
    program_id: &str,
    instruction_name: &str,
    discriminator: u8,
    accounts: &[std::string::String],
    data: &[u8],
    token_mints_by_account: &std::collections::BTreeMap<std::string::String, std::string::String>,
    token_balance_records: &[TokenBalanceRecord],
) -> std::option::Option<RaydiumStableSwapInstructionDecoded> {
    let event_kind = format!("raydium_stable_swap.{}", instruction_name);
    let discriminator_hex = match stable_instruction_discriminator_hex(discriminator) {
        Some(discriminator_hex) => discriminator_hex.to_string(),
        None => format!("{:02x}", discriminator),
    };
    let mut payload = serde_json::Map::new();
    payload.insert(
        "instructionName".to_string(),
        serde_json::Value::String(instruction_name.to_string()),
    );
    payload.insert("eventKind".to_string(), serde_json::Value::String(event_kind.clone()));
    payload.insert(
        "discriminatorHex".to_string(),
        serde_json::Value::String(discriminator_hex.clone()),
    );
    payload.insert("programId".to_string(), serde_json::Value::String(program_id.to_string()));
    payload.insert(
        "stableLayout".to_string(),
        serde_json::Value::String("pinax_one_byte".to_string()),
    );

    let pool_account: std::option::Option<std::string::String>;
    let market_account: std::option::Option<std::string::String>;
    let lp_mint: std::option::Option<std::string::String>;
    let token_a_mint: std::option::Option<std::string::String>;
    let token_b_mint: std::option::Option<std::string::String>;
    let base_vault: std::option::Option<std::string::String>;
    let quote_vault: std::option::Option<std::string::String>;

    match instruction_name {
        "initialize" => {
            pool_account = account_at(accounts, 3);
            lp_mint = account_at(accounts, 6);
            let coin_mint = account_at(accounts, 7);
            let pc_mint = account_at(accounts, 8);
            let coin_vault = account_at(accounts, 9);
            let pc_vault = account_at(accounts, 10);
            market_account = account_at(accounts, 14);
            let normalized = normalize_pair_with_vaults(
                coin_mint.clone(),
                pc_mint.clone(),
                coin_vault.clone(),
                pc_vault.clone(),
            );
            token_a_mint = normalized.base_mint;
            token_b_mint = normalized.quote_mint;
            base_vault = normalized.base_vault;
            quote_vault = normalized.quote_vault;
            insert_optional_string(&mut payload, "poolAccount", pool_account.clone());
            insert_optional_string(&mut payload, "lpMint", lp_mint.clone());
            insert_optional_string(&mut payload, "coinMint", coin_mint);
            insert_optional_string(&mut payload, "pcMint", pc_mint);
            insert_optional_string(&mut payload, "baseVault", base_vault.clone());
            insert_optional_string(&mut payload, "quoteVault", quote_vault.clone());
            insert_optional_string(&mut payload, "marketAccount", market_account.clone());
            insert_optional_u8(&mut payload, "nonce", data.get(1).copied());
            insert_optional_u64(&mut payload, "openTimeRaw", read_u64_le(data, 2));
        },
        "pre_initialize" => {
            pool_account = None;
            lp_mint = account_at(accounts, 5);
            let coin_mint = account_at(accounts, 6);
            let pc_mint = account_at(accounts, 7);
            let coin_vault = account_at(accounts, 8);
            let pc_vault = account_at(accounts, 9);
            market_account = account_at(accounts, 10);
            let normalized = normalize_pair_with_vaults(
                coin_mint.clone(),
                pc_mint.clone(),
                coin_vault.clone(),
                pc_vault.clone(),
            );
            token_a_mint = normalized.base_mint;
            token_b_mint = normalized.quote_mint;
            base_vault = normalized.base_vault;
            quote_vault = normalized.quote_vault;
            insert_optional_string(&mut payload, "lpMint", lp_mint.clone());
            insert_optional_string(&mut payload, "coinMint", coin_mint);
            insert_optional_string(&mut payload, "pcMint", pc_mint);
            insert_optional_string(&mut payload, "baseVault", base_vault.clone());
            insert_optional_string(&mut payload, "quoteVault", quote_vault.clone());
            insert_optional_string(&mut payload, "marketAccount", market_account.clone());
            insert_optional_u8(&mut payload, "nonce", data.get(1).copied());
            payload.insert(
                "skipLifecycleReason".to_string(),
                serde_json::Value::String("stable_pre_initialize_partial_pool_context".to_string()),
            );
        },
        "deposit" => {
            pool_account = account_at(accounts, 1);
            lp_mint = account_at(accounts, 5);
            let coin_vault = account_at(accounts, 6);
            let pc_vault = account_at(accounts, 7);
            market_account = account_at(accounts, 9);
            let coin_mint = mint_for_account(coin_vault.as_deref(), token_mints_by_account);
            let pc_mint = mint_for_account(pc_vault.as_deref(), token_mints_by_account);
            let normalized = normalize_pair_with_vaults(
                coin_mint.clone(),
                pc_mint.clone(),
                coin_vault.clone(),
                pc_vault.clone(),
            );
            token_a_mint = normalized.base_mint;
            token_b_mint = normalized.quote_mint;
            base_vault = normalized.base_vault;
            quote_vault = normalized.quote_vault;
            insert_optional_string(&mut payload, "poolAccount", pool_account.clone());
            insert_optional_string(&mut payload, "lpMint", lp_mint.clone());
            insert_optional_string(&mut payload, "baseVault", base_vault.clone());
            insert_optional_string(&mut payload, "quoteVault", quote_vault.clone());
            insert_optional_string(&mut payload, "marketAccount", market_account.clone());
            insert_optional_u64_string(&mut payload, "maxCoinAmountRaw", read_u64_le(data, 1));
            insert_optional_u64_string(&mut payload, "maxPcAmountRaw", read_u64_le(data, 9));
            insert_optional_u64_string(&mut payload, "baseSideRaw", read_u64_le(data, 17));
        },
        "withdraw" => {
            pool_account = account_at(accounts, 1);
            lp_mint = account_at(accounts, 5);
            let coin_vault = account_at(accounts, 6);
            let pc_vault = account_at(accounts, 7);
            market_account = account_at(accounts, 10);
            let coin_mint = mint_for_account(coin_vault.as_deref(), token_mints_by_account);
            let pc_mint = mint_for_account(pc_vault.as_deref(), token_mints_by_account);
            let normalized = normalize_pair_with_vaults(
                coin_mint.clone(),
                pc_mint.clone(),
                coin_vault.clone(),
                pc_vault.clone(),
            );
            token_a_mint = normalized.base_mint;
            token_b_mint = normalized.quote_mint;
            base_vault = normalized.base_vault;
            quote_vault = normalized.quote_vault;
            insert_optional_string(&mut payload, "poolAccount", pool_account.clone());
            insert_optional_string(&mut payload, "lpMint", lp_mint.clone());
            insert_optional_string(&mut payload, "baseVault", base_vault.clone());
            insert_optional_string(&mut payload, "quoteVault", quote_vault.clone());
            insert_optional_string(&mut payload, "marketAccount", market_account.clone());
            insert_optional_u64_string(&mut payload, "lpAmountRaw", read_u64_le(data, 1));
        },
        "init_model_data" | "update_model_data" => {
            pool_account = account_at(accounts, 0);
            market_account = None;
            lp_mint = None;
            token_a_mint = None;
            token_b_mint = None;
            base_vault = None;
            quote_vault = None;
            insert_optional_string(&mut payload, "modelDataAccount", pool_account.clone());
            payload.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
            payload.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
            payload.insert(
                "skipTradeReason".to_string(),
                serde_json::Value::String("stable_model_data_instruction_decoded_only".to_string()),
            );
            payload.insert(
                "skipLiquidityReason".to_string(),
                serde_json::Value::String("stable_model_data_instruction_decoded_only".to_string()),
            );
            payload.insert(
                "skipLifecycleReason".to_string(),
                serde_json::Value::String("stable_model_data_instruction_decoded_only".to_string()),
            );
        },
        "monitor_step" | "admin_cancel_orders" => {
            pool_account = account_at(accounts, 1);
            market_account = account_at(accounts, 5);
            lp_mint = None;
            token_a_mint = None;
            token_b_mint = None;
            base_vault = None;
            quote_vault = None;
            insert_optional_string(&mut payload, "poolAccount", pool_account.clone());
            insert_optional_string(&mut payload, "marketAccount", market_account.clone());
            payload.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
            payload.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
            payload.insert(
                "skipTradeReason".to_string(),
                serde_json::Value::String("stable_orderbook_instruction_decoded_only".to_string()),
            );
            payload.insert(
                "skipLiquidityReason".to_string(),
                serde_json::Value::String("stable_orderbook_instruction_decoded_only".to_string()),
            );
            payload.insert(
                "skipLifecycleReason".to_string(),
                serde_json::Value::String("stable_orderbook_instruction_decoded_only".to_string()),
            );
        },
        "set_params" => {
            pool_account = account_at(accounts, 1);
            market_account = None;
            lp_mint = None;
            token_a_mint = None;
            token_b_mint = None;
            base_vault = None;
            quote_vault = None;
            insert_optional_string(&mut payload, "poolAccount", pool_account.clone());
            payload.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
            payload.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
            payload.insert(
                "skipTradeReason".to_string(),
                serde_json::Value::String("stable_admin_instruction_decoded_only".to_string()),
            );
            payload.insert(
                "skipLiquidityReason".to_string(),
                serde_json::Value::String("stable_admin_instruction_decoded_only".to_string()),
            );
            payload.insert(
                "skipLifecycleReason".to_string(),
                serde_json::Value::String("stable_admin_instruction_decoded_only".to_string()),
            );
        },
        "withdraw_pnl" | "withdraw_srm" => {
            pool_account = account_at(accounts, 1);
            market_account = account_at(accounts, 4);
            lp_mint = None;
            token_a_mint = None;
            token_b_mint = None;
            base_vault = None;
            quote_vault = None;
            insert_optional_string(&mut payload, "poolAccount", pool_account.clone());
            insert_optional_string(&mut payload, "marketAccount", market_account.clone());
            payload.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
            payload.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
            payload.insert(
                "skipTradeReason".to_string(),
                serde_json::Value::String("stable_fee_instruction_decoded_only".to_string()),
            );
            payload.insert(
                "skipLiquidityReason".to_string(),
                serde_json::Value::String("stable_fee_instruction_decoded_only".to_string()),
            );
            payload.insert(
                "skipLifecycleReason".to_string(),
                serde_json::Value::String("stable_fee_instruction_decoded_only".to_string()),
            );
        },
        "simulate_info" => {
            pool_account = account_at(accounts, 1);
            market_account = None;
            lp_mint = None;
            token_a_mint = None;
            token_b_mint = None;
            base_vault = None;
            quote_vault = None;
            insert_optional_string(&mut payload, "poolAccount", pool_account.clone());
            payload.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
            payload.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
            payload.insert(
                "skipTradeReason".to_string(),
                serde_json::Value::String("stable_simulate_info_decoded_only".to_string()),
            );
            payload.insert(
                "skipLiquidityReason".to_string(),
                serde_json::Value::String("stable_simulate_info_decoded_only".to_string()),
            );
            payload.insert(
                "skipLifecycleReason".to_string(),
                serde_json::Value::String("stable_simulate_info_decoded_only".to_string()),
            );
        },
        "swap_base_in" | "swap_base_out" => {
            pool_account = account_at(accounts, 1);
            lp_mint = None;
            let coin_vault = account_at(accounts, 4);
            let pc_vault = account_at(accounts, 5);
            market_account = account_at(accounts, 8);
            let input_token_account = account_at(accounts, 15);
            let output_token_account = account_at(accounts, 16);
            let coin_mint = mint_for_account(coin_vault.as_deref(), token_mints_by_account);
            let pc_mint = mint_for_account(pc_vault.as_deref(), token_mints_by_account);
            let input_mint =
                mint_for_account(input_token_account.as_deref(), token_mints_by_account);
            let output_mint =
                mint_for_account(output_token_account.as_deref(), token_mints_by_account);
            let normalized = normalize_pair_with_vaults(
                coin_mint.clone(),
                pc_mint.clone(),
                coin_vault.clone(),
                pc_vault.clone(),
            );
            token_a_mint = normalized.base_mint;
            token_b_mint = normalized.quote_mint;
            base_vault = normalized.base_vault;
            quote_vault = normalized.quote_vault;
            insert_optional_string(&mut payload, "poolAccount", pool_account.clone());
            insert_optional_string(&mut payload, "baseVault", base_vault.clone());
            insert_optional_string(&mut payload, "quoteVault", quote_vault.clone());
            insert_optional_string(&mut payload, "marketAccount", market_account.clone());
            insert_optional_string(&mut payload, "inputTokenAccount", input_token_account);
            insert_optional_string(&mut payload, "outputTokenAccount", output_token_account);
            insert_optional_string(&mut payload, "inputMint", input_mint.clone());
            insert_optional_string(&mut payload, "outputMint", output_mint.clone());
            let amount_in_or_max = read_u64_le(data, 1);
            let amount_out_or_min = read_u64_le(data, 9);
            if instruction_name == "swap_base_in" {
                insert_optional_u64_string(&mut payload, "amountInRaw", amount_in_or_max);
                insert_optional_u64_string(&mut payload, "minimumAmountOutRaw", amount_out_or_min);
            } else {
                insert_optional_u64_string(&mut payload, "maxAmountInRaw", amount_in_or_max);
                insert_optional_u64_string(&mut payload, "amountOutRaw", amount_out_or_min);
            }
            let instruction_amounts = map_swap_amounts(
                instruction_name,
                amount_in_or_max,
                amount_out_or_min,
                input_mint.as_deref(),
                output_mint.as_deref(),
                token_a_mint.as_deref(),
                token_b_mint.as_deref(),
            );
            if let Some(instruction_amounts) = instruction_amounts {
                insert_optional_string(
                    &mut payload,
                    "instructionTradeSide",
                    Some(instruction_amounts.trade_side.clone()),
                );
                insert_optional_string(
                    &mut payload,
                    "instructionBoundBaseAmountRaw",
                    Some(instruction_amounts.base_amount_raw),
                );
                insert_optional_string(
                    &mut payload,
                    "instructionBoundQuoteAmountRaw",
                    Some(instruction_amounts.quote_amount_raw),
                );
            }
            let exact_amounts = resolve_stable_swap_vault_delta_amounts(
                base_vault.as_deref(),
                quote_vault.as_deref(),
                token_balance_records,
            );
            match exact_amounts {
                Some(exact_amounts) => {
                    insert_optional_string(
                        &mut payload,
                        "tradeSide",
                        Some(exact_amounts.trade_side),
                    );
                    insert_optional_string(
                        &mut payload,
                        "baseAmountRaw",
                        Some(exact_amounts.base_amount_raw),
                    );
                    insert_optional_string(
                        &mut payload,
                        "quoteAmountRaw",
                        Some(exact_amounts.quote_amount_raw),
                    );
                    payload.insert("tradeCandidate".to_string(), serde_json::Value::Bool(true));
                    payload.insert("candleCandidate".to_string(), serde_json::Value::Bool(true));
                    payload.insert(
                        "amountSource".to_string(),
                        serde_json::Value::String("stable_swap_vault_balance_delta".to_string()),
                    );
                    payload.insert("skipTradeReason".to_string(), serde_json::Value::Null);
                    payload.insert("skipCandleReason".to_string(), serde_json::Value::Null);
                },
                None => {
                    payload.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
                    payload.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
                    payload.insert(
                        "amountSource".to_string(),
                        serde_json::Value::String(
                            "stable_swap_instruction_bounds_only".to_string(),
                        ),
                    );
                    payload.insert(
                        "skipTradeReason".to_string(),
                        serde_json::Value::String(
                            "stable_swap_exact_amounts_unresolved".to_string(),
                        ),
                    );
                    payload.insert(
                        "skipCandleReason".to_string(),
                        serde_json::Value::String(
                            "stable_swap_exact_amounts_unresolved".to_string(),
                        ),
                    );
                },
            }
        },
        _ => return None,
    }

    insert_optional_string(&mut payload, "tokenAMint", token_a_mint.clone());
    insert_optional_string(&mut payload, "tokenBMint", token_b_mint.clone());
    return Some(RaydiumStableSwapInstructionDecoded {
        transaction_id,
        instruction_id,
        signature: signature.to_string(),
        program_id: program_id.to_string(),
        event_kind,
        instruction_name: instruction_name.to_string(),
        discriminator_hex,
        pool_account,
        market_account,
        lp_mint,
        token_a_mint,
        token_b_mint,
        base_vault,
        quote_vault,
        payload_json: serde_json::Value::Object(payload),
    });
}

fn stable_instruction_name(discriminator: u8) -> std::option::Option<&'static str> {
    match discriminator {
        0 => return Some("initialize"),
        1 => return Some("init_model_data"),
        2 => return Some("update_model_data"),
        3 => return Some("deposit"),
        4 => return Some("withdraw"),
        5 => return Some("monitor_step"),
        6 => return Some("set_params"),
        7 => return Some("withdraw_pnl"),
        8 => return Some("withdraw_srm"),
        9 => return Some("swap_base_in"),
        10 => return Some("pre_initialize"),
        11 => return Some("swap_base_out"),
        12 => return Some("simulate_info"),
        13 => return Some("admin_cancel_orders"),
        _ => return None,
    }
}

fn stable_instruction_discriminator_hex(discriminator: u8) -> std::option::Option<&'static str> {
    match discriminator {
        0 => return Some(RAYDIUM_STABLE_SWAP_INITIALIZE_DISCRIMINATOR_HEX),
        1 => return Some(RAYDIUM_STABLE_SWAP_INIT_MODEL_DATA_DISCRIMINATOR_HEX),
        2 => return Some(RAYDIUM_STABLE_SWAP_UPDATE_MODEL_DATA_DISCRIMINATOR_HEX),
        3 => return Some(RAYDIUM_STABLE_SWAP_DEPOSIT_DISCRIMINATOR_HEX),
        4 => return Some(RAYDIUM_STABLE_SWAP_WITHDRAW_DISCRIMINATOR_HEX),
        5 => return Some(RAYDIUM_STABLE_SWAP_MONITOR_STEP_DISCRIMINATOR_HEX),
        6 => return Some(RAYDIUM_STABLE_SWAP_SET_PARAMS_DISCRIMINATOR_HEX),
        7 => return Some(RAYDIUM_STABLE_SWAP_WITHDRAW_PNL_DISCRIMINATOR_HEX),
        8 => return Some(RAYDIUM_STABLE_SWAP_WITHDRAW_SRM_DISCRIMINATOR_HEX),
        9 => return Some(RAYDIUM_STABLE_SWAP_SWAP_BASE_IN_DISCRIMINATOR_HEX),
        10 => return Some(RAYDIUM_STABLE_SWAP_PRE_INITIALIZE_DISCRIMINATOR_HEX),
        11 => return Some(RAYDIUM_STABLE_SWAP_SWAP_BASE_OUT_DISCRIMINATOR_HEX),
        12 => return Some(RAYDIUM_STABLE_SWAP_SIMULATE_INFO_DISCRIMINATOR_HEX),
        13 => return Some(RAYDIUM_STABLE_SWAP_ADMIN_CANCEL_ORDERS_DISCRIMINATOR_HEX),
        _ => return None,
    }
}

#[derive(Debug, Clone)]
struct NormalizedVaultPair {
    base_mint: std::option::Option<std::string::String>,
    quote_mint: std::option::Option<std::string::String>,
    base_vault: std::option::Option<std::string::String>,
    quote_vault: std::option::Option<std::string::String>,
}

fn normalize_pair_with_vaults(
    coin_mint: std::option::Option<std::string::String>,
    pc_mint: std::option::Option<std::string::String>,
    coin_vault: std::option::Option<std::string::String>,
    pc_vault: std::option::Option<std::string::String>,
) -> NormalizedVaultPair {
    let coin_mint_ref = match coin_mint.as_ref() {
        Some(coin_mint) => coin_mint.as_str(),
        None => {
            return NormalizedVaultPair {
                base_mint: None,
                quote_mint: None,
                base_vault: None,
                quote_vault: None,
            };
        },
    };
    let pc_mint_ref = match pc_mint.as_ref() {
        Some(pc_mint) => pc_mint.as_str(),
        None => {
            return NormalizedVaultPair {
                base_mint: None,
                quote_mint: None,
                base_vault: None,
                quote_vault: None,
            };
        },
    };
    let coin_is_quote = is_quote_mint(coin_mint_ref);
    let pc_is_quote = is_quote_mint(pc_mint_ref);
    let pc_is_normalized_quote = if pc_is_quote && !coin_is_quote {
        true
    } else if coin_is_quote && !pc_is_quote {
        false
    } else {
        pc_mint_ref <= coin_mint_ref
    };
    if pc_is_normalized_quote {
        return NormalizedVaultPair {
            base_mint: coin_mint,
            quote_mint: pc_mint,
            base_vault: coin_vault,
            quote_vault: pc_vault,
        };
    }
    return NormalizedVaultPair {
        base_mint: pc_mint,
        quote_mint: coin_mint,
        base_vault: pc_vault,
        quote_vault: coin_vault,
    };
}

fn is_quote_mint(mint: &str) -> bool {
    return mint == crate::WSOL_MINT_ID
        || mint == crate::USDC_MINT_ID
        || mint == crate::USDT_MINT_ID;
}

#[derive(Debug, Clone)]
struct StableSwapAmounts {
    trade_side: std::string::String,
    base_amount_raw: std::string::String,
    quote_amount_raw: std::string::String,
}

fn map_swap_amounts(
    instruction_name: &str,
    first_amount: std::option::Option<u64>,
    second_amount: std::option::Option<u64>,
    input_mint: std::option::Option<&str>,
    output_mint: std::option::Option<&str>,
    base_mint: std::option::Option<&str>,
    quote_mint: std::option::Option<&str>,
) -> std::option::Option<StableSwapAmounts> {
    let first_amount = match first_amount {
        Some(first_amount) => first_amount,
        None => return None,
    };
    let second_amount = match second_amount {
        Some(second_amount) => second_amount,
        None => return None,
    };
    let input_mint = match input_mint {
        Some(input_mint) => input_mint,
        None => return None,
    };
    if output_mint.is_none() {
        return None;
    }
    let base_mint = match base_mint {
        Some(base_mint) => base_mint,
        None => return None,
    };
    let quote_mint = match quote_mint {
        Some(quote_mint) => quote_mint,
        None => return None,
    };
    if instruction_name == "swap_base_in" {
        if input_mint == base_mint {
            return Some(StableSwapAmounts {
                trade_side: "sell".to_string(),
                base_amount_raw: first_amount.to_string(),
                quote_amount_raw: second_amount.to_string(),
            });
        }
        if input_mint == quote_mint {
            return Some(StableSwapAmounts {
                trade_side: "buy".to_string(),
                base_amount_raw: second_amount.to_string(),
                quote_amount_raw: first_amount.to_string(),
            });
        }
    }
    if instruction_name == "swap_base_out" {
        if input_mint == base_mint {
            return Some(StableSwapAmounts {
                trade_side: "sell".to_string(),
                base_amount_raw: first_amount.to_string(),
                quote_amount_raw: second_amount.to_string(),
            });
        }
        if input_mint == quote_mint {
            return Some(StableSwapAmounts {
                trade_side: "buy".to_string(),
                base_amount_raw: second_amount.to_string(),
                quote_amount_raw: first_amount.to_string(),
            });
        }
    }
    return None;
}

#[derive(Debug, Clone)]
struct StableSwapExactAmounts {
    trade_side: std::string::String,
    base_amount_raw: std::string::String,
    quote_amount_raw: std::string::String,
}

#[derive(Debug, Clone)]
struct TokenBalanceRecord {
    account_address: std::option::Option<std::string::String>,
    pre_amount_raw: std::option::Option<std::string::String>,
    post_amount_raw: std::option::Option<std::string::String>,
}

#[derive(Debug, Clone)]
struct TokenBalanceAccumulator {
    account_index: std::option::Option<u64>,
    account_address: std::option::Option<std::string::String>,
    mint: std::string::String,
    pre_amount_raw: std::option::Option<std::string::String>,
    post_amount_raw: std::option::Option<std::string::String>,
}

impl TokenBalanceRecord {
    fn delta_raw(&self) -> std::option::Option<i128> {
        let pre = parse_i128_or_zero(self.pre_amount_raw.as_deref());
        let post = parse_i128_or_zero(self.post_amount_raw.as_deref());
        match (pre, post) {
            (Some(pre), Some(post)) => return Some(post - pre),
            _ => return None,
        }
    }
}

fn resolve_stable_swap_vault_delta_amounts(
    base_vault: std::option::Option<&str>,
    quote_vault: std::option::Option<&str>,
    token_balance_records: &[TokenBalanceRecord],
) -> std::option::Option<StableSwapExactAmounts> {
    let base_vault = match base_vault {
        Some(base_vault) => base_vault,
        None => return None,
    };
    let quote_vault = match quote_vault {
        Some(quote_vault) => quote_vault,
        None => return None,
    };
    let base_record = token_balance_record_for_account(token_balance_records, base_vault);
    let base_record = match base_record {
        Some(base_record) => base_record,
        None => return None,
    };
    let quote_record = token_balance_record_for_account(token_balance_records, quote_vault);
    let quote_record = match quote_record {
        Some(quote_record) => quote_record,
        None => return None,
    };
    let base_delta = match base_record.delta_raw() {
        Some(base_delta) => base_delta,
        None => return None,
    };
    let quote_delta = match quote_record.delta_raw() {
        Some(quote_delta) => quote_delta,
        None => return None,
    };
    if base_delta == 0 || quote_delta == 0 {
        return None;
    }
    if base_delta < 0 && quote_delta > 0 {
        return Some(StableSwapExactAmounts {
            trade_side: "buy".to_string(),
            base_amount_raw: (-base_delta).to_string(),
            quote_amount_raw: quote_delta.to_string(),
        });
    }
    if base_delta > 0 && quote_delta < 0 {
        return Some(StableSwapExactAmounts {
            trade_side: "sell".to_string(),
            base_amount_raw: base_delta.to_string(),
            quote_amount_raw: (-quote_delta).to_string(),
        });
    }
    return None;
}

fn token_balance_record_for_account<'a>(
    token_balance_records: &'a [TokenBalanceRecord],
    account: &str,
) -> std::option::Option<&'a TokenBalanceRecord> {
    for record in token_balance_records {
        if record.account_address.as_deref() == Some(account) {
            return Some(record);
        }
    }
    return None;
}

fn parse_i128_or_zero(value: std::option::Option<&str>) -> std::option::Option<i128> {
    let value = match value {
        Some(value) => value.trim(),
        None => return Some(0),
    };
    if value.is_empty() {
        return Some(0);
    }
    let parsed = value.parse::<i128>();
    match parsed {
        Ok(parsed) => return Some(parsed),
        Err(_) => return None,
    }
}

fn parse_accounts_json(
    accounts_json: &str,
) -> std::option::Option<std::vec::Vec<std::string::String>> {
    let value = match serde_json::from_str::<serde_json::Value>(accounts_json) {
        Ok(value) => value,
        Err(_) => return None,
    };
    let array = match value.as_array() {
        Some(array) => array,
        None => return None,
    };
    let mut accounts = std::vec::Vec::new();
    for item in array {
        if let Some(text) = item.as_str() {
            accounts.push(text.to_string());
        } else if let Some(pubkey) = item.get("pubkey").and_then(|value| return value.as_str()) {
            accounts.push(pubkey.to_string());
        }
    }
    return Some(accounts);
}

fn decode_data_json_base58(data_json: &str) -> std::option::Option<std::vec::Vec<u8>> {
    let value = match serde_json::from_str::<serde_json::Value>(data_json) {
        Ok(value) => value,
        Err(_) => return None,
    };
    let data_base58 = if let Some(text) = value.as_str() {
        text.to_string()
    } else if let Some(text) = value.get("data").and_then(|value| return value.as_str()) {
        text.to_string()
    } else {
        return None;
    };
    let decoded = bs58::decode(data_base58.as_str()).into_vec();
    match decoded {
        Ok(decoded) => return Some(decoded),
        Err(_) => return None,
    }
}

fn account_at(
    accounts: &[std::string::String],
    index: usize,
) -> std::option::Option<std::string::String> {
    return accounts.get(index).cloned();
}

fn read_u64_le(data: &[u8], offset: usize) -> std::option::Option<u64> {
    let end = offset.saturating_add(8);
    if data.len() < end {
        return None;
    }
    let bytes = [
        data[offset],
        data[offset + 1],
        data[offset + 2],
        data[offset + 3],
        data[offset + 4],
        data[offset + 5],
        data[offset + 6],
        data[offset + 7],
    ];
    return Some(u64::from_le_bytes(bytes));
}

fn parse_json_value(input: &str) -> std::option::Option<serde_json::Value> {
    let parsed = serde_json::from_str::<serde_json::Value>(input);
    match parsed {
        Ok(parsed) => return Some(parsed),
        Err(_) => return None,
    }
}

fn parse_optional_json_value(
    input: std::option::Option<&str>,
) -> std::option::Option<serde_json::Value> {
    let input = match input {
        Some(input) => input,
        None => return None,
    };
    return parse_json_value(input);
}

fn extract_transaction_account_keys(
    transaction_json: std::option::Option<&serde_json::Value>,
    meta_json: std::option::Option<&serde_json::Value>,
) -> std::vec::Vec<std::string::String> {
    let mut account_keys = std::vec::Vec::new();
    let message = transaction_json
        .and_then(|value| return value.get("transaction"))
        .and_then(|value| return value.get("message"));
    if let Some(message) = message {
        extract_account_keys_from_message(message, &mut account_keys);
    }
    if account_keys.is_empty() {
        let message = transaction_json.and_then(|value| return value.get("message"));
        if let Some(message) = message {
            extract_account_keys_from_message(message, &mut account_keys);
        }
    }
    if account_keys.is_empty() {
        let loaded = meta_json.and_then(|value| return value.get("loadedAddresses"));
        if let Some(loaded) = loaded {
            append_loaded_addresses(loaded, &mut account_keys);
        }
    }
    return account_keys;
}

fn extract_account_keys_from_message(
    message: &serde_json::Value,
    account_keys: &mut std::vec::Vec<std::string::String>,
) {
    let keys = match message.get("accountKeys").and_then(|value| return value.as_array()) {
        Some(keys) => keys,
        None => return,
    };
    for key in keys {
        if let Some(text) = key.as_str() {
            account_keys.push(text.to_string());
        } else if let Some(pubkey) = key.get("pubkey").and_then(|value| return value.as_str()) {
            account_keys.push(pubkey.to_string());
        }
    }
    if let Some(loaded) = message.get("loadedAddresses") {
        append_loaded_addresses(loaded, account_keys);
    }
}

fn append_loaded_addresses(
    loaded: &serde_json::Value,
    account_keys: &mut std::vec::Vec<std::string::String>,
) {
    for section in ["writable", "readonly"] {
        let array = loaded.get(section).and_then(|value| return value.as_array());
        let array = match array {
            Some(array) => array,
            None => continue,
        };
        for item in array {
            if let Some(text) = item.as_str() {
                account_keys.push(text.to_string());
            }
        }
    }
}

fn extract_token_balance_records(
    transaction_json: std::option::Option<&serde_json::Value>,
    meta_json: std::option::Option<&serde_json::Value>,
    account_keys: &[std::string::String],
) -> std::vec::Vec<TokenBalanceRecord> {
    let mut accumulators = std::vec::Vec::new();
    let meta_candidates = [
        meta_json,
        transaction_json.and_then(|value| return value.get("meta")),
        transaction_json
            .and_then(|value| return value.get("transaction"))
            .and_then(|value| return value.get("meta")),
    ];
    for meta in meta_candidates.iter().flatten() {
        collect_token_balance_side(
            meta.get("preTokenBalances"),
            account_keys,
            true,
            &mut accumulators,
        );
        collect_token_balance_side(
            meta.get("postTokenBalances"),
            account_keys,
            false,
            &mut accumulators,
        );
    }
    let mut records = std::vec::Vec::new();
    for accumulator in accumulators {
        records.push(TokenBalanceRecord {
            account_address: accumulator.account_address,
            pre_amount_raw: accumulator.pre_amount_raw,
            post_amount_raw: accumulator.post_amount_raw,
        });
    }
    return records;
}

fn collect_token_balance_side(
    balances: std::option::Option<&serde_json::Value>,
    account_keys: &[std::string::String],
    is_pre: bool,
    accumulators: &mut std::vec::Vec<TokenBalanceAccumulator>,
) {
    let array = balances.and_then(|value| return value.as_array());
    let array = match array {
        Some(array) => array,
        None => return,
    };
    for balance in array {
        let account_index = balance.get("accountIndex").and_then(|value| return value.as_u64());
        let mint = match balance.get("mint").and_then(|value| return value.as_str()) {
            Some(mint) => mint.to_string(),
            None => continue,
        };
        let amount = balance
            .get("uiTokenAmount")
            .and_then(|value| return value.get("amount"))
            .and_then(|value| return value.as_str())
            .map(|value| return value.to_string());
        let account_address = account_address_by_index(account_keys, account_index);
        let accumulator_index =
            find_token_balance_accumulator(accumulators.as_slice(), account_index, mint.as_str());
        let index = match accumulator_index {
            Some(index) => index,
            None => {
                accumulators.push(TokenBalanceAccumulator {
                    account_index,
                    account_address,
                    mint,
                    pre_amount_raw: None,
                    post_amount_raw: None,
                });
                accumulators.len() - 1
            },
        };
        if is_pre {
            accumulators[index].pre_amount_raw = amount;
        } else {
            accumulators[index].post_amount_raw = amount;
        }
    }
}

fn find_token_balance_accumulator(
    accumulators: &[TokenBalanceAccumulator],
    account_index: std::option::Option<u64>,
    mint: &str,
) -> std::option::Option<usize> {
    let mut index = 0usize;
    while index < accumulators.len() {
        let accumulator = &accumulators[index];
        if accumulator.account_index == account_index && accumulator.mint == mint {
            return Some(index);
        }
        index += 1;
    }
    return None;
}

fn account_address_by_index(
    account_keys: &[std::string::String],
    account_index: std::option::Option<u64>,
) -> std::option::Option<std::string::String> {
    let account_index = match account_index {
        Some(account_index) => account_index,
        None => return None,
    };
    let index = match usize::try_from(account_index) {
        Ok(index) => index,
        Err(_) => return None,
    };
    return account_keys.get(index).cloned();
}

fn extract_token_mints_by_account(
    transaction_json: std::option::Option<&serde_json::Value>,
    meta_json: std::option::Option<&serde_json::Value>,
    account_keys: &[std::string::String],
) -> std::collections::BTreeMap<std::string::String, std::string::String> {
    let mut map = std::collections::BTreeMap::new();
    let meta_candidates = [
        meta_json,
        transaction_json.and_then(|value| return value.get("meta")),
        transaction_json
            .and_then(|value| return value.get("transaction"))
            .and_then(|value| return value.get("meta")),
    ];
    for meta in meta_candidates.iter().flatten() {
        append_token_balance_mints(meta.get("preTokenBalances"), account_keys, &mut map);
        append_token_balance_mints(meta.get("postTokenBalances"), account_keys, &mut map);
    }
    return map;
}

fn append_token_balance_mints(
    balances: std::option::Option<&serde_json::Value>,
    account_keys: &[std::string::String],
    map: &mut std::collections::BTreeMap<std::string::String, std::string::String>,
) {
    let array = balances.and_then(|value| return value.as_array());
    let array = match array {
        Some(array) => array,
        None => return,
    };
    for balance in array {
        let account_index = balance.get("accountIndex").and_then(|value| return value.as_u64());
        let mint = balance.get("mint").and_then(|value| return value.as_str());
        let owner = balance.get("owner").and_then(|value| return value.as_str());
        let account_index = match account_index {
            Some(account_index) => account_index,
            None => continue,
        };
        let mint = match mint {
            Some(mint) => mint,
            None => continue,
        };
        let index_result = usize::try_from(account_index);
        let index = match index_result {
            Ok(index) => index,
            Err(_) => continue,
        };
        if let Some(account) = account_keys.get(index) {
            map.insert(account.clone(), mint.to_string());
        }
        if let Some(owner) = owner {
            map.insert(owner.to_string(), mint.to_string());
        }
    }
}

fn mint_for_account(
    account: std::option::Option<&str>,
    token_mints_by_account: &std::collections::BTreeMap<std::string::String, std::string::String>,
) -> std::option::Option<std::string::String> {
    let account = match account {
        Some(account) => account,
        None => return None,
    };
    return token_mints_by_account.get(account).cloned();
}

fn insert_optional_string(
    object: &mut serde_json::Map<std::string::String, serde_json::Value>,
    key: &str,
    value: std::option::Option<std::string::String>,
) {
    if let Some(value) = value {
        object.insert(key.to_string(), serde_json::Value::String(value));
    }
}

fn insert_optional_u8(
    object: &mut serde_json::Map<std::string::String, serde_json::Value>,
    key: &str,
    value: std::option::Option<u8>,
) {
    if let Some(value) = value {
        object.insert(key.to_string(), serde_json::Value::Number(serde_json::Number::from(value)));
    }
}

fn insert_optional_u64(
    object: &mut serde_json::Map<std::string::String, serde_json::Value>,
    key: &str,
    value: std::option::Option<u64>,
) {
    if let Some(value) = value {
        object.insert(key.to_string(), serde_json::Value::Number(serde_json::Number::from(value)));
    }
}

fn insert_optional_u64_string(
    object: &mut serde_json::Map<std::string::String, serde_json::Value>,
    key: &str,
    value: std::option::Option<u64>,
) {
    if let Some(value) = value {
        object.insert(key.to_string(), serde_json::Value::String(value.to_string()));
    }
}

fn bytes_to_hex(data: &[u8]) -> std::string::String {
    let mut output = std::string::String::new();
    for byte in data {
        output.push_str(format!("{:02x}", byte).as_str());
    }
    return output;
}

#[cfg(test)]
mod tests {
    fn encode_data(data: &[u8]) -> std::string::String {
        return bs58::encode(data).into_string();
    }

    #[test]
    fn classifies_one_byte_stable_swap_discriminators() {
        let initialize = serde_json::json!(encode_data(&[0_u8, 1_u8, 0, 0, 0, 0, 0, 0, 0, 0]));
        let deposit = serde_json::json!(encode_data(&[
            3_u8, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
        ]));
        let initialize_text = initialize.to_string();
        let deposit_text = deposit.to_string();
        assert_eq!(
            super::classify_raydium_stable_swap_instruction_data(initialize_text.as_str()),
            Some("initialize")
        );
        assert_eq!(
            super::classify_raydium_stable_swap_instruction_data(deposit_text.as_str()),
            Some("deposit")
        );
    }

    #[test]
    fn decodes_stable_swap_initialize_with_normalized_pair() {
        let accounts = serde_json::json!([
            "TokenProgram1111111111111111111111111111111111",
            "System111111111111111111111111111111111111",
            "Rent111111111111111111111111111111111111111",
            "StablePool11111111111111111111111111111111111",
            "Authority111111111111111111111111111111111111",
            "OpenOrders1111111111111111111111111111111111",
            "LpMint11111111111111111111111111111111111111",
            crate::USDT_MINT_ID,
            crate::USDC_MINT_ID,
            "CoinVault1111111111111111111111111111111111",
            "PcVault11111111111111111111111111111111111",
            "TargetOrders11111111111111111111111111111111",
            "ModelData1111111111111111111111111111111111",
            "SerumProgram111111111111111111111111111111",
            "SerumMarket1111111111111111111111111111111",
            "UserLp111111111111111111111111111111111111",
            "Wallet111111111111111111111111111111111111"
        ]);
        let mut data = vec![0_u8, 1_u8];
        data.extend_from_slice(&123_u64.to_le_bytes());
        let events = super::decode_raydium_stable_swap_instruction_with_context(
            1,
            2,
            "sig",
            crate::RAYDIUM_STABLE_SWAP_AMM_PROGRAM_ID,
            accounts.to_string().as_str(),
            Some(serde_json::json!(encode_data(data.as_slice())).to_string().as_str()),
            &std::collections::BTreeMap::new(),
            &[],
        );
        assert_eq!(events.len(), 1);
        match &events[0] {
            crate::RaydiumStableSwapDecodedEvent::Instruction(event) => {
                assert_eq!(event.event_kind, "raydium_stable_swap.initialize");
                assert_eq!(
                    event.pool_account.as_deref(),
                    Some("StablePool11111111111111111111111111111111111")
                );
                assert_eq!(event.token_b_mint.as_deref(), Some(crate::USDC_MINT_ID));
            },
            _ => panic!("expected instruction event"),
        }
    }
}
