//! SVM (Solana) query types and field selection for the ingest layer.

#[cfg(feature = "pyo3")]
use anyhow::Context;
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};

/// SVM (Solana) blockchain data query specifying slot range, filters, and field selections.
#[derive(Default, Debug, Clone)]
#[cfg_attr(feature = "pyo3", derive(pyo3::FromPyObject))]
pub struct Query {
    pub from_block: u64,
    pub to_block: Option<u64>,
    pub include_all_blocks: bool,
    pub fields: Fields,
    pub instructions: Vec<InstructionRequest>,
    pub transactions: Vec<TransactionRequest>,
    pub logs: Vec<LogRequest>,
    pub balances: Vec<BalanceRequest>,
    pub token_balances: Vec<TokenBalanceRequest>,
    pub rewards: Vec<RewardRequest>,
}

/// A 32-byte Solana public key.
#[derive(Debug, Clone, Copy)]
pub struct Address(pub [u8; 32]);

/// Variable-length binary data (instruction data or discriminator).
#[derive(Debug, Clone)]
pub struct Data(pub Vec<u8>);

/// Fixed-size data prefix for instruction filtering (1 byte).
#[derive(Debug, Clone, Copy)]
pub struct D1(pub [u8; 1]);

/// Fixed-size data prefix for instruction filtering (2 bytes).
#[derive(Debug, Clone, Copy)]
pub struct D2(pub [u8; 2]);

/// Fixed-size data prefix for instruction filtering (3 bytes).
#[derive(Debug, Clone, Copy)]
pub struct D3(pub [u8; 3]);

/// Fixed-size data prefix for instruction filtering (4 bytes).
#[derive(Debug, Clone, Copy)]
pub struct D4(pub [u8; 4]);

/// Fixed-size data prefix for instruction filtering (8 bytes).
#[derive(Debug, Clone, Copy)]
pub struct D8(pub [u8; 8]);

#[cfg(feature = "pyo3")]
fn extract_base58<const N: usize>(ob: &pyo3::Bound<'_, pyo3::PyAny>) -> pyo3::PyResult<[u8; N]> {
    use pyo3::types::PyAnyMethods;

    let s: &str = ob.extract()?;
    let mut out = [0; N];

    bs58::decode(s)
        .with_alphabet(bs58::Alphabet::BITCOIN)
        .onto(&mut out)
        .context("decode base58")?;

    Ok(out)
}

#[cfg(feature = "pyo3")]
fn extract_data<const N: usize>(ob: &pyo3::Bound<'_, pyo3::PyAny>) -> pyo3::PyResult<[u8; N]> {
    use pyo3::types::PyAnyMethods;
    use pyo3::types::PyTypeMethods;

    let ob_type: String = ob.get_type().name()?.to_string();
    match ob_type.as_str() {
        "str" => {
            let s: &str = ob.extract()?;
            let out = hex_to_bytes(s).context("failed to decode hex")?;
            if out.len() != N {
                return Err(anyhow!("expected length {}, got {}", N, out.len()).into());
            }
            let out: [u8; N] = out
                .try_into()
                .map_err(|e| anyhow!("failed to convert to array: {e:?}"))?;
            Ok(out)
        }
        "bytes" => {
            let out: Vec<u8> = ob.extract()?;
            if out.len() != N {
                return Err(anyhow!("expected length {}, got {}", N, out.len()).into());
            }
            let out: [u8; N] = out
                .try_into()
                .map_err(|e| anyhow!("failed to convert to array: {e:?}"))?;
            Ok(out)
        }
        _ => Err(anyhow!("unknown type: {ob_type}").into()),
    }
}

#[cfg(feature = "pyo3")]
fn hex_to_bytes(hex_string: &str) -> Result<Vec<u8>> {
    let hex_string = hex_string.strip_prefix("0x").unwrap_or(hex_string);
    let hex_string = if hex_string.len() % 2 == 1 {
        format!("0{hex_string}",)
    } else {
        hex_string.to_string()
    };
    let out = (0..hex_string.len())
        .step_by(2)
        .map(|i| {
            u8::from_str_radix(&hex_string[i..i + 2], 16)
                .context("failed to parse hexstring to bytes")
        })
        .collect::<Result<Vec<_>, _>>()?;

    Ok(out)
}

#[cfg(feature = "pyo3")]
impl<'py> pyo3::FromPyObject<'py> for Address {
    fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
        let out = extract_base58(ob)?;
        Ok(Self(out))
    }
}

#[cfg(feature = "pyo3")]
impl<'py> pyo3::FromPyObject<'py> for Data {
    fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
        use pyo3::types::PyAnyMethods;
        use pyo3::types::PyTypeMethods;

        let ob_type: String = ob.get_type().name()?.to_string();
        match ob_type.as_str() {
            "str" => {
                let s: &str = ob.extract()?;
                let out = hex_to_bytes(s).context("failed to decode hex")?;
                Ok(Self(out))
            }
            "bytes" => {
                let out: Vec<u8> = ob.extract()?;
                Ok(Self(out))
            }
            _ => Err(anyhow!("unknown type: {ob_type}").into()),
        }
    }
}

#[cfg(feature = "pyo3")]
impl<'py> pyo3::FromPyObject<'py> for D1 {
    fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
        let out = extract_data(ob)?;
        Ok(Self(out))
    }
}

#[cfg(feature = "pyo3")]
impl<'py> pyo3::FromPyObject<'py> for D2 {
    fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
        let out = extract_data(ob)?;
        Ok(Self(out))
    }
}

#[cfg(feature = "pyo3")]
impl<'py> pyo3::FromPyObject<'py> for D3 {
    fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
        let out = extract_data(ob)?;
        Ok(Self(out))
    }
}

#[cfg(feature = "pyo3")]
impl<'py> pyo3::FromPyObject<'py> for D4 {
    fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
        let out = extract_data(ob)?;
        Ok(Self(out))
    }
}

#[cfg(feature = "pyo3")]
impl<'py> pyo3::FromPyObject<'py> for D8 {
    fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
        let out = extract_data(ob)?;
        Ok(Self(out))
    }
}

/// Filters for selecting Solana instructions by program ID, discriminator, and accounts.
#[derive(Default, Debug, Clone)]
#[cfg_attr(feature = "pyo3", derive(pyo3::FromPyObject))]
#[expect(clippy::struct_excessive_bools, reason = "fields selection flags")]
pub struct InstructionRequest {
    pub program_id: Vec<Address>,
    pub discriminator: Vec<Data>,
    pub d1: Vec<D1>,
    pub d2: Vec<D2>,
    pub d3: Vec<D3>,
    pub d4: Vec<D4>,
    pub d8: Vec<D8>,
    pub a0: Vec<Address>,
    pub a1: Vec<Address>,
    pub a2: Vec<Address>,
    pub a3: Vec<Address>,
    pub a4: Vec<Address>,
    pub a5: Vec<Address>,
    pub a6: Vec<Address>,
    pub a7: Vec<Address>,
    pub a8: Vec<Address>,
    pub a9: Vec<Address>,
    pub is_committed: bool,
    pub include_transactions: bool,
    pub include_transaction_token_balances: bool,
    pub include_logs: bool,
    pub include_inner_instructions: bool,
    pub include_blocks: bool,
}

/// Filters for selecting Solana transactions by fee payer.
#[derive(Default, Debug, Clone)]
#[cfg_attr(feature = "pyo3", derive(pyo3::FromPyObject))]
pub struct TransactionRequest {
    pub fee_payer: Vec<Address>,
    pub include_instructions: bool,
    pub include_logs: bool,
    pub include_blocks: bool,
}

/// Filters for selecting Solana program logs by program ID and kind.
#[derive(Default, Debug, Clone)]
#[cfg_attr(feature = "pyo3", derive(pyo3::FromPyObject))]
pub struct LogRequest {
    pub program_id: Vec<Address>,
    pub kind: Vec<LogKind>,
    pub include_transactions: bool,
    pub include_instructions: bool,
    pub include_blocks: bool,
}

/// The type of Solana program log message.
#[derive(Debug, Clone, Copy)]
pub enum LogKind {
    /// Standard program log message (sol_log).
    Log,
    /// Base64-encoded program data message (sol_log_data).
    Data,
    /// Other log message types.
    Other,
}

impl LogKind {
    pub fn as_str(&self) -> &str {
        match self {
            Self::Log => "log",
            Self::Data => "data",
            Self::Other => "other",
        }
    }

    pub fn from_str(s: &str) -> Result<Self> {
        match s {
            "log" => Ok(Self::Log),
            "data" => Ok(Self::Data),
            "other" => Ok(Self::Other),
            _ => Err(anyhow!("unknown log kind: {s}")),
        }
    }
}

#[cfg(feature = "pyo3")]
impl<'py> pyo3::FromPyObject<'py> for LogKind {
    fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
        use pyo3::types::PyAnyMethods;

        let s: &str = ob.extract().context("extract string")?;

        Ok(Self::from_str(s).context("from str")?)
    }
}

/// Filters for selecting Solana native SOL balance changes.
#[derive(Default, Debug, Clone)]
#[cfg_attr(feature = "pyo3", derive(pyo3::FromPyObject))]
pub struct BalanceRequest {
    pub account: Vec<Address>,
    pub include_transactions: bool,
    pub include_transaction_instructions: bool,
    pub include_blocks: bool,
}

/// Filters for selecting Solana SPL token balance changes.
#[derive(Default, Debug, Clone)]
#[cfg_attr(feature = "pyo3", derive(pyo3::FromPyObject))]
pub struct TokenBalanceRequest {
    pub account: Vec<Address>,
    pub pre_program_id: Vec<Address>,
    pub post_program_id: Vec<Address>,
    pub pre_mint: Vec<Address>,
    pub post_mint: Vec<Address>,
    pub pre_owner: Vec<Address>,
    pub post_owner: Vec<Address>,
    pub include_transactions: bool,
    pub include_transaction_instructions: bool,
    pub include_blocks: bool,
}

/// Filters for selecting Solana validator rewards.
#[derive(Default, Debug, Clone)]
#[cfg_attr(feature = "pyo3", derive(pyo3::FromPyObject))]
pub struct RewardRequest {
    pub pubkey: Vec<Address>,
    pub include_blocks: bool,
}

/// Controls which columns are included in the response for each SVM table type.
#[derive(Deserialize, Serialize, Default, Debug, Clone, Copy)]
#[serde(default)]
#[cfg_attr(feature = "pyo3", derive(pyo3::FromPyObject))]
pub struct Fields {
    pub instruction: InstructionFields,
    pub transaction: TransactionFields,
    pub log: LogFields,
    pub balance: BalanceFields,
    pub token_balance: TokenBalanceFields,
    pub reward: RewardFields,
    pub block: BlockFields,
}

impl Fields {
    pub fn all() -> Self {
        Self {
            instruction: InstructionFields::all(),
            transaction: TransactionFields::all(),
            log: LogFields::all(),
            balance: BalanceFields::all(),
            token_balance: TokenBalanceFields::all(),
            reward: RewardFields::all(),
            block: BlockFields::all(),
        }
    }
}

/// Field selector for Solana instruction data columns.
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(default)]
#[cfg_attr(feature = "pyo3", derive(pyo3::FromPyObject))]
#[expect(clippy::struct_excessive_bools, reason = "fields selection flags")]
pub struct InstructionFields {
    pub block_slot: bool,
    pub block_hash: bool,
    pub transaction_index: bool,
    pub instruction_address: bool,
    pub program_id: bool,
    pub a0: bool,
    pub a1: bool,
    pub a2: bool,
    pub a3: bool,
    pub a4: bool,
    pub a5: bool,
    pub a6: bool,
    pub a7: bool,
    pub a8: bool,
    pub a9: bool,
    pub rest_of_accounts: bool,
    pub data: bool,
    pub d1: bool,
    pub d2: bool,
    pub d4: bool,
    pub d8: bool,
    pub error: bool,
    pub compute_units_consumed: bool,
    pub is_committed: bool,
    pub has_dropped_log_messages: bool,
}

impl InstructionFields {
    pub fn all() -> Self {
        InstructionFields {
            block_slot: true,
            block_hash: true,
            transaction_index: true,
            instruction_address: true,
            program_id: true,
            a0: true,
            a1: true,
            a2: true,
            a3: true,
            a4: true,
            a5: true,
            a6: true,
            a7: true,
            a8: true,
            a9: true,
            rest_of_accounts: true,
            data: true,
            d1: true,
            d2: true,
            d4: true,
            d8: true,
            error: true,
            compute_units_consumed: true,
            is_committed: true,
            has_dropped_log_messages: true,
        }
    }
}

/// Field selector for Solana transaction data columns.
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(default)]
#[cfg_attr(feature = "pyo3", derive(pyo3::FromPyObject))]
#[expect(clippy::struct_excessive_bools, reason = "fields selection flags")]
pub struct TransactionFields {
    pub block_slot: bool,
    pub block_hash: bool,
    pub transaction_index: bool,
    pub signature: bool,
    pub version: bool,
    pub account_keys: bool,
    pub address_table_lookups: bool,
    pub num_readonly_signed_accounts: bool,
    pub num_readonly_unsigned_accounts: bool,
    pub num_required_signatures: bool,
    pub recent_blockhash: bool,
    pub signatures: bool,
    pub err: bool,
    pub fee: bool,
    pub compute_units_consumed: bool,
    pub loaded_readonly_addresses: bool,
    pub loaded_writable_addresses: bool,
    pub fee_payer: bool,
    pub has_dropped_log_messages: bool,
}

impl TransactionFields {
    pub fn all() -> Self {
        TransactionFields {
            block_slot: true,
            block_hash: true,
            transaction_index: true,
            signature: true,
            version: true,
            account_keys: true,
            address_table_lookups: true,
            num_readonly_signed_accounts: true,
            num_readonly_unsigned_accounts: true,
            num_required_signatures: true,
            recent_blockhash: true,
            signatures: true,
            err: true,
            fee: true,
            compute_units_consumed: true,
            loaded_readonly_addresses: true,
            loaded_writable_addresses: true,
            fee_payer: true,
            has_dropped_log_messages: true,
        }
    }
}

/// Field selector for Solana log data columns.
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(default)]
#[cfg_attr(feature = "pyo3", derive(pyo3::FromPyObject))]
#[expect(clippy::struct_excessive_bools, reason = "fields selection flags")]
pub struct LogFields {
    pub block_slot: bool,
    pub block_hash: bool,
    pub transaction_index: bool,
    pub log_index: bool,
    pub instruction_address: bool,
    pub program_id: bool,
    pub kind: bool,
    pub message: bool,
}

impl LogFields {
    pub fn all() -> Self {
        LogFields {
            block_slot: true,
            block_hash: true,
            transaction_index: true,
            log_index: true,
            instruction_address: true,
            program_id: true,
            kind: true,
            message: true,
        }
    }
}

/// Field selector for Solana balance data columns.
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(default)]
#[cfg_attr(feature = "pyo3", derive(pyo3::FromPyObject))]
#[expect(clippy::struct_excessive_bools, reason = "fields selection flags")]
pub struct BalanceFields {
    pub block_slot: bool,
    pub block_hash: bool,
    pub transaction_index: bool,
    pub account: bool,
    pub pre: bool,
    pub post: bool,
}

impl BalanceFields {
    pub fn all() -> Self {
        BalanceFields {
            block_slot: true,
            block_hash: true,
            transaction_index: true,
            account: true,
            pre: true,
            post: true,
        }
    }
}

/// Field selector for Solana token balance data columns.
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(default)]
#[cfg_attr(feature = "pyo3", derive(pyo3::FromPyObject))]
#[expect(clippy::struct_excessive_bools, reason = "fields selection flags")]
pub struct TokenBalanceFields {
    pub block_slot: bool,
    pub block_hash: bool,
    pub transaction_index: bool,
    pub account: bool,
    pub pre_mint: bool,
    pub post_mint: bool,
    pub pre_decimals: bool,
    pub post_decimals: bool,
    pub pre_program_id: bool,
    pub post_program_id: bool,
    pub pre_owner: bool,
    pub post_owner: bool,
    pub pre_amount: bool,
    pub post_amount: bool,
}

impl TokenBalanceFields {
    pub fn all() -> Self {
        TokenBalanceFields {
            block_slot: true,
            block_hash: true,
            transaction_index: true,
            account: true,
            pre_mint: true,
            post_mint: true,
            pre_decimals: true,
            post_decimals: true,
            pre_program_id: true,
            post_program_id: true,
            pre_owner: true,
            post_owner: true,
            pre_amount: true,
            post_amount: true,
        }
    }
}

/// Field selector for Solana reward data columns.
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(default)]
#[cfg_attr(feature = "pyo3", derive(pyo3::FromPyObject))]
#[expect(clippy::struct_excessive_bools, reason = "fields selection flags")]
pub struct RewardFields {
    pub block_slot: bool,
    pub block_hash: bool,
    pub pubkey: bool,
    pub lamports: bool,
    pub post_balance: bool,
    pub reward_type: bool,
    pub commission: bool,
}

impl RewardFields {
    pub fn all() -> Self {
        RewardFields {
            block_slot: true,
            block_hash: true,
            pubkey: true,
            lamports: true,
            post_balance: true,
            reward_type: true,
            commission: true,
        }
    }
}

/// Field selector for Solana block data columns.
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(default)]
#[cfg_attr(feature = "pyo3", derive(pyo3::FromPyObject))]
#[expect(clippy::struct_excessive_bools, reason = "fields selection flags")]
pub struct BlockFields {
    pub slot: bool,
    pub hash: bool,
    pub parent_slot: bool,
    pub parent_hash: bool,
    pub height: bool,
    pub timestamp: bool,
}

impl BlockFields {
    pub fn all() -> Self {
        BlockFields {
            slot: true,
            hash: true,
            parent_slot: true,
            parent_hash: true,
            height: true,
            timestamp: true,
        }
    }
}
