//! Ethereum event reader — decodes AS-ABI EthereumEvent objects from WASM memory.
//!
//! When graph-node calls a handler such as `handle_transfer(event_ptr: i32)`, the
//! `event_ptr` is an AscPtr pointing at an EthereumEvent object in linear memory.
//! This module reads that object and returns a plain Rust struct.
//!
//! # Safety
//!
//! All functions in this module read raw WASM linear memory via pointer arithmetic.
//! Callers must ensure the pointers originate from graph-node and have not been
//! tampered with.  In normal subgraph operation this is always the case.
//!
//! # Memory layout reference
//!
//! Each AS object is preceded by a 16-byte header; AscPtr values point *past* the
//! header into the payload.  All multi-byte values are little-endian (wasm32 is LE).
//!
//! ```text
//! EthereumEvent (payload, each field is a u32 AscPtr):
//!   offset  0  address              -> Uint8Array (20 bytes)
//!   offset  4  logIndex             -> BigInt (Uint8Array)
//!   offset  8  transactionLogIndex  -> BigInt (Uint8Array)
//!   offset 12  logType              -> AscString (nullable)
//!   offset 16  block                -> EthereumBlock
//!   offset 20  transaction          -> EthereumTransaction
//!   offset 24  parameters           -> Array<EthereumEventParam>
//!   offset 28  receipt              -> nullable
//!
//! EthereumEventParam:
//!   offset 0  name   -> AscString
//!   offset 4  value  -> EthereumValue (AscEnum)
//!
//! EthereumValue (AscEnum, 16-byte payload):
//!   offset 0  kind   u32
//!   offset 4  _pad   u32
//!   offset 8  data   u64  (AscPtr cast to u64 for pointer kinds)
//!
//! Uint8Array / BigInt / Bytes (12-byte payload):
//!   offset 0  buffer_ptr   u32  -> ArrayBuffer
//!   offset 4  data_start   u32  -> absolute ptr to start of data
//!   offset 8  byte_length  u32  -> number of bytes
//!
//! Array<T> (16-byte payload):
//!   offset  0  buffer_ptr         u32
//!   offset  4  buffer_data_start  u32  -> absolute ptr to first element
//!   offset  8  buffer_data_len    u32  -> total byte length of data
//!   offset 12  length             u32  -> element count
//!
//! AscString:
//!   The 16-byte header field rt_size holds the UTF-16LE byte count.
//!   The payload is the UTF-16LE code units directly (no null terminator).
//!   rt_size is at header offset 12 (i.e. ptr - 4).
//!
//! EthereumBlock (payload, u32 AscPtrs):
//!   offset  0  hash             -> Bytes (32)
//!   offset  4  parentHash       -> Bytes (32)
//!   offset  8  unclesHash       -> Bytes (32)
//!   offset 12  author           -> Address (20)
//!   offset 16  stateRoot        -> Bytes (32)
//!   offset 20  transactionsRoot -> Bytes (32)
//!   offset 24  receiptsRoot     -> Bytes (32)
//!   offset 28  number           -> BigInt
//!   offset 32  gasUsed          -> BigInt
//!   offset 36  gasLimit         -> BigInt
//!   offset 40  timestamp        -> BigInt
//!   offset 44  difficulty       -> BigInt
//!   offset 48  totalDifficulty  -> BigInt
//!   offset 52  size             -> BigInt (nullable)
//!   offset 56  baseFeePerGas    -> BigInt (nullable)
//!
//! EthereumTransaction (payload, u32 AscPtrs):
//!   offset  0  hash     -> Bytes (32)
//!   offset  4  index    -> BigInt
//!   offset  8  from     -> Address (20)
//!   offset 12  to       -> Address (nullable)
//!   offset 16  value    -> BigInt
//!   offset 20  gasLimit -> BigInt
//!   offset 24  gasPrice -> BigInt
//!   offset 28  input    -> Bytes
//! ```

use alloc::{string::String, vec, vec::Vec};

// ============================================================================
// FromRawEvent trait
// ============================================================================

/// Trait implemented by generated event structs to decode from a
/// `RawEthereumEvent` read out of AS memory.
///
/// Implementations are generated by `graphite codegen` for each ABI event.
pub trait FromRawEvent: Sized {
    fn from_raw_event(raw: &RawEthereumEvent) -> Result<Self, &'static str>;
}

/// Trait implemented by generated call-input structs to decode from a
/// `RawEthereumCall` read out of AS memory.
///
/// Implementations are generated by `graphite codegen` for each ABI function
/// that has a corresponding call handler.
pub trait FromRawCall: Sized {
    fn from_raw_call(raw: &RawEthereumCall) -> Result<Self, &'static str>;
}

// ============================================================================
// EthereumValueKind discriminants (match graph-ts EthereumValueKind enum)
// ============================================================================

pub const KIND_ADDRESS: u32 = 0;
pub const KIND_FIXED_BYTES: u32 = 1;
pub const KIND_BYTES: u32 = 2;
pub const KIND_INT: u32 = 3;
pub const KIND_UINT: u32 = 4;
pub const KIND_BOOL: u32 = 5;
pub const KIND_STRING: u32 = 6;
pub const KIND_FIXED_ARRAY: u32 = 7;
pub const KIND_ARRAY: u32 = 8;
pub const KIND_TUPLE: u32 = 9;

// ============================================================================
// Public types
// ============================================================================

/// A decoded Ethereum event parameter value.
#[derive(Clone)]
pub enum EthereumValue {
    Address([u8; 20]),
    FixedBytes(Vec<u8>),
    Bytes(Vec<u8>),
    /// Little-endian two's-complement signed integer bytes (graph-ts BigInt).
    Int(Vec<u8>),
    /// Little-endian unsigned integer bytes (graph-ts BigInt).
    Uint(Vec<u8>),
    Bool(bool),
    String(String),
    /// Dynamic array: `T[]`
    Array(Vec<EthereumValue>),
    /// Fixed-size array: `T[N]`
    FixedArray(Vec<EthereumValue>),
    /// Struct/tuple: `(T1, T2, ...)`
    Tuple(Vec<EthereumValue>),
    Unknown(u32),
}

impl EthereumValue {
    pub fn as_uint(&self) -> Option<Vec<u8>> {
        if let EthereumValue::Uint(b) = self { Some(b.clone()) } else { None }
    }
    pub fn as_int(&self) -> Option<Vec<u8>> {
        if let EthereumValue::Int(b) = self { Some(b.clone()) } else { None }
    }
    pub fn as_address(&self) -> Option<[u8; 20]> {
        if let EthereumValue::Address(a) = self { Some(*a) } else { None }
    }
    pub fn as_bool(&self) -> Option<bool> {
        if let EthereumValue::Bool(b) = self { Some(*b) } else { None }
    }
    pub fn as_bytes(&self) -> Option<Vec<u8>> {
        match self {
            EthereumValue::Bytes(b) | EthereumValue::FixedBytes(b) => Some(b.clone()),
            _ => None,
        }
    }
    pub fn as_string(&self) -> Option<&str> {
        if let EthereumValue::String(s) = self { Some(s.as_str()) } else { None }
    }
    /// Returns the elements of an `Array` or `FixedArray` value.
    pub fn as_array(&self) -> Option<&Vec<EthereumValue>> {
        match self {
            EthereumValue::Array(v) | EthereumValue::FixedArray(v) => Some(v),
            _ => None,
        }
    }
    /// Returns the elements of a `Tuple` value.
    pub fn as_tuple(&self) -> Option<&Vec<EthereumValue>> {
        if let EthereumValue::Tuple(v) = self { Some(v) } else { None }
    }
}

/// A single named event parameter.
pub struct EventParam {
    pub name: String,
    pub value: EthereumValue,
}

/// Transaction receipt data — only present when the subgraph manifest enables
/// receipt access via `receipt: true` on the data source mapping.
///
/// # Memory layout (AS `EthereumTransactionReceipt` payload)
///
/// ```text
///   offset  0  transactionHash    -> Bytes (32)
///   offset  4  transactionIndex   -> BigInt
///   offset  8  blockHash          -> Bytes (32)
///   offset 12  blockNumber        -> BigInt
///   offset 16  cumulativeGasUsed  -> BigInt
///   offset 20  gasUsed            -> BigInt
///   offset 24  contractAddress    -> Address (nullable)
///   offset 28  logs               -> Array<Log>
///   offset 32  status             -> BigInt (nullable, EIP-658)
///   offset 36  root               -> Bytes (nullable)
///   offset 40  logsBloom          -> Bytes
/// ```
pub struct EthereumTransactionReceipt {
    /// EIP-658 transaction status. `true` = success, `false` = reverted.
    /// `None` for pre-EIP-658 blocks (mainnet before block ~4.37 M).
    pub status: Option<bool>,
    /// Gas used by this transaction (BigInt bytes, little-endian).
    pub gas_used: Vec<u8>,
    /// Cumulative gas used in the block up to and including this transaction
    /// (BigInt bytes, little-endian).
    pub cumulative_gas_used: Vec<u8>,
    /// Contract address created by this transaction, if it was a deployment.
    /// `None` for regular calls.
    pub contract_address: Option<[u8; 20]>,
    /// Number of logs emitted by this transaction.
    pub log_count: u32,
}

/// Everything a handler needs from an EthereumEvent, in plain Rust types.
#[derive(Default)]
pub struct RawEthereumEvent {
    /// The contract address that emitted the event (20 bytes).
    pub address: [u8; 20],
    /// Log index (BigInt bytes, little-endian).
    pub log_index: Vec<u8>,

    // ── Block fields ─────────────────────────────────────────────────────────
    /// Block hash (32 bytes).
    pub block_hash: [u8; 32],
    /// Block number (BigInt bytes, little-endian).
    pub block_number: Vec<u8>,
    /// Block timestamp (BigInt bytes, little-endian).
    pub block_timestamp: Vec<u8>,
    /// Block gas used (BigInt bytes, little-endian).
    pub block_gas_used: Vec<u8>,
    /// Block gas limit (BigInt bytes, little-endian).
    pub block_gas_limit: Vec<u8>,
    /// Block difficulty (BigInt bytes, little-endian).
    pub block_difficulty: Vec<u8>,
    /// Base fee per gas (EIP-1559). `None` for pre-EIP-1559 blocks.
    pub block_base_fee_per_gas: Option<Vec<u8>>,

    // ── Transaction fields ───────────────────────────────────────────────────
    /// Transaction hash (32 bytes).
    pub tx_hash: [u8; 32],
    /// Transaction index in the block (BigInt bytes, little-endian).
    pub tx_index: Vec<u8>,
    /// Transaction sender (20 bytes).
    pub tx_from: [u8; 20],
    /// Transaction recipient. `None` for contract creation transactions.
    pub tx_to: Option<[u8; 20]>,
    /// ETH value sent (BigInt bytes, little-endian).
    pub tx_value: Vec<u8>,
    /// Gas limit for this transaction (BigInt bytes, little-endian).
    pub tx_gas_limit: Vec<u8>,
    /// Gas price (BigInt bytes, little-endian).
    pub tx_gas_price: Vec<u8>,
    /// Transaction nonce (BigInt bytes, little-endian).
    pub tx_nonce: Vec<u8>,

    /// Decoded event parameters in declaration order.
    pub params: Vec<EventParam>,
    /// Transaction receipt, if the manifest enables `receipt: true`.
    pub receipt: Option<EthereumTransactionReceipt>,
}

/// Everything a call handler needs from an `ethereum.Call`, in plain Rust types.
///
/// # Memory layout
///
/// The `EthereumCall` AS payload (u32 AscPtrs, in declaration order):
/// ```text
///   offset  0  address      -> Uint8Array (20 bytes)
///   offset  4  block        -> EthereumBlock
///   offset  8  transaction  -> EthereumTransaction
///   offset 12  inputValues  -> Array<EthereumEventParam>
///   offset 16  outputValues -> Array<EthereumEventParam>
/// ```
pub struct RawEthereumCall {
    /// The contract address that was called (20 bytes).
    pub address: [u8; 20],

    // ── Block fields ─────────────────────────────────────────────────────────
    /// Block hash (32 bytes).
    pub block_hash: [u8; 32],
    /// Block number (BigInt bytes, little-endian).
    pub block_number: Vec<u8>,
    /// Block timestamp (BigInt bytes, little-endian).
    pub block_timestamp: Vec<u8>,
    /// Block gas used (BigInt bytes, little-endian).
    pub block_gas_used: Vec<u8>,
    /// Block gas limit (BigInt bytes, little-endian).
    pub block_gas_limit: Vec<u8>,
    /// Block difficulty (BigInt bytes, little-endian).
    pub block_difficulty: Vec<u8>,
    /// Base fee per gas (EIP-1559). `None` for pre-EIP-1559 blocks.
    pub block_base_fee_per_gas: Option<Vec<u8>>,

    // ── Transaction fields ───────────────────────────────────────────────────
    /// Transaction hash (32 bytes).
    pub tx_hash: [u8; 32],
    /// Transaction index in the block (BigInt bytes, little-endian).
    pub tx_index: Vec<u8>,
    /// Transaction sender address (20 bytes).
    pub from: [u8; 20],
    /// Transaction recipient. `None` for contract creation transactions.
    pub tx_to: Option<[u8; 20]>,
    /// ETH value sent (BigInt bytes, little-endian).
    pub tx_value: Vec<u8>,
    /// Gas limit (BigInt bytes, little-endian).
    pub tx_gas_limit: Vec<u8>,
    /// Gas price (BigInt bytes, little-endian).
    pub tx_gas_price: Vec<u8>,
    /// Transaction nonce (BigInt bytes, little-endian).
    pub tx_nonce: Vec<u8>,

    /// Decoded call input parameters in declaration order.
    pub inputs: Vec<EventParam>,
    /// Decoded call output values in declaration order.
    pub outputs: Vec<EventParam>,
}

impl RawEthereumCall {
    /// Find a named input parameter by name.
    pub fn find_input(&self, name: &str) -> Option<&EthereumValue> {
        self.inputs.iter().find(|p| p.name == name).map(|p| &p.value)
    }

    /// Find a named output value by name (or `"value"` for single-return functions).
    pub fn find_output(&self, name: &str) -> Option<&EthereumValue> {
        self.outputs.iter().find(|p| p.name == name).map(|p| &p.value)
    }

    /// Find a named input parameter and return its uint bytes.
    pub fn find_input_uint(&self, name: &str) -> Result<Vec<u8>, &'static str> {
        match self.find_input(name) {
            Some(EthereumValue::Uint(b)) => Ok(b.clone()),
            _ => Err("input param not found or wrong type"),
        }
    }

    /// Find a named input parameter and return its address bytes.
    pub fn find_input_address(&self, name: &str) -> Result<[u8; 20], &'static str> {
        match self.find_input(name) {
            Some(EthereumValue::Address(a)) => Ok(*a),
            _ => Err("input param not found or wrong type"),
        }
    }

    /// Find a named input parameter and return its bool value.
    pub fn find_input_bool(&self, name: &str) -> Result<bool, &'static str> {
        match self.find_input(name) {
            Some(EthereumValue::Bool(b)) => Ok(*b),
            _ => Err("input param not found or wrong type"),
        }
    }

    /// Find a named input parameter and return its bytes.
    pub fn find_input_bytes(&self, name: &str) -> Result<Vec<u8>, &'static str> {
        match self.find_input(name) {
            Some(EthereumValue::Bytes(b)) | Some(EthereumValue::FixedBytes(b)) => Ok(b.clone()),
            _ => Err("input param not found or wrong type"),
        }
    }
}

// ============================================================================
// Low-level memory helpers
// ============================================================================

/// Read a `u32` from `base + offset` (unaligned-safe).
#[inline(always)]
unsafe fn read_u32_at(base: u32, offset: u32) -> u32 {
    let ptr = (base + offset) as *const u32;
    unsafe { ptr.read_unaligned() }
}

/// Read a `u64` from `base + offset` (unaligned-safe).
#[inline(always)]
unsafe fn read_u64_at(base: u32, offset: u32) -> u64 {
    let ptr = (base + offset) as *const u64;
    unsafe { ptr.read_unaligned() }
}

/// Read `len` bytes starting at absolute address `data_ptr`.
#[inline(always)]
unsafe fn read_bytes(data_ptr: u32, len: u32) -> Vec<u8> {
    if len == 0 {
        return vec![];
    }
    let ptr = data_ptr as *const u8;
    let slice = unsafe { core::slice::from_raw_parts(ptr, len as usize) };
    slice.to_vec()
}

// ============================================================================
// AS object readers
// ============================================================================

/// Read a Uint8Array / Bytes / BigInt — returns the raw bytes.
///
/// The AS TypedArray payload is:
///   [buffer_ptr: u32][data_start: u32][byte_length: u32]
///
/// `data_start` is the absolute pointer into the ArrayBuffer payload.
unsafe fn read_uint8array(ptr: u32) -> Vec<u8> {
    unsafe {
        let data_start = read_u32_at(ptr, 4);
        let byte_length = read_u32_at(ptr, 8);
        read_bytes(data_start, byte_length)
    }
}

/// Read exactly `N` bytes from a Uint8Array, zero-padding if shorter.
unsafe fn read_fixed_bytes<const N: usize>(ptr: u32) -> [u8; N] {
    let bytes = unsafe { read_uint8array(ptr) };
    let mut out = [0u8; N];
    let copy_len = bytes.len().min(N);
    out[..copy_len].copy_from_slice(&bytes[..copy_len]);
    out
}

/// Read an AS String and decode UTF-16LE → Rust String.
///
/// The rt_size field in the header (at ptr - 4) gives the UTF-16LE byte count.
/// The payload starts at ptr and contains that many bytes of UTF-16LE.
unsafe fn read_asc_string(ptr: u32) -> String {
    if ptr == 0 {
        return String::new();
    }
    // rt_size is at header offset 12, i.e. *(ptr - 4)
    let byte_len = unsafe { read_u32_at(ptr - 4, 0) };
    if byte_len == 0 {
        return String::new();
    }
    let char_count = byte_len / 2;
    let raw_ptr = ptr as *const u16;
    let slice = unsafe { core::slice::from_raw_parts(raw_ptr, char_count as usize) };
    // Decode UTF-16LE; replace surrogates with U+FFFD.
    char::decode_utf16(slice.iter().copied())
        .map(|r| r.unwrap_or('\u{FFFD}'))
        .collect()
}

/// Read an `Array<AscPtr<EthereumValue>>` and return a `Vec<EthereumValue>`.
///
/// Used for array and tuple parameters. Layout is the same as
/// `Array<EthereumEventParam>`: element pointers are u32 values packed
/// at `buffer_data_start`.
unsafe fn read_ethereum_value_array(arr_ptr: u32) -> Vec<EthereumValue> {
    if arr_ptr == 0 {
        return vec![];
    }
    unsafe {
        let data_start = read_u32_at(arr_ptr, 4);
        let length = read_u32_at(arr_ptr, 12);
        let mut values = Vec::with_capacity(length as usize);
        for i in 0..length {
            let elem_ptr = read_u32_at(data_start, i * 4);
            values.push(if elem_ptr != 0 {
                read_ethereum_value(elem_ptr)
            } else {
                EthereumValue::Unknown(0)
            });
        }
        values
    }
}

/// Read an `EthereumTransactionReceipt` from AS memory.
///
/// `EthereumTransactionReceipt` payload offsets (u32 AscPtrs):
///   offset 16  cumulativeGasUsed -> BigInt
///   offset 20  gasUsed           -> BigInt
///   offset 24  contractAddress   -> Address (nullable)
///   offset 28  logs              -> Array<Log>  (we only read `.length`)
///   offset 32  status            -> BigInt (nullable; null = pre-EIP-658)
unsafe fn read_receipt(ptr: u32) -> EthereumTransactionReceipt {
    unsafe {
        // cumulativeGasUsed at offset 16
        let cumul_gas_ptr = read_u32_at(ptr, 16);
        let cumulative_gas_used = if cumul_gas_ptr != 0 { read_uint8array(cumul_gas_ptr) } else { vec![0u8] };

        // gasUsed at offset 20
        let gas_ptr = read_u32_at(ptr, 20);
        let gas_used = if gas_ptr != 0 { read_uint8array(gas_ptr) } else { vec![0u8] };

        // contractAddress at offset 24 — nullable Address (Uint8Array of 20 bytes)
        let contract_addr_ptr = read_u32_at(ptr, 24);
        let contract_address = if contract_addr_ptr != 0 {
            Some(read_fixed_bytes::<20>(contract_addr_ptr))
        } else {
            None
        };

        // logs array at offset 28 — we only need the element count
        let logs_ptr = read_u32_at(ptr, 28);
        let log_count = if logs_ptr != 0 { read_u32_at(logs_ptr, 12) } else { 0 };

        // status at offset 32 — nullable BigInt
        // null pointer → pre-EIP-658 block → None
        // BigInt bytes: 0 = reverted, anything else = success
        let status_ptr = read_u32_at(ptr, 32);
        let status = if status_ptr == 0 {
            None
        } else {
            let status_bytes = read_uint8array(status_ptr);
            let is_success = status_bytes.iter().any(|&b| b != 0);
            Some(is_success)
        };

        EthereumTransactionReceipt { status, gas_used, cumulative_gas_used, contract_address, log_count }
    }
}

/// Read an Array<EthereumEventParam> and return a Vec of EventParam.
///
/// Array<T> payload:
///   [buffer_ptr: u32][buffer_data_start: u32][buffer_data_len: u32][length: u32]
///
/// Each element is a u32 AscPtr to an EthereumEventParam.
unsafe fn read_event_params_array(arr_ptr: u32) -> Vec<EventParam> {
    if arr_ptr == 0 {
        return vec![];
    }
    unsafe {
        let data_start = read_u32_at(arr_ptr, 4);
        let length = read_u32_at(arr_ptr, 12);
        let mut params = Vec::with_capacity(length as usize);
        for i in 0..length {
            let elem_ptr = read_u32_at(data_start, i * 4);
            if elem_ptr == 0 {
                continue;
            }
            let param = read_event_param(elem_ptr);
            params.push(param);
        }
        params
    }
}

/// Read a single EthereumEventParam.
///
/// Layout:
///   offset 0  name   u32 AscPtr<AscString>
///   offset 4  value  u32 AscPtr<EthereumValue>
unsafe fn read_event_param(ptr: u32) -> EventParam {
    unsafe {
        let name_ptr = read_u32_at(ptr, 0);
        let value_ptr = read_u32_at(ptr, 4);
        let name = read_asc_string(name_ptr);
        let value = read_ethereum_value(value_ptr);
        EventParam { name, value }
    }
}

/// Read an EthereumValue (AscEnum).
///
/// AscEnum payload (16 bytes):
///   offset 0  kind   u32
///   offset 4  _pad   u32
///   offset 8  data   u64
///
/// For pointer-typed variants, data is a u32 AscPtr cast to u64.
unsafe fn read_ethereum_value(ptr: u32) -> EthereumValue {
    if ptr == 0 {
        return EthereumValue::Unknown(0xff);
    }
    unsafe {
        let kind = read_u32_at(ptr, 0);
        let data = read_u64_at(ptr, 8);
        let data_ptr = data as u32; // lower 32 bits used for AscPtr

        match kind {
            KIND_ADDRESS => {
                // data points to a Uint8Array of 20 bytes
                let bytes = read_fixed_bytes::<20>(data_ptr);
                EthereumValue::Address(bytes)
            }
            KIND_FIXED_BYTES => {
                let bytes = read_uint8array(data_ptr);
                EthereumValue::FixedBytes(bytes)
            }
            KIND_BYTES => {
                let bytes = read_uint8array(data_ptr);
                EthereumValue::Bytes(bytes)
            }
            KIND_INT => {
                // BigInt: Uint8Array in little-endian two's-complement
                let bytes = read_uint8array(data_ptr);
                EthereumValue::Int(bytes)
            }
            KIND_UINT => {
                let bytes = read_uint8array(data_ptr);
                EthereumValue::Uint(bytes)
            }
            KIND_BOOL => {
                // data is 0 or 1 stored as u64
                EthereumValue::Bool(data != 0)
            }
            KIND_STRING => {
                let s = read_asc_string(data_ptr);
                EthereumValue::String(s)
            }
            KIND_FIXED_ARRAY => {
                let items = read_ethereum_value_array(data_ptr);
                EthereumValue::FixedArray(items)
            }
            KIND_ARRAY => {
                let items = read_ethereum_value_array(data_ptr);
                EthereumValue::Array(items)
            }
            KIND_TUPLE => {
                let items = read_ethereum_value_array(data_ptr);
                EthereumValue::Tuple(items)
            }
            other => EthereumValue::Unknown(other),
        }
    }
}

// ============================================================================
// RawEthereumEvent helper methods
// ============================================================================

impl RawEthereumEvent {
    /// Find a named parameter and return its address bytes.
    pub fn find_address(&self, name: &str) -> Result<[u8; 20], &'static str> {
        for p in &self.params {
            if p.name == name {
                if let EthereumValue::Address(a) = &p.value {
                    return Ok(*a);
                }
            }
        }
        Err("param not found or wrong type")
    }

    /// Find a named parameter and return its uint bytes (little-endian).
    pub fn find_uint(&self, name: &str) -> Result<Vec<u8>, &'static str> {
        for p in &self.params {
            if p.name == name {
                if let EthereumValue::Uint(b) = &p.value {
                    return Ok(b.clone());
                }
            }
        }
        Err("param not found or wrong type")
    }

    /// Find a named parameter and return its int bytes (little-endian two's-complement).
    pub fn find_int(&self, name: &str) -> Result<Vec<u8>, &'static str> {
        for p in &self.params {
            if p.name == name {
                if let EthereumValue::Int(b) = &p.value {
                    return Ok(b.clone());
                }
            }
        }
        Err("param not found or wrong type")
    }

    /// Find a named parameter and return its bool value.
    pub fn find_bool(&self, name: &str) -> Result<bool, &'static str> {
        for p in &self.params {
            if p.name == name {
                if let EthereumValue::Bool(b) = &p.value {
                    return Ok(*b);
                }
            }
        }
        Err("param not found or wrong type")
    }

    /// Find a named parameter and return its string value.
    pub fn find_string(&self, name: &str) -> Result<String, &'static str> {
        for p in &self.params {
            if p.name == name {
                if let EthereumValue::String(s) = &p.value {
                    return Ok(s.clone());
                }
            }
        }
        Err("param not found or wrong type")
    }

    /// Find a named parameter and return its bytes (dynamic or fixed).
    pub fn find_bytes(&self, name: &str) -> Result<Vec<u8>, &'static str> {
        for p in &self.params {
            if p.name == name {
                match &p.value {
                    EthereumValue::Bytes(b) | EthereumValue::FixedBytes(b) => return Ok(b.clone()),
                    _ => {}
                }
            }
        }
        Err("param not found or wrong type")
    }

    /// Find a named parameter and return its array elements (`T[]` or `T[N]`).
    pub fn find_array(&self, name: &str) -> Result<&Vec<EthereumValue>, &'static str> {
        for p in &self.params {
            if p.name == name {
                match &p.value {
                    EthereumValue::Array(v) | EthereumValue::FixedArray(v) => return Ok(v),
                    _ => {}
                }
            }
        }
        Err("param not found or wrong type")
    }

    /// Find a named parameter and return its tuple elements (`(T1, T2, ...)`).
    pub fn find_tuple(&self, name: &str) -> Result<&Vec<EthereumValue>, &'static str> {
        for p in &self.params {
            if p.name == name {
                if let EthereumValue::Tuple(v) = &p.value {
                    return Ok(v);
                }
            }
        }
        Err("param not found or wrong type")
    }
}

// ============================================================================
// ethereum.decode host function wrapper
// ============================================================================

/// Decode ABI-encoded bytes according to a Solidity type string.
///
/// Calls the `ethereum.decode` host function provided by graph-node and
/// decodes the returned `EthereumValue` from AS memory.
///
/// Returns `None` if decoding fails (e.g., invalid types string or malformed data).
///
/// # Example
///
/// ```ignore
/// let value = ethereum_decode("uint256", &token_id_bytes);
/// if let Some(EthereumValue::Uint(bytes)) = value {
///     // bytes is little-endian BigInt representation
/// }
/// ```
///
/// On native (non-WASM) builds this always returns `None` — mock it in tests
/// by constructing `EthereumValue` directly.
pub fn ethereum_decode(types: &str, data: &[u8]) -> Option<EthereumValue> {
    #[cfg(target_arch = "wasm32")]
    {
        use crate::{as_types, ffi};
        let types_ptr = as_types::new_asc_string(types);
        let data_ptr = as_types::new_asc_bytes(data);
        let result_ptr = unsafe { ffi::ethereum_decode(types_ptr, data_ptr) };
        if result_ptr == 0 {
            return None;
        }
        Some(unsafe { read_ethereum_value(result_ptr) })
    }
    #[cfg(not(target_arch = "wasm32"))]
    {
        let _ = (types, data);
        None
    }
}

// ============================================================================
// Top-level entry point
// ============================================================================

/// Read a full EthereumEvent from AS memory.
///
/// # Safety
///
/// `ptr` must be a valid AscPtr to an EthereumEvent object written by graph-node.
pub unsafe fn read_ethereum_event(ptr: u32) -> RawEthereumEvent {
    unsafe {
        // EthereumEvent payload field offsets (each is a u32 AscPtr):
        //   0  address
        //   4  logIndex
        //   8  transactionLogIndex
        //  12  logType
        //  16  block
        //  20  transaction
        //  24  parameters
        //  28  receipt

        let address_ptr = read_u32_at(ptr, 0);
        let log_index_ptr = read_u32_at(ptr, 4);
        let block_ptr = read_u32_at(ptr, 16);
        let tx_ptr = read_u32_at(ptr, 20);
        let params_ptr = read_u32_at(ptr, 24);
        let receipt_ptr = read_u32_at(ptr, 28);

        // address: Uint8Array (20 bytes)
        let address = if address_ptr != 0 {
            read_fixed_bytes::<20>(address_ptr)
        } else {
            [0u8; 20]
        };

        // logIndex: BigInt (Uint8Array)
        let log_index = if log_index_ptr != 0 {
            read_uint8array(log_index_ptr)
        } else {
            vec![0u8]
        };

        // EthereumBlock offsets (u32 AscPtrs each):
        //   0  hash             -> Bytes (32)
        //   4  parentHash       -> Bytes (32)
        //   8  unclesHash       -> Bytes (32)
        //  12  author           -> Address (20)
        //  16  stateRoot        -> Bytes (32)
        //  20  transactionsRoot -> Bytes (32)
        //  24  receiptsRoot     -> Bytes (32)
        //  28  number           -> BigInt
        //  32  gasUsed          -> BigInt
        //  36  gasLimit         -> BigInt
        //  40  timestamp        -> BigInt
        //  44  difficulty       -> BigInt
        //  48  totalDifficulty  -> BigInt
        //  52  size             -> BigInt (nullable)
        //  56  baseFeePerGas    -> BigInt (nullable)
        let (block_hash, block_number, block_timestamp, block_gas_used, block_gas_limit, block_difficulty, block_base_fee_per_gas) = if block_ptr != 0 {
            let hash_ptr     = read_u32_at(block_ptr, 0);
            let num_ptr      = read_u32_at(block_ptr, 28);
            let gas_used_ptr = read_u32_at(block_ptr, 32);
            let gas_lim_ptr  = read_u32_at(block_ptr, 36);
            let ts_ptr       = read_u32_at(block_ptr, 40);
            let diff_ptr     = read_u32_at(block_ptr, 44);
            let base_fee_ptr = read_u32_at(block_ptr, 56);

            let hash     = if hash_ptr != 0 { read_fixed_bytes::<32>(hash_ptr) } else { [0u8; 32] };
            let num      = if num_ptr != 0 { read_uint8array(num_ptr) } else { vec![0u8] };
            let gas_used = if gas_used_ptr != 0 { read_uint8array(gas_used_ptr) } else { vec![0u8] };
            let gas_lim  = if gas_lim_ptr != 0 { read_uint8array(gas_lim_ptr) } else { vec![0u8] };
            let ts       = if ts_ptr != 0 { read_uint8array(ts_ptr) } else { vec![0u8] };
            let diff     = if diff_ptr != 0 { read_uint8array(diff_ptr) } else { vec![0u8] };
            let base_fee = if base_fee_ptr != 0 { Some(read_uint8array(base_fee_ptr)) } else { None };

            (hash, num, ts, gas_used, gas_lim, diff, base_fee)
        } else {
            ([0u8; 32], vec![0u8], vec![0u8], vec![0u8], vec![0u8], vec![0u8], None)
        };

        // EthereumTransaction offsets (u32 AscPtrs):
        //   0  hash     -> Bytes (32)
        //   4  index    -> BigInt
        //   8  from     -> Address (20)
        //  12  to       -> Address (nullable)
        //  16  value    -> BigInt
        //  20  gasLimit -> BigInt
        //  24  gasPrice -> BigInt
        //  28  input    -> Bytes
        //  32  nonce    -> BigInt
        let (tx_hash, tx_index, tx_from, tx_to, tx_value, tx_gas_limit, tx_gas_price, tx_nonce) = if tx_ptr != 0 {
            let hash_ptr      = read_u32_at(tx_ptr, 0);
            let index_ptr     = read_u32_at(tx_ptr, 4);
            let from_ptr      = read_u32_at(tx_ptr, 8);
            let to_ptr        = read_u32_at(tx_ptr, 12);
            let value_ptr     = read_u32_at(tx_ptr, 16);
            let gas_lim_ptr   = read_u32_at(tx_ptr, 20);
            let gas_price_ptr = read_u32_at(tx_ptr, 24);
            let nonce_ptr     = read_u32_at(tx_ptr, 32);

            let hash      = if hash_ptr != 0 { read_fixed_bytes::<32>(hash_ptr) } else { [0u8; 32] };
            let index     = if index_ptr != 0 { read_uint8array(index_ptr) } else { vec![0u8] };
            let from      = if from_ptr != 0 { read_fixed_bytes::<20>(from_ptr) } else { [0u8; 20] };
            let to        = if to_ptr != 0 { Some(read_fixed_bytes::<20>(to_ptr)) } else { None };
            let value     = if value_ptr != 0 { read_uint8array(value_ptr) } else { vec![0u8] };
            let gas_lim   = if gas_lim_ptr != 0 { read_uint8array(gas_lim_ptr) } else { vec![0u8] };
            let gas_price = if gas_price_ptr != 0 { read_uint8array(gas_price_ptr) } else { vec![0u8] };
            let nonce     = if nonce_ptr != 0 { read_uint8array(nonce_ptr) } else { vec![0u8] };

            (hash, index, from, to, value, gas_lim, gas_price, nonce)
        } else {
            ([0u8; 32], vec![0u8], [0u8; 20], None, vec![0u8], vec![0u8], vec![0u8], vec![0u8])
        };

        // parameters: Array<EthereumEventParam>
        let params = if params_ptr != 0 {
            read_event_params_array(params_ptr)
        } else {
            vec![]
        };

        // receipt: nullable pointer to EthereumTransactionReceipt
        let receipt = if receipt_ptr != 0 {
            Some(read_receipt(receipt_ptr))
        } else {
            None
        };

        RawEthereumEvent {
            address,
            log_index,
            block_hash,
            block_number,
            block_timestamp,
            block_gas_used,
            block_gas_limit,
            block_difficulty,
            block_base_fee_per_gas,
            tx_hash,
            tx_index,
            tx_from,
            tx_to,
            tx_value,
            tx_gas_limit,
            tx_gas_price,
            tx_nonce,
            params,
            receipt,
        }
    }
}

/// Read a full `EthereumCall` from AS memory.
///
/// Called by `#[handler(call)]` WASM entry points with the `call_ptr`
/// argument passed by graph-node.
///
/// # Safety
///
/// `ptr` must be a valid AscPtr to an `EthereumCall` object written by graph-node.
///
/// # Memory layout
///
/// ```text
/// EthereumCall payload (u32 AscPtrs):
///   offset  0  address      -> Uint8Array (20 bytes)
///   offset  4  block        -> EthereumBlock
///   offset  8  transaction  -> EthereumTransaction
///   offset 12  inputValues  -> Array<EthereumEventParam>
///   offset 16  outputValues -> Array<EthereumEventParam>
/// ```
pub unsafe fn read_ethereum_call(ptr: u32) -> RawEthereumCall {
    unsafe {
        let address_ptr = read_u32_at(ptr, 0);
        let block_ptr = read_u32_at(ptr, 4);
        let tx_ptr = read_u32_at(ptr, 8);
        let inputs_ptr = read_u32_at(ptr, 12);
        let outputs_ptr = read_u32_at(ptr, 16);

        // address: Uint8Array (20 bytes)
        let address = if address_ptr != 0 {
            read_fixed_bytes::<20>(address_ptr)
        } else {
            [0u8; 20]
        };

        // Block fields (same offsets as in EthereumEvent)
        let (block_hash, block_number, block_timestamp, block_gas_used, block_gas_limit, block_difficulty, block_base_fee_per_gas) = if block_ptr != 0 {
            let hash_ptr     = read_u32_at(block_ptr, 0);
            let num_ptr      = read_u32_at(block_ptr, 28);
            let gas_used_ptr = read_u32_at(block_ptr, 32);
            let gas_lim_ptr  = read_u32_at(block_ptr, 36);
            let ts_ptr       = read_u32_at(block_ptr, 40);
            let diff_ptr     = read_u32_at(block_ptr, 44);
            let base_fee_ptr = read_u32_at(block_ptr, 56);

            let hash     = if hash_ptr != 0 { read_fixed_bytes::<32>(hash_ptr) } else { [0u8; 32] };
            let num      = if num_ptr != 0 { read_uint8array(num_ptr) } else { vec![0u8] };
            let gas_used = if gas_used_ptr != 0 { read_uint8array(gas_used_ptr) } else { vec![0u8] };
            let gas_lim  = if gas_lim_ptr != 0 { read_uint8array(gas_lim_ptr) } else { vec![0u8] };
            let ts       = if ts_ptr != 0 { read_uint8array(ts_ptr) } else { vec![0u8] };
            let diff     = if diff_ptr != 0 { read_uint8array(diff_ptr) } else { vec![0u8] };
            let base_fee = if base_fee_ptr != 0 { Some(read_uint8array(base_fee_ptr)) } else { None };

            (hash, num, ts, gas_used, gas_lim, diff, base_fee)
        } else {
            ([0u8; 32], vec![0u8], vec![0u8], vec![0u8], vec![0u8], vec![0u8], None)
        };

        // Transaction fields
        let (tx_hash, tx_index, from, tx_to, tx_value, tx_gas_limit, tx_gas_price, tx_nonce) = if tx_ptr != 0 {
            let hash_ptr      = read_u32_at(tx_ptr, 0);
            let index_ptr     = read_u32_at(tx_ptr, 4);
            let from_ptr      = read_u32_at(tx_ptr, 8);
            let to_ptr        = read_u32_at(tx_ptr, 12);
            let value_ptr     = read_u32_at(tx_ptr, 16);
            let gas_lim_ptr   = read_u32_at(tx_ptr, 20);
            let gas_price_ptr = read_u32_at(tx_ptr, 24);
            let nonce_ptr     = read_u32_at(tx_ptr, 32);

            let hash      = if hash_ptr != 0 { read_fixed_bytes::<32>(hash_ptr) } else { [0u8; 32] };
            let index     = if index_ptr != 0 { read_uint8array(index_ptr) } else { vec![0u8] };
            let from      = if from_ptr != 0 { read_fixed_bytes::<20>(from_ptr) } else { [0u8; 20] };
            let to        = if to_ptr != 0 { Some(read_fixed_bytes::<20>(to_ptr)) } else { None };
            let value     = if value_ptr != 0 { read_uint8array(value_ptr) } else { vec![0u8] };
            let gas_lim   = if gas_lim_ptr != 0 { read_uint8array(gas_lim_ptr) } else { vec![0u8] };
            let gas_price = if gas_price_ptr != 0 { read_uint8array(gas_price_ptr) } else { vec![0u8] };
            let nonce     = if nonce_ptr != 0 { read_uint8array(nonce_ptr) } else { vec![0u8] };

            (hash, index, from, to, value, gas_lim, gas_price, nonce)
        } else {
            ([0u8; 32], vec![0u8], [0u8; 20], None, vec![0u8], vec![0u8], vec![0u8], vec![0u8])
        };

        // inputValues and outputValues: Array<EthereumEventParam>
        let inputs = if inputs_ptr != 0 { read_event_params_array(inputs_ptr) } else { vec![] };
        let outputs = if outputs_ptr != 0 { read_event_params_array(outputs_ptr) } else { vec![] };

        RawEthereumCall {
            address,
            block_hash,
            block_number,
            block_timestamp,
            block_gas_used,
            block_gas_limit,
            block_difficulty,
            block_base_fee_per_gas,
            tx_hash,
            tx_index,
            from,
            tx_to,
            tx_value,
            tx_gas_limit,
            tx_gas_price,
            tx_nonce,
            inputs,
            outputs,
        }
    }
}
