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

//! Meteora DAMM v2 transaction decoder.

const DAMM_V2_DISCRIMINATOR_INITIALIZE_POOL: [u8; 8] =
    [0x5f, 0xb4, 0x0a, 0xac, 0x54, 0xae, 0xe8, 0x28];

const DAMM_V2_DISCRIMINATOR_INITIALIZE_POOL_WITH_DYNAMIC_CONFIG: [u8; 8] =
    [0x95, 0x52, 0x48, 0xc5, 0xfd, 0xfc, 0x44, 0x0f];

const DAMM_V2_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_POOL: [u8; 8] =
    [0x14, 0xa1, 0xf1, 0x18, 0xbd, 0xdd, 0xb4, 0x02];

const DAMM_V2_DISCRIMINATOR_SWAP: [u8; 8] = [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8];

const DAMM_V2_DISCRIMINATOR_SWAP2: [u8; 8] = [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88];

/// Decoded Meteora DAMM v2 create-pool event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MeteoraDammV2CreatePoolDecoded {
    /// 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,
    /// Optional pool account.
    pub pool_account: std::option::Option<std::string::String>,
    /// Optional token A mint.
    pub token_a_mint: std::option::Option<std::string::String>,
    /// Optional token B mint.
    pub token_b_mint: std::option::Option<std::string::String>,
    /// Optional static or dynamic config account.
    pub config_account: std::option::Option<std::string::String>,
    /// Optional creator / payer.
    pub creator: std::option::Option<std::string::String>,
    /// Instruction flavor: static / dynamic / customizable.
    pub create_kind: std::string::String,
    /// Decoded payload.
    pub payload_json: serde_json::Value,
}

/// Decoded Meteora DAMM v2 swap event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MeteoraDammV2SwapDecoded {
    /// 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,
    /// Trade side relative to normalized base.
    pub trade_side: crate::SwapTradeSide,
    /// Optional pool account.
    pub pool_account: std::option::Option<std::string::String>,
    /// Optional token A mint.
    pub token_a_mint: std::option::Option<std::string::String>,
    /// Optional token B mint.
    pub token_b_mint: std::option::Option<std::string::String>,
    /// Whether the source instruction looked like swap2.
    pub used_swap2: bool,
    /// Decoded payload.
    pub payload_json: serde_json::Value,
}

/// Decoded Meteora DAMM v2 event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum MeteoraDammV2DecodedEvent {
    /// Pool creation.
    CreatePool(MeteoraDammV2CreatePoolDecoded),
    /// Swap / swap2.
    Swap(MeteoraDammV2SwapDecoded),
}

/// Meteora DAMM v2 decoder.
#[derive(Debug, Clone, Default)]
pub struct MeteoraDammV2Decoder;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MeteoraDammV2InstructionKind {
    CreatePoolStatic,
    CreatePoolDynamic,
    CreatePoolCustomizable,
    Swap,
    Unknown,
}

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

    /// Decodes one projected transaction into zero or more Meteora DAMM v2 events.
    pub fn decode_transaction(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::MeteoraDammV2DecodedEvent>, crate::Error> {
        let transaction_id_option = transaction.id;
        let transaction_id = match transaction_id_option {
            Some(transaction_id) => transaction_id,
            None => {
                return Err(crate::Error::InvalidState(format!(
                    "chain transaction '{}' has no internal id",
                    transaction.signature
                )));
            },
        };
        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 log_messages = extract_log_messages(&transaction_json);
        let mut decoded_events = std::vec::Vec::new();
        for instruction in instructions {
            let program_id_option = &instruction.program_id;
            let program_id = match program_id_option {
                Some(program_id) => program_id,
                None => continue,
            };
            if program_id.as_str() != crate::METEORA_DAMM_V2_PROGRAM_ID {
                continue;
            }
            let instruction_id_option = instruction.id;
            let instruction_id = match instruction_id_option {
                Some(instruction_id) => instruction_id,
                None => continue,
            };
            let accounts_result = parse_accounts_json(instruction.accounts_json.as_str());
            let accounts = match accounts_result {
                Ok(accounts) => accounts,
                Err(error) => return Err(error),
            };
            let parsed_json_result = parse_optional_parsed_json(instruction.parsed_json.as_ref());
            let parsed_json = match parsed_json_result {
                Ok(parsed_json) => parsed_json,
                Err(error) => return Err(error),
            };
            let instruction_data_result =
                decode_instruction_data_json(instruction.data_json.as_ref());
            let instruction_data = match instruction_data_result {
                Ok(instruction_data) => instruction_data,
                Err(error) => return Err(error),
            };
            if instruction.parent_instruction_id.is_some() && instruction_data.is_none() {
                continue;
            }
            let instruction_kind = classify_instruction_kind(
                parsed_json.as_ref(),
                instruction_data.as_deref(),
                &log_messages,
            );
            let pool_account = extract_string_by_candidate_keys(
                parsed_json.as_ref(),
                &["pool", "poolAddress", "poolAccount", "poolState", "cpAmm"],
            )
            .or_else(|| return extract_account(&accounts, 0));
            let token_a_mint = extract_string_by_candidate_keys(
                parsed_json.as_ref(),
                &["tokenAMint", "mintA", "baseMint", "token0Mint", "mint0"],
            )
            .or_else(|| return extract_account(&accounts, 1));
            let token_b_mint = extract_string_by_candidate_keys(
                parsed_json.as_ref(),
                &["tokenBMint", "mintB", "quoteMint", "token1Mint", "mint1"],
            )
            .or_else(|| return extract_account(&accounts, 2));
            let config_account = extract_string_by_candidate_keys(
                parsed_json.as_ref(),
                &["staticConfig", "dynamicConfig", "config", "poolConfig"],
            )
            .or_else(|| return extract_account(&accounts, 3));
            let creator = extract_string_by_candidate_keys(
                parsed_json.as_ref(),
                &["creator", "payer", "user", "owner"],
            )
            .or_else(|| return extract_account(&accounts, 4));
            if instruction_kind == MeteoraDammV2InstructionKind::CreatePoolStatic
                || instruction_kind == MeteoraDammV2InstructionKind::CreatePoolDynamic
                || instruction_kind == MeteoraDammV2InstructionKind::CreatePoolCustomizable
            {
                let create_kind = match instruction_kind {
                    MeteoraDammV2InstructionKind::CreatePoolStatic => "static".to_string(),
                    MeteoraDammV2InstructionKind::CreatePoolDynamic => "dynamic".to_string(),
                    MeteoraDammV2InstructionKind::CreatePoolCustomizable => {
                        "customizable".to_string()
                    },
                    _ => "unknown".to_string(),
                };
                let payload_json = serde_json::json!({
                    "decoder": "meteora_damm_v2",
                    "eventKind": "create_pool",
                    "dataDiscriminatorHex": instruction_data
                        .as_ref()
                        .and_then(|data| return first_8_bytes_hex(data.as_slice())),
                    "classifiedInstructionKind": create_kind,
                    "signature": transaction.signature,
                    "instructionId": instruction_id,
                    "instructionIndex": instruction.instruction_index,
                    "accounts": accounts,
                    "parsed": parsed_json,
                    "logMessages": log_messages,
                    "poolAccount": pool_account,
                    "tokenAMint": token_a_mint,
                    "tokenBMint": token_b_mint,
                    "configAccount": config_account,
                    "creator": creator
                });
                decoded_events.push(crate::MeteoraDammV2DecodedEvent::CreatePool(
                    crate::MeteoraDammV2CreatePoolDecoded {
                        transaction_id,
                        instruction_id,
                        signature: transaction.signature.clone(),
                        program_id: program_id.clone(),
                        pool_account,
                        token_a_mint,
                        token_b_mint,
                        config_account,
                        creator,
                        create_kind,
                        payload_json,
                    },
                ));
                continue;
            }
            if instruction_kind == MeteoraDammV2InstructionKind::Swap {
                let used_swap2 = log_messages_contain_keyword(&log_messages, "swap2")
                    || value_contains_any_key(parsed_json.as_ref(), &["swap2", "isSwap2"]);
                let decoded_amounts_result =
                    crate::meteora_swap_amount_inference::infer_meteora_swap_amounts_from_inner_transfers(
                        transaction,
                        instructions,
                        instruction,
                        pool_account.as_deref(),
                    );
                let decoded_amounts = match decoded_amounts_result {
                    Ok(decoded_amounts) => decoded_amounts,
                    Err(error) => return Err(error),
                };
                let fallback_trade_side = infer_trade_side(&log_messages);
                let trade_side = match decoded_amounts.as_ref() {
                    Some(decoded_amounts) => decoded_amounts.trade_side,
                    None => fallback_trade_side,
                };
                let has_direct_amount_payload =
                    parsed_json_has_trade_amount_or_price_payload(parsed_json.as_ref());
                let has_inferred_amount_payload = decoded_amounts.is_some();
                let has_trade_amount_payload =
                    has_direct_amount_payload || has_inferred_amount_payload;
                let event_actionability = if has_trade_amount_payload {
                    "trade_candidate"
                } else {
                    "non_actionable_trade"
                };
                let materialization_skip_reason = if has_trade_amount_payload {
                    serde_json::Value::Null
                } else {
                    serde_json::Value::String("swap_without_amount_payload".to_string())
                };
                let effective_token_a_mint = match decoded_amounts.as_ref() {
                    Some(decoded_amounts) => Some(decoded_amounts.base_token_mint.clone()),
                    None => token_a_mint.clone(),
                };
                let effective_token_b_mint = match decoded_amounts.as_ref() {
                    Some(decoded_amounts) => Some(decoded_amounts.quote_token_mint.clone()),
                    None => token_b_mint.clone(),
                };
                let base_vault = match decoded_amounts.as_ref() {
                    Some(decoded_amounts) => decoded_amounts.base_vault_address.clone(),
                    None => None,
                };
                let quote_vault = match decoded_amounts.as_ref() {
                    Some(decoded_amounts) => decoded_amounts.quote_vault_address.clone(),
                    None => None,
                };
                let base_amount_raw = match decoded_amounts.as_ref() {
                    Some(decoded_amounts) => {
                        serde_json::Value::String(decoded_amounts.base_amount_raw.clone())
                    },
                    None => serde_json::Value::Null,
                };
                let quote_amount_raw = match decoded_amounts.as_ref() {
                    Some(decoded_amounts) => {
                        serde_json::Value::String(decoded_amounts.quote_amount_raw.clone())
                    },
                    None => serde_json::Value::Null,
                };
                let amount_resolution_source = if has_inferred_amount_payload {
                    "flattened_cpi_pool_transfer_window"
                } else if has_direct_amount_payload {
                    "parsed_payload"
                } else {
                    "none"
                };
                let payload_json = serde_json::json!({
                    "decoder": "meteora_damm_v2",
                    "eventKind": "swap",
                    "dataDiscriminatorHex": instruction_data
                        .as_ref()
                        .and_then(|data| return first_8_bytes_hex(data.as_slice())),
                    "classifiedInstructionKind": if used_swap2 { "swap2" } else { "swap" },
                    "eventCategory": "trade",
                    "eventLifecycleKind": "trade_swap",
                    "eventActionability": event_actionability,
                    "tradeCandidate": has_trade_amount_payload,
                    "candleCandidate": has_trade_amount_payload,
                    "nonTradeUseful": false,
                    "materializationSkipReason": materialization_skip_reason,
                    "amountResolutionSource": amount_resolution_source,
                    "signature": transaction.signature,
                    "instructionId": instruction_id,
                    "instructionIndex": instruction.instruction_index,
                    "accounts": accounts,
                    "parsed": parsed_json,
                    "logMessages": log_messages,
                    "poolAccount": pool_account,
                    "tokenAMint": effective_token_a_mint.clone(),
                    "tokenBMint": effective_token_b_mint.clone(),
                    "baseVault": base_vault,
                    "quoteVault": quote_vault,
                    "baseAmountRaw": base_amount_raw,
                    "quoteAmountRaw": quote_amount_raw,
                    "tradeSide": format!("{:?}", trade_side)
                });
                decoded_events.push(crate::MeteoraDammV2DecodedEvent::Swap(
                    crate::MeteoraDammV2SwapDecoded {
                        transaction_id,
                        instruction_id,
                        signature: transaction.signature.clone(),
                        program_id: program_id.clone(),
                        trade_side,
                        pool_account,
                        token_a_mint: effective_token_a_mint,
                        token_b_mint: effective_token_b_mint,
                        used_swap2,
                        payload_json,
                    },
                ));
            }
        }
        return Ok(decoded_events);
    }
}

fn extract_log_messages(
    transaction_json: &serde_json::Value,
) -> std::vec::Vec<std::string::String> {
    let mut messages = std::vec::Vec::new();
    let meta_option = transaction_json.get("meta");
    let meta = match meta_option {
        Some(meta) => meta,
        None => return messages,
    };
    let logs_option = meta.get("logMessages");
    let logs = match logs_option {
        Some(logs) => logs,
        None => return messages,
    };
    let logs_array_option = logs.as_array();
    let logs_array = match logs_array_option {
        Some(logs_array) => logs_array,
        None => return messages,
    };
    for value in logs_array {
        let text_option = value.as_str();
        if let Some(text) = text_option {
            messages.push(text.to_string());
        }
    }
    return messages;
}

fn classify_instruction_kind(
    parsed_json: std::option::Option<&serde_json::Value>,
    instruction_data: std::option::Option<&[u8]>,
    log_messages: &[std::string::String],
) -> MeteoraDammV2InstructionKind {
    let data_kind = classify_instruction_kind_from_data(instruction_data);
    if data_kind != MeteoraDammV2InstructionKind::Unknown {
        return data_kind;
    }
    if instruction_data_has_full_discriminator(instruction_data) {
        return MeteoraDammV2InstructionKind::Unknown;
    }
    let parsed_instruction_name = extract_string_by_candidate_keys(
        parsed_json,
        &["instruction", "instructionName", "type", "name"],
    );
    if let Some(parsed_instruction_name) = parsed_instruction_name {
        let normalized = normalize_text(parsed_instruction_name.as_str());
        if normalized.contains("initializepoolwithdynamicconfig") {
            return MeteoraDammV2InstructionKind::CreatePoolDynamic;
        }
        if normalized.contains("initializecustomizablepool") {
            return MeteoraDammV2InstructionKind::CreatePoolCustomizable;
        }
        if normalized.contains("initializepool") {
            return MeteoraDammV2InstructionKind::CreatePoolStatic;
        }
        if normalized == "swap" || normalized == "swap2" {
            return MeteoraDammV2InstructionKind::Swap;
        }
    }
    if value_contains_any_key(parsed_json, &["dynamicConfig"]) {
        return MeteoraDammV2InstructionKind::CreatePoolDynamic;
    }
    if value_contains_any_key(parsed_json, &["staticConfig"]) {
        return MeteoraDammV2InstructionKind::CreatePoolStatic;
    }
    if value_contains_any_key(parsed_json, &["poolActivationPoint", "isCustomizablePool"]) {
        return MeteoraDammV2InstructionKind::CreatePoolCustomizable;
    }
    if log_messages_contain_keyword(log_messages, "initialize_pool_with_dynamic_config")
        || log_messages_contain_keyword(log_messages, "initializepoolwithdynamicconfig")
    {
        return MeteoraDammV2InstructionKind::CreatePoolDynamic;
    }
    if log_messages_contain_keyword(log_messages, "initialize_customizable_pool")
        || log_messages_contain_keyword(log_messages, "initializecustomizablepool")
    {
        return MeteoraDammV2InstructionKind::CreatePoolCustomizable;
    }
    if log_messages_contain_keyword(log_messages, "initialize_pool")
        || log_messages_contain_keyword(log_messages, "initializepool")
    {
        return MeteoraDammV2InstructionKind::CreatePoolStatic;
    }
    if log_messages_contain_keyword(log_messages, "swap2")
        || log_messages_contain_keyword(log_messages, "swap")
    {
        return MeteoraDammV2InstructionKind::Swap;
    }
    return MeteoraDammV2InstructionKind::Unknown;
}

fn instruction_data_has_full_discriminator(instruction_data: std::option::Option<&[u8]>) -> bool {
    let instruction_data = match instruction_data {
        Some(instruction_data) => instruction_data,
        None => return false,
    };
    return instruction_data.len() >= 8;
}

fn classify_instruction_kind_from_data(
    instruction_data: std::option::Option<&[u8]>,
) -> MeteoraDammV2InstructionKind {
    let instruction_data = match instruction_data {
        Some(instruction_data) => instruction_data,
        None => return MeteoraDammV2InstructionKind::Unknown,
    };
    if instruction_data.len() < 8 {
        return MeteoraDammV2InstructionKind::Unknown;
    }
    let discriminator = [
        instruction_data[0],
        instruction_data[1],
        instruction_data[2],
        instruction_data[3],
        instruction_data[4],
        instruction_data[5],
        instruction_data[6],
        instruction_data[7],
    ];
    if discriminator == DAMM_V2_DISCRIMINATOR_INITIALIZE_POOL_WITH_DYNAMIC_CONFIG {
        return MeteoraDammV2InstructionKind::CreatePoolDynamic;
    }
    if discriminator == DAMM_V2_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_POOL {
        return MeteoraDammV2InstructionKind::CreatePoolCustomizable;
    }
    if discriminator == DAMM_V2_DISCRIMINATOR_INITIALIZE_POOL {
        return MeteoraDammV2InstructionKind::CreatePoolStatic;
    }
    if discriminator == DAMM_V2_DISCRIMINATOR_SWAP || discriminator == DAMM_V2_DISCRIMINATOR_SWAP2 {
        return MeteoraDammV2InstructionKind::Swap;
    }
    return MeteoraDammV2InstructionKind::Unknown;
}

fn log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool {
    let keyword_normalized = normalize_text(keyword);
    for log_message in log_messages {
        let log_normalized = normalize_text(log_message.as_str());
        if log_normalized.contains(keyword_normalized.as_str()) {
            return true;
        }
    }
    return false;
}

fn normalize_text(normalize_text: &str) -> std::string::String {
    let mut normalized = std::string::String::new();
    for character in normalize_text.chars() {
        if character.is_ascii_alphanumeric() {
            normalized.push(character.to_ascii_lowercase());
        }
    }
    return normalized;
}

fn parse_accounts_json(
    accounts_json: &str,
) -> Result<std::vec::Vec<std::string::String>, crate::Error> {
    let values_result = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(accounts_json);
    let values = match values_result {
        Ok(values) => values,
        Err(error) => {
            return Err(crate::Error::Json(format!(
                "cannot parse instruction accounts_json '{}': {}",
                accounts_json, error
            )));
        },
    };
    let mut accounts = std::vec::Vec::new();
    for value in values {
        let text_option = value.as_str();
        if let Some(text) = text_option {
            accounts.push(text.to_string());
            continue;
        }
        if let Some(pubkey) = value.get("pubkey").and_then(|nested| return nested.as_str()) {
            accounts.push(pubkey.to_string());
        }
    }
    return Ok(accounts);
}

fn parse_optional_parsed_json(
    parsed_json: std::option::Option<&std::string::String>,
) -> Result<std::option::Option<serde_json::Value>, crate::Error> {
    let parsed_json = match parsed_json {
        Some(parsed_json) => parsed_json,
        None => return Ok(None),
    };
    let value_result = serde_json::from_str::<serde_json::Value>(parsed_json.as_str());
    match value_result {
        Ok(value) => return Ok(Some(value)),
        Err(error) => {
            return Err(crate::Error::Json(format!(
                "cannot parse instruction parsed_json '{}': {}",
                parsed_json, error
            )));
        },
    }
}

fn decode_instruction_data_json(
    data_json: std::option::Option<&std::string::String>,
) -> Result<std::option::Option<std::vec::Vec<u8>>, crate::Error> {
    let data_json = match data_json {
        Some(data_json) => data_json,
        None => return Ok(None),
    };
    let value_result = serde_json::from_str::<serde_json::Value>(data_json.as_str());
    let value = match value_result {
        Ok(value) => value,
        Err(error) => {
            return Err(crate::Error::Json(format!(
                "cannot parse Meteora DAMM v2 data_json: {}",
                error
            )));
        },
    };
    if let serde_json::Value::String(base58_text) = value {
        let decoded_result = bs58::decode(base58_text.as_str()).into_vec();
        match decoded_result {
            Ok(decoded) => return Ok(Some(decoded)),
            Err(error) => {
                return Err(crate::Error::Json(format!(
                    "cannot decode Meteora DAMM v2 instruction data from base58: {}",
                    error
                )));
            },
        }
    }
    return Ok(None);
}

fn first_8_bytes_hex(bytes: &[u8]) -> std::option::Option<std::string::String> {
    if bytes.len() < 8 {
        return None;
    }
    return Some(format!(
        "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
        bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
    ));
}

fn extract_string_by_candidate_keys(
    value: std::option::Option<&serde_json::Value>,
    candidate_keys: &[&str],
) -> std::option::Option<std::string::String> {
    let value = match value {
        Some(value) => value,
        None => return None,
    };
    return extract_string_by_candidate_keys_inner(value, candidate_keys);
}

fn extract_string_by_candidate_keys_inner(
    value: &serde_json::Value,
    candidate_keys: &[&str],
) -> std::option::Option<std::string::String> {
    if let Some(object) = value.as_object() {
        for candidate_key in candidate_keys {
            let direct_option = object.get(*candidate_key);
            if let Some(direct) = direct_option {
                let direct_text_option = direct.as_str();
                if let Some(direct_text) = direct_text_option {
                    return Some(direct_text.to_string());
                }
            }
        }
        for nested_value in object.values() {
            let nested_result =
                extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
            if nested_result.is_some() {
                return nested_result;
            }
        }
        return None;
    }
    if let Some(array) = value.as_array() {
        for nested_value in array {
            let nested_result =
                extract_string_by_candidate_keys_inner(nested_value, candidate_keys);
            if nested_result.is_some() {
                return nested_result;
            }
        }
    }
    return None;
}

fn value_contains_any_key(
    value: std::option::Option<&serde_json::Value>,
    candidate_keys: &[&str],
) -> bool {
    let value = match value {
        Some(value) => value,
        None => return false,
    };
    return value_contains_any_key_inner(value, candidate_keys);
}

fn value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[&str]) -> bool {
    if let Some(object) = value.as_object() {
        for candidate_key in candidate_keys {
            if object.contains_key(*candidate_key) {
                return true;
            }
        }
        for nested_value in object.values() {
            if value_contains_any_key_inner(nested_value, candidate_keys) {
                return true;
            }
        }
        return false;
    }
    if let Some(array) = value.as_array() {
        for nested_value in array {
            if value_contains_any_key_inner(nested_value, candidate_keys) {
                return true;
            }
        }
    }
    return false;
}

fn extract_account(
    accounts: &[std::string::String],
    index: usize,
) -> std::option::Option<std::string::String> {
    if index >= accounts.len() {
        return None;
    }
    return Some(accounts[index].clone());
}

fn infer_trade_side(log_messages: &[std::string::String]) -> crate::SwapTradeSide {
    if log_messages_contain_keyword(log_messages, "buy") {
        return crate::SwapTradeSide::BuyBase;
    }
    if log_messages_contain_keyword(log_messages, "sell") {
        return crate::SwapTradeSide::SellBase;
    }
    return crate::SwapTradeSide::Unknown;
}

fn parsed_json_has_trade_amount_or_price_payload(
    parsed_json: std::option::Option<&serde_json::Value>,
) -> bool {
    let parsed_json = match parsed_json {
        Some(parsed_json) => parsed_json,
        None => return false,
    };
    return json_value_contains_any_trade_amount_or_price_key(parsed_json);
}

fn json_value_contains_any_trade_amount_or_price_key(value: &serde_json::Value) -> bool {
    match value {
        serde_json::Value::Object(map) => {
            for key in map.keys() {
                let normalized = normalize_text(key.as_str());
                if is_trade_amount_or_price_key(normalized.as_str()) {
                    return true;
                }
            }
            for child in map.values() {
                if json_value_contains_any_trade_amount_or_price_key(child) {
                    return true;
                }
            }
            return false;
        },
        serde_json::Value::Array(values) => {
            for child in values {
                if json_value_contains_any_trade_amount_or_price_key(child) {
                    return true;
                }
            }
            return false;
        },
        _ => return false,
    }
}

fn is_trade_amount_or_price_key(normalized_key: &str) -> bool {
    return normalized_key == "baseamountraw"
        || normalized_key == "quoteamountraw"
        || normalized_key == "baseamount"
        || normalized_key == "quoteamount"
        || normalized_key == "amountin"
        || normalized_key == "amountout"
        || normalized_key == "tokenain"
        || normalized_key == "tokenaout"
        || normalized_key == "tokenbin"
        || normalized_key == "tokenbout"
        || normalized_key == "inputamount"
        || normalized_key == "outputamount"
        || normalized_key == "swapamount"
        || normalized_key == "price"
        || normalized_key == "pricequoteperbase";
}

#[cfg(test)]
mod tests {
    fn make_create_transaction() -> crate::ChainTransactionDto {
        let mut dto = crate::ChainTransactionDto::new(
            "sig-meteora_damm_v2-create-1".to_string(),
            Some(889001),
            Some(1779400001),
            Some("helius_primary_http".to_string()),
            Some("0".to_string()),
            None,
            None,
            serde_json::json!({
                "slot": 889001,
                "meta": {
                    "logMessages": [
                        "Program log: Instruction: InitializeCustomizablePool"
                    ]
                },
                "transaction": {
                    "message": {
                        "instructions": []
                    }
                }
            })
            .to_string(),
        );
        dto.id = Some(401);
        return dto;
    }

    fn make_create_instruction() -> crate::ChainInstructionDto {
        let mut dto = crate::ChainInstructionDto::new(
            401,
            None,
            0,
            None,
            Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
            Some("meteora_damm_v2".to_string()),
            Some(1),
            serde_json::json!([
                "DammV2Pool111",
                "DammV2TokenA111",
                crate::WSOL_MINT_ID,
                "DammV2Config111",
                "DammV2Creator111"
            ])
            .to_string(),
            None,
            None,
            Some(
                serde_json::json!({
                    "info": {
                        "instruction": "initialize_customizable_pool",
                        "pool": "DammV2Pool111",
                        "tokenAMint": "DammV2TokenA111",
                        "tokenBMint": crate::WSOL_MINT_ID,
                        "creator": "DammV2Creator111",
                        "isCustomizablePool": true
                    }
                })
                .to_string(),
            ),
        );
        dto.id = Some(402);
        return dto;
    }

    fn make_swap_transaction() -> crate::ChainTransactionDto {
        let mut dto = crate::ChainTransactionDto::new(
            "sig-meteora_damm_v2-swap-1".to_string(),
            Some(889002),
            Some(1779400002),
            Some("helius_primary_http".to_string()),
            Some("0".to_string()),
            None,
            None,
            serde_json::json!({
                "slot": 889002,
                "meta": {
                    "logMessages": [
                        "Program log: Instruction: Swap2"
                    ]
                },
                "transaction": {
                    "message": {
                        "instructions": []
                    }
                }
            })
            .to_string(),
        );
        dto.id = Some(403);
        return dto;
    }

    fn make_swap_instruction() -> crate::ChainInstructionDto {
        let mut dto = crate::ChainInstructionDto::new(
            403,
            None,
            0,
            None,
            Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
            Some("meteora_damm_v2".to_string()),
            Some(1),
            serde_json::json!(["DammV2SwapPool111", "DammV2SwapTokenA111", crate::WSOL_MINT_ID])
                .to_string(),
            None,
            None,
            Some(
                serde_json::json!({
                    "info": {
                        "instruction": "swap2",
                        "pool": "DammV2SwapPool111",
                        "tokenAMint": "DammV2SwapTokenA111",
                        "tokenBMint": crate::WSOL_MINT_ID
                    }
                })
                .to_string(),
            ),
        );
        dto.id = Some(404);
        return dto;
    }

    #[test]
    fn meteora_damm_v2_create_pool_is_detected() {
        let decoder = crate::MeteoraDammV2Decoder::new();
        let transaction = make_create_transaction();
        let instructions = vec![make_create_instruction()];
        let decoded_result = decoder.decode_transaction(&transaction, &instructions);
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        match &decoded[0] {
            crate::MeteoraDammV2DecodedEvent::CreatePool(event) => {
                assert_eq!(event.transaction_id, 401);
                assert_eq!(event.instruction_id, 402);
                assert_eq!(event.pool_account, Some("DammV2Pool111".to_string()));
                assert_eq!(event.token_a_mint, Some("DammV2TokenA111".to_string()));
                assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
                assert_eq!(event.create_kind, "customizable".to_string());
            },
            crate::MeteoraDammV2DecodedEvent::Swap(_) => {
                panic!("unexpected swap event")
            },
        }
    }

    #[test]
    fn meteora_damm_v2_swap_is_detected() {
        let decoder = crate::MeteoraDammV2Decoder::new();
        let transaction = make_swap_transaction();
        let instructions = vec![make_swap_instruction()];
        let decoded_result = decoder.decode_transaction(&transaction, &instructions);
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        match &decoded[0] {
            crate::MeteoraDammV2DecodedEvent::Swap(event) => {
                assert_eq!(event.transaction_id, 403);
                assert_eq!(event.instruction_id, 404);
                assert_eq!(event.pool_account, Some("DammV2SwapPool111".to_string()));
                assert_eq!(event.token_a_mint, Some("DammV2SwapTokenA111".to_string()));
                assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
                assert!(event.used_swap2);
            },
            crate::MeteoraDammV2DecodedEvent::CreatePool(_) => {
                panic!("unexpected create event")
            },
        }
    }

    #[test]
    fn meteora_damm_v2_swap2_discriminator_is_detected() {
        let data = [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88, 0x01];
        let kind = super::classify_instruction_kind_from_data(Some(&data));
        assert_eq!(kind, super::MeteoraDammV2InstructionKind::Swap);
    }

    #[test]
    fn meteora_damm_v2_unknown_data_discriminator_does_not_fallback_to_global_swap_logs() {
        let data = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x01];
        let logs = vec!["Program log: Instruction: Swap2".to_string()];
        let kind = super::classify_instruction_kind(None, Some(&data), &logs);
        assert_eq!(kind, super::MeteoraDammV2InstructionKind::Unknown);
    }

    #[test]
    fn meteora_damm_v2_inner_swap2_instruction_with_data_is_not_skipped() {
        let decoder = crate::MeteoraDammV2Decoder::new();
        let transaction = make_swap_transaction();
        let mut instruction = make_swap_instruction();
        instruction.parent_instruction_id = Some(400);
        instruction.data_json = Some(format!(
            "\"{}\"",
            bs58::encode(&[0x41_u8, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88, 0x01]).into_string()
        ));
        let decoded_result = decoder.decode_transaction(&transaction, &[instruction]);
        let decoded = match decoded_result {
            Ok(decoded) => decoded,
            Err(error) => panic!("decode must succeed: {}", error),
        };
        assert_eq!(decoded.len(), 1);
        match &decoded[0] {
            crate::MeteoraDammV2DecodedEvent::Swap(event) => {
                assert_eq!(event.pool_account, Some("DammV2SwapPool111".to_string()));
            },
            crate::MeteoraDammV2DecodedEvent::CreatePool(_) => panic!("unexpected create event"),
        }
    }
}
