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

//! Raydium CPMM decoder.
//!
//! This module decodes Raydium constant product swap instructions from
//! already-projected Solana transaction instructions.

/// Raydium CPMM `swap_base_input` discriminator.
const RAYDIUM_CPMM_SWAP_BASE_INPUT_DISCRIMINATOR: [u8; 8] = [143, 190, 90, 218, 196, 30, 51, 222];

/// Raydium CPMM `swap_base_output` discriminator.
const RAYDIUM_CPMM_SWAP_BASE_OUTPUT_DISCRIMINATOR: [u8; 8] = [55, 217, 98, 86, 163, 74, 180, 173];

/// Raydium CPMM `close_permission_pda` discriminator.
const RAYDIUM_CPMM_CLOSE_PERMISSION_PDA_DISCRIMINATOR: [u8; 8] =
    [156, 84, 32, 118, 69, 135, 70, 123];

/// Raydium CPMM `collect_creator_fee` discriminator.
const RAYDIUM_CPMM_COLLECT_CREATOR_FEE_DISCRIMINATOR: [u8; 8] =
    [20, 22, 86, 123, 198, 28, 219, 132];

/// Raydium CPMM `collect_fund_fee` discriminator.
const RAYDIUM_CPMM_COLLECT_FUND_FEE_DISCRIMINATOR: [u8; 8] = [167, 138, 78, 149, 223, 194, 6, 126];

/// Raydium CPMM `collect_protocol_fee` discriminator.
const RAYDIUM_CPMM_COLLECT_PROTOCOL_FEE_DISCRIMINATOR: [u8; 8] =
    [136, 136, 252, 221, 194, 66, 126, 89];

/// Raydium CPMM `create_amm_config` discriminator.
const RAYDIUM_CPMM_CREATE_AMM_CONFIG_DISCRIMINATOR: [u8; 8] =
    [137, 52, 237, 212, 215, 117, 108, 104];

/// Raydium CPMM `create_permission_pda` discriminator.
const RAYDIUM_CPMM_CREATE_PERMISSION_PDA_DISCRIMINATOR: [u8; 8] =
    [135, 136, 2, 216, 137, 169, 181, 202];

/// Raydium CPMM `deposit` discriminator.
const RAYDIUM_CPMM_DEPOSIT_DISCRIMINATOR: [u8; 8] = [242, 35, 198, 137, 82, 225, 242, 182];

/// Raydium CPMM `withdraw` discriminator.
const RAYDIUM_CPMM_WITHDRAW_DISCRIMINATOR: [u8; 8] = [183, 18, 70, 156, 148, 109, 161, 34];

/// Raydium CPMM `initialize` discriminator.
const RAYDIUM_CPMM_INITIALIZE_DISCRIMINATOR: [u8; 8] = [175, 175, 109, 31, 13, 152, 155, 237];

/// Raydium CPMM `initialize_with_permission` discriminator.
const RAYDIUM_CPMM_INITIALIZE_WITH_PERMISSION_DISCRIMINATOR: [u8; 8] =
    [63, 55, 254, 65, 49, 178, 89, 121];

/// Raydium CPMM `update_amm_config` discriminator.
const RAYDIUM_CPMM_UPDATE_AMM_CONFIG_DISCRIMINATOR: [u8; 8] = [49, 60, 174, 136, 154, 28, 116, 200];

/// Raydium CPMM `update_pool_status` discriminator.
const RAYDIUM_CPMM_UPDATE_POOL_STATUS_DISCRIMINATOR: [u8; 8] = [130, 87, 108, 6, 46, 224, 117, 123];

/// Anchor self-CPI log selector used by Raydium CPMM events.
const RAYDIUM_CPMM_ANCHOR_SELF_CPI_LOG_SELECTOR: [u8; 8] = [228, 69, 165, 46, 81, 203, 154, 29];

/// Anchor IDL management discriminator observed on Raydium CPMM (`IdlCreateAccount` / `IdlCloseAccount`).
const RAYDIUM_CPMM_ANCHOR_IDL_INSTRUCTION_DISCRIMINATOR: [u8; 8] =
    [64, 244, 188, 120, 167, 233, 105, 10];

/// Raydium CPMM `LpChangeEvent` Anchor event discriminator.
const RAYDIUM_CPMM_LP_CHANGE_EVENT_DISCRIMINATOR: [u8; 8] = [121, 163, 205, 201, 57, 218, 117, 60];

/// Raydium CPMM `SwapEvent` Anchor event discriminator.
const RAYDIUM_CPMM_SWAP_EVENT_DISCRIMINATOR: [u8; 8] = [64, 198, 205, 232, 38, 8, 113, 226];

/// Raydium CPMM decoded event.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
pub enum RaydiumCpmmDecodedEvent {
    /// Swap where the user fixes the input amount.
    SwapBaseInput(RaydiumCpmmSwapDecoded),
    /// Swap where the user fixes the output amount.
    SwapBaseOutput(RaydiumCpmmSwapDecoded),
    /// Pool initialization instruction.
    Initialize(RaydiumCpmmPoolInitializeDecoded),
    /// Permissioned pool initialization instruction.
    InitializeWithPermission(RaydiumCpmmPoolInitializeDecoded),
    /// Add-liquidity instruction.
    Deposit(RaydiumCpmmLiquidityDecoded),
    /// Remove-liquidity instruction.
    Withdraw(RaydiumCpmmLiquidityDecoded),
    /// Fee-collection instruction.
    FeeCollection(RaydiumCpmmFeeCollectionDecoded),
    /// Pool/config administration instruction.
    PoolAdmin(RaydiumCpmmPoolAdminDecoded),
    /// Anchor CPI liquidity change event retained as decoded audit evidence.
    LpChangeEvent(RaydiumCpmmLpChangeEventDecoded),
    /// Anchor CPI swap event retained as decoded audit evidence.
    SwapEvent(RaydiumCpmmSwapEventDecoded),
}

/// Raydium CPMM pool initialization instruction decoded from accounts and instruction data.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RaydiumCpmmPoolInitializeDecoded {
    /// Instruction name.
    pub instruction_name: std::string::String,
    /// Creator/payer account.
    pub creator: std::option::Option<std::string::String>,
    /// AMM config account.
    pub amm_config: std::option::Option<std::string::String>,
    /// Pool state account.
    pub pool_state: std::string::String,
    /// Token 0 mint.
    pub token_0_mint: std::string::String,
    /// Token 1 mint.
    pub token_1_mint: std::string::String,
    /// LP mint, when present in the projected account list.
    pub lp_mint: std::option::Option<std::string::String>,
    /// Normalized base mint.
    pub base_mint: std::string::String,
    /// Normalized quote mint.
    pub quote_mint: std::string::String,
    /// Initial amount for token 0, when present.
    pub init_amount_0_raw: std::option::Option<std::string::String>,
    /// Initial amount for token 1, when present.
    pub init_amount_1_raw: std::option::Option<std::string::String>,
    /// Opening timestamp, when present.
    pub open_time_raw: std::option::Option<std::string::String>,
}

/// Raydium CPMM liquidity instruction decoded from accounts and instruction data.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RaydiumCpmmLiquidityDecoded {
    /// Instruction name.
    pub instruction_name: std::string::String,
    /// Liquidity direction: `add` or `remove`.
    pub liquidity_direction: std::string::String,
    /// Owner/payer account.
    pub owner: std::option::Option<std::string::String>,
    /// Pool state account.
    pub pool_state: std::string::String,
    /// Token 0 mint.
    pub token_0_mint: std::string::String,
    /// Token 1 mint.
    pub token_1_mint: std::string::String,
    /// LP mint, when present in the projected account list.
    pub lp_mint: std::option::Option<std::string::String>,
    /// Normalized base mint.
    pub base_mint: std::string::String,
    /// Normalized quote mint.
    pub quote_mint: std::string::String,
    /// Amount of token 0 requested/minimum depending on direction.
    pub token_0_amount_raw: std::string::String,
    /// Amount of token 1 requested/minimum depending on direction.
    pub token_1_amount_raw: std::string::String,
    /// Base amount mapped from token 0/1.
    pub base_amount_raw: std::string::String,
    /// Quote amount mapped from token 0/1.
    pub quote_amount_raw: std::string::String,
    /// LP token amount.
    pub lp_amount_raw: std::string::String,
    /// Amount semantics.
    pub amount_semantics: std::string::String,
}

/// Raydium CPMM fee-collection instruction decoded from accounts.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RaydiumCpmmFeeCollectionDecoded {
    /// Instruction name.
    pub instruction_name: std::string::String,
    /// Pool state account.
    pub pool_state: std::string::String,
    /// Token 0 mint.
    pub token_0_mint: std::option::Option<std::string::String>,
    /// Token 1 mint.
    pub token_1_mint: std::option::Option<std::string::String>,
    /// Normalized base mint.
    pub base_mint: std::option::Option<std::string::String>,
    /// Normalized quote mint.
    pub quote_mint: std::option::Option<std::string::String>,
    /// Fee category.
    pub fee_scope: std::string::String,
    /// Actor/authority account, when known.
    pub actor_wallet: std::option::Option<std::string::String>,
}

/// Raydium CPMM pool/config administration instruction decoded from accounts.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RaydiumCpmmPoolAdminDecoded {
    /// Instruction name.
    pub instruction_name: std::string::String,
    /// Pool account, when the instruction is pool-scoped.
    pub pool_state: std::option::Option<std::string::String>,
    /// Config or permission account, when available.
    pub config_or_permission_account: std::option::Option<std::string::String>,
    /// Actor/authority account, when known.
    pub actor_wallet: std::option::Option<std::string::String>,
    /// Administration action category.
    pub admin_action: std::string::String,
    /// First u8 argument, when available.
    pub first_u8_arg: std::option::Option<u8>,
    /// First u64 argument, when available.
    pub first_u64_arg_raw: std::option::Option<std::string::String>,
}

/// Raydium CPMM swap mode.
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
pub enum RaydiumCpmmSwapMode {
    /// Fixed input swap.
    BaseInput,
    /// Fixed output swap.
    BaseOutput,
}

/// Raydium CPMM decoded swap.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
pub struct RaydiumCpmmSwapDecoded {
    /// Instruction mode.
    pub swap_mode: RaydiumCpmmSwapMode,
    /// User or payer account.
    pub payer: std::string::String,
    /// Raydium authority account.
    pub authority: std::string::String,
    /// AMM config account.
    pub amm_config: std::string::String,
    /// CPMM 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,
    /// Input token program.
    pub input_token_program: std::string::String,
    /// Output token program.
    pub output_token_program: std::string::String,
    /// Input token mint.
    pub input_token_mint: std::string::String,
    /// Output token mint.
    pub output_token_mint: std::string::String,
    /// Observation state.
    pub observation_state: std::string::String,
    /// Normalized base mint.
    pub base_mint: std::string::String,
    /// Normalized quote mint.
    pub quote_mint: std::string::String,
    /// Normalized base vault.
    pub base_vault: std::string::String,
    /// Normalized quote vault.
    pub quote_vault: std::string::String,
    /// True when input mint is the normalized base mint.
    pub input_is_base: bool,
    /// Trade side from the normalized pair perspective.
    pub trade_side: std::string::String,
    /// Fixed input amount, for swap_base_input.
    pub amount_in_raw: std::option::Option<std::string::String>,
    /// Minimum output amount, for swap_base_input.
    pub minimum_amount_out_raw: std::option::Option<std::string::String>,
    /// Maximum input amount, for swap_base_output.
    pub max_amount_in_raw: std::option::Option<std::string::String>,
    /// Fixed output amount, for swap_base_output.
    pub amount_out_raw: std::option::Option<std::string::String>,
}

/// Raydium CPMM Anchor CPI liquidity change event.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RaydiumCpmmLpChangeEventDecoded {
    /// Pool account reported by the CPI event.
    pub pool_id: std::string::String,
    /// LP amount before the change.
    pub lp_amount_before_raw: std::string::String,
    /// Token 0 vault balance before the change.
    pub token_0_vault_before_raw: std::string::String,
    /// Token 1 vault balance before the change.
    pub token_1_vault_before_raw: std::string::String,
    /// Token 0 amount involved in the change.
    pub token_0_amount_raw: std::string::String,
    /// Token 1 amount involved in the change.
    pub token_1_amount_raw: std::string::String,
    /// Token 0 transfer fee.
    pub token_0_transfer_fee_raw: std::string::String,
    /// Token 1 transfer fee.
    pub token_1_transfer_fee_raw: std::string::String,
    /// Raw Raydium change type.
    pub change_type: u8,
}

/// Raydium CPMM Anchor CPI swap event retained as audit evidence.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RaydiumCpmmSwapEventDecoded {
    /// Pool account reported by the CPI event.
    pub pool_id: std::string::String,
    /// Input vault balance before the swap.
    pub input_vault_before_raw: std::string::String,
    /// Output vault balance before the swap.
    pub output_vault_before_raw: std::string::String,
    /// Input amount reported by the event.
    pub input_amount_raw: std::string::String,
    /// Output amount reported by the event.
    pub output_amount_raw: std::string::String,
    /// Input transfer fee reported by the event.
    pub input_transfer_fee_raw: std::string::String,
    /// Output transfer fee reported by the event.
    pub output_transfer_fee_raw: std::string::String,
    /// True when the event is base-input.
    pub base_input: bool,
    /// Optional input mint, available in newer Carbon layouts.
    pub input_mint: std::option::Option<std::string::String>,
    /// Optional output mint, available in newer Carbon layouts.
    pub output_mint: std::option::Option<std::string::String>,
    /// Optional normalized base mint.
    pub base_mint: std::option::Option<std::string::String>,
    /// Optional normalized quote mint.
    pub quote_mint: std::option::Option<std::string::String>,
    /// Optional trade fee reported by newer layouts.
    pub trade_fee_raw: std::option::Option<std::string::String>,
    /// Optional creator fee reported by newer layouts.
    pub creator_fee_raw: std::option::Option<std::string::String>,
    /// Optional creator-fee side flag reported by newer layouts.
    pub creator_fee_on_input: std::option::Option<bool>,
    /// Explicitly prevents this audit event from becoming a trade.
    pub trade_candidate: bool,
    /// Explicitly prevents this audit event from becoming a candle.
    pub candle_candidate: bool,
    /// Reason this event is decoded but not materialized as a trade.
    pub skip_trade_reason: std::string::String,
    /// Reason this event is decoded but not materialized as a candle.
    pub skip_candle_reason: std::string::String,
}

impl RaydiumCpmmDecodedEvent {
    /// Returns the storage event kind.
    pub fn event_kind(&self) -> &'static str {
        match self {
            RaydiumCpmmDecodedEvent::SwapBaseInput(_) => return "raydium_cpmm.swap_base_input",
            RaydiumCpmmDecodedEvent::SwapBaseOutput(_) => return "raydium_cpmm.swap_base_output",
            RaydiumCpmmDecodedEvent::Initialize(_) => return "raydium_cpmm.initialize",
            RaydiumCpmmDecodedEvent::InitializeWithPermission(_) => {
                return "raydium_cpmm.initialize_with_permission";
            },
            RaydiumCpmmDecodedEvent::Deposit(_) => return "raydium_cpmm.deposit",
            RaydiumCpmmDecodedEvent::Withdraw(_) => return "raydium_cpmm.withdraw",
            RaydiumCpmmDecodedEvent::FeeCollection(event) => {
                match event.instruction_name.as_str() {
                    "raydium_cpmm.collect_creator_fee" => return "raydium_cpmm.collect_creator_fee",
                    "raydium_cpmm.collect_fund_fee" => return "raydium_cpmm.collect_fund_fee",
                    _ => return "raydium_cpmm.collect_protocol_fee",
                }
            },
            RaydiumCpmmDecodedEvent::PoolAdmin(event) => match event.instruction_name.as_str() {
                "raydium_cpmm.close_permission_pda" => return "raydium_cpmm.close_permission_pda",
                "raydium_cpmm.create_permission_pda" => return "raydium_cpmm.create_permission_pda",
                "raydium_cpmm.update_amm_config" => return "raydium_cpmm.update_amm_config",
                "raydium_cpmm.update_pool_status" => return "raydium_cpmm.update_pool_status",
                _ => return "raydium_cpmm.create_amm_config",
            },
            RaydiumCpmmDecodedEvent::LpChangeEvent(_) => return "raydium_cpmm.lp_change_event",
            RaydiumCpmmDecodedEvent::SwapEvent(_) => return "raydium_cpmm.swap_event",
        }
    }

    /// Returns the pool account when one is decoded.
    pub fn pool_account(&self) -> std::option::Option<&str> {
        match self {
            RaydiumCpmmDecodedEvent::SwapBaseInput(event) => {
                return Some(event.pool_state.as_str());
            },
            RaydiumCpmmDecodedEvent::SwapBaseOutput(event) => {
                return Some(event.pool_state.as_str());
            },
            RaydiumCpmmDecodedEvent::Initialize(event) => return Some(event.pool_state.as_str()),
            RaydiumCpmmDecodedEvent::InitializeWithPermission(event) => {
                return Some(event.pool_state.as_str());
            },
            RaydiumCpmmDecodedEvent::Deposit(event) => return Some(event.pool_state.as_str()),
            RaydiumCpmmDecodedEvent::Withdraw(event) => return Some(event.pool_state.as_str()),
            RaydiumCpmmDecodedEvent::FeeCollection(event) => {
                return Some(event.pool_state.as_str());
            },
            RaydiumCpmmDecodedEvent::PoolAdmin(event) => return event.pool_state.as_deref(),
            RaydiumCpmmDecodedEvent::LpChangeEvent(event) => return Some(event.pool_id.as_str()),
            RaydiumCpmmDecodedEvent::SwapEvent(event) => return Some(event.pool_id.as_str()),
        }
    }

    /// Returns the normalized base mint when one is decoded.
    pub fn base_mint(&self) -> std::option::Option<&str> {
        match self {
            RaydiumCpmmDecodedEvent::SwapBaseInput(event) => return Some(event.base_mint.as_str()),
            RaydiumCpmmDecodedEvent::SwapBaseOutput(event) => {
                return Some(event.base_mint.as_str());
            },
            RaydiumCpmmDecodedEvent::Initialize(event) => return Some(event.base_mint.as_str()),
            RaydiumCpmmDecodedEvent::InitializeWithPermission(event) => {
                return Some(event.base_mint.as_str());
            },
            RaydiumCpmmDecodedEvent::Deposit(event) => return Some(event.base_mint.as_str()),
            RaydiumCpmmDecodedEvent::Withdraw(event) => return Some(event.base_mint.as_str()),
            RaydiumCpmmDecodedEvent::FeeCollection(event) => return event.base_mint.as_deref(),
            RaydiumCpmmDecodedEvent::PoolAdmin(_) => return None,
            RaydiumCpmmDecodedEvent::LpChangeEvent(_) => return None,
            RaydiumCpmmDecodedEvent::SwapEvent(event) => return event.base_mint.as_deref(),
        }
    }

    /// Returns the normalized quote mint when one is decoded.
    pub fn quote_mint(&self) -> std::option::Option<&str> {
        match self {
            RaydiumCpmmDecodedEvent::SwapBaseInput(event) => {
                return Some(event.quote_mint.as_str());
            },
            RaydiumCpmmDecodedEvent::SwapBaseOutput(event) => {
                return Some(event.quote_mint.as_str());
            },
            RaydiumCpmmDecodedEvent::Initialize(event) => return Some(event.quote_mint.as_str()),
            RaydiumCpmmDecodedEvent::InitializeWithPermission(event) => {
                return Some(event.quote_mint.as_str());
            },
            RaydiumCpmmDecodedEvent::Deposit(event) => return Some(event.quote_mint.as_str()),
            RaydiumCpmmDecodedEvent::Withdraw(event) => return Some(event.quote_mint.as_str()),
            RaydiumCpmmDecodedEvent::FeeCollection(event) => return event.quote_mint.as_deref(),
            RaydiumCpmmDecodedEvent::PoolAdmin(_) => return None,
            RaydiumCpmmDecodedEvent::LpChangeEvent(_) => return None,
            RaydiumCpmmDecodedEvent::SwapEvent(event) => return event.quote_mint.as_deref(),
        }
    }

    /// Returns the LP mint when one is decoded.
    pub fn lp_mint(&self) -> std::option::Option<&str> {
        match self {
            RaydiumCpmmDecodedEvent::Initialize(event) => return event.lp_mint.as_deref(),
            RaydiumCpmmDecodedEvent::InitializeWithPermission(event) => {
                return event.lp_mint.as_deref();
            },
            RaydiumCpmmDecodedEvent::Deposit(event) => return event.lp_mint.as_deref(),
            RaydiumCpmmDecodedEvent::Withdraw(event) => return event.lp_mint.as_deref(),
            _ => return None,
        }
    }

    /// Converts the decoded event to JSON payload.
    pub fn to_payload_json(&self) -> std::option::Option<std::string::String> {
        match self {
            crate::RaydiumCpmmDecodedEvent::SwapBaseInput(event) => {
                let result = serde_json::to_string(event);
                match result {
                    Ok(payload) => return Some(payload),
                    Err(_) => return None,
                }
            },
            crate::RaydiumCpmmDecodedEvent::SwapBaseOutput(event) => {
                let result = serde_json::to_string(event);
                match result {
                    Ok(payload) => return Some(payload),
                    Err(_) => return None,
                }
            },
            crate::RaydiumCpmmDecodedEvent::Initialize(event) => {
                let result = serde_json::to_string(event);
                match result {
                    Ok(payload) => return Some(payload),
                    Err(_) => return None,
                }
            },
            crate::RaydiumCpmmDecodedEvent::InitializeWithPermission(event) => {
                let result = serde_json::to_string(event);
                match result {
                    Ok(payload) => return Some(payload),
                    Err(_) => return None,
                }
            },
            crate::RaydiumCpmmDecodedEvent::Deposit(event) => {
                let result = serde_json::to_string(event);
                match result {
                    Ok(payload) => return Some(payload),
                    Err(_) => return None,
                }
            },
            crate::RaydiumCpmmDecodedEvent::Withdraw(event) => {
                let result = serde_json::to_string(event);
                match result {
                    Ok(payload) => return Some(payload),
                    Err(_) => return None,
                }
            },
            crate::RaydiumCpmmDecodedEvent::FeeCollection(event) => {
                let result = serde_json::to_string(event);
                match result {
                    Ok(payload) => return Some(payload),
                    Err(_) => return None,
                }
            },
            crate::RaydiumCpmmDecodedEvent::PoolAdmin(event) => {
                let result = serde_json::to_string(event);
                match result {
                    Ok(payload) => return Some(payload),
                    Err(_) => return None,
                }
            },
            crate::RaydiumCpmmDecodedEvent::LpChangeEvent(event) => {
                let result = serde_json::to_string(event);
                match result {
                    Ok(payload) => return Some(payload),
                    Err(_) => return None,
                }
            },
            crate::RaydiumCpmmDecodedEvent::SwapEvent(event) => {
                let result = serde_json::to_string(event);
                match result {
                    Ok(payload) => return Some(payload),
                    Err(_) => return None,
                }
            },
        }
    }
}

/// Decodes one Raydium CPMM instruction from projected instruction fields.
pub fn decode_raydium_cpmm_instruction(
    accounts_json: &str,
    data_json: &str,
) -> std::vec::Vec<RaydiumCpmmDecodedEvent> {
    let accounts = match parse_accounts_json(accounts_json) {
        Some(accounts) => accounts,
        None => return std::vec::Vec::new(),
    };
    let data_base58 = match parse_data_json_as_base58(data_json) {
        Some(data_base58) => data_base58,
        None => return std::vec::Vec::new(),
    };
    let data = match bs58::decode(data_base58.as_str()).into_vec() {
        Ok(data) => data,
        Err(_) => return std::vec::Vec::new(),
    };
    if data.len() < 8 {
        return std::vec::Vec::new();
    }
    let discriminator = [data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]];
    if discriminator == RAYDIUM_CPMM_ANCHOR_SELF_CPI_LOG_SELECTOR {
        let decoded_anchor_event = decode_raydium_cpmm_anchor_cpi_event(data.as_slice());
        match decoded_anchor_event {
            Some(decoded_anchor_event) => return vec![decoded_anchor_event],
            None => return std::vec::Vec::new(),
        }
    }
    if data.len() < 24 {
        return std::vec::Vec::new();
    }
    if discriminator == RAYDIUM_CPMM_SWAP_BASE_INPUT_DISCRIMINATOR {
        let amount_in = match read_u64_le(data.as_slice(), 8) {
            Some(value) => value,
            None => return std::vec::Vec::new(),
        };
        let minimum_amount_out = match read_u64_le(data.as_slice(), 16) {
            Some(value) => value,
            None => return std::vec::Vec::new(),
        };
        let swap = match build_raydium_cpmm_swap(
            RaydiumCpmmSwapMode::BaseInput,
            accounts.as_slice(),
            Some(amount_in.to_string()),
            Some(minimum_amount_out.to_string()),
            None,
            None,
        ) {
            Some(swap) => swap,
            None => return std::vec::Vec::new(),
        };
        return vec![RaydiumCpmmDecodedEvent::SwapBaseInput(swap)];
    }
    if discriminator == RAYDIUM_CPMM_SWAP_BASE_OUTPUT_DISCRIMINATOR {
        let max_amount_in = match read_u64_le(data.as_slice(), 8) {
            Some(value) => value,
            None => return std::vec::Vec::new(),
        };
        let amount_out = match read_u64_le(data.as_slice(), 16) {
            Some(value) => value,
            None => return std::vec::Vec::new(),
        };
        let swap = match build_raydium_cpmm_swap(
            RaydiumCpmmSwapMode::BaseOutput,
            accounts.as_slice(),
            None,
            None,
            Some(max_amount_in.to_string()),
            Some(amount_out.to_string()),
        ) {
            Some(swap) => swap,
            None => return std::vec::Vec::new(),
        };
        return vec![RaydiumCpmmDecodedEvent::SwapBaseOutput(swap)];
    }
    if discriminator == RAYDIUM_CPMM_DEPOSIT_DISCRIMINATOR {
        let event = match build_raydium_cpmm_liquidity(
            "raydium_cpmm.deposit",
            "add",
            accounts.as_slice(),
            data.as_slice(),
        ) {
            Some(event) => event,
            None => return std::vec::Vec::new(),
        };
        return vec![RaydiumCpmmDecodedEvent::Deposit(event)];
    }
    if discriminator == RAYDIUM_CPMM_WITHDRAW_DISCRIMINATOR {
        let event = match build_raydium_cpmm_liquidity(
            "raydium_cpmm.withdraw",
            "remove",
            accounts.as_slice(),
            data.as_slice(),
        ) {
            Some(event) => event,
            None => return std::vec::Vec::new(),
        };
        return vec![RaydiumCpmmDecodedEvent::Withdraw(event)];
    }
    if discriminator == RAYDIUM_CPMM_INITIALIZE_DISCRIMINATOR {
        let event = match build_raydium_cpmm_initialize(
            "raydium_cpmm.initialize",
            accounts.as_slice(),
            data.as_slice(),
            false,
        ) {
            Some(event) => event,
            None => return std::vec::Vec::new(),
        };
        return vec![RaydiumCpmmDecodedEvent::Initialize(event)];
    }
    if discriminator == RAYDIUM_CPMM_INITIALIZE_WITH_PERMISSION_DISCRIMINATOR {
        let event = match build_raydium_cpmm_initialize(
            "raydium_cpmm.initialize_with_permission",
            accounts.as_slice(),
            data.as_slice(),
            true,
        ) {
            Some(event) => event,
            None => return std::vec::Vec::new(),
        };
        return vec![RaydiumCpmmDecodedEvent::InitializeWithPermission(event)];
    }
    if discriminator == RAYDIUM_CPMM_COLLECT_CREATOR_FEE_DISCRIMINATOR {
        return build_raydium_cpmm_fee_or_admin_event(
            "raydium_cpmm.collect_creator_fee",
            "creator",
            accounts.as_slice(),
            data.as_slice(),
        );
    }
    if discriminator == RAYDIUM_CPMM_COLLECT_FUND_FEE_DISCRIMINATOR {
        return build_raydium_cpmm_fee_or_admin_event(
            "raydium_cpmm.collect_fund_fee",
            "fund",
            accounts.as_slice(),
            data.as_slice(),
        );
    }
    if discriminator == RAYDIUM_CPMM_COLLECT_PROTOCOL_FEE_DISCRIMINATOR {
        return build_raydium_cpmm_fee_or_admin_event(
            "raydium_cpmm.collect_protocol_fee",
            "protocol",
            accounts.as_slice(),
            data.as_slice(),
        );
    }
    if discriminator == RAYDIUM_CPMM_CREATE_AMM_CONFIG_DISCRIMINATOR {
        return vec![RaydiumCpmmDecodedEvent::PoolAdmin(build_raydium_cpmm_admin(
            "raydium_cpmm.create_amm_config",
            "create_amm_config",
            accounts.as_slice(),
            data.as_slice(),
            None,
        ))];
    }
    if discriminator == RAYDIUM_CPMM_CREATE_PERMISSION_PDA_DISCRIMINATOR {
        return vec![RaydiumCpmmDecodedEvent::PoolAdmin(build_raydium_cpmm_admin(
            "raydium_cpmm.create_permission_pda",
            "create_permission_pda",
            accounts.as_slice(),
            data.as_slice(),
            None,
        ))];
    }
    if discriminator == RAYDIUM_CPMM_CLOSE_PERMISSION_PDA_DISCRIMINATOR {
        return vec![RaydiumCpmmDecodedEvent::PoolAdmin(build_raydium_cpmm_admin(
            "raydium_cpmm.close_permission_pda",
            "close_permission_pda",
            accounts.as_slice(),
            data.as_slice(),
            None,
        ))];
    }
    if discriminator == RAYDIUM_CPMM_UPDATE_AMM_CONFIG_DISCRIMINATOR {
        return vec![RaydiumCpmmDecodedEvent::PoolAdmin(build_raydium_cpmm_admin(
            "raydium_cpmm.update_amm_config",
            "update_amm_config",
            accounts.as_slice(),
            data.as_slice(),
            None,
        ))];
    }
    if discriminator == RAYDIUM_CPMM_UPDATE_POOL_STATUS_DISCRIMINATOR {
        let pool_state = account_at(accounts.as_slice(), 1);
        return vec![RaydiumCpmmDecodedEvent::PoolAdmin(build_raydium_cpmm_admin(
            "raydium_cpmm.update_pool_status",
            "update_pool_status",
            accounts.as_slice(),
            data.as_slice(),
            pool_state,
        ))];
    }
    return std::vec::Vec::new();
}

/// Classifies one projected Raydium CPMM instruction data payload.
pub fn classify_raydium_cpmm_instruction_data(
    data_json: &str,
) -> std::option::Option<&'static str> {
    let data_base58 = match parse_data_json_as_base58(data_json) {
        Some(data_base58) => data_base58,
        None => return None,
    };
    let data = match bs58::decode(data_base58.as_str()).into_vec() {
        Ok(data) => data,
        Err(_) => return None,
    };
    if data.len() < 8 {
        return None;
    }
    let discriminator = [data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]];
    if discriminator == RAYDIUM_CPMM_SWAP_BASE_INPUT_DISCRIMINATOR {
        return Some("swap_base_input");
    }
    if discriminator == RAYDIUM_CPMM_SWAP_BASE_OUTPUT_DISCRIMINATOR {
        return Some("swap_base_output");
    }
    if discriminator == RAYDIUM_CPMM_CLOSE_PERMISSION_PDA_DISCRIMINATOR {
        return Some("close_permission_pda");
    }
    if discriminator == RAYDIUM_CPMM_COLLECT_CREATOR_FEE_DISCRIMINATOR {
        return Some("collect_creator_fee");
    }
    if discriminator == RAYDIUM_CPMM_COLLECT_FUND_FEE_DISCRIMINATOR {
        return Some("collect_fund_fee");
    }
    if discriminator == RAYDIUM_CPMM_COLLECT_PROTOCOL_FEE_DISCRIMINATOR {
        return Some("collect_protocol_fee");
    }
    if discriminator == RAYDIUM_CPMM_CREATE_AMM_CONFIG_DISCRIMINATOR {
        return Some("create_amm_config");
    }
    if discriminator == RAYDIUM_CPMM_CREATE_PERMISSION_PDA_DISCRIMINATOR {
        return Some("create_permission_pda");
    }
    if discriminator == RAYDIUM_CPMM_DEPOSIT_DISCRIMINATOR {
        return Some("deposit");
    }
    if discriminator == RAYDIUM_CPMM_WITHDRAW_DISCRIMINATOR {
        return Some("withdraw");
    }
    if discriminator == RAYDIUM_CPMM_INITIALIZE_DISCRIMINATOR {
        return Some("initialize");
    }
    if discriminator == RAYDIUM_CPMM_INITIALIZE_WITH_PERMISSION_DISCRIMINATOR {
        return Some("initialize_with_permission");
    }
    if discriminator == RAYDIUM_CPMM_UPDATE_AMM_CONFIG_DISCRIMINATOR {
        return Some("update_amm_config");
    }
    if discriminator == RAYDIUM_CPMM_UPDATE_POOL_STATUS_DISCRIMINATOR {
        return Some("update_pool_status");
    }
    if discriminator == RAYDIUM_CPMM_ANCHOR_IDL_INSTRUCTION_DISCRIMINATOR {
        return Some("anchor_idl_instruction");
    }
    return None;
}

/// Decodes Raydium CPMM Anchor events emitted in `Program data:` logs.
pub fn decode_raydium_cpmm_program_data_event(
    data_base64: &str,
) -> std::option::Option<RaydiumCpmmDecodedEvent> {
    let decoded = decode_base64_standard(data_base64);
    let data = match decoded {
        Some(data) => data,
        None => return None,
    };
    return decode_raydium_cpmm_event_data(data.as_slice());
}

fn decode_raydium_cpmm_anchor_cpi_event(
    data: &[u8],
) -> std::option::Option<RaydiumCpmmDecodedEvent> {
    if data.len() < 16 {
        return None;
    }
    let discriminator = [data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]];
    if discriminator != RAYDIUM_CPMM_ANCHOR_SELF_CPI_LOG_SELECTOR {
        return None;
    }
    return decode_raydium_cpmm_event_data(&data[8..]);
}

fn decode_raydium_cpmm_event_data(data: &[u8]) -> std::option::Option<RaydiumCpmmDecodedEvent> {
    if data.len() < 8 {
        return None;
    }
    let event_discriminator =
        [data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]];
    if event_discriminator == RAYDIUM_CPMM_LP_CHANGE_EVENT_DISCRIMINATOR {
        let event = decode_raydium_cpmm_lp_change_event(data);
        match event {
            Some(event) => return Some(RaydiumCpmmDecodedEvent::LpChangeEvent(event)),
            None => return None,
        }
    }
    if event_discriminator == RAYDIUM_CPMM_SWAP_EVENT_DISCRIMINATOR {
        let event = decode_raydium_cpmm_swap_event(data);
        match event {
            Some(event) => return Some(RaydiumCpmmDecodedEvent::SwapEvent(event)),
            None => return None,
        }
    }
    return None;
}

fn decode_raydium_cpmm_lp_change_event(
    data: &[u8],
) -> std::option::Option<RaydiumCpmmLpChangeEventDecoded> {
    if data.len() < 97 {
        return None;
    }
    let pool_id = match read_pubkey_base58(data, 8) {
        Some(value) => value,
        None => return None,
    };
    let lp_amount_before = match read_u64_le(data, 40) {
        Some(value) => value,
        None => return None,
    };
    let token_0_vault_before = match read_u64_le(data, 48) {
        Some(value) => value,
        None => return None,
    };
    let token_1_vault_before = match read_u64_le(data, 56) {
        Some(value) => value,
        None => return None,
    };
    let token_0_amount = match read_u64_le(data, 64) {
        Some(value) => value,
        None => return None,
    };
    let token_1_amount = match read_u64_le(data, 72) {
        Some(value) => value,
        None => return None,
    };
    let token_0_transfer_fee = match read_u64_le(data, 80) {
        Some(value) => value,
        None => return None,
    };
    let token_1_transfer_fee = match read_u64_le(data, 88) {
        Some(value) => value,
        None => return None,
    };
    return Some(RaydiumCpmmLpChangeEventDecoded {
        pool_id,
        lp_amount_before_raw: lp_amount_before.to_string(),
        token_0_vault_before_raw: token_0_vault_before.to_string(),
        token_1_vault_before_raw: token_1_vault_before.to_string(),
        token_0_amount_raw: token_0_amount.to_string(),
        token_1_amount_raw: token_1_amount.to_string(),
        token_0_transfer_fee_raw: token_0_transfer_fee.to_string(),
        token_1_transfer_fee_raw: token_1_transfer_fee.to_string(),
        change_type: data[96],
    });
}

fn decode_raydium_cpmm_swap_event(data: &[u8]) -> std::option::Option<RaydiumCpmmSwapEventDecoded> {
    if data.len() < 89 {
        return None;
    }
    let pool_id = match read_pubkey_base58(data, 8) {
        Some(value) => value,
        None => return None,
    };
    let input_vault_before = match read_u64_le(data, 40) {
        Some(value) => value,
        None => return None,
    };
    let output_vault_before = match read_u64_le(data, 48) {
        Some(value) => value,
        None => return None,
    };
    let input_amount = match read_u64_le(data, 56) {
        Some(value) => value,
        None => return None,
    };
    let output_amount = match read_u64_le(data, 64) {
        Some(value) => value,
        None => return None,
    };
    let input_transfer_fee = match read_u64_le(data, 72) {
        Some(value) => value,
        None => return None,
    };
    let output_transfer_fee = match read_u64_le(data, 80) {
        Some(value) => value,
        None => return None,
    };
    let base_input = data[88] != 0;
    let input_mint = read_pubkey_base58(data, 89);
    let output_mint = read_pubkey_base58(data, 121);
    let (base_mint, quote_mint) =
        normalize_optional_raydium_cpmm_mints(input_mint.as_deref(), output_mint.as_deref());
    let trade_fee_raw = match read_u64_le(data, 153) {
        Some(value) => Some(value.to_string()),
        None => None,
    };
    let creator_fee_raw = match read_u64_le(data, 161) {
        Some(value) => Some(value.to_string()),
        None => None,
    };
    let creator_fee_on_input = if data.len() > 169 { Some(data[169] != 0) } else { None };
    return Some(RaydiumCpmmSwapEventDecoded {
        pool_id,
        input_vault_before_raw: input_vault_before.to_string(),
        output_vault_before_raw: output_vault_before.to_string(),
        input_amount_raw: input_amount.to_string(),
        output_amount_raw: output_amount.to_string(),
        input_transfer_fee_raw: input_transfer_fee.to_string(),
        output_transfer_fee_raw: output_transfer_fee.to_string(),
        base_input,
        input_mint,
        output_mint,
        base_mint,
        quote_mint,
        trade_fee_raw,
        creator_fee_raw,
        creator_fee_on_input,
        trade_candidate: false,
        candle_candidate: false,
        skip_trade_reason: "raydium_cpmm_anchor_swap_event_audit_only".to_string(),
        skip_candle_reason: "raydium_cpmm_anchor_swap_event_audit_only".to_string(),
    });
}

fn normalize_optional_raydium_cpmm_mints(
    input_mint: std::option::Option<&str>,
    output_mint: std::option::Option<&str>,
) -> (
    std::option::Option<std::string::String>,
    std::option::Option<std::string::String>,
) {
    let input_mint = match input_mint {
        Some(input_mint) => input_mint,
        None => return (None, None),
    };
    let output_mint = match output_mint {
        Some(output_mint) => output_mint,
        None => return (None, None),
    };
    let normalized = normalize_raydium_cpmm_pair(input_mint, output_mint, "", "");
    return (Some(normalized.base_mint), Some(normalized.quote_mint));
}

fn build_raydium_cpmm_initialize(
    instruction_name: &str,
    accounts: &[std::string::String],
    data: &[u8],
    with_permission: bool,
) -> std::option::Option<RaydiumCpmmPoolInitializeDecoded> {
    let pool_index = if with_permission { 4usize } else { 3usize };
    let mint_0_index = if with_permission { 5usize } else { 4usize };
    let mint_1_index = if with_permission { 6usize } else { 5usize };
    let pool_state = match account_at(accounts, pool_index) {
        Some(pool_state) => pool_state,
        None => return None,
    };
    let token_0_mint = match account_at(accounts, mint_0_index) {
        Some(token_0_mint) => token_0_mint,
        None => return None,
    };
    let token_1_mint = match account_at(accounts, mint_1_index) {
        Some(token_1_mint) => token_1_mint,
        None => return None,
    };
    let (base_mint, quote_mint) =
        normalize_raydium_cpmm_mints(token_0_mint.as_str(), token_1_mint.as_str());
    return Some(RaydiumCpmmPoolInitializeDecoded {
        instruction_name: instruction_name.to_string(),
        creator: account_at(accounts, 0),
        amm_config: account_at(accounts, 1),
        pool_state,
        token_0_mint,
        token_1_mint,
        lp_mint: account_at(accounts, if with_permission { 7usize } else { 6usize }),
        base_mint,
        quote_mint,
        init_amount_0_raw: read_u64_le(data, 8).map(|value| return value.to_string()),
        init_amount_1_raw: read_u64_le(data, 16).map(|value| return value.to_string()),
        open_time_raw: read_u64_le(data, 24).map(|value| return value.to_string()),
    });
}

fn build_raydium_cpmm_liquidity(
    instruction_name: &str,
    direction: &str,
    accounts: &[std::string::String],
    data: &[u8],
) -> std::option::Option<RaydiumCpmmLiquidityDecoded> {
    let pool_state = match account_at(accounts, 2) {
        Some(pool_state) => pool_state,
        None => return None,
    };
    let token_0_mint = match account_at(accounts, 10) {
        Some(token_0_mint) => token_0_mint,
        None => return None,
    };
    let token_1_mint = match account_at(accounts, 11) {
        Some(token_1_mint) => token_1_mint,
        None => return None,
    };
    let (base_mint, quote_mint) =
        normalize_raydium_cpmm_mints(token_0_mint.as_str(), token_1_mint.as_str());
    let lp_amount = match read_u64_le(data, 8) {
        Some(lp_amount) => lp_amount,
        None => return None,
    };
    let token_0_amount = match read_u64_le(data, 16) {
        Some(token_0_amount) => token_0_amount,
        None => return None,
    };
    let token_1_amount = match read_u64_le(data, 24) {
        Some(token_1_amount) => token_1_amount,
        None => return None,
    };
    let (base_amount, quote_amount) =
        map_token_0_1_amounts_to_base_quote(token_0_mint.as_str(), token_0_amount, token_1_amount);
    let amount_semantics = if direction == "add" {
        "lp_fixed_max_token_amounts".to_string()
    } else {
        "lp_fixed_min_token_amounts".to_string()
    };
    return Some(RaydiumCpmmLiquidityDecoded {
        instruction_name: instruction_name.to_string(),
        liquidity_direction: direction.to_string(),
        owner: account_at(accounts, 0),
        pool_state,
        token_0_mint,
        token_1_mint,
        lp_mint: account_at(accounts, 4),
        base_mint,
        quote_mint,
        token_0_amount_raw: token_0_amount.to_string(),
        token_1_amount_raw: token_1_amount.to_string(),
        base_amount_raw: base_amount.to_string(),
        quote_amount_raw: quote_amount.to_string(),
        lp_amount_raw: lp_amount.to_string(),
        amount_semantics,
    });
}

fn build_raydium_cpmm_fee_or_admin_event(
    instruction_name: &str,
    fee_scope: &str,
    accounts: &[std::string::String],
    data: &[u8],
) -> std::vec::Vec<RaydiumCpmmDecodedEvent> {
    let pool_state = match account_at(accounts, 2) {
        Some(pool_state) => pool_state,
        None => return std::vec::Vec::new(),
    };
    let token_0_mint = account_at(accounts, 6);
    let token_1_mint = account_at(accounts, 7);
    let (base_mint, quote_mint) = match (token_0_mint.as_deref(), token_1_mint.as_deref()) {
        (Some(token_0_mint), Some(token_1_mint)) => {
            let (base_mint, quote_mint) = normalize_raydium_cpmm_mints(token_0_mint, token_1_mint);
            (Some(base_mint), Some(quote_mint))
        },
        _ => (None, None),
    };
    let _ = data;
    return vec![RaydiumCpmmDecodedEvent::FeeCollection(RaydiumCpmmFeeCollectionDecoded {
        instruction_name: instruction_name.to_string(),
        pool_state,
        token_0_mint,
        token_1_mint,
        base_mint,
        quote_mint,
        fee_scope: fee_scope.to_string(),
        actor_wallet: account_at(accounts, 0),
    })];
}

fn build_raydium_cpmm_admin(
    instruction_name: &str,
    admin_action: &str,
    accounts: &[std::string::String],
    data: &[u8],
    pool_state: std::option::Option<std::string::String>,
) -> RaydiumCpmmPoolAdminDecoded {
    return RaydiumCpmmPoolAdminDecoded {
        instruction_name: instruction_name.to_string(),
        pool_state,
        config_or_permission_account: account_at(accounts, 0),
        actor_wallet: account_at(accounts, 1),
        admin_action: admin_action.to_string(),
        first_u8_arg: data.get(8).copied(),
        first_u64_arg_raw: read_u64_le(data, 8).map(|value| return value.to_string()),
    };
}

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

fn map_token_0_1_amounts_to_base_quote(
    token_0_mint: &str,
    token_0_amount: u64,
    token_1_amount: u64,
) -> (u64, u64) {
    if token_0_mint == crate::WSOL_MINT_ID {
        return (token_1_amount, token_0_amount);
    }
    return (token_0_amount, token_1_amount);
}

fn build_raydium_cpmm_swap(
    swap_mode: RaydiumCpmmSwapMode,
    accounts: &[std::string::String],
    amount_in_raw: std::option::Option<std::string::String>,
    minimum_amount_out_raw: std::option::Option<std::string::String>,
    max_amount_in_raw: std::option::Option<std::string::String>,
    amount_out_raw: std::option::Option<std::string::String>,
) -> std::option::Option<RaydiumCpmmSwapDecoded> {
    if accounts.len() < 13 {
        return None;
    }
    let input_token_mint = accounts[10].clone();
    let output_token_mint = accounts[11].clone();
    let input_vault = accounts[6].clone();
    let output_vault = accounts[7].clone();
    let normalized = normalize_raydium_cpmm_pair(
        input_token_mint.as_str(),
        output_token_mint.as_str(),
        input_vault.as_str(),
        output_vault.as_str(),
    );
    let input_is_base = normalized.input_is_base;
    let trade_side = if input_is_base { "sell".to_string() } else { "buy".to_string() };
    return Some(RaydiumCpmmSwapDecoded {
        swap_mode,
        payer: accounts[0].clone(),
        authority: accounts[1].clone(),
        amm_config: accounts[2].clone(),
        pool_state: accounts[3].clone(),
        input_token_account: accounts[4].clone(),
        output_token_account: accounts[5].clone(),
        input_vault,
        output_vault,
        input_token_program: accounts[8].clone(),
        output_token_program: accounts[9].clone(),
        input_token_mint,
        output_token_mint,
        observation_state: accounts[12].clone(),
        base_mint: normalized.base_mint,
        quote_mint: normalized.quote_mint,
        base_vault: normalized.base_vault,
        quote_vault: normalized.quote_vault,
        input_is_base,
        trade_side,
        amount_in_raw,
        minimum_amount_out_raw,
        max_amount_in_raw,
        amount_out_raw,
    });
}

fn normalize_raydium_cpmm_mints(
    mint_a: &str,
    mint_b: &str,
) -> (std::string::String, std::string::String) {
    let normalized = normalize_raydium_cpmm_pair(mint_a, mint_b, "", "");
    return (normalized.base_mint, normalized.quote_mint);
}

struct RaydiumCpmmNormalizedPair {
    base_mint: std::string::String,
    quote_mint: std::string::String,
    base_vault: std::string::String,
    quote_vault: std::string::String,
    input_is_base: bool,
}

fn normalize_raydium_cpmm_pair(
    input_mint: &str,
    output_mint: &str,
    input_vault: &str,
    output_vault: &str,
) -> RaydiumCpmmNormalizedPair {
    if is_quote_mint(output_mint) && !is_quote_mint(input_mint) {
        return RaydiumCpmmNormalizedPair {
            base_mint: input_mint.to_string(),
            quote_mint: output_mint.to_string(),
            base_vault: input_vault.to_string(),
            quote_vault: output_vault.to_string(),
            input_is_base: true,
        };
    }
    if is_quote_mint(input_mint) && !is_quote_mint(output_mint) {
        return RaydiumCpmmNormalizedPair {
            base_mint: output_mint.to_string(),
            quote_mint: input_mint.to_string(),
            base_vault: output_vault.to_string(),
            quote_vault: input_vault.to_string(),
            input_is_base: false,
        };
    }
    if input_mint <= output_mint {
        return RaydiumCpmmNormalizedPair {
            base_mint: input_mint.to_string(),
            quote_mint: output_mint.to_string(),
            base_vault: input_vault.to_string(),
            quote_vault: output_vault.to_string(),
            input_is_base: true,
        };
    }
    return RaydiumCpmmNormalizedPair {
        base_mint: output_mint.to_string(),
        quote_mint: input_mint.to_string(),
        base_vault: output_vault.to_string(),
        quote_vault: input_vault.to_string(),
        input_is_base: false,
    };
}

fn is_quote_mint(mint: &str) -> bool {
    if mint == crate::WSOL_MINT_ID {
        return true;
    }
    if mint == "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" {
        return true;
    }
    if mint == "Es9vMFrzaCERmJfrF4H2FYD4KMZbsJyNeYrBbqD6CbK" {
        return true;
    }
    if mint == "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB" {
        return true;
    }
    return false;
}

fn decode_base64_standard(encoded: &str) -> std::option::Option<std::vec::Vec<u8>> {
    use base64::Engine as _;
    let decoded = base64::engine::general_purpose::STANDARD.decode(encoded.as_bytes());
    match decoded {
        Ok(decoded) => return Some(decoded),
        Err(_) => return None,
    }
}

fn parse_accounts_json(
    accounts_json: &str,
) -> std::option::Option<std::vec::Vec<std::string::String>> {
    let result = serde_json::from_str::<std::vec::Vec<std::string::String>>(accounts_json);
    match result {
        Ok(accounts) => return Some(accounts),
        Err(_) => return None,
    }
}

fn parse_data_json_as_base58(data_json: &str) -> std::option::Option<std::string::String> {
    let json_string_result = serde_json::from_str::<std::string::String>(data_json);
    if let Ok(value) = json_string_result {
        return Some(value);
    }
    let json_array_result = serde_json::from_str::<std::vec::Vec<std::string::String>>(data_json);
    if let Ok(values) = json_array_result {
        if values.is_empty() {
            return None;
        }
        if values.len() > 1 && values[1].as_str() != "base58" {
            return None;
        }
        let value = values[0].trim();
        if value.is_empty() {
            return None;
        }
        return Some(value.to_string());
    }
    let trimmed = data_json.trim();
    if trimmed.is_empty() {
        return None;
    }
    let without_quotes = trimmed.trim_matches('"');
    if without_quotes.is_empty() {
        return None;
    }
    return Some(without_quotes.to_string());
}

fn read_pubkey_base58(data: &[u8], offset: usize) -> std::option::Option<std::string::String> {
    if data.len() < offset + 32 {
        return None;
    }
    return Some(bs58::encode(&data[offset..offset + 32]).into_string());
}

fn read_u64_le(data: &[u8], offset: usize) -> std::option::Option<u64> {
    if data.len() < offset + 8 {
        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));
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn decodes_swap_base_input() {
        let accounts_json = r#"[
            "ARu4n5mFdZogZAravu7CcizaojWnS6oqka37gdLT5SZn",
            "GpMZbSM2GgvTKHJirzeGfMFoaZ8UR2X7F4v8vHTvxFbL",
            "GtdipqAcw8eAYxGcANs3vpN7UaN7UH7u89Kd8wPqLThd",
            "2ErXvV1tKtG3wiHqdofDjMou7Jusdsfasvfh8HrTj5oV",
            "3tBz6MmUTaf2QdnAnYYJGDpgbTXdANP9v4CtUL42GZaZ",
            "C1qMskwQhQ4cn1M6RG8uWfUuMrq6ysRPEHccGxk4tjw8",
            "8aHJfDQBmngBpFwLysD1bhj8LKh7ywbxfUxsy7VQkx22",
            "A4jPLDGrkAogPgm8KqQzZQ4FoNJuKuKMCbNqgQkpT5Ci",
            "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
            "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
            "Pf9aSicGu3g6tTUBqrRbjNsGape9HopibspX5KSbonk",
            "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB",
            "9fzQ17bEnqJSsyHL5CodJqY2sdjZJBQCQLbgFZ1BYTWn"
        ]"#;
        let events = crate::decode_raydium_cpmm_instruction(
            accounts_json,
            r#""E73fXHPWvSRF4Q2ZQFvPoeJBVGDUEMmxB""#,
        );
        assert_eq!(events.len(), 1);
        match &events[0] {
            crate::RaydiumCpmmDecodedEvent::SwapBaseInput(event) => {
                assert_eq!(event.pool_state, "2ErXvV1tKtG3wiHqdofDjMou7Jusdsfasvfh8HrTj5oV");
                assert_eq!(event.base_mint, "Pf9aSicGu3g6tTUBqrRbjNsGape9HopibspX5KSbonk");
                assert_eq!(event.quote_mint, "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB");
                assert!(event.input_is_base);
                assert_eq!(event.trade_side, "sell");
                assert!(event.amount_in_raw.is_some());
                assert!(event.minimum_amount_out_raw.is_some());
            },
            _ => {
                panic!("expected swap base input");
            },
        }
    }
    #[test]
    fn decodes_swap_base_output() {
        let accounts_json = r#"[
            "JD6rVaerbyz6wjQ433nrw6bFTgFrp46MiYmi8EtUAfsG",
            "GpMZbSM2GgvTKHJirzeGfMFoaZ8UR2X7F4v8vHTvxFbL",
            "GtdipqAcw8eAYxGcANs3vpN7UaN7UH7u89Kd8wPqLThd",
            "2ErXvV1tKtG3wiHqdofDjMou7Jusdsfasvfh8HrTj5oV",
            "4Fg913oAKp7BiW6WvUF32RvACsBYusMTQ16nmA6Sd9tp",
            "Cvcy56pbaDms71KyxAWFTQ24n5KbCy8KNa7zMhKGcJa6",
            "A4jPLDGrkAogPgm8KqQzZQ4FoNJuKuKMCbNqgQkpT5Ci",
            "8aHJfDQBmngBpFwLysD1bhj8LKh7ywbxfUxsy7VQkx22",
            "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
            "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
            "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB",
            "Pf9aSicGu3g6tTUBqrRbjNsGape9HopibspX5KSbonk",
            "9fzQ17bEnqJSsyHL5CodJqY2sdjZJBQCQLbgFZ1BYTWn"
        ]"#;
        let events = crate::decode_raydium_cpmm_instruction(
            accounts_json,
            r#""66JafaVu7KNEtTngqQyD7ETVurF3rxJ47""#,
        );
        assert_eq!(events.len(), 1);
        match &events[0] {
            crate::RaydiumCpmmDecodedEvent::SwapBaseOutput(event) => {
                assert_eq!(event.pool_state, "2ErXvV1tKtG3wiHqdofDjMou7Jusdsfasvfh8HrTj5oV");
                assert_eq!(event.base_mint, "Pf9aSicGu3g6tTUBqrRbjNsGape9HopibspX5KSbonk");
                assert_eq!(event.quote_mint, "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB");
                assert!(!event.input_is_base);
                assert_eq!(event.trade_side, "buy");
                assert!(event.max_amount_in_raw.is_some());
                assert!(event.amount_out_raw.is_some());
            },
            _ => {
                panic!("expected swap base output");
            },
        }
    }

    #[test]
    fn decodes_program_data_swap_event_without_anchor_selector() {
        let event = crate::decode_raydium_cpmm_program_data_event(
            "QMbN6CYIceJO6v1PBzcyVgs32NvLbZWxigBNEVRxobydBucKB/GSLp1C5V0EAAAA064SWHVOCABn8Q8AAAAAAMCRyD8eAAAAAAAAAAAAAAAAAAAAAAAAAAAGm4hX/quBhPtof2NGGMA12sQ53BrrO1WYoPAAAAAAAcuxLv4dE0rdpr3axeHcFej4SEyTWeVah03mTAU4++RUNQoAAAAAAAAAAAAAAAAAAAE=",
        );
        match event {
            Some(crate::RaydiumCpmmDecodedEvent::SwapEvent(decoded)) => {
                assert_eq!(decoded.pool_id, "6K4dKwMvbzZWH5N8kU11DVM2Yj28j3Uask1kVcskBWyj");
                assert_eq!(decoded.input_amount_raw, "1044839");
                assert_eq!(decoded.output_amount_raw, "129919128000");
                assert_eq!(
                    decoded.input_mint,
                    Some("So11111111111111111111111111111111111111112".to_string())
                );
                assert_eq!(
                    decoded.output_mint,
                    Some("Ei8TxDKgP7hAJCLmDCtJMae7W9p1iqk6eK38LYyfuLq9".to_string())
                );
                assert!(!decoded.trade_candidate);
                assert!(!decoded.candle_candidate);
            },
            _ => panic!("expected swap event"),
        }
    }

    #[test]
    fn classifies_all_known_cpmm_instruction_data() {
        let fixtures = [
            (RAYDIUM_CPMM_SWAP_BASE_INPUT_DISCRIMINATOR, "swap_base_input"),
            (RAYDIUM_CPMM_SWAP_BASE_OUTPUT_DISCRIMINATOR, "swap_base_output"),
            (RAYDIUM_CPMM_CLOSE_PERMISSION_PDA_DISCRIMINATOR, "close_permission_pda"),
            (RAYDIUM_CPMM_COLLECT_CREATOR_FEE_DISCRIMINATOR, "collect_creator_fee"),
            (RAYDIUM_CPMM_COLLECT_FUND_FEE_DISCRIMINATOR, "collect_fund_fee"),
            (RAYDIUM_CPMM_COLLECT_PROTOCOL_FEE_DISCRIMINATOR, "collect_protocol_fee"),
            (RAYDIUM_CPMM_CREATE_AMM_CONFIG_DISCRIMINATOR, "create_amm_config"),
            (RAYDIUM_CPMM_CREATE_PERMISSION_PDA_DISCRIMINATOR, "create_permission_pda"),
            (RAYDIUM_CPMM_DEPOSIT_DISCRIMINATOR, "deposit"),
            (RAYDIUM_CPMM_WITHDRAW_DISCRIMINATOR, "withdraw"),
            (RAYDIUM_CPMM_INITIALIZE_DISCRIMINATOR, "initialize"),
            (
                RAYDIUM_CPMM_INITIALIZE_WITH_PERMISSION_DISCRIMINATOR,
                "initialize_with_permission",
            ),
            (RAYDIUM_CPMM_UPDATE_AMM_CONFIG_DISCRIMINATOR, "update_amm_config"),
            (RAYDIUM_CPMM_UPDATE_POOL_STATUS_DISCRIMINATOR, "update_pool_status"),
        ];
        for (discriminator, expected) in fixtures {
            let mut data = std::vec::Vec::from(discriminator);
            data.extend_from_slice(&[0_u8; 24]);
            let encoded = bs58::encode(data).into_string();
            let data_json = serde_json::json!([encoded, "base58"]).to_string();
            assert_eq!(
                crate::classify_raydium_cpmm_instruction_data(data_json.as_str()),
                Some(expected)
            );
        }
    }
}
