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

//! Raydium CLMM instruction decoder.

const RAYDIUM_CLMM_SWAP_V2_DISCRIMINATOR: [u8; 8] = [43, 4, 237, 11, 26, 201, 30, 98];

const RAYDIUM_CLMM_SWAP_LEGACY_DISCRIMINATOR: [u8; 8] = [248, 198, 158, 145, 225, 117, 135, 200];

const OBSERVED_JUPITER_V6_PROGRAM_ID: &str = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4";

/// Decoded Raydium CLMM event.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
pub enum RaydiumClmmDecodedEvent {
    /// Raydium CLMM legacy swap event.
    Swap(crate::RaydiumClmmSwapLegacyDecoded),
    /// Raydium CLMM swap_v2 event.
    SwapV2(crate::RaydiumClmmSwapV2Decoded),
}

impl RaydiumClmmDecodedEvent {
    /// Returns the normalized event kind.
    pub fn event_kind(&self) -> &'static str {
        match self {
            crate::RaydiumClmmDecodedEvent::Swap(_) => return "raydium_clmm.swap",
            crate::RaydiumClmmDecodedEvent::SwapV2(_) => return "raydium_clmm.swap_v2",
        }
    }

    /// Returns the pool account.
    pub fn pool_account(&self) -> &str {
        match self {
            crate::RaydiumClmmDecodedEvent::Swap(event) => return event.pool_state.as_str(),
            crate::RaydiumClmmDecodedEvent::SwapV2(event) => return event.pool_state.as_str(),
        }
    }

    /// Returns the normalized base mint.
    pub fn base_mint(&self) -> &str {
        match self {
            crate::RaydiumClmmDecodedEvent::Swap(event) => return event.base_mint.as_str(),
            crate::RaydiumClmmDecodedEvent::SwapV2(event) => return event.base_mint.as_str(),
        }
    }

    /// Returns the normalized quote mint.
    pub fn quote_mint(&self) -> &str {
        match self {
            crate::RaydiumClmmDecodedEvent::Swap(event) => return event.quote_mint.as_str(),
            crate::RaydiumClmmDecodedEvent::SwapV2(event) => return event.quote_mint.as_str(),
        }
    }

    /// Converts the decoded event to JSON payload.
    pub fn to_payload_json(&self) -> std::option::Option<std::string::String> {
        match self {
            crate::RaydiumClmmDecodedEvent::Swap(event) => {
                let result = serde_json::to_string(event);
                match result {
                    Ok(payload_json) => return Some(payload_json),
                    Err(_) => return None,
                }
            },
            crate::RaydiumClmmDecodedEvent::SwapV2(event) => {
                let result = serde_json::to_string(event);
                match result {
                    Ok(payload_json) => return Some(payload_json),
                    Err(_) => return None,
                }
            },
        }
    }
}

/// One decoded Raydium CLMM instruction associated with its projected instruction id.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct RaydiumClmmDecodedInstructionEvent {
    /// Projected chain instruction id.
    pub instruction_id: i64,
    /// Decoded Raydium CLMM event.
    pub decoded_event: crate::RaydiumClmmDecodedEvent,
}

/// Decoded Raydium CLMM legacy swap instruction.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct RaydiumClmmSwapLegacyDecoded {
    /// User performing the swap.
    pub payer: std::string::String,
    /// AMM config account.
    pub amm_config: std::string::String,
    /// CLMM pool state account.
    pub pool_state: std::string::String,
    /// User input token account.
    pub input_token_account: std::string::String,
    /// User output token account.
    pub output_token_account: std::string::String,
    /// Pool input vault.
    pub input_vault: std::string::String,
    /// Pool output vault.
    pub output_vault: std::string::String,
    /// Pool oracle observation state.
    pub observation_state: std::string::String,
    /// Input vault mint inferred from transaction token balances.
    pub input_vault_mint: std::string::String,
    /// Output vault mint inferred from transaction token balances.
    pub output_vault_mint: std::string::String,
    /// Canonical base mint.
    pub base_mint: std::string::String,
    /// Canonical quote mint.
    pub quote_mint: std::string::String,
    /// Canonical base vault.
    pub base_vault: std::string::String,
    /// Canonical quote vault.
    pub quote_vault: std::string::String,
    /// Trade side relative to the canonical base mint.
    #[serde(rename = "tradeSide")]
    pub trade_side: std::string::String,
    /// Optional raw base amount inferred from vault balance deltas.
    #[serde(rename = "baseAmountRaw")]
    pub base_amount_raw: std::option::Option<std::string::String>,
    /// Optional raw quote amount inferred from vault balance deltas.
    #[serde(rename = "quoteAmountRaw")]
    pub quote_amount_raw: std::option::Option<std::string::String>,
    /// Amount argument.
    pub amount: u64,
    /// Other amount threshold argument.
    pub other_amount_threshold: u64,
    /// Sqrt price limit as decimal string.
    pub sqrt_price_limit_x64: std::string::String,
    /// Whether the instruction uses exact input mode.
    pub is_base_input: bool,
    /// Whether this decoded event comes from the legacy CLMM swap discriminator.
    #[serde(rename = "legacyInstruction")]
    pub legacy_instruction: bool,
    /// Optional top-level caller inferred from parent instruction.
    #[serde(rename = "routeSource")]
    pub route_source: std::option::Option<std::string::String>,
}

/// Decoded Raydium CLMM swap_v2 instruction.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct RaydiumClmmSwapV2Decoded {
    /// User performing the swap.
    pub payer: std::string::String,
    /// AMM config account.
    pub amm_config: std::string::String,
    /// CLMM pool state account.
    pub pool_state: std::string::String,
    /// User input token account.
    pub input_token_account: std::string::String,
    /// User output token account.
    pub output_token_account: std::string::String,
    /// Pool input vault.
    pub input_vault: std::string::String,
    /// Pool output vault.
    pub output_vault: std::string::String,
    /// Pool oracle observation state.
    pub observation_state: std::string::String,
    /// Input vault mint.
    pub input_vault_mint: std::string::String,
    /// Output vault mint.
    pub output_vault_mint: std::string::String,
    /// Canonical base mint.
    pub base_mint: std::string::String,
    /// Canonical quote mint.
    pub quote_mint: std::string::String,
    /// Canonical base vault.
    pub base_vault: std::string::String,
    /// Canonical quote vault.
    pub quote_vault: std::string::String,
    /// Trade side relative to the canonical base mint.
    #[serde(rename = "tradeSide")]
    pub trade_side: std::string::String,
    /// Amount argument.
    pub amount: u64,
    /// Other amount threshold argument.
    pub other_amount_threshold: u64,
    /// Sqrt price limit as decimal string.
    pub sqrt_price_limit_x64: std::string::String,
    /// Whether the instruction uses exact input mode.
    pub is_base_input: bool,
}

/// Raydium CLMM transaction decoder.
#[derive(Clone, Debug, Default)]
pub struct RaydiumClmmDecoder;

impl RaydiumClmmDecoder {
    /// Creates a new Raydium CLMM decoder.
    pub fn new() -> Self {
        return Self;
    }

    /// Decodes one projected transaction into Raydium CLMM instruction events.
    pub fn decode_transaction(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::RaydiumClmmDecodedInstructionEvent>, crate::Error> {
        let transaction_json_result =
            serde_json::from_str::<serde_json::Value>(transaction.transaction_json.as_str());
        let transaction_json = match transaction_json_result {
            Ok(transaction_json) => transaction_json,
            Err(error) => {
                return Err(crate::Error::Json(format!(
                    "cannot parse transaction_json for signature '{}': {}",
                    transaction.signature, error
                )));
            },
        };
        let meta_value_result = parse_transaction_meta_value(transaction, &transaction_json);
        let meta_value = match meta_value_result {
            Ok(meta_value) => meta_value,
            Err(error) => return Err(error),
        };
        let account_keys = extract_transaction_account_keys(&transaction_json, meta_value.as_ref());
        let token_balances =
            extract_token_balance_records(meta_value.as_ref(), account_keys.as_slice());
        let mut decoded = 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_CLMM_PROGRAM_ID {
                continue;
            }
            let instruction_id = match instruction.id {
                Some(instruction_id) => instruction_id,
                None => continue,
            };
            let data_json = match instruction.data_json.as_ref() {
                Some(data_json) => data_json,
                None => continue,
            };
            let decoded_events_result = decode_raydium_clmm_instruction_with_token_balances(
                instruction.accounts_json.as_str(),
                data_json.as_str(),
                instructions,
                instruction,
                token_balances.as_slice(),
            );
            let decoded_events = match decoded_events_result {
                Ok(decoded_events) => decoded_events,
                Err(error) => return Err(error),
            };
            for decoded_event in decoded_events {
                decoded.push(crate::RaydiumClmmDecodedInstructionEvent {
                    instruction_id,
                    decoded_event,
                });
            }
        }
        return Ok(decoded);
    }
}

/// Decodes a Raydium CLMM instruction.
pub fn decode_raydium_clmm_instruction(
    accounts_json: &str,
    data_json: &str,
) -> std::vec::Vec<crate::RaydiumClmmDecodedEvent> {
    let mut decoded = std::vec::Vec::new();
    let accounts_result = serde_json::from_str::<std::vec::Vec<std::string::String>>(accounts_json);
    let accounts = match accounts_result {
        Ok(accounts) => accounts,
        Err(_) => return decoded,
    };
    let data_base58_result = serde_json::from_str::<std::string::String>(data_json);
    let data_base58 = match data_base58_result {
        Ok(data_base58) => data_base58,
        Err(_) => data_json.to_string(),
    };
    let data_option = decode_base58(data_base58.as_str());
    let data = match data_option {
        Some(data) => data,
        None => return decoded,
    };
    if data.len() < 41 {
        return decoded;
    }
    let discriminator_option = read_discriminator(data.as_slice());
    let discriminator = match discriminator_option {
        Some(discriminator) => discriminator,
        None => return decoded,
    };
    if discriminator == RAYDIUM_CLMM_SWAP_LEGACY_DISCRIMINATOR {
        return decoded;
    }
    if discriminator != RAYDIUM_CLMM_SWAP_V2_DISCRIMINATOR {
        return decoded;
    }
    let event_option = decode_swap_v2(accounts.as_slice(), data.as_slice());
    let event = match event_option {
        Some(event) => event,
        None => return decoded,
    };
    decoded.push(crate::RaydiumClmmDecodedEvent::SwapV2(event));
    return decoded;
}

fn decode_raydium_clmm_instruction_with_token_balances(
    accounts_json: &str,
    data_json: &str,
    transaction_instructions: &[crate::ChainInstructionDto],
    instruction: &crate::ChainInstructionDto,
    token_balances: &[TokenBalanceRecord],
) -> Result<std::vec::Vec<crate::RaydiumClmmDecodedEvent>, crate::Error> {
    let mut decoded = std::vec::Vec::new();
    let accounts_result = serde_json::from_str::<std::vec::Vec<std::string::String>>(accounts_json);
    let accounts = match accounts_result {
        Ok(accounts) => accounts,
        Err(error) => {
            return Err(crate::Error::Json(format!(
                "cannot parse raydium clmm accounts json: {}",
                error
            )));
        },
    };
    let data_base58_result = serde_json::from_str::<std::string::String>(data_json);
    let data_base58 = match data_base58_result {
        Ok(data_base58) => data_base58,
        Err(_) => data_json.to_string(),
    };
    let data_option = decode_base58(data_base58.as_str());
    let data = match data_option {
        Some(data) => data,
        None => return Ok(decoded),
    };
    if data.len() < 41 {
        return Ok(decoded);
    }
    let discriminator_option = read_discriminator(data.as_slice());
    let discriminator = match discriminator_option {
        Some(discriminator) => discriminator,
        None => return Ok(decoded),
    };
    if discriminator == RAYDIUM_CLMM_SWAP_V2_DISCRIMINATOR {
        let event_option = decode_swap_v2(accounts.as_slice(), data.as_slice());
        if let Some(event) = event_option {
            decoded.push(crate::RaydiumClmmDecodedEvent::SwapV2(event));
        }
        return Ok(decoded);
    }
    if discriminator == RAYDIUM_CLMM_SWAP_LEGACY_DISCRIMINATOR {
        let event_option = decode_swap_legacy(
            accounts.as_slice(),
            data.as_slice(),
            transaction_instructions,
            instruction,
            token_balances,
        );
        if let Some(event) = event_option {
            decoded.push(crate::RaydiumClmmDecodedEvent::Swap(event));
        }
        return Ok(decoded);
    }
    return Ok(decoded);
}

fn decode_swap_legacy(
    accounts: &[std::string::String],
    data: &[u8],
    transaction_instructions: &[crate::ChainInstructionDto],
    instruction: &crate::ChainInstructionDto,
    token_balances: &[TokenBalanceRecord],
) -> std::option::Option<crate::RaydiumClmmSwapLegacyDecoded> {
    let payer = match clone_account(accounts, 0) {
        Some(value) => value,
        None => return None,
    };
    let amm_config = match clone_account(accounts, 1) {
        Some(value) => value,
        None => return None,
    };
    let pool_state = match clone_account(accounts, 2) {
        Some(value) => value,
        None => return None,
    };
    let input_token_account = match clone_account(accounts, 3) {
        Some(value) => value,
        None => return None,
    };
    let output_token_account = match clone_account(accounts, 4) {
        Some(value) => value,
        None => return None,
    };
    let input_vault = match clone_account(accounts, 5) {
        Some(value) => value,
        None => return None,
    };
    let output_vault = match clone_account(accounts, 6) {
        Some(value) => value,
        None => return None,
    };
    let observation_state = match clone_account(accounts, 7) {
        Some(value) => value,
        None => return None,
    };
    let input_vault_mint = match mint_for_account(token_balances, input_vault.as_str()) {
        Some(value) => value,
        None => return None,
    };
    let output_vault_mint = match mint_for_account(token_balances, output_vault.as_str()) {
        Some(value) => value,
        None => return None,
    };
    if input_vault_mint == output_vault_mint {
        return None;
    }
    let amount = match read_u64_le(data, 8) {
        Some(value) => value,
        None => return None,
    };
    let other_amount_threshold = match read_u64_le(data, 16) {
        Some(value) => value,
        None => return None,
    };
    let sqrt_price_limit_x64 = match read_u128_le(data, 24) {
        Some(value) => value,
        None => return None,
    };
    let is_base_input = match read_bool(data, 40) {
        Some(value) => value,
        None => return None,
    };
    let mut base_mint = input_vault_mint.clone();
    let mut quote_mint = output_vault_mint.clone();
    let mut base_vault = input_vault.clone();
    let mut quote_vault = output_vault.clone();
    if is_quote_mint(input_vault_mint.as_str()) && !is_quote_mint(output_vault_mint.as_str()) {
        base_mint = output_vault_mint.clone();
        quote_mint = input_vault_mint.clone();
        base_vault = output_vault.clone();
        quote_vault = input_vault.clone();
    } else if !is_quote_mint(input_vault_mint.as_str()) && is_quote_mint(output_vault_mint.as_str())
    {
        base_mint = input_vault_mint.clone();
        quote_mint = output_vault_mint.clone();
        base_vault = input_vault.clone();
        quote_vault = output_vault.clone();
    } else if output_vault_mint.as_str() < input_vault_mint.as_str() {
        base_mint = output_vault_mint.clone();
        quote_mint = input_vault_mint.clone();
        base_vault = output_vault.clone();
        quote_vault = input_vault.clone();
    }
    let base_amount_raw = amount_delta_abs_for_account(token_balances, base_vault.as_str());
    let quote_amount_raw = amount_delta_abs_for_account(token_balances, quote_vault.as_str());
    if base_amount_raw.is_none() || quote_amount_raw.is_none() {
        return None;
    }
    let trade_side = match infer_trade_side_from_vault_deltas(
        token_balances,
        base_vault.as_str(),
        quote_vault.as_str(),
    ) {
        Some(trade_side) => trade_side,
        None => {
            if base_vault == input_vault {
                "SellBase".to_string()
            } else {
                "BuyBase".to_string()
            }
        },
    };
    let parent_program = parent_program_id_for_instruction(instruction, transaction_instructions);
    let route_source = route_source_from_parent(parent_program.as_deref());
    return Some(crate::RaydiumClmmSwapLegacyDecoded {
        payer,
        amm_config,
        pool_state,
        input_token_account,
        output_token_account,
        input_vault,
        output_vault,
        observation_state,
        input_vault_mint,
        output_vault_mint,
        base_mint,
        quote_mint,
        base_vault,
        quote_vault,
        trade_side,
        base_amount_raw,
        quote_amount_raw,
        amount,
        other_amount_threshold,
        sqrt_price_limit_x64: sqrt_price_limit_x64.to_string(),
        is_base_input,
        legacy_instruction: true,
        route_source,
    });
}

fn decode_swap_v2(
    accounts: &[std::string::String],
    data: &[u8],
) -> std::option::Option<crate::RaydiumClmmSwapV2Decoded> {
    let payer = match clone_account(accounts, 0) {
        Some(value) => value,
        None => return None,
    };
    let amm_config = match clone_account(accounts, 1) {
        Some(value) => value,
        None => return None,
    };
    let pool_state = match clone_account(accounts, 2) {
        Some(value) => value,
        None => return None,
    };
    let input_token_account = match clone_account(accounts, 3) {
        Some(value) => value,
        None => return None,
    };
    let output_token_account = match clone_account(accounts, 4) {
        Some(value) => value,
        None => return None,
    };
    let input_vault = match clone_account(accounts, 5) {
        Some(value) => value,
        None => return None,
    };
    let output_vault = match clone_account(accounts, 6) {
        Some(value) => value,
        None => return None,
    };
    let observation_state = match clone_account(accounts, 7) {
        Some(value) => value,
        None => return None,
    };
    let input_vault_mint = match clone_account(accounts, 11) {
        Some(value) => value,
        None => return None,
    };
    let output_vault_mint = match clone_account(accounts, 12) {
        Some(value) => value,
        None => return None,
    };
    let amount = match read_u64_le(data, 8) {
        Some(value) => value,
        None => return None,
    };
    let other_amount_threshold = match read_u64_le(data, 16) {
        Some(value) => value,
        None => return None,
    };
    let sqrt_price_limit_x64 = match read_u128_le(data, 24) {
        Some(value) => value,
        None => return None,
    };
    let is_base_input = match read_bool(data, 40) {
        Some(value) => value,
        None => return None,
    };
    let mut base_mint = input_vault_mint.clone();
    let mut quote_mint = output_vault_mint.clone();
    let mut base_vault = input_vault.clone();
    let mut quote_vault = output_vault.clone();
    let mut trade_side = "SellBase".to_string();
    if output_vault_mint.as_str() < input_vault_mint.as_str() {
        base_mint = output_vault_mint.clone();
        quote_mint = input_vault_mint.clone();
        base_vault = output_vault.clone();
        quote_vault = input_vault.clone();
        trade_side = "BuyBase".to_string();
    }
    return Some(crate::RaydiumClmmSwapV2Decoded {
        payer,
        amm_config,
        pool_state,
        input_token_account,
        output_token_account,
        input_vault,
        output_vault,
        observation_state,
        input_vault_mint,
        output_vault_mint,
        base_mint,
        quote_mint,
        base_vault,
        quote_vault,
        trade_side,
        amount,
        other_amount_threshold,
        sqrt_price_limit_x64: sqrt_price_limit_x64.to_string(),
        is_base_input,
    });
}

fn clone_account(
    accounts: &[std::string::String],
    index: usize,
) -> std::option::Option<std::string::String> {
    let account_option = accounts.get(index);
    match account_option {
        Some(account) => return Some(account.clone()),
        None => return None,
    }
}

fn read_discriminator(data: &[u8]) -> std::option::Option<[u8; 8]> {
    if data.len() < 8 {
        return None;
    }
    let mut bytes = [0_u8; 8];
    let mut index = 0_usize;
    while index < 8 {
        bytes[index] = data[index];
        index += 1;
    }
    return Some(bytes);
}

fn read_u64_le(data: &[u8], offset: usize) -> std::option::Option<u64> {
    if data.len() < offset + 8 {
        return None;
    }
    let mut bytes = [0_u8; 8];
    let mut index = 0_usize;
    while index < 8 {
        bytes[index] = data[offset + index];
        index += 1;
    }
    return Some(u64::from_le_bytes(bytes));
}

fn read_u128_le(data: &[u8], offset: usize) -> std::option::Option<u128> {
    if data.len() < offset + 16 {
        return None;
    }
    let mut bytes = [0_u8; 16];
    let mut index = 0_usize;
    while index < 16 {
        bytes[index] = data[offset + index];
        index += 1;
    }
    return Some(u128::from_le_bytes(bytes));
}

fn read_bool(data: &[u8], offset: usize) -> std::option::Option<bool> {
    if data.len() <= offset {
        return None;
    }
    match data[offset] {
        0 => return Some(false),
        1 => return Some(true),
        _ => return None,
    }
}

fn decode_base58(input: &str) -> std::option::Option<std::vec::Vec<u8>> {
    match bs58::decode(input).into_vec() {
        Ok(decoded) => return Some(decoded),
        Err(_) => return None,
    }
}

#[derive(Debug, Clone)]
struct TokenBalanceRecord {
    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>,
}

#[derive(Debug, Clone)]
struct TokenBalanceAccumulator {
    account_index: std::option::Option<i64>,
    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>,
}

#[derive(Debug, Clone)]
struct AccountKeyInfo {
    index: i64,
    address: std::string::String,
}

fn parse_transaction_meta_value(
    transaction: &crate::ChainTransactionDto,
    transaction_json: &serde_json::Value,
) -> Result<std::option::Option<serde_json::Value>, crate::Error> {
    if let Some(meta_json) = transaction.meta_json.as_deref() {
        let meta_result = serde_json::from_str::<serde_json::Value>(meta_json);
        match meta_result {
            Ok(meta) => return Ok(Some(meta)),
            Err(error) => {
                return Err(crate::Error::Json(format!(
                    "cannot parse meta_json for signature '{}': {}",
                    transaction.signature, error
                )));
            },
        }
    }
    let meta = transaction_json.get("meta");
    match meta {
        Some(meta) => return Ok(Some(meta.clone())),
        None => return Ok(None),
    }
}

fn extract_transaction_account_keys(
    transaction: &serde_json::Value,
    meta: std::option::Option<&serde_json::Value>,
) -> std::vec::Vec<AccountKeyInfo> {
    let mut account_keys = std::vec::Vec::new();
    let values = transaction
        .get("transaction")
        .and_then(|value| value.get("message"))
        .and_then(|value| value.get("accountKeys"))
        .and_then(serde_json::Value::as_array);
    if let Some(values) = values {
        let mut index = 0usize;
        for value in values {
            let parsed = parse_account_key_info(value, index as i64);
            if let Some(parsed) = parsed {
                account_keys.push(parsed);
            }
            index += 1;
        }
    }
    append_loaded_addresses(&mut account_keys, meta, "writable");
    append_loaded_addresses(&mut account_keys, meta, "readonly");
    return account_keys;
}

fn parse_account_key_info(
    value: &serde_json::Value,
    index: i64,
) -> std::option::Option<AccountKeyInfo> {
    if let Some(address) = value.as_str() {
        return Some(AccountKeyInfo { index, address: address.to_string() });
    }
    let address = match value.get("pubkey").and_then(serde_json::Value::as_str) {
        Some(address) => address.to_string(),
        None => return None,
    };
    return Some(AccountKeyInfo { index, address });
}

fn append_loaded_addresses(
    account_keys: &mut std::vec::Vec<AccountKeyInfo>,
    meta: std::option::Option<&serde_json::Value>,
    key: &str,
) {
    let addresses = meta
        .and_then(|value| value.get("loadedAddresses"))
        .and_then(|value| value.get(key))
        .and_then(serde_json::Value::as_array);
    let addresses = match addresses {
        Some(addresses) => addresses,
        None => return,
    };
    for value in addresses {
        let address = match value.as_str() {
            Some(address) => address,
            None => continue,
        };
        let index = account_keys.len() as i64;
        account_keys.push(AccountKeyInfo { index, address: address.to_string() });
    }
}

fn extract_token_balance_records(
    meta: std::option::Option<&serde_json::Value>,
    account_keys: &[AccountKeyInfo],
) -> std::vec::Vec<TokenBalanceRecord> {
    let mut accumulators = std::vec::Vec::new();
    collect_token_balance_side(meta, account_keys, "preTokenBalances", true, &mut accumulators);
    collect_token_balance_side(meta, account_keys, "postTokenBalances", false, &mut accumulators);
    let mut records = std::vec::Vec::new();
    for accumulator in accumulators {
        records.push(TokenBalanceRecord {
            account_address: accumulator.account_address,
            mint: accumulator.mint,
            pre_amount_raw: accumulator.pre_amount_raw,
            post_amount_raw: accumulator.post_amount_raw,
        });
    }
    return records;
}

fn collect_token_balance_side(
    meta: std::option::Option<&serde_json::Value>,
    account_keys: &[AccountKeyInfo],
    key: &str,
    is_pre: bool,
    accumulators: &mut std::vec::Vec<TokenBalanceAccumulator>,
) {
    let values = meta.and_then(|value| value.get(key)).and_then(serde_json::Value::as_array);
    let values = match values {
        Some(values) => values,
        None => return,
    };
    for value in values {
        let account_index = value.get("accountIndex").and_then(serde_json::Value::as_i64);
        let mint = match value.get("mint").and_then(serde_json::Value::as_str) {
            Some(mint) => mint.to_string(),
            None => continue,
        };
        let amount = value
            .get("uiTokenAmount")
            .and_then(|amount| amount.get("amount"))
            .and_then(serde_json::Value::as_str)
            .map(|text| text.to_string());
        let account_address = match account_index {
            Some(account_index) => account_address_by_index(account_keys, account_index),
            None => None,
        };
        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<i64>,
    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: &[AccountKeyInfo],
    account_index: i64,
) -> std::option::Option<std::string::String> {
    for account_key in account_keys {
        if account_key.index == account_index {
            return Some(account_key.address.clone());
        }
    }
    return None;
}

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 mint_for_account(
    token_balances: &[TokenBalanceRecord],
    account: &str,
) -> std::option::Option<std::string::String> {
    let record = token_balance_record_for_account(token_balances, account);
    match record {
        Some(record) => return Some(record.mint.clone()),
        None => return None,
    }
}

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

fn amount_delta_abs_for_account(
    token_balances: &[TokenBalanceRecord],
    account: &str,
) -> std::option::Option<std::string::String> {
    let record = token_balance_record_for_account(token_balances, account);
    let record = match record {
        Some(record) => record,
        None => return None,
    };
    let delta = record.delta_raw();
    let delta = match delta {
        Some(delta) => delta,
        None => return None,
    };
    if delta < 0 {
        return Some((-delta).to_string());
    }
    return Some(delta.to_string());
}

fn infer_trade_side_from_vault_deltas(
    token_balances: &[TokenBalanceRecord],
    base_vault: &str,
    quote_vault: &str,
) -> std::option::Option<std::string::String> {
    let base_record = token_balance_record_for_account(token_balances, 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_balances, quote_vault);
    let quote_record = match quote_record {
        Some(quote_record) => quote_record,
        None => return None,
    };
    let base_delta = base_record.delta_raw();
    let quote_delta = quote_record.delta_raw();
    match (base_delta, quote_delta) {
        (Some(base_delta), Some(quote_delta)) => {
            if base_delta < 0 && quote_delta > 0 {
                return Some("BuyBase".to_string());
            }
            if base_delta > 0 && quote_delta < 0 {
                return Some("SellBase".to_string());
            }
            return None;
        },
        _ => return None,
    }
}

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

fn parent_program_id_for_instruction(
    instruction: &crate::ChainInstructionDto,
    instructions: &[crate::ChainInstructionDto],
) -> std::option::Option<std::string::String> {
    let parent_id = match instruction.parent_instruction_id {
        Some(parent_id) => parent_id,
        None => return None,
    };
    for candidate in instructions {
        if candidate.id == Some(parent_id) {
            return candidate.program_id.clone();
        }
    }
    return None;
}

fn route_source_from_parent(
    parent_program: std::option::Option<&str>,
) -> std::option::Option<std::string::String> {
    let parent_program = match parent_program {
        Some(parent_program) => parent_program,
        None => return None,
    };
    if parent_program == OBSERVED_JUPITER_V6_PROGRAM_ID {
        return Some("jupiter".to_string());
    }
    return Some(parent_program.to_string());
}

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_result = value.parse::<i128>();
    match parsed_result {
        Ok(parsed) => return Some(parsed),
        Err(_) => return None,
    }
}

#[cfg(test)]
mod tests {
    fn sample_swap_v2_accounts_json() -> &'static str {
        return r#"[
            "8NQ32SyFKD1d5kenq4oM8Da6C6J9TQSMW1uAgFRveEQr",
            "A1BBtTYJd4i3xU8D6Tc2FzU6ZN4oXZWXKZnCxwbHXr8x",
            "GUrRxvnWVQSnbcz1eP9D5BqXwPZtRhmrqVfm5wY9meWR",
            "D2frZyyQ7NQaXRiEoBUM9S64Ckv7KZ7wuqupqdMhpsHy",
            "H7qe6sAyEyqztyMtRrDf5J1gugLx6yuyKPy5veVmR14W",
            "AvRzvwpSVnxsinLGQS3vZLqkZxhXZDM8F2qKccAo7rSq",
            "CTkc4xDrpzjWcFLC1cxmUZZjZLSRV46HZa8wu5eKTbuh",
            "8QtFSxNzD3zmEX8nzQKZB83TH4WGUAkLkQoRHAw5fuhn",
            "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
            "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
            "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",
            "CKvjP8FrZpaKXjASEtX2nEU9w7M4RKskfnLQbKJBodV",
            "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs",
            "8ovxZR2Gv9Mr73aoXLQYTMaZvHCSpnEohzgjVHQwmyHr",
            "9MssDxndh2Rn8DmGWL94hXVv22zxfDYHV7tvzfPgcaWe"
        ]"#;
    }

    #[test]
    fn decodes_swap_v2() {
        let events = crate::decode_raydium_clmm_instruction(
            sample_swap_v2_accounts_json(),
            r#""ASCsAbe1UnDnCsnGLPALJUXSS5JREycfhGyTzKh7xRWNyRHCqBuzR23S""#,
        );
        assert_eq!(events.len(), 1);
        match &events[0] {
            crate::RaydiumClmmDecodedEvent::SwapV2(event) => {
                assert_eq!(events[0].event_kind(), "raydium_clmm.swap_v2");
                assert_eq!(
                    events[0].pool_account(),
                    "GUrRxvnWVQSnbcz1eP9D5BqXwPZtRhmrqVfm5wY9meWR"
                );
                assert_eq!(event.pool_state, "GUrRxvnWVQSnbcz1eP9D5BqXwPZtRhmrqVfm5wY9meWR");
                assert_eq!(event.input_vault, "AvRzvwpSVnxsinLGQS3vZLqkZxhXZDM8F2qKccAo7rSq");
                assert_eq!(event.output_vault, "CTkc4xDrpzjWcFLC1cxmUZZjZLSRV46HZa8wu5eKTbuh");
                assert_eq!(event.input_vault_mint, "CKvjP8FrZpaKXjASEtX2nEU9w7M4RKskfnLQbKJBodV");
                assert_eq!(event.output_vault_mint, "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs");
                assert_eq!(event.base_mint, "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs");
                assert_eq!(event.quote_mint, "CKvjP8FrZpaKXjASEtX2nEU9w7M4RKskfnLQbKJBodV");
                assert_eq!(event.base_vault, "CTkc4xDrpzjWcFLC1cxmUZZjZLSRV46HZa8wu5eKTbuh");
                assert_eq!(event.quote_vault, "AvRzvwpSVnxsinLGQS3vZLqkZxhXZDM8F2qKccAo7rSq");
                assert_eq!(event.trade_side, "BuyBase");
                assert_eq!(event.amount, 148441657491969);
                assert_eq!(event.other_amount_threshold, 0);
                assert_eq!(event.sqrt_price_limit_x64, "0");
                assert!(event.is_base_input);
            },
            crate::RaydiumClmmDecodedEvent::Swap(_) => panic!("expected swap_v2 event"),
        }
    }

    #[test]
    fn serializes_swap_v2_payload_json() {
        let events = crate::decode_raydium_clmm_instruction(
            sample_swap_v2_accounts_json(),
            r#""ASCsAbe1UnDnCsnGLPALJUXSS5JREycfhGyTzKh7xRWNyRHCqBuzR23S""#,
        );
        assert_eq!(events.len(), 1);
        let payload_option = events[0].to_payload_json();
        let payload = match payload_option {
            Some(payload) => payload,
            None => panic!("payload json must be available"),
        };
        assert!(payload.contains("GUrRxvnWVQSnbcz1eP9D5BqXwPZtRhmrqVfm5wY9meWR"));
        assert!(payload.contains("input_vault"));
        assert!(payload.contains("output_vault"));
        assert!(payload.contains("tradeSide"));
    }

    #[test]
    fn ignores_invalid_data() {
        let events = crate::decode_raydium_clmm_instruction(
            sample_swap_v2_accounts_json(),
            r#""not-base58-data-0""#,
        );
        assert_eq!(events.len(), 0);
    }

    #[test]
    fn ignores_incomplete_accounts() {
        let accounts_json = r#"[
            "8NQ32SyFKD1d5kenq4oM8Da6C6J9TQSMW1uAgFRveEQr",
            "A1BBtTYJd4i3xU8D6Tc2FzU6ZN4oXZWXKZnCxwbHXr8x",
            "GUrRxvnWVQSnbcz1eP9D5BqXwPZtRhmrqVfm5wY9meWR"
        ]"#;
        let events = crate::decode_raydium_clmm_instruction(
            accounts_json,
            r#""ASCsAbe1UnDnCsnGLPALJUXSS5JREycfhGyTzKh7xRWNyRHCqBuzR23S""#,
        );
        assert_eq!(events.len(), 0);
    }

    #[test]
    fn ignores_legacy_swap_for_now() {
        let mut data: std::vec::Vec<u8> = vec![248, 198, 158, 145, 225, 117, 135, 200];
        while data.len() < 41 {
            data.push(0);
        }
        data[40] = 1;
        let encoded = bs58::encode(data).into_string();
        let data_json = format!("\"{}\"", encoded);
        let events = crate::decode_raydium_clmm_instruction(
            sample_swap_v2_accounts_json(),
            data_json.as_str(),
        );
        assert_eq!(events.len(), 0);
    }
}
