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

//! Raydium AmmV4 transaction decoder.

/// Decoded Raydium AmmV4 initialize2 pool event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RaydiumAmmV4Initialize2PoolDecoded {
    /// 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 lp mint.
    pub lp_mint: 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 market account.
    pub market_account: std::option::Option<std::string::String>,
    /// Decoded payload.
    pub payload_json: serde_json::Value,
}

/// Decoded Raydium AmmV4 event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum RaydiumAmmV4DecodedEvent {
    /// `initialize2` pool creation-like event.
    Initialize2Pool(RaydiumAmmV4Initialize2PoolDecoded),
}

/// Raydium AmmV4 decoder.
#[derive(Debug, Clone, Default)]
pub struct RaydiumAmmV4Decoder;

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

    /// Decodes one projected transaction into zero or more Raydium AmmV4 events.
    pub fn decode_transaction(
        &self,
        transaction: &crate::ChainTransactionDto,
        instructions: &[crate::ChainInstructionDto],
    ) -> Result<std::vec::Vec<crate::RaydiumAmmV4DecodedEvent>, 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 has_initialize2_log = log_messages_contain_initialize2(&log_messages);
        let mut decoded_events = std::vec::Vec::new();
        for instruction in instructions {
            if instruction.parent_instruction_id.is_some() {
                continue;
            }
            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::RAYDIUM_AMM_V4_PROGRAM_ID {
                continue;
            }
            if !has_initialize2_log {
                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),
            };
            if accounts.len() < 10 {
                continue;
            }
            let pool_account = extract_account(&accounts, 4);
            let lp_mint = extract_account(&accounts, 7);
            let token_a_mint = extract_account(&accounts, 8);
            let token_b_mint = extract_account(&accounts, 9);
            let market_account = extract_account(&accounts, 16);
            let payload_json = serde_json::json!({
                "decoder": "raydium_amm_v4",
                "eventKind": "initialize2_pool",
                "signature": transaction.signature,
                "instructionId": instruction_id,
                "instructionIndex": instruction.instruction_index,
                "accounts": accounts,
                "logMessages": log_messages,
                "poolAccount": pool_account,
                "lpMint": lp_mint,
                "tokenAMint": token_a_mint,
                "tokenBMint": token_b_mint,
                "marketAccount": market_account
            });
            decoded_events.push(crate::RaydiumAmmV4DecodedEvent::Initialize2Pool(
                crate::RaydiumAmmV4Initialize2PoolDecoded {
                    transaction_id,
                    instruction_id,
                    signature: transaction.signature.clone(),
                    program_id: program_id.clone(),
                    pool_account,
                    lp_mint,
                    token_a_mint,
                    token_b_mint,
                    market_account,
                    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 log_messages_contain_initialize2(log_messages: &[std::string::String]) -> bool {
    for log_message in log_messages {
        if log_message.contains("initialize2") {
            return true;
        }
    }
    return false;
}

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());
        }
    }
    return Ok(accounts);
}

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());
}

#[cfg(test)]
mod tests {
    fn make_transaction() -> crate::ChainTransactionDto {
        let mut dto = crate::ChainTransactionDto::new(
            "sig-raydium-test-1".to_string(),
            Some(888888),
            Some(1778000000),
            Some("helius_primary_http".to_string()),
            Some("0".to_string()),
            None,
            None,
            serde_json::json!({
                "slot": 888888,
                "meta": {
                    "logMessages": [
                        "Program log: initialize2"
                    ]
                },
                "transaction": {
                    "message": {
                        "instructions": []
                    }
                }
            })
            .to_string(),
        );
        dto.id = Some(42);
        return dto;
    }

    fn make_instruction() -> crate::ChainInstructionDto {
        let mut dto = crate::ChainInstructionDto::new(
            42,
            None,
            0,
            None,
            Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
            Some("raydium-amm-v4".to_string()),
            Some(1),
            serde_json::json!([
                "Account0",
                "Account1",
                "Account2",
                "Account3",
                "Pool111",
                "Account5",
                "Account6",
                "LpMint111",
                "TokenA111",
                "TokenB111",
                "Account10",
                "Account11",
                "Account12",
                "Account13",
                "Account14",
                "Account15",
                "Market111"
            ])
            .to_string(),
            None,
            None,
            None,
        );
        dto.id = Some(7);
        return dto;
    }

    #[test]
    fn raydium_amm_v4_initialize2_logs_are_detected() {
        let decoder = crate::RaydiumAmmV4Decoder::new();
        let transaction = make_transaction();
        let instructions = vec![make_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::RaydiumAmmV4DecodedEvent::Initialize2Pool(event) => {
                assert_eq!(event.transaction_id, 42);
                assert_eq!(event.instruction_id, 7);
                assert_eq!(event.pool_account, Some("Pool111".to_string()));
                assert_eq!(event.lp_mint, Some("LpMint111".to_string()));
                assert_eq!(event.token_a_mint, Some("TokenA111".to_string()));
                assert_eq!(event.token_b_mint, Some("TokenB111".to_string()));
                assert_eq!(event.market_account, Some("Market111".to_string()));
            },
        }
    }

    #[test]
    fn raydium_amm_v4_initialize2_returns_none_without_expected_log() {
        let decoder = crate::RaydiumAmmV4Decoder::new();
        let mut transaction = make_transaction();
        transaction.transaction_json = serde_json::json!({
            "slot": 888888,
            "meta": {
                "logMessages": [
                    "Program log: swap"
                ]
            },
            "transaction": {
                "message": {
                    "instructions": []
                }
            }
        })
        .to_string();
        let instructions = vec![make_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(), 0);
    }
}
